Skip to content

Commit

Permalink
feat: option to disable built-in type validation
Browse files Browse the repository at this point in the history
fixes #242
  • Loading branch information
masumsoft committed Jan 15, 2022
1 parent a1f5e6e commit 024552d
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 45 deletions.
4 changes: 2 additions & 2 deletions docs/datatypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
125 changes: 99 additions & 26 deletions docs/validators.md
Original file line number Diff line number Diff line change
@@ -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 : "<enter your email here>",
//... 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
}
}
}
Expand All @@ -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: {
Expand Down
6 changes: 4 additions & 2 deletions src/validators/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
66 changes: 54 additions & 12 deletions test/functional/crud_operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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();
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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' });
Expand Down
4 changes: 1 addition & 3 deletions test/models/PersonModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ const personSchema = {
},
age: {
type: 'int',
rule: {
validator: (value) => (value > 0),
},
rule: (value) => (value > 0),
},
ageString: {
type: 'text',
Expand Down
25 changes: 25 additions & 0 deletions test/models/simple/ValidationModel.js
Original file line number Diff line number Diff line change
@@ -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'],
};

0 comments on commit 024552d

Please sign in to comment.