Skip to content

Commit 024552d

Browse files
committed
feat: option to disable built-in type validation
fixes #242
1 parent a1f5e6e commit 024552d

File tree

6 files changed

+185
-45
lines changed

6 files changed

+185
-45
lines changed

docs/datatypes.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ When saving or retrieving the value of a column, the value is typed according to
3535
| Cassandra Field Types | Javascript Types |
3636
|------------------------|-----------------------------------|
3737
| ascii | String |
38-
| bigint | [models.datatypes.Long](https://google.github.io/closure-library/api/goog.math.Long.html)|
38+
| bigint | [models.datatypes.Long](https://docs.datastax.com/en/developer/nodejs-driver/4.6/api/module.types/class.Long/)|
3939
| blob | [Buffer](https://nodejs.org/api/buffer.html)|
4040
| boolean | Boolean |
41-
| counter | [models.datatypes.Long](https://google.github.io/closure-library/api/goog.math.Long.html)|
41+
| counter | [models.datatypes.Long](https://docs.datastax.com/en/developer/nodejs-driver/4.6/api/module.types/class.Long/)|
4242
| date | [models.datatypes.LocalDate](http://docs.datastax.com/en/developer/nodejs-driver/4.6/api/module.types/class.LocalDate/)|
4343
| decimal | [models.datatypes.BigDecimal](http://docs.datastax.com/en/developer/nodejs-driver/4.6/api/module.types/class.BigDecimal/)|
4444
| double | Number |

docs/validators.md

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,159 @@
11
# Validators
22

3-
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:
3+
## Built-in type validators
4+
5+
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.
6+
7+
For example let's have a field type defined as `int` like the following:
48

59
```js
610

711
export default {
8-
//... other properties hidden for clarity
12+
//... other fields hidden for clarity
13+
age: {
14+
type : "int"
15+
}
16+
}
17+
18+
```
19+
20+
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:
21+
22+
```js
23+
24+
var john = new models.instance.Person({
25+
//... other fields hidden for clarity
26+
age: 32.5
27+
});
28+
john.save(function(err){
29+
// invalid value error will be returned in callback
30+
if(err) {
31+
console.log(err);
32+
return;
33+
}
34+
});
35+
36+
//... trying with string will also fail
37+
john.age = '32';
38+
john.save(); // invalid value error will be thrown
39+
40+
```
41+
42+
## Disabling Built-in type validation
43+
44+
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:
45+
46+
```js
47+
48+
export default {
49+
//... other fields hidden for clarity
950
age: {
1051
type : "int",
11-
rule : function(value){ return value > 0; }
52+
rule : {
53+
type_validation: false // Won't have validation error even if the value is not an integer
54+
}
1255
}
1356
}
1457

1558
```
1659

17-
your validator must return a boolean. If someone will try to assign `john.age = -15;` an error will be thrown.
18-
You can also provide a message for validation error in this way
60+
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:
61+
62+
```js
63+
64+
john.age = '32';
65+
john.save(); // will be successfully converted by driver to int
66+
67+
john.age='abc'
68+
john.save(); // will throw db error for invalid data
69+
70+
```
71+
72+
## Required fields
73+
74+
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:
1975

2076
```js
2177

2278
export default {
23-
//... other properties hidden for clarity
79+
//... other fields hidden for clarity
2480
age: {
2581
type : "int",
2682
rule : {
27-
validator : function(value){ return value > 0; },
28-
message : 'Age must be greater than 0'
83+
required: true // If age is undefined or null, then throw validation error
2984
}
3085
}
3186
}
3287

3388
```
3489

35-
then the error will have your message. Message can also be a function; in that case it must return a string:
90+
## Custom validators
91+
92+
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:
93+
94+
```js
95+
96+
export default {
97+
//... other fields hidden for clarity
98+
age: {
99+
type : "int",
100+
rule : function(value){ return value > 0; }
101+
}
102+
}
103+
104+
```
105+
106+
your validator must return a boolean. If someone will try to assign `john.age = -15;` an error will be thrown.
107+
You can also provide a message for validation error:
36108

37109
```js
38110

39111
export default {
40-
//... other properties hidden for clarity
112+
//... other fields hidden for clarity
41113
age: {
42114
type : "int",
43115
rule : {
44116
validator : function(value){ return value > 0; },
45-
message : function(value){ return 'Age must be greater than 0. You provided '+ value; }
117+
message : 'Age must be greater than 0'
46118
}
47119
}
48120
}
49121

50122
```
51123

52-
The error message will be `Age must be greater than 0. You provided -15`
53-
54-
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.
55-
If you need to exclude defaults from being checked you can pass an extra flag:
124+
then the error will have your message. Message can also be a function; in that case it must return a string:
56125

57126
```js
58127

59128
export default {
60-
//... other properties hidden for clarity
61-
email: {
62-
type : "text",
63-
default : "<enter your email here>",
129+
//... other fields hidden for clarity
130+
age: {
131+
type : "int",
64132
rule : {
65-
validator : function(value){ /* code to check that value matches an email pattern*/ },
66-
ignore_default: true
133+
validator : function(value){ return value > 0; },
134+
message : function(value){ return 'Age must be greater than 0. You provided '+ value; }
67135
}
68136
}
69137
}
70138

71139
```
72140

73-
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:
141+
The error message will be `Age must be greater than 0. You provided -15`
142+
143+
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.
144+
145+
If you need to exclude defaults from being checked you can pass an extra flag:
74146

75147
```js
76148

77149
export default {
78-
//... other properties hidden for clarity
150+
//... other fields hidden for clarity
79151
email: {
80152
type : "text",
153+
default : "no email provided",
81154
rule : {
82155
validator : function(value){ /* code to check that value matches an email pattern*/ },
83-
required: true // If email is undefined or null, then throw validation error
156+
ignore_default: true
84157
}
85158
}
86159
}
@@ -89,9 +162,9 @@ export default {
89162

90163
You may also add multiple validators with a different validation message for each. Following is an example of using multiple validators:
91164

92-
```
165+
```js
93166
export default {
94-
//... other properties hidden for clarity
167+
//... other fields hidden for clarity
95168
age: {
96169
type: "int",
97170
rule: {

src/validators/schema.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,12 +357,14 @@ const schemer = {
357357
const validators = [];
358358
const fieldtype = this.get_field_type(modelSchema, fieldname);
359359
const typeFieldValidator = datatypes.generic_type_validator(fieldtype);
360+
const field = modelSchema.fields[fieldname];
360361

361362
if (typeFieldValidator) {
362-
validators.push(typeFieldValidator);
363+
if (!(field.rule && field.rule.type_validation === false)) {
364+
validators.push(typeFieldValidator);
365+
}
363366
}
364367

365-
const field = modelSchema.fields[fieldname];
366368
if (typeof field.rule !== 'undefined') {
367369
if (typeof field.rule === 'function') {
368370
field.rule = {

test/functional/crud_operations.js

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = () => {
1111
this.timeout(60000);
1212
this.slow(20000);
1313
models.instance.Person.truncateAsync()
14+
.then(() => models.instance.Validation.truncateAsync())
1415
.then(() => models.instance.Counter.truncateAsync())
1516
.then(() => models.instance.Event.truncateAsync())
1617
.then(() => models.instance.Simple.truncateAsync())
@@ -95,22 +96,12 @@ module.exports = () => {
9596
});
9697
});
9798

98-
it('should throw unset error due to required field', (done) => {
99+
it('should save data without errors', (done) => {
99100
alex.age = 32;
101+
alex.points = 64.0;
100102
alex.isModified().should.equal(true);
101103
alex.isModified('userID').should.equal(true);
102104
alex.isModified('address').should.equal(true);
103-
alex.save((err1) => {
104-
if (err1) {
105-
err1.name.should.equal('apollo.model.save.unsetrequired');
106-
return done();
107-
}
108-
return done(new Error('required rule is not working properly'));
109-
});
110-
});
111-
112-
it('should save data without errors', (done) => {
113-
alex.points = 64.0;
114105
alex.saveAsync()
115106
.then(() => {
116107
done();
@@ -133,6 +124,45 @@ module.exports = () => {
133124
done();
134125
});
135126
});
127+
128+
let bob;
129+
it('should throw unset error due to required field without validator', function f(done) {
130+
this.timeout(5000);
131+
this.slow(1000);
132+
bob = new models.instance.Validation({
133+
id: 'd8160520-5f6d-11eb-af42-0a1f69b793a6',
134+
});
135+
bob.save((err4) => {
136+
if (err4) {
137+
err4.name.should.equal('apollo.model.save.unsetrequired');
138+
return done();
139+
}
140+
return done(new Error('required rule is not working properly'));
141+
});
142+
});
143+
144+
it('should throw error due to rule validator with no type validation', (done) => {
145+
bob.name = 'bob';
146+
bob.age = '0';
147+
bob.save((err5) => {
148+
if (err5) {
149+
err5.name.should.equal('apollo.model.validator.invalidvalue');
150+
return done();
151+
}
152+
return done(new Error('validation rule is not working properly'));
153+
});
154+
});
155+
156+
it('should save data without errors for no type validation', (done) => {
157+
bob.age = '30';
158+
bob.saveAsync()
159+
.then(() => {
160+
done();
161+
})
162+
.catch((err6) => {
163+
done(err6);
164+
});
165+
});
136166
});
137167

138168
describe('#find after save', () => {
@@ -893,6 +923,18 @@ module.exports = () => {
893923
});
894924
});
895925

926+
describe('#find data for no type validation fields', () => {
927+
it('should find data as expected for fields with no type validation rule', (done) => {
928+
models.instance.Validation.findOne({ id: 'd8160520-5f6d-11eb-af42-0a1f69b793a6' }, (err, data) => {
929+
if (err) done(err);
930+
data.id.toString().should.equal('d8160520-5f6d-11eb-af42-0a1f69b793a6');
931+
data.name.should.equal('bob');
932+
data.age.should.equal(30);
933+
done();
934+
});
935+
});
936+
});
937+
896938
describe('#toJSON returns object with model fields only', () => {
897939
it('should return the object for new model instance', () => {
898940
const simple = new models.instance.Simple({ foo: 'bar' });

test/models/PersonModel.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ const personSchema = {
1717
},
1818
age: {
1919
type: 'int',
20-
rule: {
21-
validator: (value) => (value > 0),
22-
},
20+
rule: (value) => (value > 0),
2321
},
2422
ageString: {
2523
type: 'text',

test/models/simple/ValidationModel.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export default {
2+
fields: {
3+
id: {
4+
type: 'uuid',
5+
rule: {
6+
type_validation: false,
7+
},
8+
},
9+
name: {
10+
type: 'varchar',
11+
rule: {
12+
required: true,
13+
},
14+
},
15+
age: {
16+
type: 'int',
17+
rule: {
18+
validator: (value) => (parseInt(value, 10) > 0),
19+
message: (value) => (`Age must be greater than 0. You provided ${value}`),
20+
type_validation: false,
21+
},
22+
},
23+
},
24+
key: ['id'],
25+
};

0 commit comments

Comments
 (0)