- Motivation
- Loosely inspired by:
- Live example:
- Simple example:
- Some basic facts about functioning of the validator
- Example
- Validators references
- Addidional tools
- Other similar libraries:
- next generation
- Conclusions:
(TOC generated using markdown-toc)
I haven't found good enough implementation of JSR-303 Bean Validation for javascript, so here we go:
Main goals during implementation of this library was:
- simple and robust architecture
- asynchronous behaviour (due to asynchronous nature of javascript)
- extendability (custom asynchronous validator)
- validation of any data structure and easyness in use (guaranteed by following JSR-303)
- well tested (different node versions and browsers - done with "jest" and "karma") for polymorphic use on server and in the browser
Feel free to contribute.
https://codesandbox.io/s/ymwky9603j
import validator, {
Required,
Optional,
Collection,
All,
Blank,
Callback,
Choice,
Count,
Email,
IsFalse,
IsNull,
IsTrue,
Length,
NotBlank,
NotNull,
Regex,
Type,
ValidatorLogicError,
} from "@stopsopa/validator";
(async () => {
const errors = await validator(
{
name: "",
surname: "doe",
email: "",
terms: false,
comments: [
{
comment: "What an ugly library",
},
{
comment: "empty",
},
],
},
new Collection({
name: new Required([new NotBlank(), new Length({ min: 3, max: 255 })]),
surname: new Required([new NotBlank(), new Length({ min: 10, max: 255 })]),
email: new Required(new Email()),
terms: new Optional(new IsTrue()),
comments: new All(
new Collection({
comment: new Required(new Length({ min: 10 })),
})
),
})
);
if (errors.count()) {
// ... handle errors
console.log(JSON.stringify(errors.getFlat(), null, 4));
// {
// "name": "This value should not be blank.",
// "surname": "This value is too short. It should have 10 characters or more.",
// "email": "This value is not a valid email address.",
// "terms": "This value should be true.",
// "comments.1.comment": "This value is too short. It should have 10 characters or more."
// }
console.log(JSON.stringify(errors.getTree(), null, 4));
// {
// "name": "This value should not be blank.",
// "surname": "This value is too short. It should have 10 characters or more.",
// "email": "This value is not a valid email address.",
// "terms": "This value should be true.",
// "comments": {
// "1": {
// "comment": "This value is too short. It should have 10 characters or more."
// }
// }
// }
}
})();
- validator() don't care if some validation errors will occur or not, it will just count them and return two methods to extract them in different formats (as it is visible in above example)
- validator() always return a promise. Rejected promise returned when special ValidatorLogicError() is thrown in Callback type validator only. Only this kind of error is different because it's not "validation error" but actual error in the process of validation - that's a different thing. Usually it's not something user can "fix" in his form or in his UI -> this is rather system error that should be logged and addressed by developers.
- normally all validators are executed in single Promise.allSettled() but there is a way to group sets of validators into separate Promise.allSettled() (using integer "async" extra flag) and execute those groups one by one. This is where another "extra" flag called "stop" of individual validators comes handy because turning it ON on particular validator will result in not executing next Promise.allSettled() in case when error was detected by that single validator -> so returning resolved or rejected promise from individual validators together with stearing it through flag "stop" serves rather as an flow control mechanizm.
- read Conclusions section of this readme
const abstract = require("@stopsopa/knex-abstract");
const extend = abstract.extend;
const prototype = abstract.prototype;
const log = require("inspc");
const a = prototype.a;
const {
Collection,
All,
Required,
Optional,
NotBlank,
Length,
Email,
Type,
IsTrue,
Callback,
Regex,
} = require("@stopsopa/validator");
const ext = {
initial: async function () {
return {
updated: this.now(),
created: this.now(),
port: 80,
};
},
toDb: (row) => {
return row;
},
update: function (...args) {
let [debug, trx, entity, id] = a(args);
delete entity.created;
entity.updated = this.now();
return prototype.prototype.update.call(this, debug, trx, entity, id);
},
insert: async function (...args) {
let [debug, trx, entity] = a(args);
entity.created = this.now();
delete entity.updated;
const id = await prototype.prototype.insert.call(this, debug, trx, entity);
return id;
},
prepareToValidate: function (data = {}, mode) {
delete data.created;
delete data.updated;
return data;
},
getValidators: function (mode = null, id, entity) {
const validators = {
id: new Optional(),
cluster: new Required([
new NotBlank(),
new Length({ max: 50 }),
new Callback(
(value, context, path, extra) =>
new Promise(async (resolve, reject) => {
const { cluster, node, id } = context.rootData;
const condition = node === null ? "is" : "=";
let c;
log(mode);
if (mode === "create") {
c = await this.queryColumn(
true,
`select count(*) c from :table: where cluster = :cluster and node ${condition} :node`,
{
cluster,
node,
}
);
} else {
c = await this.queryColumn(
true,
`select count(*) c from :table: where cluster = :cluster and node ${condition} :node and id != :id`,
{
cluster,
node,
id,
}
);
}
log.dump(c);
const code = "CALLBACK-NOTUNIQUE";
if (c > 0) {
context
.buildViolation("Not unique")
.atPath(path)
.setParameter("{{ callback }}", "not equal")
.setCode(code)
.setInvalidValue(`cluster: '${cluster}' and node: '${node}'`)
.addViolation();
if (extra && extra.stop) {
return reject("reject " + code);
}
}
resolve("resolve " + code);
})
),
]),
domain: new Required([new NotBlank(), new Length({ max: 50 })]),
port: new Required([new NotBlank(), new Length({ max: 8 }), new Regex(/^\d+$/)]),
};
if (typeof entity.node !== "undefined") {
if (entity.node === null) {
validators.node = new Optional();
} else {
validators.node = new Required([new NotBlank(), new Length({ max: 50 })]);
}
}
return new Collection(validators);
},
};
module.exports = (knex) => extend(knex, prototype, Object.assign({}, require("./abstract"), ext), "clusters", "id");
const knex = require('@stopsopa/knex-abstract');
const log = require('inspc');
const validator = require('@stopsopa/validator');
...
app.all('/register', async (req, res) => {
let entity = req.body;
let id = entity.id;
const mode = id ? 'edit' : 'create';
const man = knex().model.clusters;
const validators = man.getValidators(mode, id);
if (mode === 'create') {
entity = {
...man.initial(),
...entity,
};
}
const entityPrepared = man.prepareToValidate(entity, mode);
const errors = await validator(entityPrepared, validators);
if ( ! errors.count() ) {
try {
if (mode === 'edit') {
await man.update(entityPrepared, id);
}
else {
id = await man.insert(entityPrepared);
}
entity = await man.find(id);
if ( ! entity ) {
return res.jsonError("Database state conflict: updated/created entity doesn't exist");
}
}
catch (e) {
log.dump(e);
return res.jsonError(`Can't register: ` + JSON.stringify(req.body));
}
}
return res.jsonNoCache({
entity: entity,
errors: errors.getTree(),
});
});
...
For further examples please follow test cases
Source code Blank.js
new Blank({
message: "This value should be blank.",
});
Source code Callback.js
See test example Callback.test.js
new Callback((value, context, path, extra) => {...}); // function required
Source code Choice.js
new Choice({
choices: ["..."], // required
multiple: false,
min: 0, // only if multiple=true
max: 0, // only if multiple=true
message: "The value you selected is not a valid choice.",
multipleMessage: "One or more of the given values is invalid.",
minMessage: "You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.",
maxMessage: "You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.",
});
// or shorter syntax if ony choices are given:
new Choice(["..."]); // just choices
Source code Collection.js
new Collection({
fields: {
// required type: non empty object
a: new Require(),
b: new Optional(),
},
allowExtraFields: false,
allowMissingFields: false,
extraFieldsMessage: "This field was not expected.",
missingFieldsMessage: "This field is missing.",
});
// or shorter syntax if only fields are given:
new Collection({
// required type: non empty object
a: new Require(),
b: new Optional(),
});
Source code Count.js
new Count({
// min; // min or max required (or both) - if min given then have to be > 0
// max, // min or max required (or both) - if max given then have to be > 0
minMessage:
"This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.",
maxMessage:
"This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.",
exactMessage:
"This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.",
});
// or shorter syntax if ony min and max given and min = max:
new Count(5);
Source code Email.js
new Email({
message: "This value is not a valid email address.",
});
Source code IsFalse.js
new IsFalse({
message: "This value should be false.",
});
Source code IsTrue.js
new IsTrue({
message: "This value should be true.",
});
Source code IsNull.js
new IsNull({
message: "This value should be null.",
});
Source code Length.js
new Length({
// min; // min or max required (or both)
// max, // min or max required (or both)
maxMessage:
"This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.",
minMessage:
"This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.",
exactMessage:
"This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.",
});
Source code NotBlank.js
new NotBlank({
message: "This value should not be blank.",
});
Source code NotNull.js
new NotNull({
message: "This value should not be blank.",
});
Source code Regex.js
new Regex({
pattern: /abc/gi, // required, type regex
message: "This value is not valid.",
match: true, // true - if value match regex then validation passed
// false - if value NOT match regex then validation passed
});
Source code Type.js
// available values for field 'type' are:
// 'undefined', 'object', 'boolean', 'bool', 'number', 'str', 'string',
// 'symbol', 'function', 'integer', 'int', 'array'
new Type({
type: "...", // required
message: `This value should be of type '{{ type }}'.`,
});
// or shorter syntax if ony type is given:
new Type("str");
require('@stopsopa/validator/set')
require('@stopsopa/validator/get')
require('@stopsopa/validator/delay')
require('@stopsopa/validator/each')
require('@stopsopa/validator/size')
- or validator
- condition validator
- respecting order of validators - executing in the same order as declared
Always use types for primitives and collections:
example cases:
- Length validator fires only if given data type is string (use Type('str') to avoid issues)
- Collection validator validates only if given data is object (use Type('object') to avoid issues)
(async function () {
const errors = await validator(6, new Collection({
// collection fires only if given data is object
// here it is integer
a: new Type('str'),
b: new Length({
min: 1,
max: 2,
})
])
}));
const raw = errors.getRaw();
expect(raw).toEqual([]);
//
done();
})();
fixed:
(async function () {
const errors = await validator(
undefined, // will generate error: "This value should be of type 'object'."
// {a: '', b: 7}, // will generate error: "This value should be of type 'str'." on field "b"
new Required([
new Type("object"), // this solves the problem on that level
new Collection({
a: new Type("str"),
b: new Required([
new Type("str"), // this solves the problem on that level
new Length({
min: 1,
max: 2,
}),
]),
}),
])
);
const raw = errors.getRaw();
expect(raw).toEqual([[undefined, "This value should be of type 'object'.", "INVALID_TYPE_ERROR", undefined]]);
done();
})();
with above the question might come "why Collection itself don't validate if field is an object?". The thing is that Collection can be used to check object but also array element by element, so it is better to deal with checking the type and validation of that structure separately.
Don't relay on new Optional on the root level, more about: option_require_case.test.js