From 024552d22e1e8e31968f3e5c16a5d5a54f98d259 Mon Sep 17 00:00:00 2001 From: Masum Date: Sun, 16 Jan 2022 03:04:16 +0600 Subject: [PATCH] feat: option to disable built-in type validation fixes #242 --- docs/datatypes.md | 4 +- docs/validators.md | 125 ++++++++++++++++++++------ src/validators/schema.js | 6 +- test/functional/crud_operations.js | 66 +++++++++++--- test/models/PersonModel.js | 4 +- test/models/simple/ValidationModel.js | 25 ++++++ 6 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 test/models/simple/ValidationModel.js diff --git a/docs/datatypes.md b/docs/datatypes.md index c1ed8fc6..bb97aedd 100644 --- a/docs/datatypes.md +++ b/docs/datatypes.md @@ -35,10 +35,10 @@ When saving or retrieving the value of a column, the value is typed according to | Cassandra Field Types | Javascript Types | |------------------------|-----------------------------------| | ascii | String | -| bigint | [models.datatypes.Long](https://google.github.io/closure-library/api/goog.math.Long.html)| +| bigint | [models.datatypes.Long](https://docs.datastax.com/en/developer/nodejs-driver/4.6/api/module.types/class.Long/)| | blob | [Buffer](https://nodejs.org/api/buffer.html)| | boolean | Boolean | -| counter | [models.datatypes.Long](https://google.github.io/closure-library/api/goog.math.Long.html)| +| counter | [models.datatypes.Long](https://docs.datastax.com/en/developer/nodejs-driver/4.6/api/module.types/class.Long/)| | date | [models.datatypes.LocalDate](http://docs.datastax.com/en/developer/nodejs-driver/4.6/api/module.types/class.LocalDate/)| | decimal | [models.datatypes.BigDecimal](http://docs.datastax.com/en/developer/nodejs-driver/4.6/api/module.types/class.BigDecimal/)| | double | Number | diff --git a/docs/validators.md b/docs/validators.md index 9c0bb299..9b5e6de8 100644 --- a/docs/validators.md +++ b/docs/validators.md @@ -1,86 +1,159 @@ # Validators -Every time you set a property for an instance of your model, an internal type validator checks that the value is valid. If not an error is thrown. But how to add a custom validator? You need to provide your custom validator in the schema definition. For example, if you want to check age to be a number greater than zero: +## Built-in type validators + +Every time you save a field value for an instance of your model, an internal type validator checks whether the value is valid according to the defined data type of the field. A validation error is thrown if the given data type does not match the data type of the field. + +For example let's have a field type defined as `int` like the following: ```js export default { - //... other properties hidden for clarity + //... other fields hidden for clarity + age: { + type : "int" + } +} + +``` + +So now you cannot for example save a value of type decimal or string for age field. For non integer type input, a validation error will be returned in error callback or a validation error will be thrown if no callback is defined: + +```js + +var john = new models.instance.Person({ + //... other fields hidden for clarity + age: 32.5 +}); +john.save(function(err){ + // invalid value error will be returned in callback + if(err) { + console.log(err); + return; + } +}); + +//... trying with string will also fail +john.age = '32'; +john.save(); // invalid value error will be thrown + +``` + +## Disabling Built-in type validation + +If you want to disable the built-in data type validation checks, you may overwrite the behaviour by setting a rule `type_validation: false` for the field: + +```js + +export default { + //... other fields hidden for clarity age: { type : "int", - rule : function(value){ return value > 0; } + rule : { + type_validation: false // Won't have validation error even if the value is not an integer + } } } ``` -your validator must return a boolean. If someone will try to assign `john.age = -15;` an error will be thrown. -You can also provide a message for validation error in this way +So now you may use a string and by default it will be converted to integer by cassandra driver using it's automatic safe type conversion system for prepared queries: + +```js + +john.age = '32'; +john.save(); // will be successfully converted by driver to int + +john.age='abc' +john.save(); // will throw db error for invalid data + +``` + +## Required fields + +If a field value is not set and no default value is provided, then the validators will not be executed. So if you want to have `required` fields, then you need to set the `required` flag to true like the following: ```js export default { - //... other properties hidden for clarity + //... other fields hidden for clarity age: { type : "int", rule : { - validator : function(value){ return value > 0; }, - message : 'Age must be greater than 0' + required: true // If age is undefined or null, then throw validation error } } } ``` -then the error will have your message. Message can also be a function; in that case it must return a string: +## Custom validators + +You may also add a custom validator on top of existing type validators? You need to provide your custom validator in the schema definition rule. For example, if you want to check age to be a number greater than zero: + +```js + +export default { + //... other fields hidden for clarity + age: { + type : "int", + rule : function(value){ return value > 0; } + } +} + +``` + +your validator must return a boolean. If someone will try to assign `john.age = -15;` an error will be thrown. +You can also provide a message for validation error: ```js export default { - //... other properties hidden for clarity + //... other fields hidden for clarity age: { type : "int", rule : { validator : function(value){ return value > 0; }, - message : function(value){ return 'Age must be greater than 0. You provided '+ value; } + message : 'Age must be greater than 0' } } } ``` -The error message will be `Age must be greater than 0. You provided -15` - -Note that default values _are_ validated if defined either by value or as a javascript function. Defaults defined as DB functions, on the other hand, are never validated in the model as they are retrieved _after_ the corresponding data has entered the DB. -If you need to exclude defaults from being checked you can pass an extra flag: +then the error will have your message. Message can also be a function; in that case it must return a string: ```js export default { - //... other properties hidden for clarity - email: { - type : "text", - default : "", + //... other fields hidden for clarity + age: { + type : "int", rule : { - validator : function(value){ /* code to check that value matches an email pattern*/ }, - ignore_default: true + validator : function(value){ return value > 0; }, + message : function(value){ return 'Age must be greater than 0. You provided '+ value; } } } } ``` -If a field value is not set and no default value is provided, then the validators will not be executed. So if you want to have `required` fields, then you need to set the `required` flag to true like the following: +The error message will be `Age must be greater than 0. You provided -15` + +Note that default values are validated if defined either by value or as a javascript function. Defaults defined as DB functions, on the other hand, are never validated in the model as they are retrieved after the corresponding data has entered the DB. + +If you need to exclude defaults from being checked you can pass an extra flag: ```js export default { - //... other properties hidden for clarity + //... other fields hidden for clarity email: { type : "text", + default : "no email provided", rule : { validator : function(value){ /* code to check that value matches an email pattern*/ }, - required: true // If email is undefined or null, then throw validation error + ignore_default: true } } } @@ -89,9 +162,9 @@ export default { You may also add multiple validators with a different validation message for each. Following is an example of using multiple validators: -``` +```js export default { - //... other properties hidden for clarity + //... other fields hidden for clarity age: { type: "int", rule: { diff --git a/src/validators/schema.js b/src/validators/schema.js index ced380cb..2b3172d9 100644 --- a/src/validators/schema.js +++ b/src/validators/schema.js @@ -357,12 +357,14 @@ const schemer = { const validators = []; const fieldtype = this.get_field_type(modelSchema, fieldname); const typeFieldValidator = datatypes.generic_type_validator(fieldtype); + const field = modelSchema.fields[fieldname]; if (typeFieldValidator) { - validators.push(typeFieldValidator); + if (!(field.rule && field.rule.type_validation === false)) { + validators.push(typeFieldValidator); + } } - const field = modelSchema.fields[fieldname]; if (typeof field.rule !== 'undefined') { if (typeof field.rule === 'function') { field.rule = { diff --git a/test/functional/crud_operations.js b/test/functional/crud_operations.js index f80fbfba..f6492035 100644 --- a/test/functional/crud_operations.js +++ b/test/functional/crud_operations.js @@ -11,6 +11,7 @@ module.exports = () => { this.timeout(60000); this.slow(20000); models.instance.Person.truncateAsync() + .then(() => models.instance.Validation.truncateAsync()) .then(() => models.instance.Counter.truncateAsync()) .then(() => models.instance.Event.truncateAsync()) .then(() => models.instance.Simple.truncateAsync()) @@ -95,22 +96,12 @@ module.exports = () => { }); }); - it('should throw unset error due to required field', (done) => { + it('should save data without errors', (done) => { alex.age = 32; + alex.points = 64.0; alex.isModified().should.equal(true); alex.isModified('userID').should.equal(true); alex.isModified('address').should.equal(true); - alex.save((err1) => { - if (err1) { - err1.name.should.equal('apollo.model.save.unsetrequired'); - return done(); - } - return done(new Error('required rule is not working properly')); - }); - }); - - it('should save data without errors', (done) => { - alex.points = 64.0; alex.saveAsync() .then(() => { done(); @@ -133,6 +124,45 @@ module.exports = () => { done(); }); }); + + let bob; + it('should throw unset error due to required field without validator', function f(done) { + this.timeout(5000); + this.slow(1000); + bob = new models.instance.Validation({ + id: 'd8160520-5f6d-11eb-af42-0a1f69b793a6', + }); + bob.save((err4) => { + if (err4) { + err4.name.should.equal('apollo.model.save.unsetrequired'); + return done(); + } + return done(new Error('required rule is not working properly')); + }); + }); + + it('should throw error due to rule validator with no type validation', (done) => { + bob.name = 'bob'; + bob.age = '0'; + bob.save((err5) => { + if (err5) { + err5.name.should.equal('apollo.model.validator.invalidvalue'); + return done(); + } + return done(new Error('validation rule is not working properly')); + }); + }); + + it('should save data without errors for no type validation', (done) => { + bob.age = '30'; + bob.saveAsync() + .then(() => { + done(); + }) + .catch((err6) => { + done(err6); + }); + }); }); describe('#find after save', () => { @@ -893,6 +923,18 @@ module.exports = () => { }); }); + describe('#find data for no type validation fields', () => { + it('should find data as expected for fields with no type validation rule', (done) => { + models.instance.Validation.findOne({ id: 'd8160520-5f6d-11eb-af42-0a1f69b793a6' }, (err, data) => { + if (err) done(err); + data.id.toString().should.equal('d8160520-5f6d-11eb-af42-0a1f69b793a6'); + data.name.should.equal('bob'); + data.age.should.equal(30); + done(); + }); + }); + }); + describe('#toJSON returns object with model fields only', () => { it('should return the object for new model instance', () => { const simple = new models.instance.Simple({ foo: 'bar' }); diff --git a/test/models/PersonModel.js b/test/models/PersonModel.js index 8cf54877..a6139d82 100644 --- a/test/models/PersonModel.js +++ b/test/models/PersonModel.js @@ -17,9 +17,7 @@ const personSchema = { }, age: { type: 'int', - rule: { - validator: (value) => (value > 0), - }, + rule: (value) => (value > 0), }, ageString: { type: 'text', diff --git a/test/models/simple/ValidationModel.js b/test/models/simple/ValidationModel.js new file mode 100644 index 00000000..9d02475c --- /dev/null +++ b/test/models/simple/ValidationModel.js @@ -0,0 +1,25 @@ +export default { + fields: { + id: { + type: 'uuid', + rule: { + type_validation: false, + }, + }, + name: { + type: 'varchar', + rule: { + required: true, + }, + }, + age: { + type: 'int', + rule: { + validator: (value) => (parseInt(value, 10) > 0), + message: (value) => (`Age must be greater than 0. You provided ${value}`), + type_validation: false, + }, + }, + }, + key: ['id'], +};