├── .yarnrc.yml ├── .np-config.json ├── .prettierrc ├── scripts ├── test.sh └── runTest.ts ├── .travis.yml ├── .editorconfig ├── src ├── rule │ ├── index.ts │ ├── required.ts │ ├── enum.ts │ ├── whitespace.ts │ ├── pattern.ts │ ├── range.ts │ ├── url.ts │ └── type.ts ├── validator │ ├── required.ts │ ├── any.ts │ ├── method.ts │ ├── object.ts │ ├── boolean.ts │ ├── regexp.ts │ ├── pattern.ts │ ├── type.ts │ ├── float.ts │ ├── integer.ts │ ├── array.ts │ ├── enum.ts │ ├── index.ts │ ├── number.ts │ ├── string.ts │ └── date.ts ├── messages.ts ├── interface.ts ├── util.ts └── index.ts ├── .babelrc.js ├── __tests__ ├── enum.spec.ts ├── object.spec.ts ├── unicode.spec.ts ├── any.spec.ts ├── date.spec.ts ├── number.spec.ts ├── string.spec.ts ├── pattern.spec.ts ├── messages.spec.ts ├── url.spec.ts ├── required.spec.ts ├── array.spec.ts ├── promise.spec.ts ├── deep.spec.ts └── validator.spec.ts ├── tsconfig.json ├── .gitignore ├── tests └── url.ts ├── LICENSE.md ├── HISTORY.md ├── package.json └── README.md /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules -------------------------------------------------------------------------------- /.np-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "yarn": false 3 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | NODE_ENV=test npx babel-node -x ".ts,.js,.jsx" scripts/runTest.ts -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: 5 | - yiminghe@gmail.com 6 | 7 | node_js: 8 | - 16.13.1 9 | 10 | before_install: 11 | - corepack enable 12 | 13 | script: 14 | - yarn run coverage 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /src/rule/index.ts: -------------------------------------------------------------------------------- 1 | import required from './required'; 2 | import whitespace from './whitespace'; 3 | import type from './type'; 4 | import range from './range'; 5 | import enumRule from './enum'; 6 | import pattern from './pattern'; 7 | 8 | export default { 9 | required, 10 | whitespace, 11 | type, 12 | range, 13 | enum: enumRule, 14 | pattern, 15 | }; 16 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | console.log('Load babel config'); 2 | 3 | module.exports = api => { 4 | return { 5 | presets: [ 6 | [ 7 | '@babel/preset-env', 8 | api.env('test') 9 | ? { targets: { node: true } } 10 | : { 11 | loose: true, 12 | modules: false, 13 | }, 14 | ], 15 | '@babel/preset-typescript', 16 | ], 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/validator/required.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | 4 | const required: ExecuteValidator = (rule, value, callback, source, options) => { 5 | const errors: string[] = []; 6 | const type = Array.isArray(value) ? 'array' : typeof value; 7 | rules.required(rule, value, source, errors, options, type); 8 | callback(errors); 9 | }; 10 | 11 | export default required; 12 | -------------------------------------------------------------------------------- /src/rule/required.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteRule } from '../interface'; 2 | import { format, isEmptyValue } from '../util'; 3 | 4 | const required: ExecuteRule = (rule, value, source, errors, options, type) => { 5 | if ( 6 | rule.required && 7 | (!source.hasOwnProperty(rule.field) || 8 | isEmptyValue(value, type || rule.type)) 9 | ) { 10 | errors.push(format(options.messages.required, rule.fullField)); 11 | } 12 | }; 13 | 14 | export default required; 15 | -------------------------------------------------------------------------------- /src/rule/enum.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteRule } from '../interface'; 2 | import { format } from '../util'; 3 | 4 | const ENUM = 'enum' as const; 5 | 6 | const enumerable: ExecuteRule = (rule, value, source, errors, options) => { 7 | rule[ENUM] = Array.isArray(rule[ENUM]) ? rule[ENUM] : []; 8 | if (rule[ENUM].indexOf(value) === -1) { 9 | errors.push( 10 | format(options.messages[ENUM], rule.fullField, rule[ENUM].join(', ')), 11 | ); 12 | } 13 | }; 14 | 15 | export default enumerable; 16 | -------------------------------------------------------------------------------- /__tests__/enum.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('enum', () => { 4 | it('run validation on `false`', done => { 5 | new Schema({ 6 | v: { 7 | type: 'enum', 8 | enum: [true], 9 | }, 10 | }).validate( 11 | { 12 | v: false, 13 | }, 14 | errors => { 15 | expect(errors.length).toBe(1); 16 | expect(errors[0].message).toBe('v must be one of true'); 17 | done(); 18 | }, 19 | ); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/object.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('object', () => { 4 | it('works for the required object with fields in case of empty string', done => { 5 | new Schema({ 6 | v: { 7 | type: 'object', 8 | required: true, 9 | fields: {}, 10 | }, 11 | }).validate( 12 | { 13 | v: '', 14 | }, 15 | errors => { 16 | expect(errors.length).toBe(1); 17 | expect(errors[0].message).toBe('v is not an object'); 18 | done(); 19 | }, 20 | ); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/validator/any.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const any: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const errors: string[] = []; 7 | const validate = 8 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 9 | if (validate) { 10 | if (isEmptyValue(value) && !rule.required) { 11 | return callback(); 12 | } 13 | rules.required(rule, value, source, errors, options); 14 | } 15 | callback(errors); 16 | }; 17 | 18 | export default any; 19 | -------------------------------------------------------------------------------- /scripts/runTest.ts: -------------------------------------------------------------------------------- 1 | import Schema, { 2 | ValidateCallback, 3 | ValidateFieldsError, 4 | Values, 5 | } from '../src/index'; 6 | 7 | const callback: ValidateCallback = (errors, fields) => { 8 | if (errors === null) { 9 | const f: Values = fields; 10 | console.log('transformed values:', JSON.stringify(f)); 11 | } else { 12 | const f: ValidateFieldsError = fields; 13 | console.log('validate error:', JSON.stringify(f)); 14 | } 15 | }; 16 | 17 | new Schema({ 18 | v: { 19 | required: true, 20 | type: 'array', 21 | defaultField: [{ type: 'number', max: 0, transform: i => Number(i) }], 22 | }, 23 | }).validate( 24 | { 25 | v: ['1', '2'], 26 | }, 27 | callback, 28 | ); 29 | -------------------------------------------------------------------------------- /src/validator/method.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const method: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const errors: string[] = []; 7 | const validate = 8 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 9 | if (validate) { 10 | if (isEmptyValue(value) && !rule.required) { 11 | return callback(); 12 | } 13 | rules.required(rule, value, source, errors, options); 14 | if (value !== undefined) { 15 | rules.type(rule, value, source, errors, options); 16 | } 17 | } 18 | callback(errors); 19 | }; 20 | 21 | export default method; 22 | -------------------------------------------------------------------------------- /src/validator/object.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const object: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const errors: string[] = []; 7 | const validate = 8 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 9 | if (validate) { 10 | if (isEmptyValue(value) && !rule.required) { 11 | return callback(); 12 | } 13 | rules.required(rule, value, source, errors, options); 14 | if (value !== undefined) { 15 | rules.type(rule, value, source, errors, options); 16 | } 17 | } 18 | callback(errors); 19 | }; 20 | 21 | export default object; 22 | -------------------------------------------------------------------------------- /src/validator/boolean.ts: -------------------------------------------------------------------------------- 1 | import { isEmptyValue } from '../util'; 2 | import rules from '../rule'; 3 | import { ExecuteValidator } from '../interface'; 4 | 5 | const boolean: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const errors: string[] = []; 7 | const validate = 8 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 9 | if (validate) { 10 | if (isEmptyValue(value) && !rule.required) { 11 | return callback(); 12 | } 13 | rules.required(rule, value, source, errors, options); 14 | if (value !== undefined) { 15 | rules.type(rule, value, source, errors, options); 16 | } 17 | } 18 | callback(errors); 19 | }; 20 | 21 | export default boolean; 22 | -------------------------------------------------------------------------------- /src/validator/regexp.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const regexp: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const errors: string[] = []; 7 | const validate = 8 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 9 | if (validate) { 10 | if (isEmptyValue(value) && !rule.required) { 11 | return callback(); 12 | } 13 | rules.required(rule, value, source, errors, options); 14 | if (!isEmptyValue(value)) { 15 | rules.type(rule, value, source, errors, options); 16 | } 17 | } 18 | callback(errors); 19 | }; 20 | 21 | export default regexp; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "module": "commonjs", 5 | "target": "ES2020", 6 | "lib": [ 7 | "es2016", 8 | "dom", 9 | "es5" 10 | ], 11 | "jsx": "preserve", 12 | "resolveJsonModule": true, 13 | "experimentalDecorators": true, 14 | "isolatedModules": true, 15 | "skipLibCheck": true, 16 | "declaration": true, 17 | "strictFunctionTypes": true, 18 | "allowSyntheticDefaultImports": true, 19 | "moduleResolution": "node", 20 | "plugins": [ 21 | { 22 | "transform": "@zerollup/ts-transform-paths" 23 | } 24 | ] 25 | }, 26 | "include": [ 27 | "src/" 28 | ], 29 | "exclude": [ 30 | "node_modules/" 31 | ] 32 | } -------------------------------------------------------------------------------- /src/validator/pattern.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const pattern: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const errors: string[] = []; 7 | const validate = 8 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 9 | if (validate) { 10 | if (isEmptyValue(value, 'string') && !rule.required) { 11 | return callback(); 12 | } 13 | rules.required(rule, value, source, errors, options); 14 | if (!isEmptyValue(value, 'string')) { 15 | rules.pattern(rule, value, source, errors, options); 16 | } 17 | } 18 | callback(errors); 19 | }; 20 | 21 | export default pattern; 22 | -------------------------------------------------------------------------------- /src/validator/type.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const type: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const ruleType = rule.type; 7 | const errors: string[] = []; 8 | const validate = 9 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 10 | if (validate) { 11 | if (isEmptyValue(value, ruleType) && !rule.required) { 12 | return callback(); 13 | } 14 | rules.required(rule, value, source, errors, options, ruleType); 15 | if (!isEmptyValue(value, ruleType)) { 16 | rules.type(rule, value, source, errors, options); 17 | } 18 | } 19 | callback(errors); 20 | }; 21 | 22 | export default type; 23 | -------------------------------------------------------------------------------- /src/rule/whitespace.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteRule } from '../interface'; 2 | import { format } from '../util'; 3 | 4 | /** 5 | * Rule for validating whitespace. 6 | * 7 | * @param rule The validation rule. 8 | * @param value The value of the field on the source object. 9 | * @param source The source object being validated. 10 | * @param errors An array of errors that this rule may add 11 | * validation errors to. 12 | * @param options The validation options. 13 | * @param options.messages The validation messages. 14 | */ 15 | const whitespace: ExecuteRule = (rule, value, source, errors, options) => { 16 | if (/^\s+$/.test(value) || value === '') { 17 | errors.push(format(options.messages.whitespace, rule.fullField)); 18 | } 19 | }; 20 | 21 | export default whitespace; 22 | -------------------------------------------------------------------------------- /src/validator/float.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const floatFn: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const errors: string[] = []; 7 | const validate = 8 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 9 | if (validate) { 10 | if (isEmptyValue(value) && !rule.required) { 11 | return callback(); 12 | } 13 | rules.required(rule, value, source, errors, options); 14 | if (value !== undefined) { 15 | rules.type(rule, value, source, errors, options); 16 | rules.range(rule, value, source, errors, options); 17 | } 18 | } 19 | callback(errors); 20 | }; 21 | 22 | export default floatFn; 23 | -------------------------------------------------------------------------------- /src/validator/integer.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const integer: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const errors: string[] = []; 7 | const validate = 8 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 9 | if (validate) { 10 | if (isEmptyValue(value) && !rule.required) { 11 | return callback(); 12 | } 13 | rules.required(rule, value, source, errors, options); 14 | if (value !== undefined) { 15 | rules.type(rule, value, source, errors, options); 16 | rules.range(rule, value, source, errors, options); 17 | } 18 | } 19 | callback(errors); 20 | }; 21 | 22 | export default integer; 23 | -------------------------------------------------------------------------------- /src/validator/array.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule/index'; 3 | 4 | const array: ExecuteValidator = (rule, value, callback, source, options) => { 5 | const errors: string[] = []; 6 | const validate = 7 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 8 | if (validate) { 9 | if ((value === undefined || value === null) && !rule.required) { 10 | return callback(); 11 | } 12 | rules.required(rule, value, source, errors, options, 'array'); 13 | if (value !== undefined && value !== null) { 14 | rules.type(rule, value, source, errors, options); 15 | rules.range(rule, value, source, errors, options); 16 | } 17 | } 18 | callback(errors); 19 | }; 20 | 21 | export default array; 22 | -------------------------------------------------------------------------------- /src/validator/enum.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const ENUM = 'enum' as const; 6 | 7 | const enumerable: ExecuteValidator = ( 8 | rule, 9 | value, 10 | callback, 11 | source, 12 | options, 13 | ) => { 14 | const errors: string[] = []; 15 | const validate = 16 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 17 | if (validate) { 18 | if (isEmptyValue(value) && !rule.required) { 19 | return callback(); 20 | } 21 | rules.required(rule, value, source, errors, options); 22 | if (value !== undefined) { 23 | rules[ENUM](rule, value, source, errors, options); 24 | } 25 | } 26 | callback(errors); 27 | }; 28 | 29 | export default enumerable; 30 | -------------------------------------------------------------------------------- /src/validator/index.ts: -------------------------------------------------------------------------------- 1 | import string from './string'; 2 | import method from './method'; 3 | import number from './number'; 4 | import boolean from './boolean'; 5 | import regexp from './regexp'; 6 | import integer from './integer'; 7 | import float from './float'; 8 | import array from './array'; 9 | import object from './object'; 10 | import enumValidator from './enum'; 11 | import pattern from './pattern'; 12 | import date from './date'; 13 | import required from './required'; 14 | import type from './type'; 15 | import any from './any'; 16 | 17 | export default { 18 | string, 19 | method, 20 | number, 21 | boolean, 22 | regexp, 23 | integer, 24 | float, 25 | array, 26 | object, 27 | enum: enumValidator, 28 | pattern, 29 | date, 30 | url: type, 31 | hex: type, 32 | email: type, 33 | required, 34 | any, 35 | }; 36 | -------------------------------------------------------------------------------- /src/validator/number.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const number: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const errors: string[] = []; 7 | const validate = 8 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 9 | if (validate) { 10 | if (value === '') { 11 | value = undefined; 12 | } 13 | if (isEmptyValue(value) && !rule.required) { 14 | return callback(); 15 | } 16 | rules.required(rule, value, source, errors, options); 17 | if (value !== undefined) { 18 | rules.type(rule, value, source, errors, options); 19 | rules.range(rule, value, source, errors, options); 20 | } 21 | } 22 | callback(errors); 23 | }; 24 | 25 | export default number; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.yarn/* 6 | !/.yarn/patches 7 | !/.yarn/plugins 8 | !/.yarn/releases 9 | !/.yarn/sdks 10 | !/.yarn/versions 11 | 12 | # Swap the comments on the following lines if you don't wish to use zero-installs 13 | # Documentation here: https://yarnpkg.com/features/zero-installs 14 | #!/.yarn/cache 15 | /.pnp.* 16 | 17 | 18 | # testing 19 | /coverage 20 | 21 | # production 22 | build 23 | pkg 24 | dist 25 | 26 | # misc 27 | .DS_Store 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | 37 | *.log 38 | *.aes 39 | cypress/screenshots 40 | cypress/videos 41 | cypress/logs 42 | cypress/fixtures/profile.json 43 | cypress/fixtures/users.json 44 | .history -------------------------------------------------------------------------------- /tests/url.ts: -------------------------------------------------------------------------------- 1 | import AsyncValidator from '../src'; 2 | 3 | const validator = new AsyncValidator({ 4 | v: { 5 | type: 'url', 6 | }, 7 | }); 8 | 9 | for (var i = 1; i <= 1000; i++) { 10 | var time = Date.now(); 11 | var attack_str = '//a.b' + 'c1'.repeat(i) + 'a'; 12 | validator.validate({ 13 | v: attack_str, 14 | }); 15 | var time_cost = Date.now() - time; 16 | console.log( 17 | 'attack_str.length: ' + attack_str.length + ': ' + time_cost + ' ms', 18 | ); 19 | } 20 | 21 | if (false) { 22 | console.log('*'.repeat(10)); 23 | 24 | for (var i = 1; i <= 50000; i++) { 25 | var time = Date.now(); 26 | var attack_str = '//' + ':'.repeat(i * 10000) + '@'; 27 | validator.validate({ 28 | v: attack_str, 29 | }); 30 | var time_cost = Date.now() - time; 31 | console.log( 32 | 'attack_str.length: ' + attack_str.length + ': ' + time_cost + ' ms', 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/validator/string.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const string: ExecuteValidator = (rule, value, callback, source, options) => { 6 | const errors: string[] = []; 7 | const validate = 8 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 9 | if (validate) { 10 | if (isEmptyValue(value, 'string') && !rule.required) { 11 | return callback(); 12 | } 13 | rules.required(rule, value, source, errors, options, 'string'); 14 | if (!isEmptyValue(value, 'string')) { 15 | rules.type(rule, value, source, errors, options); 16 | rules.range(rule, value, source, errors, options); 17 | rules.pattern(rule, value, source, errors, options); 18 | if (rule.whitespace === true) { 19 | rules.whitespace(rule, value, source, errors, options); 20 | } 21 | } 22 | } 23 | callback(errors); 24 | }; 25 | 26 | export default string; 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present yiminghe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/validator/date.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteValidator } from '../interface'; 2 | import rules from '../rule'; 3 | import { isEmptyValue } from '../util'; 4 | 5 | const date: ExecuteValidator = (rule, value, callback, source, options) => { 6 | // console.log('integer rule called %j', rule); 7 | const errors: string[] = []; 8 | const validate = 9 | rule.required || (!rule.required && source.hasOwnProperty(rule.field)); 10 | // console.log('validate on %s value', value); 11 | if (validate) { 12 | if (isEmptyValue(value, 'date') && !rule.required) { 13 | return callback(); 14 | } 15 | rules.required(rule, value, source, errors, options); 16 | if (!isEmptyValue(value, 'date')) { 17 | let dateObject; 18 | 19 | if (value instanceof Date) { 20 | dateObject = value; 21 | } else { 22 | dateObject = new Date(value); 23 | } 24 | 25 | rules.type(rule, dateObject, source, errors, options); 26 | if (dateObject) { 27 | rules.range(rule, dateObject.getTime(), source, errors, options); 28 | } 29 | } 30 | } 31 | callback(errors); 32 | }; 33 | 34 | export default date; 35 | -------------------------------------------------------------------------------- /__tests__/unicode.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('unicode', () => { 4 | it('works for unicode U+0000 to U+FFFF ', done => { 5 | new Schema({ 6 | v: { 7 | type: 'string', 8 | len: 4, 9 | }, 10 | }).validate( 11 | { 12 | v: '吉吉吉吉', 13 | }, 14 | errors => { 15 | expect(errors).toBe(null); 16 | done(); 17 | }, 18 | ); 19 | }); 20 | 21 | it('works for unicode gt U+FFFF ', done => { 22 | new Schema({ 23 | v: { 24 | type: 'string', 25 | len: 4, // 原来length属性应该为8,更正之后应该为4 26 | }, 27 | }).validate( 28 | { 29 | v: '𠮷𠮷𠮷𠮷', 30 | }, 31 | errors => { 32 | expect(errors).toBe(null); 33 | done(); 34 | }, 35 | ); 36 | }); 37 | 38 | it('Rich Text Format', done => { 39 | new Schema({ 40 | v: { 41 | type: 'string', 42 | len: 2, 43 | }, 44 | }).validate( 45 | { 46 | v: '💩💩', 47 | }, 48 | errors => { 49 | expect(errors).toBe(null); 50 | done(); 51 | }, 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/rule/pattern.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteRule } from '../interface'; 2 | import { format } from '../util'; 3 | 4 | const pattern: ExecuteRule = (rule, value, source, errors, options) => { 5 | if (rule.pattern) { 6 | if (rule.pattern instanceof RegExp) { 7 | // if a RegExp instance is passed, reset `lastIndex` in case its `global` 8 | // flag is accidentally set to `true`, which in a validation scenario 9 | // is not necessary and the result might be misleading 10 | rule.pattern.lastIndex = 0; 11 | if (!rule.pattern.test(value)) { 12 | errors.push( 13 | format( 14 | options.messages.pattern.mismatch, 15 | rule.fullField, 16 | value, 17 | rule.pattern, 18 | ), 19 | ); 20 | } 21 | } else if (typeof rule.pattern === 'string') { 22 | const _pattern = new RegExp(rule.pattern); 23 | if (!_pattern.test(value)) { 24 | errors.push( 25 | format( 26 | options.messages.pattern.mismatch, 27 | rule.fullField, 28 | value, 29 | rule.pattern, 30 | ), 31 | ); 32 | } 33 | } 34 | } 35 | }; 36 | 37 | export default pattern; 38 | -------------------------------------------------------------------------------- /__tests__/any.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | const testNoErrorsFor = value => done => { 4 | new Schema({ 5 | v: { 6 | type: 'any', 7 | }, 8 | }).validate( 9 | { 10 | v: value, 11 | }, 12 | errors => { 13 | expect(errors).toBe(null); 14 | done(); 15 | }, 16 | ); 17 | }; 18 | 19 | const testRequiredErrorFor = value => done => { 20 | new Schema({ 21 | v: { 22 | required: true, 23 | type: 'string', 24 | }, 25 | }).validate( 26 | { 27 | v: value, 28 | }, 29 | errors => { 30 | expect(errors.length).toBe(1); 31 | expect(errors[0].message).toBe('v is required'); 32 | done(); 33 | }, 34 | ); 35 | }; 36 | 37 | describe('any', () => { 38 | it('allows null', testNoErrorsFor(null)); 39 | it('allows undefined', testNoErrorsFor(undefined)); 40 | it('allows strings', testNoErrorsFor('foo')); 41 | it('allows numbers', testNoErrorsFor(1)); 42 | it('allows booleans', testNoErrorsFor(false)); 43 | it('allows arrays', testNoErrorsFor([])); 44 | it('allows objects', testNoErrorsFor({})); 45 | it('rejects undefined when required', testRequiredErrorFor(undefined)); 46 | it('rejects null when required', testRequiredErrorFor(null)); 47 | }); 48 | -------------------------------------------------------------------------------- /__tests__/date.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('date', () => { 4 | it('required works for undefined', done => { 5 | new Schema({ 6 | v: { 7 | type: 'date', 8 | required: true, 9 | }, 10 | }).validate( 11 | { 12 | v: undefined, 13 | }, 14 | errors => { 15 | expect(errors.length).toBe(1); 16 | expect(errors[0].message).toBe('v is required'); 17 | done(); 18 | }, 19 | ); 20 | }); 21 | 22 | it('required works for ""', done => { 23 | new Schema({ 24 | v: { 25 | type: 'date', 26 | required: true, 27 | }, 28 | }).validate( 29 | { 30 | v: '', 31 | }, 32 | errors => { 33 | expect(errors.length).toBe(1); 34 | expect(errors[0].message).toBe('v is required'); 35 | done(); 36 | }, 37 | ); 38 | }); 39 | 40 | it('required works for non-date type', done => { 41 | new Schema({ 42 | v: { 43 | type: 'date', 44 | required: true, 45 | }, 46 | }).validate( 47 | { 48 | v: {}, 49 | }, 50 | errors => { 51 | expect(errors.length).toBe(1); 52 | expect(errors[0].message).toBe('v is not a date'); 53 | done(); 54 | }, 55 | ); 56 | }); 57 | 58 | it('required works for "timestamp"', done => { 59 | new Schema({ 60 | v: { 61 | type: 'date', 62 | required: true, 63 | }, 64 | }).validate( 65 | { 66 | v: 1530374400000, 67 | }, 68 | errors => { 69 | expect(errors).toBe(null); 70 | done(); 71 | }, 72 | ); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/rule/range.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteRule } from '../interface'; 2 | import { format } from '../util'; 3 | 4 | const range: ExecuteRule = (rule, value, source, errors, options) => { 5 | const len = typeof rule.len === 'number'; 6 | const min = typeof rule.min === 'number'; 7 | const max = typeof rule.max === 'number'; 8 | // 正则匹配码点范围从U+010000一直到U+10FFFF的文字(补充平面Supplementary Plane) 9 | const spRegexp = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; 10 | let val = value; 11 | let key = null; 12 | const num = typeof value === 'number'; 13 | const str = typeof value === 'string'; 14 | const arr = Array.isArray(value); 15 | if (num) { 16 | key = 'number'; 17 | } else if (str) { 18 | key = 'string'; 19 | } else if (arr) { 20 | key = 'array'; 21 | } 22 | // if the value is not of a supported type for range validation 23 | // the validation rule rule should use the 24 | // type property to also test for a particular type 25 | if (!key) { 26 | return false; 27 | } 28 | if (arr) { 29 | val = value.length; 30 | } 31 | if (str) { 32 | // 处理码点大于U+010000的文字length属性不准确的bug,如"𠮷𠮷𠮷".length !== 3 33 | val = value.replace(spRegexp, '_').length; 34 | } 35 | if (len) { 36 | if (val !== rule.len) { 37 | errors.push(format(options.messages[key].len, rule.fullField, rule.len)); 38 | } 39 | } else if (min && !max && val < rule.min) { 40 | errors.push(format(options.messages[key].min, rule.fullField, rule.min)); 41 | } else if (max && !min && val > rule.max) { 42 | errors.push(format(options.messages[key].max, rule.fullField, rule.max)); 43 | } else if (min && max && (val < rule.min || val > rule.max)) { 44 | errors.push( 45 | format(options.messages[key].range, rule.fullField, rule.min, rule.max), 46 | ); 47 | } 48 | }; 49 | 50 | export default range; 51 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | ---- 3 | 4 | ## 4.0.0 / 2021-08-11 5 | 6 | - full ts support 7 | - support return transformed value when pass validation(promise and callback): https://github.com/yiminghe/async-validator/pull/277 8 | 9 | ## 3.5.0 / 2020-11-12 10 | 11 | - https://github.com/yiminghe/async-validator/pull/256/files 12 | 13 | ## 3.4.0 / 2020-08-05 14 | 15 | - https://github.com/yiminghe/async-validator/pull/247 16 | - https://github.com/yiminghe/async-validator/pull/246 17 | - https://github.com/yiminghe/async-validator/pull/245 18 | - https://github.com/yiminghe/async-validator/pull/240 19 | 20 | ## 3.3.0 / 2020-05-07 21 | 22 | - expose validators: https://github.com/yiminghe/async-validator/pull/232 23 | 24 | ## 3.2.0 / 2019-10-16 25 | 26 | - support `any` type: https://github.com/yiminghe/async-validator/pull/190 27 | 28 | ## 3.1.0 / 2019-09-09 29 | 30 | - add d.ts 31 | 32 | ## 3.0.0 / 2019-08-07 33 | 34 | - Enum validates `false` value: https://github.com/yiminghe/async-validator/pull/164 35 | 36 | ## 2.0.0 / 2019-07-26 37 | 38 | - use @pika/pack 39 | 40 | ## 1.11.3 / 2019-06-28 41 | 42 | - support suppressWarning option when validate 43 | 44 | ## 1.11.1 / 2019-04-22 45 | 46 | - support message as function 47 | 48 | ## 1.11.0 / 2019-03-22 49 | 50 | - support promise usage(asyncValidator) 51 | 52 | ## 1.10.1 / 2018-12-18 53 | 54 | - support override warning 55 | 56 | ## 1.10.0 / 2018-10-17 57 | 58 | - revert promise 59 | 60 | ## 1.9.0 / 2018-10-10 61 | 62 | - .validate returns promise 63 | 64 | ## 1.8.0 / 2017-08-16 65 | 66 | - validator support return promise. 67 | 68 | ## 1.7.0 / 2017-06/09 69 | 70 | - add es 71 | - support string patter 72 | 73 | ## 1.6.0 / 2016-03-30 74 | 75 | - support defaultField 76 | 77 | ## 1.5.0 / 2016-02-02 78 | 79 | - support deep merge with default messages 80 | - support rule message of any type(exp: jsx) 81 | 82 | ## 1.4.0 / 2015-01-12 83 | 84 | - fix first option. 85 | - add firstFields option. 86 | - see tests/validator.spec.js 87 | -------------------------------------------------------------------------------- /src/messages.ts: -------------------------------------------------------------------------------- 1 | import { InternalValidateMessages } from './interface'; 2 | 3 | export function newMessages(): InternalValidateMessages { 4 | return { 5 | default: 'Validation error on field %s', 6 | required: '%s is required', 7 | enum: '%s must be one of %s', 8 | whitespace: '%s cannot be empty', 9 | date: { 10 | format: '%s date %s is invalid for format %s', 11 | parse: '%s date could not be parsed, %s is invalid ', 12 | invalid: '%s date %s is invalid', 13 | }, 14 | types: { 15 | string: '%s is not a %s', 16 | method: '%s is not a %s (function)', 17 | array: '%s is not an %s', 18 | object: '%s is not an %s', 19 | number: '%s is not a %s', 20 | date: '%s is not a %s', 21 | boolean: '%s is not a %s', 22 | integer: '%s is not an %s', 23 | float: '%s is not a %s', 24 | regexp: '%s is not a valid %s', 25 | email: '%s is not a valid %s', 26 | url: '%s is not a valid %s', 27 | hex: '%s is not a valid %s', 28 | }, 29 | string: { 30 | len: '%s must be exactly %s characters', 31 | min: '%s must be at least %s characters', 32 | max: '%s cannot be longer than %s characters', 33 | range: '%s must be between %s and %s characters', 34 | }, 35 | number: { 36 | len: '%s must equal %s', 37 | min: '%s cannot be less than %s', 38 | max: '%s cannot be greater than %s', 39 | range: '%s must be between %s and %s', 40 | }, 41 | array: { 42 | len: '%s must be exactly %s in length', 43 | min: '%s cannot be less than %s in length', 44 | max: '%s cannot be greater than %s in length', 45 | range: '%s must be between %s and %s in length', 46 | }, 47 | pattern: { 48 | mismatch: '%s value %s does not match pattern %s', 49 | }, 50 | clone() { 51 | const cloned = JSON.parse(JSON.stringify(this)); 52 | cloned.clone = this.clone; 53 | return cloned; 54 | }, 55 | }; 56 | } 57 | 58 | export const messages = newMessages(); 59 | -------------------------------------------------------------------------------- /__tests__/number.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('number', () => { 4 | it('works', done => { 5 | new Schema({ 6 | v: { 7 | type: 'number', 8 | }, 9 | }).validate( 10 | { 11 | v: '1', 12 | }, 13 | errors => { 14 | expect(errors.length).toBe(1); 15 | expect(errors[0].message).toBe('v is not a number'); 16 | done(); 17 | }, 18 | ); 19 | }); 20 | 21 | it('works for no-required', done => { 22 | new Schema({ 23 | v: { 24 | type: 'number', 25 | }, 26 | }).validate( 27 | { 28 | v: undefined, 29 | }, 30 | errors => { 31 | expect(errors).toBeFalsy(); 32 | done(); 33 | }, 34 | ); 35 | }); 36 | 37 | it('works for no-required in case of empty string', done => { 38 | new Schema({ 39 | v: { 40 | type: 'number', 41 | required: false, 42 | }, 43 | }).validate( 44 | { 45 | v: '', 46 | }, 47 | errors => { 48 | expect(errors).toBeFalsy(); 49 | done(); 50 | }, 51 | ); 52 | }); 53 | 54 | it('works for required', done => { 55 | new Schema({ 56 | v: { 57 | type: 'number', 58 | required: true, 59 | }, 60 | }).validate( 61 | { 62 | v: undefined, 63 | }, 64 | errors => { 65 | expect(errors.length).toBe(1); 66 | expect(errors[0].message).toBe('v is required'); 67 | done(); 68 | }, 69 | ); 70 | }); 71 | 72 | it('transform does not change value', done => { 73 | const value = { 74 | v: '1', 75 | }; 76 | new Schema({ 77 | v: { 78 | type: 'number', 79 | transform: Number, 80 | }, 81 | }).validate(value, (errors, data) => { 82 | expect(data).toEqual({ 83 | v: 1, 84 | }); 85 | expect(value.v).toBe('1'); 86 | expect(errors).toBeFalsy(); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('return transformed value in promise.then', done => { 92 | const value = { 93 | v: '1', 94 | }; 95 | new Schema({ 96 | v: { 97 | type: 'number', 98 | transform: Number, 99 | }, 100 | }) 101 | .validate(value, errors => { 102 | expect(value.v).toBe('1'); 103 | expect(errors).toBeFalsy(); 104 | }) 105 | .then(source => { 106 | expect(source).toEqual({ 107 | v: 1, 108 | }); 109 | done(); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-validator", 3 | "version": "4.2.5", 4 | "description": "validate form asynchronous", 5 | "typings": "typings/index.d.ts", 6 | "keywords": [ 7 | "validator", 8 | "validate", 9 | "async" 10 | ], 11 | "homepage": "https://github.com/yiminghe/async-validator", 12 | "author": "yiminghe@gmail.com", 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:yiminghe/async-validator.git" 16 | }, 17 | "@pika/pack": { 18 | "pipeline": [ 19 | [ 20 | "pika-plugin-ts-types", 21 | { 22 | "args": [ 23 | "--rootDir", 24 | "src" 25 | ] 26 | } 27 | ], 28 | [ 29 | "pika-plugin-build-web-babel", 30 | { 31 | "format": "cjs" 32 | } 33 | ], 34 | [ 35 | "pika-plugin-build-web-babel" 36 | ] 37 | ] 38 | }, 39 | "jest": { 40 | "collectCoverageFrom": [ 41 | "src/*" 42 | ], 43 | "testMatch": [ 44 | "**/__tests__/**/*.spec.[j|t]s?(x)" 45 | ] 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/yiminghe/async-validator/issues" 49 | }, 50 | "license": "MIT", 51 | "config": { 52 | "port": 8010 53 | }, 54 | "scripts": { 55 | "test-url": "yarn ts-node tests/url.ts", 56 | "lint-staged": "lint-staged", 57 | "prettier": "prettier --write \"{src,__tests__,tests}/**/*.{js,tsx,ts}\"", 58 | "pub": "np --no-cleanup --no-publish --no-release-draft && npm run build && cd pkg && npm publish", 59 | "build": "pika-pack build", 60 | "test": "jest", 61 | "test:watch": "jest --watch", 62 | "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand", 63 | "coverage": "jest --coverage && cat ./coverage/lcov.info | coveralls", 64 | "coverage:gha": "jest --coverage", 65 | "version": "npm run build" 66 | }, 67 | "devDependencies": { 68 | "@babel/core": "^7.15.0", 69 | "@babel/node": "^7.14.9", 70 | "@babel/preset-env": "^7.8.7", 71 | "@babel/preset-typescript": "^7.13.0", 72 | "@pika/pack": "^0.5.0", 73 | "@types/jest": "27.x", 74 | "babel-jest": "27.x", 75 | "coveralls": "^2.13.1", 76 | "jest": "27.x", 77 | "lint-staged": "^7.2.0", 78 | "np": "7.x", 79 | "pika-plugin-build-web-babel": "^0.10.0", 80 | "pika-plugin-ts-types": "0.1.x", 81 | "pre-commit": "^1.2.2", 82 | "prettier": "^1.11.1", 83 | "ts-node": "^10.8.1", 84 | "typescript": "^4.3.2" 85 | }, 86 | "lint-staged": { 87 | "*.{tsx,js,jsx,ts}": [ 88 | "prettier --write", 89 | "git add" 90 | ] 91 | }, 92 | "pre-commit": [ 93 | "lint-staged" 94 | ], 95 | "packageManager": "yarn@3.2.2" 96 | } 97 | -------------------------------------------------------------------------------- /__tests__/string.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('string', () => { 4 | it('works for none require', done => { 5 | let data = { 6 | v: '', 7 | }; 8 | new Schema({ 9 | v: { 10 | type: 'string', 11 | }, 12 | }).validate(data, (errors, d) => { 13 | expect(errors).toBe(null); 14 | expect(d).toEqual(data); 15 | done(); 16 | }); 17 | }); 18 | 19 | it('works for empty string', done => { 20 | new Schema({ 21 | v: { 22 | required: true, 23 | type: 'string', 24 | }, 25 | }).validate( 26 | { 27 | v: '', 28 | }, 29 | errors => { 30 | expect(errors.length).toBe(1); 31 | expect(errors[0].message).toBe('v is required'); 32 | done(); 33 | }, 34 | ); 35 | }); 36 | 37 | it('works for undefined string', done => { 38 | new Schema({ 39 | v: { 40 | required: true, 41 | type: 'string', 42 | }, 43 | }).validate( 44 | { 45 | v: undefined, 46 | }, 47 | errors => { 48 | expect(errors.length).toBe(1); 49 | expect(errors[0].message).toBe('v is required'); 50 | done(); 51 | }, 52 | ); 53 | }); 54 | 55 | it('works for null string', done => { 56 | new Schema({ 57 | v: { 58 | required: true, 59 | type: 'string', 60 | }, 61 | }).validate( 62 | { 63 | v: null, 64 | }, 65 | errors => { 66 | expect(errors.length).toBe(1); 67 | expect(errors[0].message).toBe('v is required'); 68 | done(); 69 | }, 70 | ); 71 | }); 72 | 73 | it('works for message', done => { 74 | new Schema({ 75 | v: { 76 | required: true, 77 | type: 'string', 78 | message: 'haha', 79 | }, 80 | }).validate( 81 | { 82 | v: null, 83 | }, 84 | errors => { 85 | expect(errors.length).toBe(1); 86 | expect(errors[0].message).toBe('haha'); 87 | done(); 88 | }, 89 | ); 90 | }); 91 | 92 | it('works for none empty', done => { 93 | new Schema({ 94 | v: { 95 | required: true, 96 | type: 'string', 97 | message: 'haha', 98 | }, 99 | }).validate( 100 | { 101 | v: ' ', 102 | }, 103 | errors => { 104 | expect(errors).toBe(null); 105 | done(); 106 | }, 107 | ); 108 | }); 109 | 110 | it('works for whitespace empty', done => { 111 | new Schema({ 112 | v: { 113 | required: true, 114 | type: 'string', 115 | whitespace: true, 116 | message: 'haha', 117 | }, 118 | }).validate( 119 | { 120 | v: ' ', 121 | }, 122 | errors => { 123 | expect(errors.length).toBe(1); 124 | expect(errors[0].message).toBe('haha'); 125 | done(); 126 | }, 127 | ); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /__tests__/pattern.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('pattern', () => { 4 | it('works for non-required empty string', done => { 5 | new Schema({ 6 | v: { 7 | pattern: /^\d+$/, 8 | message: 'haha', 9 | }, 10 | }).validate( 11 | { 12 | // useful for web, input's value defaults to '' 13 | v: '', 14 | }, 15 | errors => { 16 | expect(errors).toBe(null); 17 | done(); 18 | }, 19 | ); 20 | }); 21 | 22 | it('work for non-required empty string with string regexp', done => { 23 | new Schema({ 24 | v: { 25 | pattern: '^\\d+$', 26 | message: 'haha', 27 | }, 28 | }).validate( 29 | { 30 | // useful for web, input's value defaults to '' 31 | v: 's', 32 | }, 33 | errors => { 34 | expect(errors.length).toBe(1); 35 | expect(errors[0].message).toBe('haha'); 36 | done(); 37 | }, 38 | ); 39 | }); 40 | 41 | it('works for required empty string', done => { 42 | new Schema({ 43 | v: { 44 | pattern: /^\d+$/, 45 | message: 'haha', 46 | required: true, 47 | }, 48 | }).validate( 49 | { 50 | // useful for web, input's value defaults to '' 51 | v: '', 52 | }, 53 | errors => { 54 | expect(errors.length).toBe(1); 55 | expect(errors[0].message).toBe('haha'); 56 | done(); 57 | }, 58 | ); 59 | }); 60 | 61 | it('works for non-required null', done => { 62 | new Schema({ 63 | v: { 64 | pattern: /^\d+$/, 65 | message: 'haha', 66 | }, 67 | }).validate( 68 | { 69 | v: null, 70 | }, 71 | errors => { 72 | expect(errors).toBe(null); 73 | done(); 74 | }, 75 | ); 76 | }); 77 | 78 | it('works for non-required undefined', done => { 79 | new Schema({ 80 | v: { 81 | pattern: /^\d+$/, 82 | message: 'haha', 83 | }, 84 | }).validate( 85 | { 86 | v: undefined, 87 | }, 88 | errors => { 89 | expect(errors).toBe(null); 90 | done(); 91 | }, 92 | ); 93 | }); 94 | 95 | it('works', done => { 96 | new Schema({ 97 | v: { 98 | pattern: /^\d+$/, 99 | message: 'haha', 100 | }, 101 | }).validate( 102 | { 103 | v: ' ', 104 | }, 105 | errors => { 106 | expect(errors.length).toBe(1); 107 | expect(errors[0].message).toBe('haha'); 108 | done(); 109 | }, 110 | ); 111 | }); 112 | 113 | it('works for RegExp with global flag', done => { 114 | const schema = new Schema({ 115 | v: { 116 | pattern: /global/g, 117 | message: 'haha', 118 | }, 119 | }); 120 | 121 | schema.validate( 122 | { 123 | v: 'globalflag', 124 | }, 125 | errors => { 126 | expect(errors).toBe(null); 127 | }, 128 | ); 129 | 130 | schema.validate( 131 | { 132 | v: 'globalflag', 133 | }, 134 | errors => { 135 | expect(errors).toBe(null); 136 | done(); 137 | }, 138 | ); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /src/rule/url.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/kevva/url-regex/blob/master/index.js 2 | let urlReg: RegExp; 3 | 4 | export default () => { 5 | if (urlReg) { 6 | return urlReg; 7 | } 8 | 9 | const word = '[a-fA-F\\d:]'; 10 | const b = options => 11 | options && options.includeBoundaries 12 | ? `(?:(?<=\\s|^)(?=${word})|(?<=${word})(?=\\s|$))` 13 | : ''; 14 | 15 | const v4 = 16 | '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}'; 17 | 18 | const v6seg = '[a-fA-F\\d]{1,4}'; 19 | const v6 = ` 20 | (?: 21 | (?:${v6seg}:){7}(?:${v6seg}|:)| // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8 22 | (?:${v6seg}:){6}(?:${v4}|:${v6seg}|:)| // 1:2:3:4:5:6:: 1:2:3:4:5:6::8 1:2:3:4:5:6::8 1:2:3:4:5:6::1.2.3.4 23 | (?:${v6seg}:){5}(?::${v4}|(?::${v6seg}){1,2}|:)| // 1:2:3:4:5:: 1:2:3:4:5::7:8 1:2:3:4:5::8 1:2:3:4:5::7:1.2.3.4 24 | (?:${v6seg}:){4}(?:(?::${v6seg}){0,1}:${v4}|(?::${v6seg}){1,3}|:)| // 1:2:3:4:: 1:2:3:4::6:7:8 1:2:3:4::8 1:2:3:4::6:7:1.2.3.4 25 | (?:${v6seg}:){3}(?:(?::${v6seg}){0,2}:${v4}|(?::${v6seg}){1,4}|:)| // 1:2:3:: 1:2:3::5:6:7:8 1:2:3::8 1:2:3::5:6:7:1.2.3.4 26 | (?:${v6seg}:){2}(?:(?::${v6seg}){0,3}:${v4}|(?::${v6seg}){1,5}|:)| // 1:2:: 1:2::4:5:6:7:8 1:2::8 1:2::4:5:6:7:1.2.3.4 27 | (?:${v6seg}:){1}(?:(?::${v6seg}){0,4}:${v4}|(?::${v6seg}){1,6}|:)| // 1:: 1::3:4:5:6:7:8 1::8 1::3:4:5:6:7:1.2.3.4 28 | (?::(?:(?::${v6seg}){0,5}:${v4}|(?::${v6seg}){1,7}|:)) // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::1.2.3.4 29 | )(?:%[0-9a-zA-Z]{1,})? // %eth0 %1 30 | ` 31 | .replace(/\s*\/\/.*$/gm, '') 32 | .replace(/\n/g, '') 33 | .trim(); 34 | 35 | // Pre-compile only the exact regexes because adding a global flag make regexes stateful 36 | const v46Exact = new RegExp(`(?:^${v4}$)|(?:^${v6}$)`); 37 | const v4exact = new RegExp(`^${v4}$`); 38 | const v6exact = new RegExp(`^${v6}$`); 39 | 40 | const ip = options => 41 | options && options.exact 42 | ? v46Exact 43 | : new RegExp( 44 | `(?:${b(options)}${v4}${b(options)})|(?:${b(options)}${v6}${b( 45 | options, 46 | )})`, 47 | 'g', 48 | ); 49 | 50 | ip.v4 = (options?) => 51 | options && options.exact 52 | ? v4exact 53 | : new RegExp(`${b(options)}${v4}${b(options)}`, 'g'); 54 | ip.v6 = (options?) => 55 | options && options.exact 56 | ? v6exact 57 | : new RegExp(`${b(options)}${v6}${b(options)}`, 'g'); 58 | 59 | const protocol = `(?:(?:[a-z]+:)?//)`; 60 | const auth = '(?:\\S+(?::\\S*)?@)?'; 61 | const ipv4 = ip.v4().source; 62 | const ipv6 = ip.v6().source; 63 | const host = '(?:(?:[a-z\\u00a1-\\uffff0-9][-_]*)*[a-z\\u00a1-\\uffff0-9]+)'; 64 | const domain = 65 | '(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*'; 66 | const tld = `(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))`; 67 | const port = '(?::\\d{2,5})?'; 68 | const path = '(?:[/?#][^\\s"]*)?'; 69 | const regex = `(?:${protocol}|www\\.)${auth}(?:localhost|${ipv4}|${ipv6}|${host}${domain}${tld})${port}${path}`; 70 | urlReg = new RegExp(`(?:^${regex}$)`, 'i'); 71 | return urlReg; 72 | }; 73 | -------------------------------------------------------------------------------- /src/rule/type.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteRule, Value } from '../interface'; 2 | import { format } from '../util'; 3 | import required from './required'; 4 | import getUrlRegex from './url'; 5 | /* eslint max-len:0 */ 6 | 7 | const pattern = { 8 | // http://emailregex.com/ 9 | email: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/, 10 | // url: new RegExp( 11 | // '^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$', 12 | // 'i', 13 | // ), 14 | hex: /^#?([a-f0-9]{6}|[a-f0-9]{3})$/i, 15 | }; 16 | 17 | const types = { 18 | integer(value: Value) { 19 | return types.number(value) && parseInt(value, 10) === value; 20 | }, 21 | float(value: Value) { 22 | return types.number(value) && !types.integer(value); 23 | }, 24 | array(value: Value) { 25 | return Array.isArray(value); 26 | }, 27 | regexp(value: Value) { 28 | if (value instanceof RegExp) { 29 | return true; 30 | } 31 | try { 32 | return !!new RegExp(value); 33 | } catch (e) { 34 | return false; 35 | } 36 | }, 37 | date(value: Value) { 38 | return ( 39 | typeof value.getTime === 'function' && 40 | typeof value.getMonth === 'function' && 41 | typeof value.getYear === 'function' && 42 | !isNaN(value.getTime()) 43 | ); 44 | }, 45 | number(value: Value) { 46 | if (isNaN(value)) { 47 | return false; 48 | } 49 | return typeof value === 'number'; 50 | }, 51 | object(value: Value) { 52 | return typeof value === 'object' && !types.array(value); 53 | }, 54 | method(value: Value) { 55 | return typeof value === 'function'; 56 | }, 57 | email(value: Value) { 58 | return ( 59 | typeof value === 'string' && 60 | value.length <= 320 && 61 | !!value.match(pattern.email) 62 | ); 63 | }, 64 | url(value: Value) { 65 | return ( 66 | typeof value === 'string' && 67 | value.length <= 2048 && 68 | !!value.match(getUrlRegex()) 69 | ); 70 | }, 71 | hex(value: Value) { 72 | return typeof value === 'string' && !!value.match(pattern.hex); 73 | }, 74 | }; 75 | 76 | const type: ExecuteRule = (rule, value, source, errors, options) => { 77 | if (rule.required && value === undefined) { 78 | required(rule, value, source, errors, options); 79 | return; 80 | } 81 | const custom = [ 82 | 'integer', 83 | 'float', 84 | 'array', 85 | 'regexp', 86 | 'object', 87 | 'method', 88 | 'email', 89 | 'number', 90 | 'date', 91 | 'url', 92 | 'hex', 93 | ]; 94 | const ruleType = rule.type; 95 | if (custom.indexOf(ruleType) > -1) { 96 | if (!types[ruleType](value)) { 97 | errors.push( 98 | format(options.messages.types[ruleType], rule.fullField, rule.type), 99 | ); 100 | } 101 | // straight typeof check 102 | } else if (ruleType && typeof value !== rule.type) { 103 | errors.push( 104 | format(options.messages.types[ruleType], rule.fullField, rule.type), 105 | ); 106 | } 107 | }; 108 | 109 | export default type; 110 | -------------------------------------------------------------------------------- /__tests__/messages.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema, { ValidateMessages } from '../src'; 2 | 3 | describe('messages', () => { 4 | it('can call messages', done => { 5 | const messages = { 6 | required(f) { 7 | return `${f} required!`; 8 | }, 9 | }; 10 | const schema = new Schema({ 11 | v: { 12 | required: true, 13 | }, 14 | v2: { 15 | type: 'array', 16 | }, 17 | }); 18 | schema.messages(messages); 19 | schema.validate( 20 | { 21 | v: '', 22 | v2: '1', 23 | }, 24 | errors => { 25 | expect(errors.length).toBe(2); 26 | expect(errors[0].message).toBe('v required!'); 27 | expect(errors[1].message).toBe('v2 is not an array'); 28 | expect(Object.keys(messages).length).toBe(1); 29 | done(); 30 | }, 31 | ); 32 | }); 33 | 34 | it('can use options.messages', done => { 35 | const messages = { 36 | required(f) { 37 | return `${f} required!`; 38 | }, 39 | }; 40 | const schema = new Schema({ 41 | v: { 42 | required: true, 43 | }, 44 | v2: { 45 | type: 'array', 46 | }, 47 | }); 48 | schema.validate( 49 | { 50 | v: '', 51 | v2: '1', 52 | }, 53 | { 54 | messages, 55 | }, 56 | errors => { 57 | expect(errors.length).toBe(2); 58 | expect(errors[0].message).toBe('v required!'); 59 | expect(errors[1].message).toBe('v2 is not an array'); 60 | expect(Object.keys(messages).length).toBe(1); 61 | done(); 62 | }, 63 | ); 64 | }); 65 | 66 | it('messages with parameters', done => { 67 | const messages = { 68 | required: 'Field %s required!', 69 | }; 70 | const schema = new Schema({ 71 | v: { 72 | required: true, 73 | }, 74 | }); 75 | schema.messages(messages); 76 | schema.validate( 77 | { 78 | v: '', 79 | }, 80 | errors => { 81 | expect(errors).toBeTruthy(); 82 | expect(errors.length).toBe(1); 83 | expect(errors[0].message).toBe('Field v required!'); 84 | expect(Object.keys(messages).length).toBe(1); 85 | done(); 86 | }, 87 | ); 88 | }); 89 | 90 | it('messages can be without parameters', done => { 91 | const messages = { 92 | required: 'required!', 93 | }; 94 | const schema = new Schema({ 95 | v: { 96 | required: true, 97 | }, 98 | }); 99 | schema.messages(messages); 100 | schema.validate( 101 | { 102 | v: '', 103 | }, 104 | errors => { 105 | expect(errors).toBeTruthy(); 106 | expect(errors.length).toBe(1); 107 | expect(errors[0].message).toBe('required!'); 108 | expect(Object.keys(messages).length).toBe(1); 109 | expect(messages.required).toBe('required!'); 110 | done(); 111 | }, 112 | ); 113 | }); 114 | 115 | it('message can be a function', done => { 116 | const message = 'this is a function'; 117 | new Schema({ 118 | v: { 119 | required: true, 120 | message: () => message, 121 | }, 122 | }).validate( 123 | { 124 | v: '', // provide empty value, this will trigger the message. 125 | }, 126 | errors => { 127 | expect(errors).toBeTruthy(); 128 | expect(errors.length).toBe(1); 129 | expect(errors[0].message).toBe(message); 130 | done(); 131 | }, 132 | ); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /__tests__/url.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('url', () => { 4 | it('works for empty string', done => { 5 | new Schema({ 6 | v: { 7 | type: 'url', 8 | }, 9 | }).validate( 10 | { 11 | v: '', 12 | }, 13 | errors => { 14 | expect(errors).toBe(null); 15 | done(); 16 | }, 17 | ); 18 | }); 19 | 20 | it('works for ip url', done => { 21 | new Schema({ 22 | v: { 23 | type: 'url', 24 | }, 25 | }).validate( 26 | { 27 | v: 'http://10.218.136.29/talent-tree/src/index.html', 28 | }, 29 | errors => { 30 | expect(errors).toBe(null); 31 | done(); 32 | }, 33 | ); 34 | }); 35 | 36 | it('works for required empty string', done => { 37 | new Schema({ 38 | v: { 39 | type: 'url', 40 | required: true, 41 | }, 42 | }).validate( 43 | { 44 | v: '', 45 | }, 46 | errors => { 47 | expect(errors.length).toBe(1); 48 | expect(errors[0].message).toBe('v is required'); 49 | done(); 50 | }, 51 | ); 52 | }); 53 | 54 | it('works for type url', done => { 55 | new Schema({ 56 | v: { 57 | type: 'url', 58 | }, 59 | }).validate( 60 | { 61 | v: 'http://www.taobao.com', 62 | }, 63 | errors => { 64 | expect(errors).toBe(null); 65 | done(); 66 | }, 67 | ); 68 | }); 69 | 70 | it('works for type url has query', done => { 71 | new Schema({ 72 | v: { 73 | type: 'url', 74 | }, 75 | }).validate( 76 | { 77 | v: 'http://www.taobao.com/abc?a=a', 78 | }, 79 | errors => { 80 | expect(errors).toBe(null); 81 | done(); 82 | }, 83 | ); 84 | }); 85 | 86 | it('works for type url has hash', done => { 87 | new Schema({ 88 | v: { 89 | type: 'url', 90 | }, 91 | }).validate( 92 | { 93 | v: 'http://www.taobao.com/abc#!abc', 94 | }, 95 | errors => { 96 | expect(errors).toBe(null); 97 | done(); 98 | }, 99 | ); 100 | }); 101 | 102 | it('works for type url has query and has', done => { 103 | new Schema({ 104 | v: { 105 | type: 'url', 106 | }, 107 | }).validate( 108 | { 109 | v: 'http://www.taobao.com/abc?abc=%23&b=a~c#abc', 110 | }, 111 | errors => { 112 | expect(errors).toBe(null); 113 | done(); 114 | }, 115 | ); 116 | }); 117 | 118 | it('works for type url has multi hyphen', done => { 119 | new Schema({ 120 | v: { 121 | type: 'url', 122 | }, 123 | }).validate( 124 | { 125 | v: 'https://www.tao---bao.com', 126 | }, 127 | errors => { 128 | expect(errors).toBe(null); 129 | done(); 130 | }, 131 | ); 132 | }); 133 | 134 | it('works for type not a valid url', done => { 135 | new Schema({ 136 | v: { 137 | type: 'url', 138 | }, 139 | }).validate( 140 | { 141 | v: 'http://www.taobao.com/abc?abc=%23&b= a~c#abc ', 142 | }, 143 | errors => { 144 | expect(errors.length).toBe(1); 145 | expect(errors[0].message).toBe('v is not a valid url'); 146 | done(); 147 | }, 148 | ); 149 | }); 150 | 151 | it('support skip schema', done => { 152 | new Schema({ 153 | v: { 154 | type: 'url', 155 | }, 156 | }).validate( 157 | { 158 | v: '//g.cn', 159 | }, 160 | errors => { 161 | expect(errors).toBe(null); 162 | done(); 163 | }, 164 | ); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /__tests__/required.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | const required = true; 3 | 4 | describe('required', () => { 5 | it('works for array required=true', done => { 6 | new Schema({ 7 | v: [ 8 | { 9 | required, 10 | message: 'no', 11 | }, 12 | ], 13 | }).validate( 14 | { 15 | v: [], 16 | }, 17 | errors => { 18 | expect(errors.length).toBe(1); 19 | expect(errors[0].message).toBe('no'); 20 | done(); 21 | }, 22 | ); 23 | }); 24 | 25 | it('works for array required=true & custom message', done => { 26 | // allow custom message 27 | new Schema({ 28 | v: [ 29 | { 30 | required, 31 | message: 'no', 32 | }, 33 | ], 34 | }).validate( 35 | { 36 | v: [1], 37 | }, 38 | errors => { 39 | expect(errors).toBeFalsy(); 40 | done(); 41 | }, 42 | ); 43 | }); 44 | 45 | it('works for array required=false', done => { 46 | new Schema({ 47 | v: { 48 | required: false, 49 | }, 50 | }).validate( 51 | { 52 | v: [], 53 | }, 54 | errors => { 55 | expect(errors).toBeFalsy(); 56 | done(); 57 | }, 58 | ); 59 | }); 60 | 61 | it('works for string required=true', done => { 62 | new Schema({ 63 | v: { 64 | required, 65 | }, 66 | }).validate( 67 | { 68 | v: '', 69 | }, 70 | errors => { 71 | expect(errors.length).toBe(1); 72 | expect(errors[0].message).toBe('v is required'); 73 | done(); 74 | }, 75 | ); 76 | }); 77 | 78 | it('works for string required=false', done => { 79 | new Schema({ 80 | v: { 81 | required: false, 82 | }, 83 | }).validate( 84 | { 85 | v: '', 86 | }, 87 | errors => { 88 | expect(errors).toBeFalsy(); 89 | done(); 90 | }, 91 | ); 92 | }); 93 | 94 | it('works for number required=true', done => { 95 | new Schema({ 96 | v: { 97 | required, 98 | }, 99 | }).validate( 100 | { 101 | v: 1, 102 | }, 103 | errors => { 104 | expect(errors).toBeFalsy(); 105 | done(); 106 | }, 107 | ); 108 | }); 109 | 110 | it('works for number required=false', done => { 111 | new Schema({ 112 | v: { 113 | required: false, 114 | }, 115 | }).validate( 116 | { 117 | v: 1, 118 | }, 119 | errors => { 120 | expect(errors).toBeFalsy(); 121 | done(); 122 | }, 123 | ); 124 | }); 125 | 126 | it('works for null required=true', done => { 127 | new Schema({ 128 | v: { 129 | required, 130 | }, 131 | }).validate( 132 | { 133 | v: null, 134 | }, 135 | errors => { 136 | expect(errors.length).toBe(1); 137 | expect(errors[0].message).toBe('v is required'); 138 | done(); 139 | }, 140 | ); 141 | }); 142 | 143 | it('works for null required=false', done => { 144 | new Schema({ 145 | v: { 146 | required: false, 147 | }, 148 | }).validate( 149 | { 150 | v: null, 151 | }, 152 | errors => { 153 | expect(errors).toBeFalsy(); 154 | done(); 155 | }, 156 | ); 157 | }); 158 | 159 | it('works for undefined required=true', done => { 160 | new Schema({ 161 | v: { 162 | required, 163 | }, 164 | }).validate( 165 | { 166 | v: undefined, 167 | }, 168 | errors => { 169 | expect(errors.length).toBe(1); 170 | expect(errors[0].message).toBe('v is required'); 171 | done(); 172 | }, 173 | ); 174 | }); 175 | 176 | it('works for undefined required=false', done => { 177 | new Schema({ 178 | v: { 179 | required: false, 180 | }, 181 | }).validate( 182 | { 183 | v: undefined, 184 | }, 185 | errors => { 186 | expect(errors).toBeFalsy(); 187 | done(); 188 | }, 189 | ); 190 | }); 191 | 192 | it('should support empty string message', done => { 193 | new Schema({ 194 | v: { 195 | required, 196 | message: '', 197 | }, 198 | }).validate( 199 | { 200 | v: '', 201 | }, 202 | errors => { 203 | expect(errors.length).toBe(1); 204 | expect(errors[0].message).toBe(''); 205 | done(); 206 | }, 207 | ); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /__tests__/array.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('array', () => { 4 | it('works for type', done => { 5 | new Schema({ 6 | v: { 7 | type: 'array', 8 | }, 9 | }).validate( 10 | { 11 | v: '', 12 | }, 13 | errors => { 14 | expect(errors.length).toBe(1); 15 | expect(errors[0].message).toBe('v is not an array'); 16 | done(); 17 | }, 18 | ); 19 | }); 20 | 21 | it('works for type and required', done => { 22 | new Schema({ 23 | v: { 24 | required: true, 25 | type: 'array', 26 | }, 27 | }).validate( 28 | { 29 | v: '', 30 | }, 31 | (errors, fields) => { 32 | expect(errors.length).toBe(1); 33 | expect(fields).toMatchInlineSnapshot(` 34 | Object { 35 | "v": Array [ 36 | Object { 37 | "field": "v", 38 | "fieldValue": "", 39 | "message": "v is not an array", 40 | }, 41 | ], 42 | } 43 | `); 44 | expect(errors[0].message).toBe('v is not an array'); 45 | done(); 46 | }, 47 | ); 48 | }); 49 | 50 | it('works for none require', done => { 51 | new Schema({ 52 | v: { 53 | type: 'array', 54 | }, 55 | }).validate( 56 | { 57 | v: [], 58 | }, 59 | errors => { 60 | expect(errors).toBe(null); 61 | done(); 62 | }, 63 | ); 64 | }); 65 | 66 | it('works for empty array', done => { 67 | new Schema({ 68 | v: { 69 | required: true, 70 | type: 'array', 71 | }, 72 | }).validate( 73 | { 74 | v: [], 75 | }, 76 | errors => { 77 | expect(errors.length).toBe(1); 78 | expect(errors[0].message).toBe('v is required'); 79 | done(); 80 | }, 81 | ); 82 | }); 83 | 84 | it('works for undefined array', done => { 85 | new Schema({ 86 | v: { 87 | type: 'array', 88 | }, 89 | }).validate( 90 | { 91 | v: undefined, 92 | }, 93 | errors => { 94 | expect(errors).toBe(null); 95 | done(); 96 | }, 97 | ); 98 | }); 99 | 100 | it('works for undefined array and required', done => { 101 | new Schema({ 102 | v: { 103 | required: true, 104 | type: 'array', 105 | }, 106 | }).validate( 107 | { 108 | v: undefined, 109 | }, 110 | errors => { 111 | expect(errors.length).toBe(1); 112 | expect(errors[0].message).toBe('v is required'); 113 | done(); 114 | }, 115 | ); 116 | }); 117 | 118 | it('works for undefined array and defaultField', done => { 119 | new Schema({ 120 | v: { 121 | type: 'array', 122 | defaultField: { type: 'string' }, 123 | }, 124 | }).validate( 125 | { 126 | v: undefined, 127 | }, 128 | errors => { 129 | expect(errors).toBe(null); 130 | done(); 131 | }, 132 | ); 133 | }); 134 | 135 | it('works for null array', done => { 136 | new Schema({ 137 | v: { 138 | required: true, 139 | type: 'array', 140 | }, 141 | }).validate( 142 | { 143 | v: null, 144 | }, 145 | errors => { 146 | expect(errors.length).toBe(1); 147 | expect(errors[0].message).toBe('v is required'); 148 | done(); 149 | }, 150 | ); 151 | }); 152 | 153 | it('works for none empty', done => { 154 | new Schema({ 155 | v: { 156 | required: true, 157 | type: 'array', 158 | message: 'haha', 159 | }, 160 | }).validate( 161 | { 162 | v: [1], 163 | }, 164 | errors => { 165 | expect(errors).toBe(null); 166 | done(); 167 | }, 168 | ); 169 | }); 170 | 171 | it('works for empty array with min', done => { 172 | new Schema({ 173 | v: { 174 | min: 1, 175 | max: 3, 176 | type: 'array', 177 | }, 178 | }).validate( 179 | { 180 | v: [], 181 | }, 182 | errors => { 183 | expect(errors.length).toBe(1); 184 | expect(errors[0].message).toBe('v must be between 1 and 3 in length'); 185 | done(); 186 | }, 187 | ); 188 | }); 189 | 190 | it('works for empty array with max', done => { 191 | new Schema({ 192 | v: { 193 | min: 1, 194 | max: 3, 195 | type: 'array', 196 | }, 197 | }).validate( 198 | { 199 | v: [1, 2, 3, 4], 200 | }, 201 | errors => { 202 | expect(errors.length).toBe(1); 203 | expect(errors[0].message).toBe('v must be between 1 and 3 in length'); 204 | done(); 205 | }, 206 | ); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /__tests__/promise.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('asyncValidator', () => { 4 | it('works', done => { 5 | new Schema({ 6 | v: [ 7 | { 8 | asyncValidator(rule, value) { 9 | return Promise.reject(new Error('e1')); 10 | }, 11 | }, 12 | { 13 | asyncValidator(rule, value) { 14 | return Promise.reject(new Error('e2')); 15 | }, 16 | }, 17 | ], 18 | v2: [ 19 | { 20 | asyncValidator(rule, value) { 21 | return Promise.reject(new Error('e3')); 22 | }, 23 | }, 24 | ], 25 | }).validate( 26 | { 27 | v: 2, 28 | }, 29 | errors => { 30 | expect(errors.length).toBe(3); 31 | expect(errors[0].message).toBe('e1'); 32 | expect(errors[1].message).toBe('e2'); 33 | expect(errors[2].message).toBe('e3'); 34 | done(); 35 | }, 36 | ); 37 | }); 38 | 39 | it('first works', done => { 40 | new Schema({ 41 | v: [ 42 | { 43 | asyncValidator(rule, value) { 44 | return Promise.reject(new Error('e1')); 45 | }, 46 | }, 47 | { 48 | asyncValidator(rule, value) { 49 | return Promise.reject(new Error('e2')); 50 | }, 51 | }, 52 | ], 53 | v2: [ 54 | { 55 | asyncValidator(rule, value) { 56 | return Promise.reject(new Error('e3')); 57 | }, 58 | }, 59 | ], 60 | }).validate( 61 | { 62 | v: 2, 63 | v2: 1, 64 | }, 65 | { 66 | first: true, 67 | }, 68 | errors => { 69 | expect(errors.length).toBe(1); 70 | expect(errors[0].message).toBe('e1'); 71 | done(); 72 | }, 73 | ); 74 | }); 75 | 76 | describe('firstFields', () => { 77 | it('works for true', done => { 78 | new Schema({ 79 | v: [ 80 | { 81 | asyncValidator(rule, value) { 82 | return Promise.reject(new Error('e1')); 83 | }, 84 | }, 85 | { 86 | asyncValidator(rule, value) { 87 | return Promise.reject(new Error('e2')); 88 | }, 89 | }, 90 | ], 91 | 92 | v2: [ 93 | { 94 | asyncValidator(rule, value) { 95 | return Promise.reject(new Error('e3')); 96 | }, 97 | }, 98 | ], 99 | v3: [ 100 | { 101 | asyncValidator(rule, value) { 102 | return Promise.reject(new Error('e4')); 103 | }, 104 | }, 105 | { 106 | asyncValidator(rule, value) { 107 | return Promise.reject(new Error('e5')); 108 | }, 109 | }, 110 | ], 111 | }).validate( 112 | { 113 | v: 1, 114 | v2: 1, 115 | v3: 1, 116 | }, 117 | { 118 | firstFields: true, 119 | }, 120 | errors => { 121 | expect(errors.length).toBe(3); 122 | expect(errors[0].message).toBe('e1'); 123 | expect(errors[1].message).toBe('e3'); 124 | expect(errors[2].message).toBe('e4'); 125 | done(); 126 | }, 127 | ); 128 | }); 129 | 130 | it('works for array', done => { 131 | new Schema({ 132 | v: [ 133 | { 134 | asyncValidator: (rule, value) => { 135 | return Promise.reject(new Error('e1')); 136 | }, 137 | }, 138 | { 139 | asyncValidator(rule, value) { 140 | return Promise.reject(new Error('e2')); 141 | }, 142 | }, 143 | ], 144 | 145 | v2: [ 146 | { 147 | asyncValidator(rule, value) { 148 | return Promise.reject(new Error('e3')); 149 | }, 150 | }, 151 | ], 152 | v3: [ 153 | { 154 | asyncValidator(rule, value) { 155 | return Promise.reject(new Error('e4')); 156 | }, 157 | }, 158 | { 159 | asyncValidator(rule, value) { 160 | return Promise.reject(new Error('e5')); 161 | }, 162 | }, 163 | ], 164 | v4: [ 165 | { 166 | asyncValidator: () => 167 | new Promise((resolve, reject) => { 168 | setTimeout(resolve, 100); 169 | }), 170 | }, 171 | { 172 | asyncValidator: () => 173 | new Promise((resolve, reject) => { 174 | setTimeout(() => reject(new Error('e6')), 100); 175 | }), 176 | }, 177 | { 178 | asyncValidator: () => 179 | new Promise((resolve, reject) => { 180 | setTimeout(() => reject(new Error('')), 100); 181 | }), 182 | }, 183 | ], 184 | }).validate( 185 | { 186 | v: 1, 187 | v2: 1, 188 | v3: 1, 189 | }, 190 | { 191 | firstFields: ['v'], 192 | }, 193 | errors => { 194 | expect(errors.length).toBe(6); 195 | expect(errors[0].message).toBe('e1'); 196 | expect(errors[1].message).toBe('e3'); 197 | expect(errors[2].message).toBe('e4'); 198 | expect(errors[3].message).toBe('e5'); 199 | expect(errors[4].message).toBe('e6'); 200 | expect(errors[5].message).toBe(''); 201 | done(); 202 | }, 203 | ); 204 | }); 205 | it("Whether to remove the 'Uncaught (in promise)' warning", async () => { 206 | let allCorrect = true; 207 | try { 208 | await new Schema({ 209 | async: { 210 | asyncValidator(rule, value) { 211 | return new Promise((resolve, reject) => { 212 | setTimeout(() => { 213 | reject([ 214 | new Error( 215 | typeof rule.message === 'function' 216 | ? rule.message() 217 | : rule.message, 218 | ), 219 | ]); 220 | }, 100); 221 | }); 222 | }, 223 | message: 'async fails', 224 | }, 225 | }).validate({ 226 | v: 1, 227 | }); 228 | } catch ({ errors }) { 229 | allCorrect = errors.length === 1; 230 | } 231 | expect(allCorrect).toBe(true); 232 | }); 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | // >>>>> Rule 2 | // Modified from https://github.com/yiminghe/async-validator/blob/0d51d60086a127b21db76f44dff28ae18c165c47/src/index.d.ts 3 | export type RuleType = 4 | | 'string' 5 | | 'number' 6 | | 'boolean' 7 | | 'method' 8 | | 'regexp' 9 | | 'integer' 10 | | 'float' 11 | | 'array' 12 | | 'object' 13 | | 'enum' 14 | | 'date' 15 | | 'url' 16 | | 'hex' 17 | | 'email' 18 | | 'pattern' 19 | | 'any'; 20 | 21 | export interface ValidateOption { 22 | // whether to suppress internal warning 23 | suppressWarning?: boolean; 24 | 25 | // whether to suppress validator error 26 | suppressValidatorError?: boolean; 27 | 28 | // when the first validation rule generates an error stop processed 29 | first?: boolean; 30 | 31 | // when the first validation rule of the specified field generates an error stop the field processed, 'true' means all fields. 32 | firstFields?: boolean | string[]; 33 | 34 | messages?: Partial; 35 | 36 | /** The name of rules need to be trigger. Will validate all rules if leave empty */ 37 | keys?: string[]; 38 | 39 | error?: (rule: InternalRuleItem, message: string) => ValidateError; 40 | } 41 | 42 | export type SyncErrorType = Error | string; 43 | export type SyncValidateResult = boolean | SyncErrorType | SyncErrorType[]; 44 | export type ValidateResult = void | Promise | SyncValidateResult; 45 | 46 | export interface RuleItem { 47 | type?: RuleType; // default type is 'string' 48 | required?: boolean; 49 | pattern?: RegExp | string; 50 | min?: number; // Range of type 'string' and 'array' 51 | max?: number; // Range of type 'string' and 'array' 52 | len?: number; // Length of type 'string' and 'array' 53 | enum?: Array; // possible values of type 'enum' 54 | whitespace?: boolean; 55 | fields?: Record; // ignore when without required 56 | options?: ValidateOption; 57 | defaultField?: Rule; // 'object' or 'array' containing validation rules 58 | transform?: (value: Value) => Value; 59 | message?: string | ((a?: string) => string); 60 | asyncValidator?: ( 61 | rule: InternalRuleItem, 62 | value: Value, 63 | callback: (error?: string | Error) => void, 64 | source: Values, 65 | options: ValidateOption, 66 | ) => void | Promise; 67 | validator?: ( 68 | rule: InternalRuleItem, 69 | value: Value, 70 | callback: (error?: string | Error) => void, 71 | source: Values, 72 | options: ValidateOption, 73 | ) => SyncValidateResult | void; 74 | } 75 | 76 | export type Rule = RuleItem | RuleItem[]; 77 | 78 | export type Rules = Record; 79 | 80 | /** 81 | * Rule for validating a value exists in an enumerable list. 82 | * 83 | * @param rule The validation rule. 84 | * @param value The value of the field on the source object. 85 | * @param source The source object being validated. 86 | * @param errors An array of errors that this rule may add 87 | * validation errors to. 88 | * @param options The validation options. 89 | * @param options.messages The validation messages. 90 | * @param type Rule type 91 | */ 92 | export type ExecuteRule = ( 93 | rule: InternalRuleItem, 94 | value: Value, 95 | source: Values, 96 | errors: string[], 97 | options: ValidateOption, 98 | type?: string, 99 | ) => void; 100 | 101 | /** 102 | * Performs validation for any type. 103 | * 104 | * @param rule The validation rule. 105 | * @param value The value of the field on the source object. 106 | * @param callback The callback function. 107 | * @param source The source object being validated. 108 | * @param options The validation options. 109 | * @param options.messages The validation messages. 110 | */ 111 | export type ExecuteValidator = ( 112 | rule: InternalRuleItem, 113 | value: Value, 114 | callback: (error?: string[]) => void, 115 | source: Values, 116 | options: ValidateOption, 117 | ) => void; 118 | 119 | // >>>>> Message 120 | type ValidateMessage = 121 | | string 122 | | ((...args: T) => string); 123 | type FullField = string | undefined; 124 | type EnumString = string | undefined; 125 | type Pattern = string | RegExp | undefined; 126 | type Range = number | undefined; 127 | type Type = string | undefined; 128 | 129 | export interface ValidateMessages { 130 | default?: ValidateMessage; 131 | required?: ValidateMessage<[FullField]>; 132 | enum?: ValidateMessage<[FullField, EnumString]>; 133 | whitespace?: ValidateMessage<[FullField]>; 134 | date?: { 135 | format?: ValidateMessage; 136 | parse?: ValidateMessage; 137 | invalid?: ValidateMessage; 138 | }; 139 | types?: { 140 | string?: ValidateMessage<[FullField, Type]>; 141 | method?: ValidateMessage<[FullField, Type]>; 142 | array?: ValidateMessage<[FullField, Type]>; 143 | object?: ValidateMessage<[FullField, Type]>; 144 | number?: ValidateMessage<[FullField, Type]>; 145 | date?: ValidateMessage<[FullField, Type]>; 146 | boolean?: ValidateMessage<[FullField, Type]>; 147 | integer?: ValidateMessage<[FullField, Type]>; 148 | float?: ValidateMessage<[FullField, Type]>; 149 | regexp?: ValidateMessage<[FullField, Type]>; 150 | email?: ValidateMessage<[FullField, Type]>; 151 | url?: ValidateMessage<[FullField, Type]>; 152 | hex?: ValidateMessage<[FullField, Type]>; 153 | }; 154 | string?: { 155 | len?: ValidateMessage<[FullField, Range]>; 156 | min?: ValidateMessage<[FullField, Range]>; 157 | max?: ValidateMessage<[FullField, Range]>; 158 | range?: ValidateMessage<[FullField, Range, Range]>; 159 | }; 160 | number?: { 161 | len?: ValidateMessage<[FullField, Range]>; 162 | min?: ValidateMessage<[FullField, Range]>; 163 | max?: ValidateMessage<[FullField, Range]>; 164 | range?: ValidateMessage<[FullField, Range, Range]>; 165 | }; 166 | array?: { 167 | len?: ValidateMessage<[FullField, Range]>; 168 | min?: ValidateMessage<[FullField, Range]>; 169 | max?: ValidateMessage<[FullField, Range]>; 170 | range?: ValidateMessage<[FullField, Range, Range]>; 171 | }; 172 | pattern?: { 173 | mismatch?: ValidateMessage<[FullField, Value, Pattern]>; 174 | }; 175 | } 176 | 177 | export interface InternalValidateMessages extends ValidateMessages { 178 | clone: () => InternalValidateMessages; 179 | } 180 | 181 | // >>>>> Values 182 | export type Value = any; 183 | export type Values = Record; 184 | 185 | // >>>>> Validate 186 | export interface ValidateError { 187 | message?: string; 188 | fieldValue?: Value; 189 | field?: string; 190 | } 191 | 192 | export type ValidateFieldsError = Record; 193 | 194 | export type ValidateCallback = ( 195 | errors: ValidateError[] | null, 196 | fields: ValidateFieldsError | Values, 197 | ) => void; 198 | 199 | export interface RuleValuePackage { 200 | rule: InternalRuleItem; 201 | value: Value; 202 | source: Values; 203 | field: string; 204 | } 205 | 206 | export interface InternalRuleItem extends Omit { 207 | field?: string; 208 | fullField?: string; 209 | fullFields?: string[]; 210 | validator?: RuleItem['validator'] | ExecuteValidator; 211 | } 212 | -------------------------------------------------------------------------------- /__tests__/deep.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema, { Rules } from '../src'; 2 | 3 | describe('deep', () => { 4 | it('deep array specific validation', done => { 5 | new Schema({ 6 | v: { 7 | required: true, 8 | type: 'array', 9 | fields: { 10 | '0': [{ type: 'string' }], 11 | '1': [{ type: 'string' }], 12 | }, 13 | }, 14 | }).validate( 15 | { 16 | v: [1, 'b'], 17 | }, 18 | (errors, fields) => { 19 | expect(errors.length).toBe(1); 20 | expect(fields).toMatchInlineSnapshot(` 21 | Object { 22 | "v.0": Array [ 23 | Object { 24 | "field": "v.0", 25 | "fieldValue": 1, 26 | "message": "v.0 is not a string", 27 | }, 28 | ], 29 | } 30 | `); 31 | expect(errors[0].message).toBe('v.0 is not a string'); 32 | done(); 33 | }, 34 | ); 35 | }); 36 | 37 | it('deep object specific validation', done => { 38 | new Schema({ 39 | v: { 40 | required: true, 41 | type: 'object', 42 | fields: { 43 | a: [{ type: 'string' }], 44 | b: [{ type: 'string' }], 45 | }, 46 | }, 47 | }).validate( 48 | { 49 | v: { 50 | a: 1, 51 | b: 'c', 52 | }, 53 | }, 54 | (errors, fields) => { 55 | expect(errors.length).toBe(1); 56 | expect(fields).toMatchInlineSnapshot(` 57 | Object { 58 | "v.a": Array [ 59 | Object { 60 | "field": "v.a", 61 | "fieldValue": 1, 62 | "message": "v.a is not a string", 63 | }, 64 | ], 65 | } 66 | `); 67 | expect(errors[0].message).toBe('v.a is not a string'); 68 | done(); 69 | }, 70 | ); 71 | }); 72 | 73 | describe('defaultField', () => { 74 | it('deep array all values validation', done => { 75 | new Schema({ 76 | v: { 77 | required: true, 78 | type: 'array', 79 | defaultField: [{ type: 'string' }], 80 | }, 81 | }).validate( 82 | { 83 | v: [1, 2, 'c'], 84 | }, 85 | (errors, fields) => { 86 | expect(errors.length).toBe(2); 87 | expect(fields).toMatchInlineSnapshot(` 88 | Object { 89 | "v.0": Array [ 90 | Object { 91 | "field": "v.0", 92 | "fieldValue": 1, 93 | "message": "v.0 is not a string", 94 | }, 95 | ], 96 | "v.1": Array [ 97 | Object { 98 | "field": "v.1", 99 | "fieldValue": 2, 100 | "message": "v.1 is not a string", 101 | }, 102 | ], 103 | } 104 | `); 105 | expect(errors[0].message).toBe('v.0 is not a string'); 106 | expect(errors[1].message).toBe('v.1 is not a string'); 107 | done(); 108 | }, 109 | ); 110 | }); 111 | 112 | it('deep transform array all values validation', done => { 113 | new Schema({ 114 | v: { 115 | required: true, 116 | type: 'array', 117 | defaultField: [{ type: 'number', max: 0, transform: Number }], 118 | }, 119 | }).validate( 120 | { 121 | v: ['1', '2'], 122 | }, 123 | (errors, fields) => { 124 | expect(errors.length).toBe(2); 125 | expect(fields).toMatchInlineSnapshot(` 126 | Object { 127 | "v.0": Array [ 128 | Object { 129 | "field": "v.0", 130 | "fieldValue": 1, 131 | "message": "v.0 cannot be greater than 0", 132 | }, 133 | ], 134 | "v.1": Array [ 135 | Object { 136 | "field": "v.1", 137 | "fieldValue": 2, 138 | "message": "v.1 cannot be greater than 0", 139 | }, 140 | ], 141 | } 142 | `); 143 | expect(errors).toMatchInlineSnapshot(` 144 | Array [ 145 | Object { 146 | "field": "v.0", 147 | "fieldValue": 1, 148 | "message": "v.0 cannot be greater than 0", 149 | }, 150 | Object { 151 | "field": "v.1", 152 | "fieldValue": 2, 153 | "message": "v.1 cannot be greater than 0", 154 | }, 155 | ] 156 | `); 157 | done(); 158 | }, 159 | ); 160 | }); 161 | 162 | it('will merge top validation', () => { 163 | const obj = { 164 | value: '', 165 | test: [ 166 | { 167 | name: 'aa', 168 | }, 169 | ], 170 | }; 171 | 172 | const descriptor: Rules = { 173 | test: { 174 | type: 'array', 175 | min: 2, 176 | required: true, 177 | message: '至少两项', 178 | defaultField: [ 179 | { 180 | type: 'object', 181 | required: true, 182 | message: 'test 必须有', 183 | fields: { 184 | name: { 185 | type: 'string', 186 | required: true, 187 | message: 'name 必须有', 188 | }, 189 | }, 190 | }, 191 | ], 192 | }, 193 | }; 194 | 195 | new Schema(descriptor).validate(obj, errors => { 196 | expect(errors).toMatchInlineSnapshot(` 197 | Array [ 198 | Object { 199 | "field": "test", 200 | "fieldValue": Array [ 201 | Object { 202 | "name": "aa", 203 | }, 204 | ], 205 | "message": "至少两项", 206 | }, 207 | ] 208 | `); 209 | }); 210 | }); 211 | 212 | it('array & required works', done => { 213 | const descriptor: Rules = { 214 | testArray: { 215 | type: 'array', 216 | required: true, 217 | defaultField: [{ type: 'string' }], 218 | }, 219 | }; 220 | const record = { 221 | testArray: [], 222 | }; 223 | const validator = new Schema(descriptor); 224 | validator.validate(record, (errors, fields) => { 225 | done(); 226 | }); 227 | }); 228 | 229 | it('deep object all values validation', done => { 230 | new Schema({ 231 | v: { 232 | required: true, 233 | type: 'object', 234 | defaultField: [{ type: 'string' }], 235 | }, 236 | }).validate( 237 | { 238 | v: { 239 | a: 1, 240 | b: 'c', 241 | }, 242 | }, 243 | errors => { 244 | expect(errors.length).toBe(1); 245 | expect(errors[0].message).toBe('v.a is not a string'); 246 | done(); 247 | }, 248 | ); 249 | }); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | 3 | import { 4 | ValidateError, 5 | ValidateOption, 6 | RuleValuePackage, 7 | InternalRuleItem, 8 | SyncErrorType, 9 | RuleType, 10 | Value, 11 | Values, 12 | } from './interface'; 13 | 14 | const formatRegExp = /%[sdj%]/g; 15 | 16 | declare var ASYNC_VALIDATOR_NO_WARNING; 17 | 18 | export let warning: (type: string, errors: SyncErrorType[]) => void = () => {}; 19 | 20 | // don't print warning message when in production env or node runtime 21 | if ( 22 | typeof process !== 'undefined' && 23 | process.env && 24 | process.env.NODE_ENV !== 'production' && 25 | typeof window !== 'undefined' && 26 | typeof document !== 'undefined' 27 | ) { 28 | warning = (type, errors) => { 29 | if ( 30 | typeof console !== 'undefined' && 31 | console.warn && 32 | typeof ASYNC_VALIDATOR_NO_WARNING === 'undefined' 33 | ) { 34 | if (errors.every(e => typeof e === 'string')) { 35 | console.warn(type, errors); 36 | } 37 | } 38 | }; 39 | } 40 | 41 | export function convertFieldsError( 42 | errors: ValidateError[], 43 | ): Record { 44 | if (!errors || !errors.length) return null; 45 | const fields = {}; 46 | errors.forEach(error => { 47 | const field = error.field; 48 | fields[field] = fields[field] || []; 49 | fields[field].push(error); 50 | }); 51 | return fields; 52 | } 53 | 54 | export function format( 55 | template: ((...args: any[]) => string) | string, 56 | ...args: any[] 57 | ): string { 58 | let i = 0; 59 | const len = args.length; 60 | if (typeof template === 'function') { 61 | return template.apply(null, args); 62 | } 63 | if (typeof template === 'string') { 64 | let str = template.replace(formatRegExp, x => { 65 | if (x === '%%') { 66 | return '%'; 67 | } 68 | if (i >= len) { 69 | return x; 70 | } 71 | switch (x) { 72 | case '%s': 73 | return String(args[i++]); 74 | case '%d': 75 | return (Number(args[i++]) as unknown) as string; 76 | case '%j': 77 | try { 78 | return JSON.stringify(args[i++]); 79 | } catch (_) { 80 | return '[Circular]'; 81 | } 82 | break; 83 | default: 84 | return x; 85 | } 86 | }); 87 | return str; 88 | } 89 | return template; 90 | } 91 | 92 | function isNativeStringType(type: string) { 93 | return ( 94 | type === 'string' || 95 | type === 'url' || 96 | type === 'hex' || 97 | type === 'email' || 98 | type === 'date' || 99 | type === 'pattern' 100 | ); 101 | } 102 | 103 | export function isEmptyValue(value: Value, type?: string) { 104 | if (value === undefined || value === null) { 105 | return true; 106 | } 107 | if (type === 'array' && Array.isArray(value) && !value.length) { 108 | return true; 109 | } 110 | if (isNativeStringType(type) && typeof value === 'string' && !value) { 111 | return true; 112 | } 113 | return false; 114 | } 115 | 116 | export function isEmptyObject(obj: object) { 117 | return Object.keys(obj).length === 0; 118 | } 119 | 120 | function asyncParallelArray( 121 | arr: RuleValuePackage[], 122 | func: ValidateFunc, 123 | callback: (errors: ValidateError[]) => void, 124 | ) { 125 | const results: ValidateError[] = []; 126 | let total = 0; 127 | const arrLength = arr.length; 128 | 129 | function count(errors: ValidateError[]) { 130 | results.push(...(errors || [])); 131 | total++; 132 | if (total === arrLength) { 133 | callback(results); 134 | } 135 | } 136 | 137 | arr.forEach(a => { 138 | func(a, count); 139 | }); 140 | } 141 | 142 | function asyncSerialArray( 143 | arr: RuleValuePackage[], 144 | func: ValidateFunc, 145 | callback: (errors: ValidateError[]) => void, 146 | ) { 147 | let index = 0; 148 | const arrLength = arr.length; 149 | 150 | function next(errors: ValidateError[]) { 151 | if (errors && errors.length) { 152 | callback(errors); 153 | return; 154 | } 155 | const original = index; 156 | index = index + 1; 157 | if (original < arrLength) { 158 | func(arr[original], next); 159 | } else { 160 | callback([]); 161 | } 162 | } 163 | 164 | next([]); 165 | } 166 | 167 | function flattenObjArr(objArr: Record) { 168 | const ret: RuleValuePackage[] = []; 169 | Object.keys(objArr).forEach(k => { 170 | ret.push(...(objArr[k] || [])); 171 | }); 172 | return ret; 173 | } 174 | 175 | export class AsyncValidationError extends Error { 176 | errors: ValidateError[]; 177 | fields: Record; 178 | 179 | constructor( 180 | errors: ValidateError[], 181 | fields: Record, 182 | ) { 183 | super('Async Validation Error'); 184 | this.errors = errors; 185 | this.fields = fields; 186 | } 187 | } 188 | 189 | type ValidateFunc = ( 190 | data: RuleValuePackage, 191 | doIt: (errors: ValidateError[]) => void, 192 | ) => void; 193 | 194 | export function asyncMap( 195 | objArr: Record, 196 | option: ValidateOption, 197 | func: ValidateFunc, 198 | callback: (errors: ValidateError[]) => void, 199 | source: Values, 200 | ): Promise { 201 | if (option.first) { 202 | const pending = new Promise((resolve, reject) => { 203 | const next = (errors: ValidateError[]) => { 204 | callback(errors); 205 | return errors.length 206 | ? reject(new AsyncValidationError(errors, convertFieldsError(errors))) 207 | : resolve(source); 208 | }; 209 | const flattenArr = flattenObjArr(objArr); 210 | asyncSerialArray(flattenArr, func, next); 211 | }); 212 | pending.catch(e => e); 213 | return pending; 214 | } 215 | const firstFields = 216 | option.firstFields === true 217 | ? Object.keys(objArr) 218 | : option.firstFields || []; 219 | 220 | const objArrKeys = Object.keys(objArr); 221 | const objArrLength = objArrKeys.length; 222 | let total = 0; 223 | const results: ValidateError[] = []; 224 | const pending = new Promise((resolve, reject) => { 225 | const next = (errors: ValidateError[]) => { 226 | results.push.apply(results, errors); 227 | total++; 228 | if (total === objArrLength) { 229 | callback(results); 230 | return results.length 231 | ? reject( 232 | new AsyncValidationError(results, convertFieldsError(results)), 233 | ) 234 | : resolve(source); 235 | } 236 | }; 237 | if (!objArrKeys.length) { 238 | callback(results); 239 | resolve(source); 240 | } 241 | objArrKeys.forEach(key => { 242 | const arr = objArr[key]; 243 | if (firstFields.indexOf(key) !== -1) { 244 | asyncSerialArray(arr, func, next); 245 | } else { 246 | asyncParallelArray(arr, func, next); 247 | } 248 | }); 249 | }); 250 | pending.catch(e => e); 251 | return pending; 252 | } 253 | 254 | function isErrorObj( 255 | obj: ValidateError | string | (() => string), 256 | ): obj is ValidateError { 257 | return !!(obj && (obj as ValidateError).message !== undefined); 258 | } 259 | 260 | function getValue(value: Values, path: string[]) { 261 | let v = value; 262 | for (let i = 0; i < path.length; i++) { 263 | if (v == undefined) { 264 | return v; 265 | } 266 | v = v[path[i]]; 267 | } 268 | return v; 269 | } 270 | 271 | export function complementError(rule: InternalRuleItem, source: Values) { 272 | return (oe: ValidateError | (() => string) | string): ValidateError => { 273 | let fieldValue; 274 | if (rule.fullFields) { 275 | fieldValue = getValue(source, rule.fullFields); 276 | } else { 277 | fieldValue = source[(oe as any).field || rule.fullField]; 278 | } 279 | if (isErrorObj(oe)) { 280 | oe.field = oe.field || rule.fullField; 281 | oe.fieldValue = fieldValue; 282 | return oe; 283 | } 284 | return { 285 | message: typeof oe === 'function' ? oe() : oe, 286 | fieldValue, 287 | field: ((oe as unknown) as ValidateError).field || rule.fullField, 288 | }; 289 | }; 290 | } 291 | 292 | export function deepMerge(target: T, source: Partial): T { 293 | if (source) { 294 | for (const s in source) { 295 | if (source.hasOwnProperty(s)) { 296 | const value = source[s]; 297 | if (typeof value === 'object' && typeof target[s] === 'object') { 298 | target[s] = { 299 | ...target[s], 300 | ...value, 301 | }; 302 | } else { 303 | target[s] = value; 304 | } 305 | } 306 | } 307 | } 308 | return target; 309 | } 310 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | format, 3 | complementError, 4 | asyncMap, 5 | warning, 6 | deepMerge, 7 | convertFieldsError, 8 | } from './util'; 9 | import validators from './validator/index'; 10 | import { messages as defaultMessages, newMessages } from './messages'; 11 | import { 12 | InternalRuleItem, 13 | InternalValidateMessages, 14 | Rule, 15 | RuleItem, 16 | Rules, 17 | ValidateCallback, 18 | ValidateMessages, 19 | ValidateOption, 20 | Values, 21 | RuleValuePackage, 22 | ValidateError, 23 | ValidateFieldsError, 24 | SyncErrorType, 25 | ValidateResult, 26 | } from './interface'; 27 | 28 | export * from './interface'; 29 | 30 | /** 31 | * Encapsulates a validation schema. 32 | * 33 | * @param descriptor An object declaring validation rules 34 | * for this schema. 35 | */ 36 | class Schema { 37 | // ========================= Static ========================= 38 | static register = function register(type: string, validator) { 39 | if (typeof validator !== 'function') { 40 | throw new Error( 41 | 'Cannot register a validator by type, validator is not a function', 42 | ); 43 | } 44 | validators[type] = validator; 45 | }; 46 | 47 | static warning = warning; 48 | 49 | static messages = defaultMessages; 50 | 51 | static validators = validators; 52 | 53 | // ======================== Instance ======================== 54 | rules: Record = null; 55 | _messages: InternalValidateMessages = defaultMessages; 56 | 57 | constructor(descriptor: Rules) { 58 | this.define(descriptor); 59 | } 60 | 61 | define(rules: Rules) { 62 | if (!rules) { 63 | throw new Error('Cannot configure a schema with no rules'); 64 | } 65 | if (typeof rules !== 'object' || Array.isArray(rules)) { 66 | throw new Error('Rules must be an object'); 67 | } 68 | this.rules = {}; 69 | 70 | Object.keys(rules).forEach(name => { 71 | const item: Rule = rules[name]; 72 | this.rules[name] = Array.isArray(item) ? item : [item]; 73 | }); 74 | } 75 | 76 | messages(messages?: ValidateMessages) { 77 | if (messages) { 78 | this._messages = deepMerge(newMessages(), messages); 79 | } 80 | return this._messages; 81 | } 82 | 83 | validate( 84 | source: Values, 85 | option?: ValidateOption, 86 | callback?: ValidateCallback, 87 | ): Promise; 88 | validate(source: Values, callback: ValidateCallback): Promise; 89 | validate(source: Values): Promise; 90 | 91 | validate(source_: Values, o: any = {}, oc: any = () => {}): Promise { 92 | let source: Values = source_; 93 | let options: ValidateOption = o; 94 | let callback: ValidateCallback = oc; 95 | if (typeof options === 'function') { 96 | callback = options; 97 | options = {}; 98 | } 99 | if (!this.rules || Object.keys(this.rules).length === 0) { 100 | if (callback) { 101 | callback(null, source); 102 | } 103 | return Promise.resolve(source); 104 | } 105 | 106 | function complete(results: (ValidateError | ValidateError[])[]) { 107 | let errors: ValidateError[] = []; 108 | let fields: ValidateFieldsError = {}; 109 | 110 | function add(e: ValidateError | ValidateError[]) { 111 | if (Array.isArray(e)) { 112 | errors = errors.concat(...e); 113 | } else { 114 | errors.push(e); 115 | } 116 | } 117 | 118 | for (let i = 0; i < results.length; i++) { 119 | add(results[i]); 120 | } 121 | if (!errors.length) { 122 | callback(null, source); 123 | } else { 124 | fields = convertFieldsError(errors); 125 | (callback as ( 126 | errors: ValidateError[], 127 | fields: ValidateFieldsError, 128 | ) => void)(errors, fields); 129 | } 130 | } 131 | 132 | if (options.messages) { 133 | let messages = this.messages(); 134 | if (messages === defaultMessages) { 135 | messages = newMessages(); 136 | } 137 | deepMerge(messages, options.messages); 138 | options.messages = messages; 139 | } else { 140 | options.messages = this.messages(); 141 | } 142 | 143 | const series: Record = {}; 144 | const keys = options.keys || Object.keys(this.rules); 145 | keys.forEach(z => { 146 | const arr = this.rules[z]; 147 | let value = source[z]; 148 | arr.forEach(r => { 149 | let rule: InternalRuleItem = r; 150 | if (typeof rule.transform === 'function') { 151 | if (source === source_) { 152 | source = { ...source }; 153 | } 154 | value = source[z] = rule.transform(value); 155 | } 156 | if (typeof rule === 'function') { 157 | rule = { 158 | validator: rule, 159 | }; 160 | } else { 161 | rule = { ...rule }; 162 | } 163 | 164 | // Fill validator. Skip if nothing need to validate 165 | rule.validator = this.getValidationMethod(rule); 166 | if (!rule.validator) { 167 | return; 168 | } 169 | 170 | rule.field = z; 171 | rule.fullField = rule.fullField || z; 172 | rule.type = this.getType(rule); 173 | series[z] = series[z] || []; 174 | series[z].push({ 175 | rule, 176 | value, 177 | source, 178 | field: z, 179 | }); 180 | }); 181 | }); 182 | const errorFields = {}; 183 | return asyncMap( 184 | series, 185 | options, 186 | (data, doIt) => { 187 | const rule = data.rule; 188 | let deep = 189 | (rule.type === 'object' || rule.type === 'array') && 190 | (typeof rule.fields === 'object' || 191 | typeof rule.defaultField === 'object'); 192 | deep = deep && (rule.required || (!rule.required && data.value)); 193 | rule.field = data.field; 194 | 195 | function addFullField(key: string, schema: RuleItem) { 196 | return { 197 | ...schema, 198 | fullField: `${rule.fullField}.${key}`, 199 | fullFields: rule.fullFields ? [...rule.fullFields, key] : [key], 200 | }; 201 | } 202 | 203 | function cb(e: SyncErrorType | SyncErrorType[] = []) { 204 | let errorList = Array.isArray(e) ? e : [e]; 205 | if (!options.suppressWarning && errorList.length) { 206 | Schema.warning('async-validator:', errorList); 207 | } 208 | if (errorList.length && rule.message !== undefined) { 209 | errorList = [].concat(rule.message); 210 | } 211 | 212 | // Fill error info 213 | let filledErrors = errorList.map(complementError(rule, source)); 214 | 215 | if (options.first && filledErrors.length) { 216 | errorFields[rule.field] = 1; 217 | return doIt(filledErrors); 218 | } 219 | if (!deep) { 220 | doIt(filledErrors); 221 | } else { 222 | // if rule is required but the target object 223 | // does not exist fail at the rule level and don't 224 | // go deeper 225 | if (rule.required && !data.value) { 226 | if (rule.message !== undefined) { 227 | filledErrors = [] 228 | .concat(rule.message) 229 | .map(complementError(rule, source)); 230 | } else if (options.error) { 231 | filledErrors = [ 232 | options.error( 233 | rule, 234 | format(options.messages.required, rule.field), 235 | ), 236 | ]; 237 | } 238 | return doIt(filledErrors); 239 | } 240 | 241 | let fieldsSchema: Record = {}; 242 | if (rule.defaultField) { 243 | Object.keys(data.value).map(key => { 244 | fieldsSchema[key] = rule.defaultField; 245 | }); 246 | } 247 | fieldsSchema = { 248 | ...fieldsSchema, 249 | ...data.rule.fields, 250 | }; 251 | 252 | const paredFieldsSchema: Record = {}; 253 | 254 | Object.keys(fieldsSchema).forEach(field => { 255 | const fieldSchema = fieldsSchema[field]; 256 | const fieldSchemaList = Array.isArray(fieldSchema) 257 | ? fieldSchema 258 | : [fieldSchema]; 259 | paredFieldsSchema[field] = fieldSchemaList.map( 260 | addFullField.bind(null, field), 261 | ); 262 | }); 263 | const schema = new Schema(paredFieldsSchema); 264 | schema.messages(options.messages); 265 | if (data.rule.options) { 266 | data.rule.options.messages = options.messages; 267 | data.rule.options.error = options.error; 268 | } 269 | schema.validate(data.value, data.rule.options || options, errs => { 270 | const finalErrors = []; 271 | if (filledErrors && filledErrors.length) { 272 | finalErrors.push(...filledErrors); 273 | } 274 | if (errs && errs.length) { 275 | finalErrors.push(...errs); 276 | } 277 | doIt(finalErrors.length ? finalErrors : null); 278 | }); 279 | } 280 | } 281 | 282 | let res: ValidateResult; 283 | if (rule.asyncValidator) { 284 | res = rule.asyncValidator(rule, data.value, cb, data.source, options); 285 | } else if (rule.validator) { 286 | try { 287 | res = rule.validator(rule, data.value, cb, data.source, options); 288 | } catch (error) { 289 | console.error?.(error); 290 | // rethrow to report error 291 | if (!options.suppressValidatorError) { 292 | setTimeout(() => { 293 | throw error; 294 | }, 0); 295 | } 296 | cb(error.message); 297 | } 298 | if (res === true) { 299 | cb(); 300 | } else if (res === false) { 301 | cb( 302 | typeof rule.message === 'function' 303 | ? rule.message(rule.fullField || rule.field) 304 | : rule.message || `${rule.fullField || rule.field} fails`, 305 | ); 306 | } else if (res instanceof Array) { 307 | cb(res); 308 | } else if (res instanceof Error) { 309 | cb(res.message); 310 | } 311 | } 312 | if (res && (res as Promise).then) { 313 | (res as Promise).then( 314 | () => cb(), 315 | e => cb(e), 316 | ); 317 | } 318 | }, 319 | results => { 320 | complete(results); 321 | }, 322 | source, 323 | ); 324 | } 325 | 326 | getType(rule: InternalRuleItem) { 327 | if (rule.type === undefined && rule.pattern instanceof RegExp) { 328 | rule.type = 'pattern'; 329 | } 330 | if ( 331 | typeof rule.validator !== 'function' && 332 | rule.type && 333 | !validators.hasOwnProperty(rule.type) 334 | ) { 335 | throw new Error(format('Unknown rule type %s', rule.type)); 336 | } 337 | return rule.type || 'string'; 338 | } 339 | 340 | getValidationMethod(rule: InternalRuleItem) { 341 | if (typeof rule.validator === 'function') { 342 | return rule.validator; 343 | } 344 | const keys = Object.keys(rule); 345 | const messageIndex = keys.indexOf('message'); 346 | if (messageIndex !== -1) { 347 | keys.splice(messageIndex, 1); 348 | } 349 | if (keys.length === 1 && keys[0] === 'required') { 350 | return validators.required; 351 | } 352 | return validators[this.getType(rule)] || undefined; 353 | } 354 | } 355 | 356 | export default Schema; 357 | -------------------------------------------------------------------------------- /__tests__/validator.spec.ts: -------------------------------------------------------------------------------- 1 | import Schema from '../src'; 2 | 3 | describe('validator', () => { 4 | it('works', done => { 5 | new Schema({ 6 | v: [ 7 | { 8 | validator(rule, value, callback) { 9 | callback(new Error('e1')); 10 | }, 11 | }, 12 | { 13 | validator(rule, value, callback) { 14 | callback(new Error('e2')); 15 | }, 16 | }, 17 | ], 18 | v2: [ 19 | { 20 | validator(rule, value, callback) { 21 | callback(new Error('e3')); 22 | }, 23 | }, 24 | ], 25 | v3: [ 26 | { 27 | validator() { 28 | return false; 29 | }, 30 | }, 31 | { 32 | validator() { 33 | return new Error('e5'); 34 | }, 35 | }, 36 | { 37 | validator() { 38 | return false; 39 | }, 40 | message: 'e6', 41 | }, 42 | { 43 | validator() { 44 | return true; 45 | }, 46 | }, 47 | // Customize with empty message 48 | { 49 | validator() { 50 | return false; 51 | }, 52 | message: '', 53 | }, 54 | ], 55 | }).validate( 56 | { 57 | v: 2, 58 | }, 59 | errors => { 60 | expect(errors.length).toBe(7); 61 | expect(errors[0].message).toBe('e1'); 62 | expect(errors[1].message).toBe('e2'); 63 | expect(errors[2].message).toBe('e3'); 64 | expect(errors[3].message).toBe('v3 fails'); 65 | expect(errors[4].message).toBe('e5'); 66 | expect(errors[5].message).toBe('e6'); 67 | expect(errors[6].message).toBe(''); 68 | done(); 69 | }, 70 | ); 71 | }); 72 | 73 | it('first works', done => { 74 | new Schema({ 75 | v: [ 76 | { 77 | validator(rule, value, callback) { 78 | callback(new Error('e1')); 79 | }, 80 | }, 81 | { 82 | validator(rule, value, callback) { 83 | callback(new Error('e2')); 84 | }, 85 | }, 86 | ], 87 | v2: [ 88 | { 89 | validator(rule, value, callback) { 90 | callback(new Error('e3')); 91 | }, 92 | }, 93 | ], 94 | }).validate( 95 | { 96 | v: 2, 97 | v2: 1, 98 | }, 99 | { 100 | first: true, 101 | }, 102 | errors => { 103 | expect(errors.length).toBe(1); 104 | expect(errors[0].message).toBe('e1'); 105 | done(); 106 | }, 107 | ); 108 | }); 109 | 110 | describe('firstFields', () => { 111 | it('works for true', done => { 112 | new Schema({ 113 | v: [ 114 | { 115 | validator(rule, value, callback) { 116 | callback(new Error('e1')); 117 | }, 118 | }, 119 | { 120 | validator(rule, value, callback) { 121 | callback(new Error('e2')); 122 | }, 123 | }, 124 | ], 125 | 126 | v2: [ 127 | { 128 | validator(rule, value, callback) { 129 | callback(new Error('e3')); 130 | }, 131 | }, 132 | ], 133 | v3: [ 134 | { 135 | validator(rule, value, callback) { 136 | callback(new Error('e4')); 137 | }, 138 | }, 139 | { 140 | validator(rule, value, callback) { 141 | callback(new Error('e5')); 142 | }, 143 | }, 144 | ], 145 | }).validate( 146 | { 147 | v: 1, 148 | v2: 1, 149 | v3: 1, 150 | }, 151 | { 152 | firstFields: true, 153 | }, 154 | errors => { 155 | expect(errors.length).toBe(3); 156 | expect(errors[0].message).toBe('e1'); 157 | expect(errors[1].message).toBe('e3'); 158 | expect(errors[2].message).toBe('e4'); 159 | done(); 160 | }, 161 | ); 162 | }); 163 | 164 | it('works for array', done => { 165 | new Schema({ 166 | v: [ 167 | { 168 | validator(rule, value, callback) { 169 | callback(new Error('e1')); 170 | }, 171 | }, 172 | { 173 | validator(rule, value, callback) { 174 | callback(new Error('e2')); 175 | }, 176 | }, 177 | ], 178 | 179 | v2: [ 180 | { 181 | validator(rule, value, callback) { 182 | callback(new Error('e3')); 183 | }, 184 | }, 185 | ], 186 | v3: [ 187 | { 188 | validator(rule, value, callback) { 189 | callback(new Error('e4')); 190 | }, 191 | }, 192 | { 193 | validator(rule, value, callback) { 194 | callback(new Error('e5')); 195 | }, 196 | }, 197 | ], 198 | }).validate( 199 | { 200 | v: 1, 201 | v2: 1, 202 | v3: 1, 203 | }, 204 | { 205 | firstFields: ['v'], 206 | }, 207 | errors => { 208 | expect(errors.length).toBe(4); 209 | expect(errors[0].message).toBe('e1'); 210 | expect(errors[1].message).toBe('e3'); 211 | expect(errors[2].message).toBe('e4'); 212 | expect(errors[3].message).toBe('e5'); 213 | done(); 214 | }, 215 | ); 216 | }); 217 | }); 218 | 219 | describe('promise api', () => { 220 | it('works', done => { 221 | new Schema({ 222 | v: [ 223 | { 224 | validator(rule, value, callback) { 225 | callback(new Error('e1')); 226 | }, 227 | }, 228 | { 229 | validator(rule, value, callback) { 230 | callback(new Error('e2')); 231 | }, 232 | }, 233 | ], 234 | v2: [ 235 | { 236 | validator(rule, value, callback) { 237 | callback(new Error('e3')); 238 | }, 239 | }, 240 | ], 241 | v3: [ 242 | { 243 | validator() { 244 | return false; 245 | }, 246 | }, 247 | { 248 | validator() { 249 | return new Error('e5'); 250 | }, 251 | }, 252 | { 253 | validator() { 254 | return false; 255 | }, 256 | message: 'e6', 257 | }, 258 | { 259 | validator() { 260 | return true; 261 | }, 262 | }, 263 | ], 264 | }) 265 | .validate({ 266 | v: 2, 267 | }) 268 | .catch(({ errors, fields }) => { 269 | expect(errors.length).toBe(6); 270 | expect(errors[0].message).toBe('e1'); 271 | expect(errors[1].message).toBe('e2'); 272 | expect(errors[2].message).toBe('e3'); 273 | expect(errors[3].message).toBe('v3 fails'); 274 | expect(errors[4].message).toBe('e5'); 275 | expect(errors[5].message).toBe('e6'); 276 | expect(fields.v[0].fieldValue).toBe(2); 277 | expect(fields).toMatchInlineSnapshot(` 278 | Object { 279 | "v": Array [ 280 | [Error: e1], 281 | [Error: e2], 282 | ], 283 | "v2": Array [ 284 | [Error: e3], 285 | ], 286 | "v3": Array [ 287 | Object { 288 | "field": "v3", 289 | "fieldValue": undefined, 290 | "message": "v3 fails", 291 | }, 292 | Object { 293 | "field": "v3", 294 | "fieldValue": undefined, 295 | "message": "e5", 296 | }, 297 | Object { 298 | "field": "v3", 299 | "fieldValue": undefined, 300 | "message": "e6", 301 | }, 302 | ], 303 | } 304 | `); 305 | done(); 306 | }); 307 | }); 308 | 309 | it('first works', done => { 310 | new Schema({ 311 | v: [ 312 | { 313 | validator(rule, value, callback) { 314 | callback(new Error('e1')); 315 | }, 316 | }, 317 | { 318 | validator(rule, value, callback) { 319 | callback(new Error('e2')); 320 | }, 321 | }, 322 | ], 323 | v2: [ 324 | { 325 | validator(rule, value, callback) { 326 | callback(new Error('e3')); 327 | }, 328 | }, 329 | ], 330 | }) 331 | .validate( 332 | { 333 | v: 2, 334 | v2: 1, 335 | }, 336 | { 337 | first: true, 338 | }, 339 | ) 340 | .catch(({ errors }) => { 341 | expect(errors.length).toBe(1); 342 | expect(errors[0].message).toBe('e1'); 343 | done(); 344 | }); 345 | }); 346 | 347 | describe('firstFields', () => { 348 | it('works for true', done => { 349 | new Schema({ 350 | v: [ 351 | { 352 | validator(rule, value, callback) { 353 | callback(new Error('e1')); 354 | }, 355 | }, 356 | { 357 | validator(rule, value, callback) { 358 | callback(new Error('e2')); 359 | }, 360 | }, 361 | ], 362 | 363 | v2: [ 364 | { 365 | validator(rule, value, callback) { 366 | callback(new Error('e3')); 367 | }, 368 | }, 369 | ], 370 | v3: [ 371 | { 372 | validator(rule, value, callback) { 373 | callback(new Error('e4')); 374 | }, 375 | }, 376 | { 377 | validator(rule, value, callback) { 378 | callback(new Error('e5')); 379 | }, 380 | }, 381 | ], 382 | }) 383 | .validate( 384 | { 385 | v: 1, 386 | v2: 1, 387 | v3: 1, 388 | }, 389 | { 390 | firstFields: true, 391 | }, 392 | ) 393 | .catch(({ errors }) => { 394 | expect(errors.length).toBe(3); 395 | expect(errors[0].message).toBe('e1'); 396 | expect(errors[1].message).toBe('e3'); 397 | expect(errors[2].message).toBe('e4'); 398 | done(); 399 | }); 400 | }); 401 | 402 | it('works for array', done => { 403 | new Schema({ 404 | v: [ 405 | { 406 | validator(rule, value, callback) { 407 | callback(new Error('e1')); 408 | }, 409 | }, 410 | { 411 | validator(rule, value, callback) { 412 | callback(new Error('e2')); 413 | }, 414 | }, 415 | ], 416 | 417 | v2: [ 418 | { 419 | validator(rule, value, callback) { 420 | callback(new Error('e3')); 421 | }, 422 | }, 423 | ], 424 | v3: [ 425 | { 426 | validator(rule, value, callback) { 427 | callback(new Error('e4')); 428 | }, 429 | }, 430 | { 431 | validator(rule, value, callback) { 432 | callback(new Error('e5')); 433 | }, 434 | }, 435 | ], 436 | }) 437 | .validate( 438 | { 439 | v: 1, 440 | v2: 1, 441 | v3: 1, 442 | }, 443 | { 444 | firstFields: ['v'], 445 | }, 446 | ) 447 | .catch(({ errors }) => { 448 | expect(errors.length).toBe(4); 449 | expect(errors[0].message).toBe('e1'); 450 | expect(errors[1].message).toBe('e3'); 451 | expect(errors[2].message).toBe('e4'); 452 | expect(errors[3].message).toBe('e5'); 453 | done(); 454 | }); 455 | }); 456 | 457 | it('works for no rules fields', done => { 458 | new Schema({ 459 | v: [], 460 | v2: [], 461 | }) 462 | .validate({ 463 | v: 2, 464 | v2: 1, 465 | }) 466 | .then(source => { 467 | expect(source).toMatchObject({ v: 2, v2: 1 }); 468 | done(); 469 | }); 470 | }); 471 | }); 472 | }); 473 | 474 | it('custom validate function throw error', done => { 475 | new Schema({ 476 | v: [ 477 | { 478 | validator(rule, value, callback) { 479 | throw new Error('something wrong'); 480 | }, 481 | }, 482 | ], 483 | }) 484 | .validate( 485 | { v: '' }, 486 | { 487 | suppressValidatorError: true, 488 | }, 489 | ) 490 | .catch(({ errors }) => { 491 | expect(errors.length).toBe(1); 492 | expect(errors[0].message).toBe('something wrong'); 493 | done(); 494 | }); 495 | }); 496 | }); 497 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-validator 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![build status][travis-image]][travis-url] 5 | [![Test coverage][coveralls-image]][coveralls-url] 6 | [![node version][node-image]][node-url] 7 | [![npm download][download-image]][download-url] 8 | [![npm bundle size (minified + gzip)][bundlesize-image]][bundlesize-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/async-validator.svg?style=flat-square 11 | [npm-url]: https://npmjs.org/package/async-validator 12 | [travis-image]:https://app.travis-ci.com/yiminghe/async-validator.svg?branch=master 13 | [travis-url]: https://app.travis-ci.com/github/yiminghe/async-validator 14 | [coveralls-image]: https://img.shields.io/coveralls/yiminghe/async-validator.svg?style=flat-square 15 | [coveralls-url]: https://coveralls.io/r/yiminghe/async-validator?branch=master 16 | [node-image]: https://img.shields.io/badge/node.js-%3E=4.0.0-green.svg?style=flat-square 17 | [node-url]: https://nodejs.org/download/ 18 | [download-image]: https://img.shields.io/npm/dm/async-validator.svg?style=flat-square 19 | [download-url]: https://npmjs.org/package/async-validator 20 | [bundlesize-image]: https://img.shields.io/bundlephobia/minzip/async-validator.svg?label=gzip%20size 21 | [bundlesize-url]: https://bundlephobia.com/result?p=async-validator 22 | 23 | Validate form asynchronous. A variation of https://github.com/freeformsystems/async-validate 24 | 25 | ## Install 26 | 27 | ```bash 28 | npm i async-validator 29 | ``` 30 | 31 | ## Usage 32 | 33 | Basic usage involves defining a descriptor, assigning it to a schema and passing the object to be validated and a callback function to the `validate` method of the schema: 34 | 35 | ```js 36 | import Schema from 'async-validator'; 37 | const descriptor = { 38 | name: { 39 | type: 'string', 40 | required: true, 41 | validator: (rule, value) => value === 'muji', 42 | }, 43 | age: { 44 | type: 'number', 45 | asyncValidator: (rule, value) => { 46 | return new Promise((resolve, reject) => { 47 | if (value < 18) { 48 | reject('too young'); // reject with error message 49 | } else { 50 | resolve(); 51 | } 52 | }); 53 | }, 54 | }, 55 | }; 56 | const validator = new Schema(descriptor); 57 | validator.validate({ name: 'muji' }, (errors, fields) => { 58 | if (errors) { 59 | // validation failed, errors is an array of all errors 60 | // fields is an object keyed by field name with an array of 61 | // errors per field 62 | return handleErrors(errors, fields); 63 | } 64 | // validation passed 65 | }); 66 | 67 | // PROMISE USAGE 68 | validator.validate({ name: 'muji', age: 16 }).then(() => { 69 | // validation passed or without error message 70 | }).catch(({ errors, fields }) => { 71 | return handleErrors(errors, fields); 72 | }); 73 | ``` 74 | 75 | ## API 76 | 77 | ### Validate 78 | 79 | ```js 80 | function(source, [options], callback): Promise 81 | ``` 82 | 83 | * `source`: The object to validate (required). 84 | * `options`: An object describing processing options for the validation (optional). 85 | * `callback`: A callback function to invoke when validation completes (optional). 86 | 87 | The method will return a Promise object like: 88 | * `then()`,validation passed 89 | * `catch({ errors, fields })`,validation failed, errors is an array of all errors, fields is an object keyed by field name with an array of errors per field 90 | 91 | ### Options 92 | 93 | * `suppressWarning`: Boolean, whether to suppress internal warning about invalid value. 94 | 95 | * `first`: Boolean, Invoke `callback` when the first validation rule generates an error, 96 | no more validation rules are processed. 97 | If your validation involves multiple asynchronous calls (for example, database queries) and you only need the first error use this option. 98 | 99 | * `firstFields`: Boolean|String[], Invoke `callback` when the first validation rule of the specified field generates an error, 100 | no more validation rules of the same field are processed. `true` means all fields. 101 | 102 | ### Rules 103 | 104 | Rules may be functions that perform validation. 105 | 106 | ```js 107 | function(rule, value, callback, source, options) 108 | ``` 109 | 110 | * `rule`: The validation rule in the source descriptor that corresponds to the field name being validated. It is always assigned a `field` property with the name of the field being validated. 111 | * `value`: The value of the source object property being validated. 112 | * `callback`: A callback function to invoke once validation is complete. It expects to be passed an array of `Error` instances to indicate validation failure. If the check is synchronous, you can directly return a ` false ` or ` Error ` or ` Error Array `. 113 | * `source`: The source object that was passed to the `validate` method. 114 | * `options`: Additional options. 115 | * `options.messages`: The object containing validation error messages, will be deep merged with defaultMessages. 116 | 117 | The options passed to `validate` or `asyncValidate` are passed on to the validation functions so that you may reference transient data (such as model references) in validation functions. However, some option names are reserved; if you use these properties of the options object they are overwritten. The reserved properties are `messages`, `exception` and `error`. 118 | 119 | ```js 120 | import Schema from 'async-validator'; 121 | const descriptor = { 122 | name(rule, value, callback, source, options) { 123 | const errors = []; 124 | if (!/^[a-z0-9]+$/.test(value)) { 125 | errors.push(new Error( 126 | util.format('%s must be lowercase alphanumeric characters', rule.field), 127 | )); 128 | } 129 | return errors; 130 | }, 131 | }; 132 | const validator = new Schema(descriptor); 133 | validator.validate({ name: 'Firstname' }, (errors, fields) => { 134 | if (errors) { 135 | return handleErrors(errors, fields); 136 | } 137 | // validation passed 138 | }); 139 | ``` 140 | 141 | It is often useful to test against multiple validation rules for a single field, to do so make the rule an array of objects, for example: 142 | 143 | ```js 144 | const descriptor = { 145 | email: [ 146 | { type: 'string', required: true, pattern: Schema.pattern.email }, 147 | { 148 | validator(rule, value, callback, source, options) { 149 | const errors = []; 150 | // test if email address already exists in a database 151 | // and add a validation error to the errors array if it does 152 | return errors; 153 | }, 154 | }, 155 | ], 156 | }; 157 | ``` 158 | 159 | #### Type 160 | 161 | Indicates the `type` of validator to use. Recognised type values are: 162 | 163 | * `string`: Must be of type `string`. `This is the default type.` 164 | * `number`: Must be of type `number`. 165 | * `boolean`: Must be of type `boolean`. 166 | * `method`: Must be of type `function`. 167 | * `regexp`: Must be an instance of `RegExp` or a string that does not generate an exception when creating a new `RegExp`. 168 | * `integer`: Must be of type `number` and an integer. 169 | * `float`: Must be of type `number` and a floating point number. 170 | * `array`: Must be an array as determined by `Array.isArray`. 171 | * `object`: Must be of type `object` and not `Array.isArray`. 172 | * `enum`: Value must exist in the `enum`. 173 | * `date`: Value must be valid as determined by `Date` 174 | * `url`: Must be of type `url`. 175 | * `hex`: Must be of type `hex`. 176 | * `email`: Must be of type `email`. 177 | * `any`: Can be any type. 178 | 179 | #### Required 180 | 181 | The `required` rule property indicates that the field must exist on the source object being validated. 182 | 183 | #### Pattern 184 | 185 | The `pattern` rule property indicates a regular expression that the value must match to pass validation. 186 | 187 | #### Range 188 | 189 | A range is defined using the `min` and `max` properties. For `string` and `array` types comparison is performed against the `length`, for `number` types the number must not be less than `min` nor greater than `max`. 190 | 191 | #### Length 192 | 193 | To validate an exact length of a field specify the `len` property. For `string` and `array` types comparison is performed on the `length` property, for the `number` type this property indicates an exact match for the `number`, ie, it may only be strictly equal to `len`. 194 | 195 | If the `len` property is combined with the `min` and `max` range properties, `len` takes precedence. 196 | 197 | #### Enumerable 198 | 199 | > Since version 3.0.0 if you want to validate the values `0` or `false` inside `enum` types, you have to include them explicitly. 200 | 201 | To validate a value from a list of possible values use the `enum` type with a `enum` property listing the valid values for the field, for example: 202 | 203 | ```js 204 | const descriptor = { 205 | role: { type: 'enum', enum: ['admin', 'user', 'guest'] }, 206 | }; 207 | ``` 208 | 209 | #### Whitespace 210 | 211 | It is typical to treat required fields that only contain whitespace as errors. To add an additional test for a string that consists solely of whitespace add a `whitespace` property to a rule with a value of `true`. The rule must be a `string` type. 212 | 213 | You may wish to sanitize user input instead of testing for whitespace, see [transform](#transform) for an example that would allow you to strip whitespace. 214 | 215 | 216 | #### Deep Rules 217 | 218 | If you need to validate deep object properties you may do so for validation rules that are of the `object` or `array` type by assigning nested rules to a `fields` property of the rule. 219 | 220 | ```js 221 | const descriptor = { 222 | address: { 223 | type: 'object', 224 | required: true, 225 | fields: { 226 | street: { type: 'string', required: true }, 227 | city: { type: 'string', required: true }, 228 | zip: { type: 'string', required: true, len: 8, message: 'invalid zip' }, 229 | }, 230 | }, 231 | name: { type: 'string', required: true }, 232 | }; 233 | const validator = new Schema(descriptor); 234 | validator.validate({ address: {} }, (errors, fields) => { 235 | // errors for address.street, address.city, address.zip 236 | }); 237 | ``` 238 | 239 | Note that if you do not specify the `required` property on the parent rule it is perfectly valid for the field not to be declared on the source object and the deep validation rules will not be executed as there is nothing to validate against. 240 | 241 | Deep rule validation creates a schema for the nested rules so you can also specify the `options` passed to the `schema.validate()` method. 242 | 243 | ```js 244 | const descriptor = { 245 | address: { 246 | type: 'object', 247 | required: true, 248 | options: { first: true }, 249 | fields: { 250 | street: { type: 'string', required: true }, 251 | city: { type: 'string', required: true }, 252 | zip: { type: 'string', required: true, len: 8, message: 'invalid zip' }, 253 | }, 254 | }, 255 | name: { type: 'string', required: true }, 256 | }; 257 | const validator = new Schema(descriptor); 258 | 259 | validator.validate({ address: {} }) 260 | .catch(({ errors, fields }) => { 261 | // now only errors for street and name 262 | }); 263 | ``` 264 | 265 | The parent rule is also validated so if you have a set of rules such as: 266 | 267 | ```js 268 | const descriptor = { 269 | roles: { 270 | type: 'array', 271 | required: true, 272 | len: 3, 273 | fields: { 274 | 0: { type: 'string', required: true }, 275 | 1: { type: 'string', required: true }, 276 | 2: { type: 'string', required: true }, 277 | }, 278 | }, 279 | }; 280 | ``` 281 | 282 | And supply a source object of `{ roles: ['admin', 'user'] }` then two errors will be created. One for the array length mismatch and one for the missing required array entry at index 2. 283 | 284 | #### defaultField 285 | 286 | The `defaultField` property can be used with the `array` or `object` type for validating all values of the container. 287 | It may be an `object` or `array` containing validation rules. For example: 288 | 289 | ```js 290 | const descriptor = { 291 | urls: { 292 | type: 'array', 293 | required: true, 294 | defaultField: { type: 'url' }, 295 | }, 296 | }; 297 | ``` 298 | 299 | Note that `defaultField` is expanded to `fields`, see [deep rules](#deep-rules). 300 | 301 | #### Transform 302 | 303 | Sometimes it is necessary to transform a value before validation, possibly to coerce the value or to sanitize it in some way. To do this add a `transform` function to the validation rule. The property is transformed prior to validation and returned as promise result or callback result when pass validation. 304 | 305 | ```js 306 | import Schema from 'async-validator'; 307 | const descriptor = { 308 | name: { 309 | type: 'string', 310 | required: true, 311 | pattern: /^[a-z]+$/, 312 | transform(value) { 313 | return value.trim(); 314 | }, 315 | }, 316 | }; 317 | const validator = new Schema(descriptor); 318 | const source = { name: ' user ' }; 319 | 320 | validator.validate(source) 321 | .then((data) => assert.equal(data.name, 'user')); 322 | 323 | validator.validate(source,(errors, data)=>{ 324 | assert.equal(data.name, 'user')); 325 | }); 326 | ``` 327 | 328 | Without the `transform` function validation would fail due to the pattern not matching as the input contains leading and trailing whitespace, but by adding the transform function validation passes and the field value is sanitized at the same time. 329 | 330 | 331 | #### Messages 332 | 333 | Depending upon your application requirements, you may need i18n support or you may prefer different validation error messages. 334 | 335 | The easiest way to achieve this is to assign a `message` to a rule: 336 | 337 | ```js 338 | { name: { type: 'string', required: true, message: 'Name is required' } } 339 | ``` 340 | 341 | Message can be any type, such as jsx format. 342 | 343 | ```js 344 | { name: { type: 'string', required: true, message: 'Name is required' } } 345 | ``` 346 | 347 | Message can also be a function, e.g. if you use vue-i18n: 348 | ```js 349 | { name: { type: 'string', required: true, message: () => this.$t( 'name is required' ) } } 350 | ``` 351 | 352 | Potentially you may require the same schema validation rules for different languages, in which case duplicating the schema rules for each language does not make sense. 353 | 354 | In this scenario you could just provide your own messages for the language and assign it to the schema: 355 | 356 | ```js 357 | import Schema from 'async-validator'; 358 | const cn = { 359 | required: '%s 必填', 360 | }; 361 | const descriptor = { name: { type: 'string', required: true } }; 362 | const validator = new Schema(descriptor); 363 | // deep merge with defaultMessages 364 | validator.messages(cn); 365 | ... 366 | ``` 367 | 368 | If you are defining your own validation functions it is better practice to assign the message strings to a messages object and then access the messages via the `options.messages` property within the validation function. 369 | 370 | #### asyncValidator 371 | 372 | You can customize the asynchronous validation function for the specified field: 373 | 374 | ```js 375 | const fields = { 376 | asyncField: { 377 | asyncValidator(rule, value, callback) { 378 | ajax({ 379 | url: 'xx', 380 | value: value, 381 | }).then(function(data) { 382 | callback(); 383 | }, function(error) { 384 | callback(new Error(error)); 385 | }); 386 | }, 387 | }, 388 | 389 | promiseField: { 390 | asyncValidator(rule, value) { 391 | return ajax({ 392 | url: 'xx', 393 | value: value, 394 | }); 395 | }, 396 | }, 397 | }; 398 | ``` 399 | 400 | #### validator 401 | 402 | You can custom validate function for specified field: 403 | 404 | ```js 405 | const fields = { 406 | field: { 407 | validator(rule, value, callback) { 408 | return value === 'test'; 409 | }, 410 | message: 'Value is not equal to "test".', 411 | }, 412 | 413 | field2: { 414 | validator(rule, value, callback) { 415 | return new Error(`${value} is not equal to 'test'.`); 416 | }, 417 | }, 418 | 419 | arrField: { 420 | validator(rule, value) { 421 | return [ 422 | new Error('Message 1'), 423 | new Error('Message 2'), 424 | ]; 425 | }, 426 | }, 427 | }; 428 | ``` 429 | 430 | ## FAQ 431 | 432 | ### How to avoid global warning 433 | 434 | ```js 435 | import Schema from 'async-validator'; 436 | Schema.warning = function(){}; 437 | ``` 438 | 439 | or 440 | ```js 441 | globalThis.ASYNC_VALIDATOR_NO_WARNING = 1; 442 | ``` 443 | 444 | ### How to check if it is `true` 445 | 446 | Use `enum` type passing `true` as option. 447 | 448 | ```js 449 | { 450 | type: 'enum', 451 | enum: [true], 452 | message: '', 453 | } 454 | ``` 455 | 456 | ## Test Case 457 | 458 | ```bash 459 | npm test 460 | ``` 461 | 462 | ## Coverage 463 | 464 | ```bash 465 | npm run coverage 466 | ``` 467 | 468 | Open coverage/ dir 469 | 470 | ## License 471 | 472 | Everything is [MIT](https://en.wikipedia.org/wiki/MIT_License). 473 | --------------------------------------------------------------------------------