├── types ├── index.d.ts ├── tslint.json ├── tsconfig.json └── test.ts ├── .npmrc ├── .codesandbox └── ci.json ├── .npmignore ├── .prettierrc.js ├── src ├── utils │ ├── isEmpty.ts │ ├── basicEmptyCheck.ts │ ├── pathTransform.ts │ ├── checkRequired.ts │ ├── index.ts │ ├── formatErrorMessage.ts │ ├── createValidatorAsync.ts │ ├── createValidator.ts │ └── shallowEqual.ts ├── BooleanType.ts ├── index.ts ├── locales │ ├── index.ts │ └── default.ts ├── DateType.ts ├── types.ts ├── NumberType.ts ├── ArrayType.ts ├── StringType.ts ├── ObjectType.ts ├── Schema.ts └── MixedType.ts ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── 2.feature_request.md │ └── 1.bug_report.md └── workflows │ ├── nodejs-publish.yml │ └── nodejs-ci.yml ├── tsconfig-es.json ├── tsconfig.json ├── .mocharc.js ├── .gitignore ├── .eslintrc.js ├── LICENSE ├── test ├── BooleanTypeSpec.js ├── DateTypeSpec.js ├── utilsSpec.js ├── NumberTypeSpec.js ├── StringTypeSpec.js ├── SchemaSpec.js ├── ObjectTypeSpec.js └── ArrayTypeSpec.js ├── package.json ├── CHANGELOG.md └── README.md /types/index.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | message="build: bump %s" 2 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["new"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | types/*.json 3 | types/test.ts 4 | 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | singleQuote: true, 5 | arrowParens: 'avoid', 6 | trailingComma: 'none' 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/isEmpty.ts: -------------------------------------------------------------------------------- 1 | function isEmpty(value?: any) { 2 | return typeof value === 'undefined' || value === null || value === ''; 3 | } 4 | 5 | export default isEmpty; 6 | -------------------------------------------------------------------------------- /src/utils/basicEmptyCheck.ts: -------------------------------------------------------------------------------- 1 | function basicEmptyCheck(value?: any) { 2 | return typeof value === 'undefined' || value === null; 3 | } 4 | 5 | export default basicEmptyCheck; 6 | -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", // Or "dtslint/dt.json" if on DefinitelyTyped 3 | "rules": { 4 | "semicolon": false, 5 | "indent": [true, "tabs"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🤔 Ask a question 4 | url: https://github.com/rsuite/rsuite/discussions 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6", "dom", "es2017"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictNullChecks": true, 8 | "strictFunctionTypes": true, 9 | "downlevelIteration": true, 10 | "noEmit": true, 11 | "baseUrl": ".", 12 | "paths": { "schema-typed": ["."] } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig-es.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "esModuleInterop": true, 7 | "noImplicitAny": false, 8 | "noUnusedParameters": true, 9 | "noUnusedLocals": true, 10 | "sourceMap": true, 11 | "moduleResolution": "node", 12 | "target": "ES2019" 13 | }, 14 | "include": ["./src/**/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "esModuleInterop": true, 7 | "noImplicitAny": false, 8 | "noUnusedParameters": true, 9 | "noUnusedLocals": true, 10 | "sourceMap": true, 11 | "moduleResolution": "node", 12 | "lib": ["ES2019"], 13 | "module": "commonjs", 14 | "target": "ES2019" 15 | 16 | }, 17 | "include": ["./src/**/*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/pathTransform.ts: -------------------------------------------------------------------------------- 1 | export default function pathTransform(path: string) { 2 | const arr = path.split('.'); 3 | 4 | if (arr.length === 1) { 5 | return path; 6 | } 7 | 8 | return path 9 | .split('.') 10 | .map((item, index) => { 11 | if (index === 0) { 12 | return item; 13 | } 14 | 15 | // Check if the item is a number, e.g. `list.0` 16 | return /^\d+$/.test(item) ? `array.${item}` : `object.${item}`; 17 | }) 18 | .join('.'); 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/checkRequired.ts: -------------------------------------------------------------------------------- 1 | import basicEmptyCheck from './basicEmptyCheck'; 2 | import isEmpty from './isEmpty'; 3 | 4 | function checkRequired(value: any, trim: boolean, emptyAllowed: boolean) { 5 | // String trim 6 | if (trim && typeof value === 'string') { 7 | value = value.replace(/(^\s*)|(\s*$)/g, ''); 8 | } 9 | 10 | if (emptyAllowed) { 11 | return !basicEmptyCheck(value); 12 | } 13 | 14 | // Array 15 | if (Array.isArray(value)) { 16 | return !!value.length; 17 | } 18 | 19 | return !isEmpty(value); 20 | } 21 | 22 | export default checkRequired; 23 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // https://github.com/mochajs/mocha-examples/tree/master/packages/typescript-babel 4 | const config = { 5 | diff: true, 6 | extension: ['js', 'ts'], 7 | package: './package.json', 8 | reporter: 'spec', 9 | slow: 75, 10 | timeout: 2000, 11 | ui: 'bdd', 12 | require: 'ts-node/register', 13 | 'watch-files': ['src/**/*.ts', 'test/**/*.js'] 14 | }; 15 | 16 | const M = process.env.M; 17 | 18 | /** 19 | * @example: 20 | * M=ObjectType npm run tdd 21 | */ 22 | if (M) { 23 | config.spec = 'test/' + M + 'Spec.js'; 24 | } 25 | 26 | module.exports = config; 27 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as get } from 'lodash/get'; 2 | export { default as set } from 'lodash/set'; 3 | export { default as basicEmptyCheck } from './basicEmptyCheck'; 4 | export { default as checkRequired } from './checkRequired'; 5 | export { default as createValidator } from './createValidator'; 6 | export { default as createValidatorAsync } from './createValidatorAsync'; 7 | export { default as isEmpty } from './isEmpty'; 8 | export { default as formatErrorMessage } from './formatErrorMessage'; 9 | export { default as shallowEqual } from './shallowEqual'; 10 | export { default as pathTransform } from './pathTransform'; 11 | -------------------------------------------------------------------------------- /src/utils/formatErrorMessage.ts: -------------------------------------------------------------------------------- 1 | import isEmpty from './isEmpty'; 2 | 3 | export function joinName(name: string | string[]) { 4 | return Array.isArray(name) ? name.join('.') : name; 5 | } 6 | 7 | /** 8 | * formatErrorMessage('${name} is a required field', {name: 'email'}); 9 | * output: 'email is a required field' 10 | */ 11 | export default function formatErrorMessage(errorMessage?: string | E, params?: any) { 12 | if (typeof errorMessage === 'string') { 13 | return errorMessage.replace(/\$\{\s*(\w+)\s*\}/g, (_, key) => { 14 | return isEmpty(params?.[key]) ? `$\{${key}\}` : params?.[key]; 15 | }); 16 | } 17 | 18 | return errorMessage; 19 | } 20 | -------------------------------------------------------------------------------- /src/BooleanType.ts: -------------------------------------------------------------------------------- 1 | import { MixedType } from './MixedType'; 2 | import { ErrorMessageType } from './types'; 3 | import { BooleanTypeLocale } from './locales'; 4 | 5 | export class BooleanType extends MixedType< 6 | boolean, 7 | DataType, 8 | E, 9 | BooleanTypeLocale 10 | > { 11 | constructor(errorMessage?: E | string) { 12 | super('boolean'); 13 | super.pushRule({ 14 | onValid: v => typeof v === 'boolean', 15 | errorMessage: errorMessage || this.locale.type 16 | }); 17 | } 18 | } 19 | 20 | export default function getBooleanType(errorMessage?: E) { 21 | return new BooleanType(errorMessage); 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2.feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💄 Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 13 | 14 | ### What problem does this feature solve? 15 | 16 | 19 | 20 | ### What does the proposed API look like? 21 | 22 | 25 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-publish.yml: -------------------------------------------------------------------------------- 1 | # see https://help.github.com/cn/actions/language-and-framework-guides/publishing-nodejs-packages 2 | 3 | name: Node.js Package 4 | 5 | on: 6 | push: 7 | tags: ['*'] 8 | 9 | jobs: 10 | publish: 11 | name: 'Publish' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | # Setup .npmrc file to publish to npm 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: '12.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | - name: Install dependencies 21 | run: npm install 22 | - run: npm publish --access public 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | lib 40 | es 41 | 42 | .vscode 43 | yarn.lock 44 | .DS_Store -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { SchemaModel, Schema } from './Schema'; 2 | import { default as MixedType } from './MixedType'; 3 | import { default as StringType } from './StringType'; 4 | import { default as NumberType } from './NumberType'; 5 | import { default as ArrayType } from './ArrayType'; 6 | import { default as DateType } from './DateType'; 7 | import { default as ObjectType } from './ObjectType'; 8 | import { default as BooleanType } from './BooleanType'; 9 | 10 | export type { 11 | CheckResult, 12 | SchemaCheckResult, 13 | SchemaDeclaration, 14 | CheckType, 15 | RuleType, 16 | ValidCallbackType 17 | } from './types'; 18 | 19 | export { 20 | SchemaModel, 21 | Schema, 22 | MixedType, 23 | StringType, 24 | NumberType, 25 | ArrayType, 26 | DateType, 27 | ObjectType, 28 | BooleanType 29 | }; 30 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import defaultLocale from './default'; 2 | 3 | export type PickKeys = { 4 | [keys in keyof T]: T[keys]; 5 | }; 6 | 7 | export type Locale = PickKeys; 8 | export type MixedTypeLocale = PickKeys; 9 | export type ArrayTypeLocale = PickKeys & MixedTypeLocale; 10 | export type ObjectTypeLocale = PickKeys & MixedTypeLocale; 11 | export type BooleanTypeLocale = PickKeys & MixedTypeLocale; 12 | export type StringTypeLocale = PickKeys & MixedTypeLocale; 13 | export type NumberTypeLocale = PickKeys & MixedTypeLocale; 14 | export type DateTypeLocale = PickKeys & MixedTypeLocale; 15 | 16 | export default defaultLocale; 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Report a reproducible bug or regression 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 13 | 14 | ### What version of schema-typed are you using? 15 | 16 | 17 | 18 | ### Describe the Bug 19 | 20 | 21 | 22 | ### Expected Behavior 23 | 24 | 25 | 26 | ### To Reproduce 27 | 28 | 29 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const OFF = 0; 2 | const WARNING = 1; 3 | const ERROR = 2; 4 | 5 | module.exports = { 6 | env: { 7 | browser: true, 8 | es6: true 9 | }, 10 | parser: '@typescript-eslint/parser', 11 | extends: [ 12 | 'plugin:@typescript-eslint/recommended', 13 | 'prettier/@typescript-eslint', 14 | 'plugin:prettier/recommended' 15 | ], 16 | parserOptions: {}, 17 | plugins: ['@typescript-eslint'], 18 | rules: { 19 | quotes: [ERROR, 'single'], 20 | semi: [ERROR, 'always'], 21 | 'space-infix-ops': ERROR, 22 | 'prefer-spread': ERROR, 23 | 'no-multi-spaces': ERROR, 24 | 'class-methods-use-this': WARNING, 25 | 'arrow-parens': [ERROR, 'as-needed'], 26 | '@typescript-eslint/no-unused-vars': ERROR, 27 | '@typescript-eslint/no-explicit-any': OFF, 28 | '@typescript-eslint/explicit-function-return-type': OFF, 29 | '@typescript-eslint/explicit-member-accessibility': OFF, 30 | '@typescript-eslint/no-namespace': OFF, 31 | '@typescript-eslint/explicit-module-boundary-types': OFF 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 RSuite Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | node-version: [16.x, 18.x, 20.x] 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm run build --if-present 30 | - run: npm test 31 | 32 | - name: Coveralls GitHub Action 33 | uses: coverallsapp/github-action@v2 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/BooleanTypeSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | require('chai').should(); 4 | 5 | const schema = require('../src'); 6 | const { BooleanType, Schema } = schema; 7 | 8 | describe('#BooleanType', () => { 9 | it('Should be a valid boolean', () => { 10 | const schemaData = { 11 | data: BooleanType(), 12 | data2: BooleanType().isRequired() 13 | }; 14 | 15 | const schema = new Schema(schemaData); 16 | 17 | schema.checkForField('data', { data: true }).hasError.should.equal(false); 18 | schema.checkForField('data', { data: false }).hasError.should.equal(false); 19 | schema.checkForField('data', { data: 0 }).hasError.should.equal(true); 20 | schema.checkForField('data', { data: '' }).hasError.should.equal(false); 21 | schema.checkForField('data', { data: null }).hasError.should.equal(false); 22 | schema.checkForField('data', { data: undefined }).hasError.should.equal(false); 23 | schema.checkForField('data', { data: 0 }).errorMessage.should.equal('data must be a boolean'); 24 | 25 | schema.checkForField('data2', { data2: '' }).hasError.should.equal(true); 26 | schema.checkForField('data2', { data2: null }).hasError.should.equal(true); 27 | schema.checkForField('data2', { data2: undefined }).hasError.should.equal(true); 28 | schema 29 | .checkForField('data2', { data2: '' }) 30 | .errorMessage.should.equal('data2 is a required field'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/utils/createValidatorAsync.ts: -------------------------------------------------------------------------------- 1 | import { CheckResult, RuleType } from '../types'; 2 | import formatErrorMessage, { joinName } from './formatErrorMessage'; 3 | 4 | /** 5 | * Create a data asynchronous validator 6 | * @param data 7 | */ 8 | export function createValidatorAsync(data?: D, name?: string | string[], label?: string) { 9 | function check(errorMessage?: E | string) { 10 | return (checkResult: CheckResult | boolean): CheckResult | null => { 11 | if (checkResult === false) { 12 | return { hasError: true, errorMessage }; 13 | } else if (typeof checkResult === 'object' && (checkResult.hasError || checkResult.array)) { 14 | return checkResult; 15 | } 16 | return null; 17 | }; 18 | } 19 | 20 | return (value: V, rules: RuleType[]) => { 21 | const promises = rules.map(rule => { 22 | const { onValid, errorMessage, params } = rule; 23 | const errorMsg = typeof errorMessage === 'function' ? errorMessage() : errorMessage; 24 | 25 | return Promise.resolve(onValid(value, data, name)).then( 26 | check( 27 | formatErrorMessage(errorMsg, { 28 | ...params, 29 | name: label || joinName(name) 30 | }) 31 | ) 32 | ); 33 | }); 34 | 35 | return Promise.all(promises).then(results => 36 | results.find((item: CheckResult | null) => item && item?.hasError) 37 | ); 38 | }; 39 | } 40 | 41 | export default createValidatorAsync; 42 | -------------------------------------------------------------------------------- /src/DateType.ts: -------------------------------------------------------------------------------- 1 | import { MixedType } from './MixedType'; 2 | import { ErrorMessageType } from './types'; 3 | import { DateTypeLocale } from './locales'; 4 | 5 | export class DateType extends MixedType< 6 | string | Date, 7 | DataType, 8 | E, 9 | DateTypeLocale 10 | > { 11 | constructor(errorMessage?: E | string) { 12 | super('date'); 13 | super.pushRule({ 14 | onValid: value => !/Invalid|NaN/.test(new Date(value).toString()), 15 | errorMessage: errorMessage || this.locale.type 16 | }); 17 | } 18 | 19 | range(min: string | Date, max: string | Date, errorMessage: E | string = this.locale.range) { 20 | super.pushRule({ 21 | onValid: value => new Date(value) >= new Date(min) && new Date(value) <= new Date(max), 22 | errorMessage, 23 | params: { min, max } 24 | }); 25 | return this; 26 | } 27 | 28 | min(min: string | Date, errorMessage: E | string = this.locale.min) { 29 | super.pushRule({ 30 | onValid: value => new Date(value) >= new Date(min), 31 | errorMessage, 32 | params: { min } 33 | }); 34 | return this; 35 | } 36 | 37 | max(max: string | Date, errorMessage: E | string = this.locale.max) { 38 | super.pushRule({ 39 | onValid: value => new Date(value) <= new Date(max), 40 | errorMessage, 41 | params: { max } 42 | }); 43 | return this; 44 | } 45 | } 46 | 47 | export default function getDateType(errorMessage?: E) { 48 | return new DateType(errorMessage); 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/createValidator.ts: -------------------------------------------------------------------------------- 1 | import { CheckResult, RuleType } from '../types'; 2 | import formatErrorMessage from './formatErrorMessage'; 3 | function isObj(o: unknown): o is Record { 4 | return o != null && (typeof o === 'object' || typeof o == 'function'); 5 | } 6 | function isPromiseLike(v: unknown): v is Promise { 7 | return v instanceof Promise || (isObj(v) && typeof v.then === 'function'); 8 | } 9 | /** 10 | * Create a data validator 11 | * @param data 12 | */ 13 | export function createValidator(data?: D, name?: string | string[], label?: string) { 14 | return (value: V, rules: RuleType[]): CheckResult | null => { 15 | for (let i = 0; i < rules.length; i += 1) { 16 | const { onValid, errorMessage, params, isAsync } = rules[i]; 17 | if (isAsync) continue; 18 | const checkResult = onValid(value, data, name); 19 | const errorMsg = typeof errorMessage === 'function' ? errorMessage() : errorMessage; 20 | 21 | if (checkResult === false) { 22 | return { 23 | hasError: true, 24 | errorMessage: formatErrorMessage(errorMsg, { 25 | ...params, 26 | name: label || (Array.isArray(name) ? name.join('.') : name) 27 | }) 28 | }; 29 | } else if (isPromiseLike(checkResult)) { 30 | throw new Error( 31 | 'synchronous validator had an async result, you should probably call "checkAsync()"' 32 | ); 33 | } else if (typeof checkResult === 'object' && (checkResult.hasError || checkResult.array)) { 34 | return checkResult; 35 | } 36 | } 37 | 38 | return null; 39 | }; 40 | } 41 | 42 | export default createValidator; 43 | -------------------------------------------------------------------------------- /src/utils/shallowEqual.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * From: https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/shallowEqual.js 3 | * @providesModule shallowEqual 4 | * @typechecks 5 | * @flow 6 | */ 7 | 8 | const hasOwnProperty = Object.prototype.hasOwnProperty; 9 | 10 | /** 11 | * inlined Object.is polyfill to avoid requiring consumers ship their own 12 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is 13 | */ 14 | function is(x: any, y: any): boolean { 15 | // SameValue algorithm 16 | if (x === y) { 17 | // Steps 1-5, 7-10 18 | // Steps 6.b-6.e: +0 != -0 19 | // Added the nonzero y check to make Flow happy, but it is redundant 20 | return x !== 0 || y !== 0 || 1 / x === 1 / y; 21 | } 22 | // Step 6.a: NaN == NaN 23 | return x !== x && y !== y; 24 | } 25 | 26 | /** 27 | * Performs equality by iterating through keys on an object and returning false 28 | * when any key has values which are not strictly equal between the arguments. 29 | * Returns true when the values of all keys are strictly equal. 30 | */ 31 | function shallowEqual(objA: any, objB: any): boolean { 32 | if (is(objA, objB)) { 33 | return true; 34 | } 35 | 36 | if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { 37 | return false; 38 | } 39 | 40 | const keysA = Object.keys(objA); 41 | const keysB = Object.keys(objB); 42 | 43 | if (keysA.length !== keysB.length) { 44 | return false; 45 | } 46 | 47 | // Test for A's keys different from B. 48 | for (let i = 0; i < keysA.length; i += 1) { 49 | if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { 50 | return false; 51 | } 52 | } 53 | 54 | return true; 55 | } 56 | 57 | export default shallowEqual; 58 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { ArrayType } from './ArrayType'; 2 | import { BooleanType } from './BooleanType'; 3 | import { DateType } from './DateType'; 4 | import { NumberType } from './NumberType'; 5 | import { StringType } from './StringType'; 6 | import { ObjectType } from './ObjectType'; 7 | 8 | export type TypeName = 'array' | 'string' | 'boolean' | 'number' | 'object' | 'date'; 9 | 10 | export interface CheckResult { 11 | hasError?: boolean; 12 | errorMessage?: E | string; 13 | object?: { 14 | [P in keyof DataType]: CheckResult; 15 | }; 16 | array?: CheckResult[]; 17 | } 18 | export type ErrorMessageType = string; 19 | export type ValidCallbackType = ( 20 | value: V, 21 | data?: D, 22 | fieldName?: string | string[] 23 | ) => CheckResult | boolean; 24 | 25 | export type AsyncValidCallbackType = ( 26 | value: V, 27 | data?: D, 28 | fieldName?: string | string[] 29 | ) => CheckResult | boolean | Promise>; 30 | 31 | export type PlainObject = any> = { 32 | [P in keyof T]: T; 33 | }; 34 | 35 | export interface RuleType { 36 | onValid: AsyncValidCallbackType; 37 | errorMessage?: any; 38 | priority?: boolean; 39 | params?: any; 40 | isAsync?: boolean; 41 | } 42 | 43 | export type CheckType = X extends string 44 | ? StringType | DateType | NumberType 45 | : X extends number 46 | ? NumberType 47 | : X extends boolean 48 | ? BooleanType 49 | : X extends Date 50 | ? DateType 51 | : X extends Array 52 | ? ArrayType 53 | : X extends Record 54 | ? ObjectType 55 | : 56 | | StringType 57 | | NumberType 58 | | BooleanType 59 | | ArrayType 60 | | DateType 61 | | ObjectType; 62 | 63 | export type SchemaDeclaration = { 64 | [P in keyof T]: CheckType; 65 | }; 66 | 67 | export type SchemaCheckResult = { 68 | [P in keyof T]?: CheckResult; 69 | }; 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schema-typed", 3 | "version": "2.4.2", 4 | "description": "Schema for data modeling & validation", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "types": "lib/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint src/**/*.ts", 10 | "build": "tsc --outDir lib && tsc -p tsconfig-es.json --outDir es", 11 | "prepublishOnly": "npm run test && npm run build", 12 | "tdd": "mocha --watch", 13 | "test": "npm run lint && npm run test:once", 14 | "test:once": "nyc --reporter=lcovonly --reporter=html mocha", 15 | "doctoc:": "doctoc README.md", 16 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 17 | "version": "npm run changelog && git add -A" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/rsuite/schema-typed.git" 22 | }, 23 | "keywords": [ 24 | "schema", 25 | "validation" 26 | ], 27 | "contributors": [ 28 | "A2ZH", 29 | "Simon Guo " 30 | ], 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/rsuite/schema-typed/issues" 34 | }, 35 | "files": [ 36 | "lib", 37 | "es", 38 | "src", 39 | "types" 40 | ], 41 | "homepage": "https://github.com/rsuite/schema-typed#readme", 42 | "dependencies": { 43 | "lodash": "^4.17.21" 44 | }, 45 | "devDependencies": { 46 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 47 | "@types/node": "^20.12.5", 48 | "@typescript-eslint/eslint-plugin": "^4.29.3", 49 | "@typescript-eslint/parser": "^4.29.3", 50 | "chai": "^3.5.0", 51 | "conventional-changelog-cli": "^2.1.1", 52 | "coveralls": "^3.1.0", 53 | "cross-env": "^6.0.3", 54 | "del": "^6.0.0", 55 | "eslint": "^6.7.2", 56 | "eslint-config-prettier": "^6.11.0", 57 | "eslint-plugin-import": "^2.19.1", 58 | "eslint-plugin-prettier": "^3.1.4", 59 | "istanbul": "^0.4.5", 60 | "mocha": "^10.2.0", 61 | "nyc": "^15.1.0", 62 | "object-flaser": "^0.1.1", 63 | "prettier": "^2.2.1", 64 | "ts-node": "^10.9.2", 65 | "typescript": "^4.2.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/locales/default.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | mixed: { 3 | isRequired: '${name} is a required field', 4 | isRequiredOrEmpty: '${name} is a required field', 5 | equalTo: '${name} must be the same as ${toFieldName}' 6 | }, 7 | array: { 8 | type: '${name} must be an array', 9 | rangeLength: '${name} must contain ${minLength} to ${maxLength} items', 10 | minLength: '${name} field must have at least ${minLength} items', 11 | maxLength: '${name} field must have less than or equal to ${maxLength} items', 12 | unrepeatable: '${name} must have non-repeatable items' 13 | }, 14 | boolean: { 15 | type: '${name} must be a boolean' 16 | }, 17 | date: { 18 | type: '${name} must be a date', 19 | min: '${name} field must be later than ${min}', 20 | max: '${name} field must be at earlier than ${max}', 21 | range: '${name} field must be between ${min} and ${max}' 22 | }, 23 | number: { 24 | type: '${name} must be a number', 25 | isInteger: '${name} must be an integer', 26 | pattern: '${name} is invalid', 27 | isOneOf: '${name} must be one of the following values: ${values}', 28 | range: '${name} field must be between ${min} and ${max}', 29 | min: '${name} must be greater than or equal to ${min}', 30 | max: '${name} must be less than or equal to ${max}' 31 | }, 32 | string: { 33 | type: '${name} must be a string', 34 | containsLetter: '${name} field must contain letters', 35 | containsUppercaseLetter: '${name} must be a upper case string', 36 | containsLowercaseLetter: '${name} must be a lowercase string', 37 | containsLetterOnly: '${name} must all be letters', 38 | containsNumber: '${name} field must contain numbers', 39 | isOneOf: '${name} must be one of the following values: ${values}', 40 | isEmail: '${name} must be a valid email', 41 | isURL: '${name} must be a valid URL', 42 | isHex: '${name} must be a valid hexadecimal', 43 | pattern: '${name} is invalid', 44 | rangeLength: '${name} must contain ${minLength} to ${maxLength} characters', 45 | minLength: '${name} must be at least ${minLength} characters', 46 | maxLength: '${name} must be at most ${maxLength} characters' 47 | }, 48 | object: { 49 | type: '${name} must be an object' 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/NumberType.ts: -------------------------------------------------------------------------------- 1 | import { MixedType } from './MixedType'; 2 | import { ErrorMessageType } from './types'; 3 | import { NumberTypeLocale } from './locales'; 4 | 5 | function toNumber(value: string | number) { 6 | return +value; 7 | } 8 | 9 | export class NumberType extends MixedType< 10 | number | string, 11 | DataType, 12 | E, 13 | NumberTypeLocale 14 | > { 15 | constructor(errorMessage?: E | string) { 16 | super('number'); 17 | super.pushRule({ 18 | onValid: value => /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(value + ''), 19 | errorMessage: errorMessage || this.locale.type 20 | }); 21 | } 22 | 23 | isInteger(errorMessage: E | string = this.locale.isInteger) { 24 | super.pushRule({ 25 | onValid: value => /^-?\d+$/.test(value + ''), 26 | errorMessage 27 | }); 28 | 29 | return this; 30 | } 31 | 32 | pattern(regexp: RegExp, errorMessage: E | string = this.locale.pattern) { 33 | super.pushRule({ 34 | onValid: value => regexp.test(value + ''), 35 | errorMessage, 36 | params: { regexp } 37 | }); 38 | return this; 39 | } 40 | 41 | isOneOf(values: number[], errorMessage: E | string = this.locale.isOneOf) { 42 | super.pushRule({ 43 | onValid: value => values.includes(toNumber(value)), 44 | errorMessage, 45 | params: { values } 46 | }); 47 | return this; 48 | } 49 | 50 | range(min: number, max: number, errorMessage: E | string = this.locale.range) { 51 | super.pushRule({ 52 | onValid: value => toNumber(value) >= min && toNumber(value) <= max, 53 | errorMessage, 54 | params: { min, max } 55 | }); 56 | return this; 57 | } 58 | 59 | min(min: number, errorMessage: E | string = this.locale.min) { 60 | super.pushRule({ 61 | onValid: value => toNumber(value) >= min, 62 | errorMessage, 63 | params: { min } 64 | }); 65 | return this; 66 | } 67 | 68 | max(max: number, errorMessage: E | string = this.locale.max) { 69 | super.pushRule({ 70 | onValid: value => toNumber(value) <= max, 71 | errorMessage, 72 | params: { max } 73 | }); 74 | return this; 75 | } 76 | } 77 | 78 | export default function getNumberType(errorMessage?: E) { 79 | return new NumberType(errorMessage); 80 | } 81 | -------------------------------------------------------------------------------- /test/DateTypeSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | require('chai').should(); 4 | 5 | const schema = require('../src'); 6 | const { DateType, Schema } = schema; 7 | 8 | describe('#DateType', () => { 9 | it('Should be a valid date', () => { 10 | const schemaData = { 11 | data: DateType(), 12 | data2: DateType().isRequired() 13 | }; 14 | 15 | const schema = new Schema(schemaData); 16 | 17 | schema.checkForField('data', { data: new Date() }).hasError.should.equal(false); 18 | schema.checkForField('data', { data: 'date' }).hasError.should.equal(true); 19 | 20 | schema.checkForField('data', { data: '' }).hasError.should.equal(false); 21 | schema.checkForField('data', { data: null }).hasError.should.equal(false); 22 | schema.checkForField('data', { data: undefined }).hasError.should.equal(false); 23 | schema.checkForField('data', { data: 'date' }).errorMessage.should.equal('data must be a date'); 24 | 25 | schema.checkForField('data2', { data2: '' }).hasError.should.equal(true); 26 | schema.checkForField('data2', { data2: null }).hasError.should.equal(true); 27 | schema.checkForField('data2', { data2: undefined }).hasError.should.equal(true); 28 | schema 29 | .checkForField('data2', { data2: '' }) 30 | .errorMessage.should.equal('data2 is a required field'); 31 | }); 32 | 33 | it('Should be within the date range', () => { 34 | const schemaData = { 35 | data: DateType().range('2020-01-01', '2020-02-01') 36 | }; 37 | const schema = new Schema(schemaData); 38 | schema.checkForField('data', { data: '2020-01-02' }).hasError.should.equal(false); 39 | schema.checkForField('data', { data: '2020-02-02' }).hasError.should.equal(true); 40 | schema 41 | .checkForField('data', { data: '2020-02-02' }) 42 | .errorMessage.should.equal('data field must be between 2020-01-01 and 2020-02-01'); 43 | }); 44 | 45 | it('Should not be less than the minimum date', () => { 46 | const schemaData = { 47 | data: DateType().min('2020-01-01') 48 | }; 49 | const schema = new Schema(schemaData); 50 | schema.checkForField('data', { data: '2020-01-02' }).hasError.should.equal(false); 51 | schema.checkForField('data', { data: '2019-12-30' }).hasError.should.equal(true); 52 | schema 53 | .checkForField('data', { data: '2019-12-30' }) 54 | .errorMessage.should.equal('data field must be later than 2020-01-01'); 55 | }); 56 | 57 | it('Should not exceed the maximum date', () => { 58 | const schemaData = { 59 | data: DateType().max('2020-01-01') 60 | }; 61 | const schema = new Schema(schemaData); 62 | schema.checkForField('data', { data: '2019-12-30' }).hasError.should.equal(false); 63 | schema.checkForField('data', { data: '2020-01-02' }).hasError.should.equal(true); 64 | schema 65 | .checkForField('data', { data: '2020-01-02' }) 66 | .errorMessage.should.equal('data field must be at earlier than 2020-01-01'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/utilsSpec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { formatErrorMessage, checkRequired, shallowEqual, pathTransform } from '../src/utils'; 3 | 4 | chai.should(); 5 | 6 | describe('#utils', () => { 7 | describe('## formatErrorMessage', () => { 8 | it('Should output the parameter `email`', () => { 9 | const str = formatErrorMessage('${name} is a required field', { name: 'email' }); 10 | const str2 = formatErrorMessage('${name} is a required field', { name1: 'email' }); 11 | str.should.equal('email is a required field'); 12 | str2.should.equal('${name} is a required field'); 13 | }); 14 | 15 | it('Should output multiple parameters', () => { 16 | const str = formatErrorMessage('${name} must contain ${minLength} to ${maxLength} items', { 17 | name: 'tag', 18 | minLength: 3, 19 | maxLength: 10 20 | }); 21 | 22 | const str2 = formatErrorMessage('${name} must contain ${minLength} to ${maxLength} items', { 23 | name: 'tag', 24 | minLength1: 3, 25 | maxLength: 10 26 | }); 27 | str.should.equal('tag must contain 3 to 10 items'); 28 | str2.should.equal('tag must contain ${minLength} to 10 items'); 29 | }); 30 | 31 | it('Should not replace parameters', () => { 32 | const str = formatErrorMessage('name is a required field'); 33 | str.should.equal('name is a required field'); 34 | }); 35 | 36 | it('Should return unprocessed parameters', () => { 37 | const str = formatErrorMessage(true); 38 | str.should.equal(true); 39 | }); 40 | }); 41 | 42 | describe('## checkRequired', () => { 43 | it('Should check string, null and undefined', () => { 44 | checkRequired('1').should.equal(true); 45 | checkRequired(0).should.equal(true); 46 | checkRequired(' ').should.equal(true); 47 | checkRequired(' ', true).should.equal(false); 48 | 49 | checkRequired('').should.equal(false); 50 | checkRequired().should.equal(false); 51 | checkRequired(null).should.equal(false); 52 | 53 | checkRequired('', false, true).should.equal(true); 54 | checkRequired(undefined, false, true).should.equal(false); 55 | checkRequired(null, false, true).should.equal(false); 56 | }); 57 | 58 | it('Should check array', () => { 59 | checkRequired([]).should.equal(false); 60 | checkRequired([1]).should.equal(true); 61 | checkRequired([undefined]).should.equal(true); 62 | checkRequired(['']).should.equal(true); 63 | }); 64 | }); 65 | 66 | describe('## shallowEqual', () => { 67 | it('Should compare the object', () => { 68 | const obj1 = { a: 1, b: 2 }; 69 | const obj2 = { a: 1, b: 2 }; 70 | const obj3 = { a: 1, b: 3 }; 71 | const obj4 = { a: 1, b: 2, c: 3 }; 72 | 73 | shallowEqual(obj1, obj2).should.equal(true); 74 | shallowEqual(obj1, obj3).should.equal(false); 75 | shallowEqual(obj1, obj4).should.equal(false); 76 | }); 77 | 78 | it('Should compare the array', () => { 79 | const arr1 = [1, 2]; 80 | const arr2 = [1, 2]; 81 | const arr3 = [1, 3]; 82 | const arr4 = [1, 2, 3]; 83 | 84 | shallowEqual(arr1, arr2).should.equal(true); 85 | shallowEqual(arr1, arr3).should.equal(false); 86 | shallowEqual(arr1, arr4).should.equal(false); 87 | }); 88 | 89 | it('Should compare the object and array', () => { 90 | const obj = { a: 1, b: [1, 2] }; 91 | const obj1 = { a: 1, b: [1, 2] }; 92 | const obj2 = { a: 1, b: [1, 3] }; 93 | const obj3 = { a: 1, b: [1, 2, 3] }; 94 | 95 | shallowEqual(obj, obj1).should.equal(false); 96 | shallowEqual(obj, obj2).should.equal(false); 97 | shallowEqual(obj, obj3).should.equal(false); 98 | }); 99 | }); 100 | 101 | describe('## pathTransform', () => { 102 | it('Should transform the path', () => { 103 | pathTransform('a').should.equal('a'); 104 | pathTransform('a.b').should.equal('a.object.b'); 105 | pathTransform('a.0').should.equal('a.array.0'); 106 | pathTransform('a.0.1').should.equal('a.array.0.array.1'); 107 | pathTransform('a.b.c').should.equal('a.object.b.object.c'); 108 | pathTransform('a.0.b').should.equal('a.array.0.object.b'); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/ArrayType.ts: -------------------------------------------------------------------------------- 1 | import { MixedType, arrayTypeSchemaSpec } from './MixedType'; 2 | import { PlainObject, CheckResult, ErrorMessageType } from './types'; 3 | import { ArrayTypeLocale } from './locales'; 4 | 5 | export class ArrayType extends MixedType< 6 | any[], 7 | DataType, 8 | E, 9 | ArrayTypeLocale 10 | > { 11 | [arrayTypeSchemaSpec]: MixedType | MixedType[]; 12 | private isArrayTypeNested = false; 13 | 14 | constructor(errorMessage?: E | string) { 15 | super('array'); 16 | super.pushRule({ 17 | onValid: v => { 18 | // Skip array type check for nested array elements 19 | if (this.isArrayTypeNested) { 20 | return true; 21 | } 22 | return Array.isArray(v); 23 | }, 24 | errorMessage: errorMessage || this.locale.type 25 | }); 26 | } 27 | 28 | rangeLength( 29 | minLength: number, 30 | maxLength: number, 31 | errorMessage: E | string = this.locale.rangeLength 32 | ) { 33 | super.pushRule({ 34 | onValid: (value: string[]) => value.length >= minLength && value.length <= maxLength, 35 | errorMessage, 36 | params: { minLength, maxLength } 37 | }); 38 | return this; 39 | } 40 | 41 | minLength(minLength: number, errorMessage: E | string = this.locale.minLength) { 42 | super.pushRule({ 43 | onValid: value => value.length >= minLength, 44 | errorMessage, 45 | params: { minLength } 46 | }); 47 | 48 | return this; 49 | } 50 | 51 | maxLength(maxLength: number, errorMessage: E | string = this.locale.maxLength) { 52 | super.pushRule({ 53 | onValid: value => value.length <= maxLength, 54 | errorMessage, 55 | params: { maxLength } 56 | }); 57 | return this; 58 | } 59 | 60 | unrepeatable(errorMessage: E | string = this.locale.unrepeatable) { 61 | super.pushRule({ 62 | onValid: items => { 63 | const hash: PlainObject = {}; 64 | for (const i in items) { 65 | if (hash[items[i]]) { 66 | return false; 67 | } 68 | hash[items[i]] = true; 69 | } 70 | return true; 71 | }, 72 | errorMessage 73 | }); 74 | 75 | return this; 76 | } 77 | 78 | of(...types: MixedType[]) { 79 | if (types.length === 1) { 80 | const type = types[0]; 81 | this[arrayTypeSchemaSpec] = type; 82 | 83 | // Mark inner ArrayType as nested when dealing with nested arrays 84 | if (type instanceof ArrayType) { 85 | type.isArrayTypeNested = true; 86 | } 87 | 88 | super.pushRule({ 89 | onValid: (items, data, fieldName) => { 90 | // For non-array values in nested arrays, pass directly to inner type validation 91 | if (!Array.isArray(items) && this.isArrayTypeNested) { 92 | return type.check(items, data, fieldName); 93 | } 94 | 95 | // For non-array values in non-nested arrays, return array type error 96 | if (!Array.isArray(items)) { 97 | return { 98 | hasError: true, 99 | errorMessage: this.locale.type 100 | }; 101 | } 102 | 103 | const checkResults = items.map((value, index) => { 104 | const name = Array.isArray(fieldName) 105 | ? [...fieldName, `[${index}]`] 106 | : [fieldName, `[${index}]`]; 107 | 108 | return type.check(value, data, name as string[]); 109 | }); 110 | const hasError = !!checkResults.find(item => item?.hasError); 111 | 112 | return { 113 | hasError, 114 | array: checkResults 115 | } as CheckResult; 116 | } 117 | }); 118 | } else { 119 | this[arrayTypeSchemaSpec] = types; 120 | super.pushRule({ 121 | onValid: (items, data, fieldName) => { 122 | const checkResults = items.map((value, index) => { 123 | const name = Array.isArray(fieldName) 124 | ? [...fieldName, `[${index}]`] 125 | : [fieldName, `[${index}]`]; 126 | 127 | return types[index].check(value, data, name as string[]); 128 | }); 129 | const hasError = !!checkResults.find(item => item?.hasError); 130 | 131 | return { 132 | hasError, 133 | array: checkResults 134 | } as CheckResult; 135 | } 136 | }); 137 | } 138 | 139 | return this; 140 | } 141 | } 142 | 143 | export default function getArrayType(errorMessage?: E) { 144 | return new ArrayType(errorMessage); 145 | } 146 | -------------------------------------------------------------------------------- /src/StringType.ts: -------------------------------------------------------------------------------- 1 | import { MixedType } from './MixedType'; 2 | import { ErrorMessageType } from './types'; 3 | import { StringTypeLocale } from './locales'; 4 | 5 | export class StringType extends MixedType< 6 | string, 7 | DataType, 8 | E, 9 | StringTypeLocale 10 | > { 11 | constructor(errorMessage?: E | string) { 12 | super('string'); 13 | super.pushRule({ 14 | onValid: v => typeof v === 'string', 15 | errorMessage: errorMessage || this.locale.type 16 | }); 17 | } 18 | 19 | containsLetter(errorMessage: E | string = this.locale.containsLetter) { 20 | super.pushRule({ 21 | onValid: v => /[a-zA-Z]/.test(v), 22 | errorMessage 23 | }); 24 | return this; 25 | } 26 | 27 | containsUppercaseLetter(errorMessage: E | string = this.locale.containsUppercaseLetter) { 28 | super.pushRule({ 29 | onValid: v => /[A-Z]/.test(v), 30 | errorMessage 31 | }); 32 | return this; 33 | } 34 | 35 | containsLowercaseLetter(errorMessage: E | string = this.locale.containsLowercaseLetter) { 36 | super.pushRule({ 37 | onValid: v => /[a-z]/.test(v), 38 | errorMessage 39 | }); 40 | return this; 41 | } 42 | 43 | containsLetterOnly(errorMessage: E | string = this.locale.containsLetterOnly) { 44 | super.pushRule({ 45 | onValid: v => /^[a-zA-Z]+$/.test(v), 46 | errorMessage 47 | }); 48 | return this; 49 | } 50 | 51 | containsNumber(errorMessage: E | string = this.locale.containsNumber) { 52 | super.pushRule({ 53 | onValid: v => /[0-9]/.test(v), 54 | errorMessage 55 | }); 56 | return this; 57 | } 58 | 59 | isOneOf(values: string[], errorMessage: E | string = this.locale.isOneOf) { 60 | super.pushRule({ 61 | onValid: v => !!~values.indexOf(v), 62 | errorMessage, 63 | params: { values } 64 | }); 65 | return this; 66 | } 67 | 68 | isEmail(errorMessage: E | string = this.locale.isEmail) { 69 | // http://emailregex.com/ 70 | const regexp = 71 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 72 | super.pushRule({ 73 | onValid: v => regexp.test(v), 74 | errorMessage 75 | }); 76 | return this; 77 | } 78 | 79 | isURL( 80 | errorMessage: E | string = this.locale.isURL, 81 | options?: { 82 | allowMailto?: boolean; 83 | } 84 | ) { 85 | const regexp = new RegExp( 86 | options?.allowMailto ?? false 87 | ? '^(?: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]*)?$' 88 | : '^(?!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]*)?$', 89 | 'i' 90 | ); 91 | super.pushRule({ 92 | onValid: v => regexp.test(v), 93 | errorMessage 94 | }); 95 | return this; 96 | } 97 | isHex(errorMessage: E | string = this.locale.isHex) { 98 | const regexp = /^#?([a-f0-9]{6}|[a-f0-9]{3})$/i; 99 | super.pushRule({ 100 | onValid: v => regexp.test(v), 101 | errorMessage 102 | }); 103 | return this; 104 | } 105 | pattern(regexp: RegExp, errorMessage: E | string = this.locale.pattern) { 106 | super.pushRule({ 107 | onValid: v => regexp.test(v), 108 | errorMessage, 109 | params: { regexp } 110 | }); 111 | return this; 112 | } 113 | 114 | rangeLength( 115 | minLength: number, 116 | maxLength: number, 117 | errorMessage: E | string = this.locale.rangeLength 118 | ) { 119 | super.pushRule({ 120 | onValid: value => value.length >= minLength && value.length <= maxLength, 121 | errorMessage, 122 | params: { minLength, maxLength } 123 | }); 124 | return this; 125 | } 126 | 127 | minLength(minLength: number, errorMessage: E | string = this.locale.minLength) { 128 | super.pushRule({ 129 | onValid: value => Array.from(value).length >= minLength, 130 | errorMessage, 131 | params: { minLength } 132 | }); 133 | return this; 134 | } 135 | 136 | maxLength(maxLength: number, errorMessage: E | string = this.locale.maxLength) { 137 | super.pushRule({ 138 | onValid: value => Array.from(value).length <= maxLength, 139 | errorMessage, 140 | params: { maxLength } 141 | }); 142 | return this; 143 | } 144 | } 145 | 146 | export default function getStringType(errorMessage?: E) { 147 | return new StringType(errorMessage); 148 | } 149 | -------------------------------------------------------------------------------- /src/ObjectType.ts: -------------------------------------------------------------------------------- 1 | import { MixedType, schemaSpecKey } from './MixedType'; 2 | import { 3 | createValidator, 4 | createValidatorAsync, 5 | checkRequired, 6 | isEmpty, 7 | formatErrorMessage 8 | } from './utils'; 9 | import { PlainObject, SchemaDeclaration, CheckResult, ErrorMessageType } from './types'; 10 | import { ObjectTypeLocale } from './locales'; 11 | 12 | export class ObjectType extends MixedType< 13 | PlainObject, 14 | DataType, 15 | E, 16 | ObjectTypeLocale 17 | > { 18 | [schemaSpecKey]: SchemaDeclaration; 19 | constructor(errorMessage?: E | string) { 20 | super('object'); 21 | super.pushRule({ 22 | onValid: v => typeof v === 'object', 23 | errorMessage: errorMessage || this.locale.type 24 | }); 25 | } 26 | 27 | check(value: PlainObject = this.value, data?: DataType, fieldName?: string | string[]) { 28 | const check = (value: any, data: any, type: any, childFieldKey?: string) => { 29 | if (type.required && !checkRequired(value, type.trim, type.emptyAllowed)) { 30 | return { 31 | hasError: true, 32 | errorMessage: formatErrorMessage(type.requiredMessage || type.locale?.isRequired, { 33 | name: type.fieldLabel || childFieldKey || fieldName 34 | }) 35 | }; 36 | } 37 | 38 | if (type[schemaSpecKey] && typeof value === 'object') { 39 | const checkResultObject: any = {}; 40 | let hasError = false; 41 | Object.entries(type[schemaSpecKey]).forEach(([k, v]) => { 42 | const checkResult = check(value[k], value, v, k); 43 | if (checkResult?.hasError) { 44 | hasError = true; 45 | } 46 | checkResultObject[k] = checkResult; 47 | }); 48 | 49 | return { hasError, object: checkResultObject }; 50 | } 51 | 52 | const validator = createValidator( 53 | data, 54 | childFieldKey || fieldName, 55 | type.fieldLabel 56 | ); 57 | const checkStatus = validator(value, type.priorityRules); 58 | 59 | if (checkStatus) { 60 | return checkStatus; 61 | } 62 | 63 | if (!type.required && isEmpty(value)) { 64 | return { hasError: false }; 65 | } 66 | 67 | return validator(value, type.rules) || { hasError: false }; 68 | }; 69 | 70 | return check(value, data, this) as CheckResult; 71 | } 72 | 73 | checkAsync(value: PlainObject = this.value, data?: DataType, fieldName?: string | string[]) { 74 | const check = (value: any, data: any, type: any, childFieldKey?: string) => { 75 | if (type.required && !checkRequired(value, type.trim, type.emptyAllowed)) { 76 | return Promise.resolve({ 77 | hasError: true, 78 | errorMessage: formatErrorMessage(type.requiredMessage || type.locale?.isRequired, { 79 | name: type.fieldLabel || childFieldKey || fieldName 80 | }) 81 | }); 82 | } 83 | 84 | const validator = createValidatorAsync( 85 | data, 86 | childFieldKey || fieldName, 87 | type.fieldLabel 88 | ); 89 | 90 | return new Promise(resolve => { 91 | if (type[schemaSpecKey] && typeof value === 'object') { 92 | const checkResult: any = {}; 93 | const checkAll: Promise[] = []; 94 | const keys: string[] = []; 95 | Object.entries(type[schemaSpecKey]).forEach(([k, v]) => { 96 | checkAll.push(check(value[k], value, v, k)); 97 | keys.push(k); 98 | }); 99 | 100 | return Promise.all(checkAll).then(values => { 101 | let hasError = false; 102 | values.forEach((v: any, index: number) => { 103 | if (v?.hasError) { 104 | hasError = true; 105 | } 106 | checkResult[keys[index]] = v; 107 | }); 108 | 109 | resolve({ hasError, object: checkResult }); 110 | }); 111 | } 112 | 113 | return validator(value, type.priorityRules) 114 | .then((checkStatus: CheckResult | void | null) => { 115 | if (checkStatus) { 116 | resolve(checkStatus); 117 | } 118 | }) 119 | .then(() => { 120 | if (!type.required && isEmpty(value)) { 121 | resolve({ hasError: false }); 122 | } 123 | }) 124 | .then(() => validator(value, type.rules)) 125 | .then((checkStatus: CheckResult | void | null) => { 126 | if (checkStatus) { 127 | resolve(checkStatus); 128 | } 129 | resolve({ hasError: false }); 130 | }); 131 | }); 132 | }; 133 | 134 | return check(value, data, this) as Promise>; 135 | } 136 | 137 | /** 138 | * @example 139 | * ObjectType().shape({ 140 | * name: StringType(), 141 | * age: NumberType() 142 | * }) 143 | */ 144 | shape(fields: SchemaDeclaration) { 145 | this[schemaSpecKey] = fields; 146 | return this; 147 | } 148 | } 149 | 150 | export default function getObjectType(errorMessage?: E) { 151 | return new ObjectType(errorMessage); 152 | } 153 | -------------------------------------------------------------------------------- /test/NumberTypeSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | require('chai').should(); 3 | const schema = require('../src'); 4 | const { NumberType, Schema } = schema; 5 | 6 | describe('#NumberType', () => { 7 | let schemaData = { data: NumberType() }; 8 | let schema = new Schema(schemaData); 9 | 10 | it('Should be a valid number', () => { 11 | schema.checkForField('data', { data: '2.22' }).hasError.should.equal(false); 12 | schema.checkForField('data', { data: 2.22 }).hasError.should.equal(false); 13 | schema.checkForField('data', { data: 2 }).hasError.should.equal(false); 14 | schema.checkForField('data', { data: -222 }).hasError.should.equal(false); 15 | }); 16 | 17 | it('Should not be checked', () => { 18 | schema.checkForField('data', { data: null }).hasError.should.equal(false); 19 | schema.checkForField('data', { data: undefined }).hasError.should.equal(false); 20 | schema.checkForField('data', { data: '' }).hasError.should.equal(false); 21 | }); 22 | 23 | it('Should be a invalid number', () => { 24 | schema.checkForField('data', { data: 'abc' }).hasError.should.equal(true); 25 | schema.checkForField('data', { data: '1abc' }).hasError.should.equal(true); 26 | schema.checkForField('data', { data: {} }).hasError.should.equal(true); 27 | schema.checkForField('data', { data: [] }).hasError.should.equal(true); 28 | schema.checkForField('data', { data: [] }).errorMessage.should.equal('data must be a number'); 29 | }); 30 | 31 | it('True should be a invalid number', () => { 32 | schema.checkForField('data', { data: true }).hasError.should.equal(true); 33 | }); 34 | 35 | it('Function should be a invalid number', () => { 36 | schema.checkForField('data', { data: () => 0 }).hasError.should.equal(true); 37 | }); 38 | 39 | it('Null and Undefined should be a invalid number', () => { 40 | let schemaData = { data: NumberType().isRequired() }; 41 | let schema = new Schema(schemaData); 42 | schema.checkForField('data', { data: null }).hasError.should.equal(true); 43 | schema.checkForField('data', { data: undefined }).hasError.should.equal(true); 44 | }); 45 | 46 | it('Should be an integer', () => { 47 | let schemaData = { data: NumberType().isInteger() }; 48 | let schema = new Schema(schemaData); 49 | schema.checkForField('data', { data: 1 }).hasError.should.equal(false); 50 | schema.checkForField('data', { data: '1' }).hasError.should.equal(false); 51 | schema.checkForField('data', { data: -1 }).hasError.should.equal(false); 52 | schema.checkForField('data', { data: 1.1 }).hasError.should.equal(true); 53 | schema 54 | .checkForField('data', { data: 1.1 }) 55 | .errorMessage.should.equal('data must be an integer'); 56 | }); 57 | 58 | it('Should not be lower than the minimum', () => { 59 | let schemaData = { data: NumberType().min(10) }; 60 | let schema = new Schema(schemaData); 61 | schema.checkForField('data', { data: 10 }).hasError.should.equal(false); 62 | schema.checkForField('data', { data: 9 }).hasError.should.equal(true); 63 | schema 64 | .checkForField('data', { data: 9 }) 65 | .errorMessage.should.equal('data must be greater than or equal to 10'); 66 | }); 67 | 68 | it('Should not exceed the maximum', () => { 69 | let schemaData = { data: NumberType().max(10) }; 70 | let schema = new Schema(schemaData); 71 | schema.checkForField('data', { data: 10 }).hasError.should.equal(false); 72 | schema.checkForField('data', { data: 11 }).hasError.should.equal(true); 73 | schema 74 | .checkForField('data', { data: 11 }) 75 | .errorMessage.should.equal('data must be less than or equal to 10'); 76 | }); 77 | 78 | it('Should be within the range of optional values', () => { 79 | let schemaData = { data: NumberType().range(0, 20) }; 80 | let schema = new Schema(schemaData); 81 | schema.checkForField('data', { data: 0 }).hasError.should.equal(false); 82 | schema.checkForField('data', { data: 20 }).hasError.should.equal(false); 83 | schema.checkForField('data', { data: -1 }).hasError.should.equal(true); 84 | schema.checkForField('data', { data: 21 }).hasError.should.equal(true); 85 | schema 86 | .checkForField('data', { data: -1 }) 87 | .errorMessage.should.equal('data field must be between 0 and 20'); 88 | schema 89 | .checkForField('data', { data: 21 }) 90 | .errorMessage.should.equal('data field must be between 0 and 20'); 91 | }); 92 | 93 | it('Should be within the following value range: 1,2,3,4', () => { 94 | let schemaData = { data: NumberType().isOneOf([1, 2, 3, 4]) }; 95 | let schema = new Schema(schemaData); 96 | schema.checkForField('data', { data: 1 }).hasError.should.equal(false); 97 | schema.checkForField('data', { data: 2 }).hasError.should.equal(false); 98 | schema.checkForField('data', { data: 5 }).hasError.should.equal(true); 99 | schema 100 | .checkForField('data', { data: 5 }) 101 | .errorMessage.should.equal('data must be one of the following values: 1,2,3,4'); 102 | }); 103 | 104 | it('Should allow custom rules', () => { 105 | let schemaData = { data: NumberType().pattern(/^-?1\d+$/) }; 106 | let schema = new Schema(schemaData); 107 | schema.checkForField('data', { data: 11 }).hasError.should.equal(false); 108 | schema.checkForField('data', { data: 12 }).hasError.should.equal(false); 109 | schema.checkForField('data', { data: 22 }).hasError.should.equal(true); 110 | schema.checkForField('data', { data: 22 }).errorMessage.should.equal('data is invalid'); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.4.2](https://github.com/rsuite/schema-typed/compare/v2.4.1...v2.4.2) (2025-04-11) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * prevent infinite loops in circular proxy validation ([#85](https://github.com/rsuite/schema-typed/issues/85)) ([4d147b9](https://github.com/rsuite/schema-typed/commit/4d147b94f8f5752530984ab1602e53eb2c0c708e)) 7 | 8 | 9 | 10 | ## [2.4.1](https://github.com/rsuite/schema-typed/compare/v2.4.0...v2.4.1) (2025-03-24) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * when schema is empty getFieldType throw error with nestedObject ([#84](https://github.com/rsuite/schema-typed/issues/84)) ([1c6754e](https://github.com/rsuite/schema-typed/commit/1c6754edfad2ba426e8610b86111c1e9c9809e04)) 16 | 17 | 18 | 19 | # [2.4.0](https://github.com/rsuite/schema-typed/compare/v2.3.0...v2.4.0) (2025-03-24) 20 | 21 | 22 | ### Features 23 | 24 | * Array support explicit type ([#83](https://github.com/rsuite/schema-typed/issues/83)) ([e716ab4](https://github.com/rsuite/schema-typed/commit/e716ab4b9f20d66da8e3b6b1d2ae46181c59641a)) 25 | 26 | 27 | 28 | # [2.3.0](https://github.com/rsuite/schema-typed/compare/v2.2.2...v2.3.0) (2025-02-06) 29 | 30 | 31 | ### Features 32 | 33 | * **ArrayType:** enhance nested array validation ([#82](https://github.com/rsuite/schema-typed/issues/82)) ([db389b9](https://github.com/rsuite/schema-typed/commit/db389b90a016627982202214dee23f7ee34d6de4)) 34 | 35 | 36 | 37 | ## [2.2.2](https://github.com/rsuite/schema-typed/compare/2.2.1...2.2.2) (2024-04-12) 38 | 39 | 40 | 41 | ## [2.2.1](https://github.com/rsuite/schema-typed/compare/2.2.0...2.2.1) (2024-04-12) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * **ObjectType:** fix required message for properties not being replaced ([#79](https://github.com/rsuite/schema-typed/issues/79)) ([2aab276](https://github.com/rsuite/schema-typed/commit/2aab2768994b42d3572c2d90a926329912811c80)) 47 | 48 | 49 | 50 | # [2.2.0](https://github.com/rsuite/schema-typed/compare/2.1.3...2.2.0) (2024-04-11) 51 | 52 | 53 | ### Features 54 | 55 | * add support for `equalTo` and `proxy` ([#78](https://github.com/rsuite/schema-typed/issues/78)) ([d9f0e55](https://github.com/rsuite/schema-typed/commit/d9f0e555cf532731839584b0c036648001fe0503)) 56 | * add support for `label` method ([#77](https://github.com/rsuite/schema-typed/issues/77)) ([9ff16c3](https://github.com/rsuite/schema-typed/commit/9ff16c346d6f13caabd4910a7d920c1c11eced18)) 57 | * **Schema:** support nested object check with `checkForField` and `checkForFieldAsync` ([#76](https://github.com/rsuite/schema-typed/issues/76)) ([e315aec](https://github.com/rsuite/schema-typed/commit/e315aec657ee230f2cf235861e05b37a7eedd274)) 58 | * **StringType:** add alllowMailto option to isURL rule ([#72](https://github.com/rsuite/schema-typed/issues/72)) ([349dc42](https://github.com/rsuite/schema-typed/commit/349dc429b51db89e7b261ed24aa006435c501685)) 59 | 60 | 61 | 62 | ## [2.1.3](https://github.com/rsuite/schema-typed/compare/2.1.2...2.1.3) (2023-05-06) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * wrong error message when parameter is 0 ([#69](https://github.com/rsuite/schema-typed/issues/69)) ([8b399f7](https://github.com/rsuite/schema-typed/commit/8b399f78143dbf36dd2c837c992687c7560027b3)) 68 | 69 | 70 | 71 | ## [2.1.2](https://github.com/rsuite/schema-typed/compare/2.1.1...2.1.2) (2023-03-10) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * **build:** fix unpublished source code ([#67](https://github.com/rsuite/schema-typed/issues/67)) ([c21ae0a](https://github.com/rsuite/schema-typed/commit/c21ae0a94578907e3fdd0467e5d1a1e3ec7c4d85)) 77 | 78 | 79 | 80 | ## [2.1.1](https://github.com/rsuite/schema-typed/compare/2.1.0...2.1.1) (2023-03-08) 81 | 82 | - chore: change the compilation target of TypeScript from esnext to es2019 83 | 84 | # [2.1.0](https://github.com/rsuite/schema-typed/compare/2.0.4...2.1.0) (2023-03-02) 85 | 86 | ### Features 87 | 88 | - addAsyncRule to allow sync and async rules to run ([#63](https://github.com/rsuite/schema-typed/issues/63)) ([574f9ad](https://github.com/rsuite/schema-typed/commit/574f9ad973af97b8c1bae44c3fcfa3dad608c4d6)) 89 | 90 | ## [2.0.4](https://github.com/rsuite/schema-typed/compare/2.0.3...2.0.4) (2023-03-01) 91 | 92 | ### Bug Fixes 93 | 94 | - promises where not allowed by type ([#61](https://github.com/rsuite/schema-typed/issues/61)) ([9cc665c](https://github.com/rsuite/schema-typed/commit/9cc665c4f72b5a22942d351c961263c179888a7a)) 95 | 96 | ## [2.0.3](https://github.com/rsuite/schema-typed/compare/2.0.2...2.0.3) (2022-06-30) 97 | 98 | ### Bug Fixes 99 | 100 | - **ObjectType:** specifies type of property `object` in the `ObjectType` check result ([#46](https://github.com/rsuite/schema-typed/issues/46)) ([0571e09](https://github.com/rsuite/schema-typed/commit/0571e097217b0c999acaf9e5780bdd289aa46a46)) 101 | 102 | # 2.0.2 103 | 104 | - build(deps): add @babel/runtime #37 105 | 106 | # 2.0.1 107 | 108 | - fix ArrayType.of type error #35 109 | 110 | # 2.0.0 111 | 112 | - feat(locales): add default error messages for all checks ([#27](https://github.com/rsuite/schema-typed/issues/27)) ([03e21d7](https://github.com/rsuite/schema-typed/commit/03e21d77e9a6e0cd4fddcb1adfe8c485025f246b)) 113 | - refactor: refactor the project through typescript. 114 | - feat(MixedType): Added support for `when` method on all types 115 | - feat(MixedType): Replace Type with MixedType. 116 | - feat(ObjectType): Support nested objects in the `shape` method of ObjectType. 117 | 118 | # 1.5.1 119 | 120 | - Update the typescript definition of `addRule` 121 | 122 | # 1.5.0 123 | 124 | - Added support for `isRequiredOrEmpty` in StringType and ArrayType 125 | 126 | # 1.4.0 127 | 128 | - Adding the typescript types declaration in to package 129 | 130 | # 1.3.1 131 | 132 | - Fixed an issue where `isOneOf` was not valid in `StringType` (#18) 133 | 134 | # 1.3.0 135 | 136 | - Added support for ESM 137 | 138 | # 1.2.2 139 | 140 | > Aug 30, 2019 141 | 142 | - **Bugfix**: Fix an issue where addRule is not called 143 | 144 | # 1.2.0 145 | 146 | > Aug 20, 2019 147 | 148 | - **Feature**: Support for async check. ([#14]) 149 | 150 | --- 151 | 152 | [#14]: https://github.com/rsuite/rsuite/pull/14 153 | -------------------------------------------------------------------------------- /src/Schema.ts: -------------------------------------------------------------------------------- 1 | import { SchemaDeclaration, SchemaCheckResult, CheckResult, PlainObject } from './types'; 2 | import { MixedType, getFieldType, getFieldValue } from './MixedType'; 3 | import { set, get, isEmpty, pathTransform } from './utils'; 4 | 5 | interface CheckOptions { 6 | /** 7 | * Check for nested object 8 | */ 9 | nestedObject?: boolean; 10 | } 11 | 12 | export class Schema { 13 | readonly $spec: SchemaDeclaration; 14 | private data: PlainObject; 15 | private checkedFields: string[] = []; 16 | private checkResult: SchemaCheckResult = {}; 17 | 18 | constructor(schema: SchemaDeclaration) { 19 | this.$spec = schema; 20 | } 21 | 22 | private getFieldType( 23 | fieldName: T, 24 | nestedObject?: boolean 25 | ): SchemaDeclaration[T] { 26 | return getFieldType(this.$spec, fieldName as string, nestedObject); 27 | } 28 | 29 | private setFieldCheckResult( 30 | fieldName: string, 31 | checkResult: CheckResult, 32 | nestedObject?: boolean 33 | ) { 34 | if (nestedObject) { 35 | const namePath = fieldName.split('.').join('.object.'); 36 | set(this.checkResult, namePath, checkResult); 37 | 38 | return; 39 | } 40 | 41 | this.checkResult[fieldName] = checkResult; 42 | } 43 | 44 | private setSchemaOptionsForAllType(data: PlainObject) { 45 | if (data === this.data) { 46 | return; 47 | } 48 | 49 | Object.entries(this.$spec).forEach(([key, type]) => { 50 | (type as MixedType).setSchemaOptions(this.$spec as any, data?.[key]); 51 | }); 52 | 53 | this.data = data; 54 | } 55 | 56 | /** 57 | * Get the check result of the schema 58 | * @returns CheckResult 59 | */ 60 | getCheckResult(path?: string, result = this.checkResult): CheckResult { 61 | if (path) { 62 | return result?.[path] || get(result, pathTransform(path)) || { hasError: false }; 63 | } 64 | 65 | return result; 66 | } 67 | 68 | /** 69 | * Get the error messages of the schema 70 | */ 71 | getErrorMessages(path?: string, result = this.checkResult): (string | ErrorMsgType)[] { 72 | let messages: (string | ErrorMsgType)[] = []; 73 | 74 | if (path) { 75 | const { errorMessage, object, array } = 76 | result?.[path] || get(result, pathTransform(path)) || {}; 77 | 78 | if (errorMessage) { 79 | messages = [errorMessage]; 80 | } else if (object) { 81 | messages = Object.keys(object).map(key => object[key]?.errorMessage); 82 | } else if (array) { 83 | messages = array.map(item => item?.errorMessage); 84 | } 85 | } else { 86 | messages = Object.keys(result).map(key => result[key]?.errorMessage); 87 | } 88 | 89 | return messages.filter(Boolean); 90 | } 91 | 92 | /** 93 | * Get all the keys of the schema 94 | */ 95 | getKeys() { 96 | return Object.keys(this.$spec); 97 | } 98 | 99 | /** 100 | * Get the schema specification 101 | */ 102 | getSchemaSpec() { 103 | return this.$spec; 104 | } 105 | _checkForField( 106 | fieldName: T, 107 | data: DataType, 108 | options: CheckOptions = {} 109 | ): CheckResult { 110 | this.setSchemaOptionsForAllType(data); 111 | 112 | const { nestedObject } = options; 113 | 114 | // Add current field to checked list 115 | this.checkedFields = [...this.checkedFields, fieldName as string]; 116 | 117 | const fieldChecker = this.getFieldType(fieldName, nestedObject); 118 | 119 | if (!fieldChecker) { 120 | return { hasError: false }; 121 | } 122 | 123 | const fieldValue = getFieldValue(data, fieldName as string, nestedObject); 124 | const checkResult = fieldChecker.check(fieldValue, data, fieldName as string); 125 | 126 | this.setFieldCheckResult(fieldName as string, checkResult, nestedObject); 127 | 128 | if (!checkResult.hasError) { 129 | const { checkIfValueExists } = fieldChecker.proxyOptions; 130 | 131 | fieldChecker.otherFields?.forEach((field: string) => { 132 | if (!this.checkedFields.includes(field)) { 133 | if (checkIfValueExists) { 134 | if (!isEmpty(getFieldValue(data, field, nestedObject))) { 135 | this._checkForField(field as T, data, { ...options }); 136 | } 137 | return; 138 | } 139 | this._checkForField(field as T, data, { ...options }); 140 | } 141 | }); 142 | } 143 | 144 | return checkResult; 145 | } 146 | 147 | checkForField( 148 | fieldName: T, 149 | data: DataType, 150 | options: CheckOptions = {} 151 | ): CheckResult { 152 | const result = this._checkForField(fieldName, data, options); 153 | // clean checked fields after check finished 154 | this.checkedFields = []; 155 | return result; 156 | } 157 | 158 | checkForFieldAsync( 159 | fieldName: T, 160 | data: DataType, 161 | options: CheckOptions = {} 162 | ): Promise> { 163 | this.setSchemaOptionsForAllType(data); 164 | 165 | const { nestedObject } = options; 166 | const fieldChecker = this.getFieldType(fieldName, nestedObject); 167 | 168 | if (!fieldChecker) { 169 | // fieldValue can be anything if no schema defined 170 | return Promise.resolve({ hasError: false }); 171 | } 172 | 173 | const fieldValue = getFieldValue(data, fieldName as string, nestedObject); 174 | const checkResult = fieldChecker.checkAsync(fieldValue, data, fieldName as string); 175 | 176 | return checkResult.then(async result => { 177 | this.setFieldCheckResult(fieldName as string, result, nestedObject); 178 | 179 | if (!result.hasError) { 180 | const { checkIfValueExists } = fieldChecker.proxyOptions; 181 | const checkAll: Promise>[] = []; 182 | 183 | // Check other fields if the field depends on them for validation 184 | fieldChecker.otherFields?.forEach((field: string) => { 185 | if (checkIfValueExists) { 186 | if (!isEmpty(getFieldValue(data, field, nestedObject))) { 187 | checkAll.push(this.checkForFieldAsync(field as T, data, options)); 188 | } 189 | return; 190 | } 191 | 192 | checkAll.push(this.checkForFieldAsync(field as T, data, options)); 193 | }); 194 | 195 | await Promise.all(checkAll); 196 | } 197 | 198 | return result; 199 | }); 200 | } 201 | 202 | check(data: DataType) { 203 | const checkResult: SchemaCheckResult = {}; 204 | Object.keys(this.$spec).forEach(key => { 205 | if (typeof data === 'object') { 206 | checkResult[key] = this.checkForField(key as T, data); 207 | } 208 | }); 209 | 210 | return checkResult; 211 | } 212 | 213 | checkAsync(data: DataType) { 214 | const checkResult: SchemaCheckResult = {}; 215 | const promises: Promise>[] = []; 216 | const keys: string[] = []; 217 | 218 | Object.keys(this.$spec).forEach((key: string) => { 219 | keys.push(key); 220 | promises.push(this.checkForFieldAsync(key as T, data)); 221 | }); 222 | 223 | return Promise.all(promises).then(values => { 224 | for (let i = 0; i < values.length; i += 1) { 225 | checkResult[keys[i]] = values[i]; 226 | } 227 | 228 | return checkResult; 229 | }); 230 | } 231 | } 232 | 233 | export function SchemaModel( 234 | o: SchemaDeclaration 235 | ) { 236 | return new Schema(o); 237 | } 238 | 239 | SchemaModel.combine = function combine( 240 | ...specs: Schema[] 241 | ) { 242 | return new Schema( 243 | specs 244 | .map(model => model.$spec) 245 | .reduce((accumulator, currentValue) => Object.assign(accumulator, currentValue), {} as any) 246 | ); 247 | }; 248 | -------------------------------------------------------------------------------- /src/MixedType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SchemaDeclaration, 3 | CheckResult, 4 | ValidCallbackType, 5 | AsyncValidCallbackType, 6 | RuleType, 7 | ErrorMessageType, 8 | TypeName, 9 | PlainObject 10 | } from './types'; 11 | import { 12 | checkRequired, 13 | createValidator, 14 | createValidatorAsync, 15 | isEmpty, 16 | shallowEqual, 17 | formatErrorMessage, 18 | get 19 | } from './utils'; 20 | import { joinName } from './utils/formatErrorMessage'; 21 | import locales, { MixedTypeLocale } from './locales'; 22 | 23 | type ProxyOptions = { 24 | // Check if the value exists 25 | checkIfValueExists?: boolean; 26 | }; 27 | 28 | export const schemaSpecKey = 'objectTypeSchemaSpec'; 29 | export const arrayTypeSchemaSpec = 'arrayTypeSchemaSpec'; 30 | 31 | /** 32 | * Get the field type from the schema object 33 | */ 34 | export function getFieldType(schemaSpec: any, fieldName: string, nestedObject?: boolean) { 35 | if (schemaSpec) { 36 | if (nestedObject) { 37 | const namePath = fieldName.split('.'); 38 | const currentField = namePath[0]; 39 | const arrayMatch = currentField.match(/(\w+)\[(\d+)\]/); 40 | if (arrayMatch) { 41 | const [, arrayField, arrayIndex] = arrayMatch; 42 | const type = schemaSpec[arrayField]; 43 | if (type?.[arrayTypeSchemaSpec]) { 44 | const arrayType = type[arrayTypeSchemaSpec]; 45 | 46 | if (namePath.length > 1) { 47 | if (arrayType[schemaSpecKey]) { 48 | return getFieldType(arrayType[schemaSpecKey], namePath.slice(1).join('.'), true); 49 | } 50 | if (Array.isArray(arrayType) && arrayType[parseInt(arrayIndex)][schemaSpecKey]) { 51 | return getFieldType( 52 | arrayType[parseInt(arrayIndex)][schemaSpecKey], 53 | namePath.slice(1).join('.'), 54 | true 55 | ); 56 | } 57 | } 58 | if (Array.isArray(arrayType)) { 59 | return arrayType[parseInt(arrayIndex)]; 60 | } 61 | // Otherwise return the array element type directly 62 | return arrayType; 63 | } 64 | return type; 65 | } else { 66 | const type = schemaSpec[currentField]; 67 | 68 | if (namePath.length === 1) { 69 | return type; 70 | } 71 | 72 | if (namePath.length > 1 && type && type[schemaSpecKey]) { 73 | return getFieldType(type[schemaSpecKey], namePath.slice(1).join('.'), true); 74 | } 75 | } 76 | } 77 | return schemaSpec?.[fieldName]; 78 | } 79 | } 80 | 81 | /** 82 | * Get the field value from the data object 83 | */ 84 | export function getFieldValue(data: PlainObject, fieldName: string, nestedObject?: boolean) { 85 | return nestedObject ? get(data, fieldName) : data?.[fieldName]; 86 | } 87 | 88 | export class MixedType { 89 | readonly $typeName?: string; 90 | protected required = false; 91 | protected requiredMessage: E | string = ''; 92 | protected trim = false; 93 | protected emptyAllowed = false; 94 | protected rules: RuleType[] = []; 95 | protected priorityRules: RuleType[] = []; 96 | protected fieldLabel?: string; 97 | 98 | $schemaSpec: SchemaDeclaration; 99 | value: any; 100 | locale: L & MixedTypeLocale; 101 | 102 | // The field name that depends on the verification of other fields 103 | otherFields: string[] = []; 104 | proxyOptions: ProxyOptions = {}; 105 | 106 | constructor(name?: TypeName) { 107 | this.$typeName = name; 108 | this.locale = Object.assign(name ? locales[name] : {}, locales.mixed) as L & MixedTypeLocale; 109 | } 110 | 111 | setSchemaOptions(schemaSpec: SchemaDeclaration, value: any) { 112 | this.$schemaSpec = schemaSpec; 113 | this.value = value; 114 | } 115 | 116 | check(value: any = this.value, data?: DataType, fieldName?: string | string[]) { 117 | if (this.required && !checkRequired(value, this.trim, this.emptyAllowed)) { 118 | return { 119 | hasError: true, 120 | errorMessage: formatErrorMessage(this.requiredMessage, { 121 | name: this.fieldLabel || joinName(fieldName) 122 | }) 123 | }; 124 | } 125 | 126 | const validator = createValidator( 127 | data, 128 | fieldName, 129 | this.fieldLabel 130 | ); 131 | 132 | const checkResult = validator(value, this.priorityRules); 133 | 134 | // If the priority rule fails, return the result directly 135 | if (checkResult) { 136 | return checkResult; 137 | } 138 | 139 | if (!this.required && isEmpty(value)) { 140 | return { hasError: false }; 141 | } 142 | 143 | return validator(value, this.rules) || { hasError: false }; 144 | } 145 | 146 | checkAsync( 147 | value: any = this.value, 148 | data?: DataType, 149 | fieldName?: string | string[] 150 | ): Promise> { 151 | if (this.required && !checkRequired(value, this.trim, this.emptyAllowed)) { 152 | return Promise.resolve({ 153 | hasError: true, 154 | errorMessage: formatErrorMessage(this.requiredMessage, { 155 | name: this.fieldLabel || joinName(fieldName) 156 | }) 157 | }); 158 | } 159 | 160 | const validator = createValidatorAsync( 161 | data, 162 | fieldName, 163 | this.fieldLabel 164 | ); 165 | 166 | return new Promise(resolve => 167 | validator(value, this.priorityRules) 168 | .then((checkResult: CheckResult | void | null) => { 169 | // If the priority rule fails, return the result directly 170 | if (checkResult) { 171 | resolve(checkResult); 172 | } 173 | }) 174 | .then(() => { 175 | if (!this.required && isEmpty(value)) { 176 | resolve({ hasError: false }); 177 | } 178 | }) 179 | .then(() => validator(value, this.rules)) 180 | .then((checkResult: CheckResult | void | null) => { 181 | if (checkResult) { 182 | resolve(checkResult); 183 | } 184 | resolve({ hasError: false }); 185 | }) 186 | ); 187 | } 188 | protected pushRule(rule: RuleType) { 189 | const { onValid, errorMessage, priority, params } = rule; 190 | const nextRule = { 191 | onValid, 192 | params, 193 | isAsync: rule.isAsync, 194 | errorMessage: errorMessage || this.rules?.[0]?.errorMessage 195 | }; 196 | 197 | if (priority) { 198 | this.priorityRules.push(nextRule); 199 | } else { 200 | this.rules.push(nextRule); 201 | } 202 | } 203 | addRule( 204 | onValid: ValidCallbackType, 205 | errorMessage?: E | string | (() => E | string), 206 | priority?: boolean 207 | ) { 208 | this.pushRule({ onValid, errorMessage, priority }); 209 | return this; 210 | } 211 | addAsyncRule( 212 | onValid: AsyncValidCallbackType, 213 | errorMessage?: E | string, 214 | priority?: boolean 215 | ) { 216 | this.pushRule({ onValid, isAsync: true, errorMessage, priority }); 217 | return this; 218 | } 219 | isRequired(errorMessage: E | string = this.locale.isRequired, trim = true) { 220 | this.required = true; 221 | this.trim = trim; 222 | this.requiredMessage = errorMessage; 223 | return this; 224 | } 225 | isRequiredOrEmpty(errorMessage: E | string = this.locale.isRequiredOrEmpty, trim = true) { 226 | this.required = true; 227 | this.trim = trim; 228 | this.emptyAllowed = true; 229 | this.requiredMessage = errorMessage; 230 | return this; 231 | } 232 | 233 | /** 234 | * Define data verification rules based on conditions. 235 | * @param condition 236 | * @example 237 | * 238 | * ```js 239 | * SchemaModel({ 240 | * option: StringType().isOneOf(['a', 'b', 'other']), 241 | * other: StringType().when(schema => { 242 | * const { value } = schema.option; 243 | * return value === 'other' ? StringType().isRequired('Other required') : StringType(); 244 | * }) 245 | * }); 246 | * ``` 247 | */ 248 | when(condition: (schemaSpec: SchemaDeclaration) => MixedType) { 249 | this.addRule( 250 | (value, data, fieldName) => { 251 | return condition(this.$schemaSpec).check(value, data, fieldName); 252 | }, 253 | undefined, 254 | true 255 | ); 256 | return this; 257 | } 258 | 259 | /** 260 | * Check if the value is equal to the value of another field. 261 | * @example 262 | * 263 | * ```js 264 | * SchemaModel({ 265 | * password: StringType().isRequired(), 266 | * confirmPassword: StringType().equalTo('password').isRequired() 267 | * }); 268 | * ``` 269 | */ 270 | equalTo(fieldName: string, errorMessage: E | string = this.locale.equalTo) { 271 | const errorMessageFunc = () => { 272 | const type = getFieldType(this.$schemaSpec, fieldName, true); 273 | return formatErrorMessage(errorMessage, { toFieldName: type?.fieldLabel || fieldName }); 274 | }; 275 | 276 | this.addRule((value, data) => { 277 | return shallowEqual(value, get(data, fieldName)); 278 | }, errorMessageFunc); 279 | return this; 280 | } 281 | 282 | /** 283 | * After the field verification passes, proxy verification of other fields. 284 | * @param options.checkIfValueExists When the value of other fields exists, the verification is performed (default: false) 285 | * @example 286 | * 287 | * ```js 288 | * SchemaModel({ 289 | * password: StringType().isRequired().proxy(['confirmPassword']), 290 | * confirmPassword: StringType().equalTo('password').isRequired() 291 | * }); 292 | * ``` 293 | */ 294 | proxy(fieldNames: string[], options?: ProxyOptions) { 295 | this.otherFields = fieldNames; 296 | this.proxyOptions = options || {}; 297 | return this; 298 | } 299 | 300 | /** 301 | * Overrides the key name in error messages. 302 | * 303 | * @example 304 | * ```js 305 | * SchemaModel({ 306 | * first_name: StringType().label('First name'), 307 | * age: NumberType().label('Age') 308 | * }); 309 | * ``` 310 | */ 311 | label(label: string) { 312 | this.fieldLabel = label; 313 | return this; 314 | } 315 | } 316 | 317 | export default function getMixedType() { 318 | return new MixedType(); 319 | } 320 | -------------------------------------------------------------------------------- /types/test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BooleanType, 3 | NumberType, 4 | StringType, 5 | DateType, 6 | ArrayType, 7 | ObjectType, 8 | Schema, 9 | SchemaModel 10 | } from '../src'; 11 | 12 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 13 | // PASSING SCENARIO 1: Should not fail if proper check types are used 14 | 15 | interface PassObj { 16 | s: string; 17 | } 18 | interface Pass { 19 | n?: number; 20 | b?: boolean; 21 | s?: string; 22 | d?: Date; 23 | a?: Array; 24 | o: PassObj; 25 | } 26 | 27 | const passSchema = new Schema({ 28 | n: NumberType(), 29 | b: BooleanType(), 30 | s: StringType(), 31 | d: DateType(), 32 | a: ArrayType(), 33 | o: ObjectType() 34 | }); 35 | 36 | passSchema.check({ a: ['a'], b: false, d: new Date(), n: 0, o: { s: '' }, s: '' }); 37 | passSchema.checkAsync({ a: ['a'], b: false, d: new Date(), n: 0, o: { s: '' }, s: '' }); 38 | passSchema.checkForField('o', { o: { s: '1' } }); 39 | passSchema.checkForFieldAsync('o', { o: { s: '1' } }); 40 | 41 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 42 | // PASSING SCENARIO 2: Should allows combine proper schemas 43 | 44 | SchemaModel.combine<{ x: string; y: string }>( 45 | new Schema<{ x: string }>({ x: StringType() }), 46 | new Schema<{ y: string }>({ y: StringType() }) 47 | ); 48 | 49 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 50 | // PASSING SCENARIO 3: Should allows adding custom rules which have proper types on callback 51 | 52 | SchemaModel<{ password1: string; password2: string }>({ 53 | password1: StringType(), 54 | password2: StringType().addRule( 55 | (value, data) => value.toLowerCase() === data.password1.toLowerCase() 56 | ) 57 | }); 58 | 59 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 60 | // PASSING SCENARIO 4: Should allows to use custom error message type 61 | SchemaModel<{ a: string }, number>({ 62 | a: StringType<{ a: string }, number>().addRule(() => ({ 63 | hasError: true, 64 | errorMessage: 500 65 | })) 66 | }); 67 | 68 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 69 | // PASSING SCENARIO 5: Should allows to use NumberType on string field 70 | SchemaModel<{ a: string }>({ 71 | a: NumberType<{ a: string }>() 72 | }); 73 | 74 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 75 | // PASSING SCENARIO 6: Should allows to use DataType on string field 76 | SchemaModel<{ a: string }>({ 77 | a: DateType<{ a: string }>() 78 | }); 79 | 80 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 81 | // PASSING SCENARIO 7: Should allow other types to be included in ArrayType 82 | 83 | ArrayType().of(NumberType()); 84 | ArrayType().of(ObjectType()); 85 | ArrayType().of(ArrayType()); 86 | ArrayType().of(BooleanType()); 87 | ArrayType().of(StringType()); 88 | ArrayType().of(DateType()); 89 | 90 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 91 | // FAIL SCENARIO 1: Should fail if type check is not matching declared type 92 | 93 | interface F1 { 94 | a: string; 95 | } 96 | new Schema({ 97 | // $ExpectError 98 | a: BooleanType() 99 | // TS2322: Type 'BooleanType' is not assignable to type 'StringType | DateType | NumberType'. 100 | // Type 'BooleanType' is missing the following properties from type 'NumberType': isInteger, pattern, isOneOf, range, and 2 more. 101 | }); 102 | 103 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 104 | // FAIL SCENARIO 2: Should fail if checks declaration provides check for undeclared property 105 | 106 | interface F2 { 107 | a: string; 108 | } 109 | new Schema({ 110 | // $ExpectError 111 | b: NumberType() 112 | // TS2345: Argument of type '{ b: NumberType; }' is not assignable to parameter of type 'SchemaDeclaration'. 113 | // Object literal may only specify known properties, and 'b' does not exist in type 'SchemaDeclaration'. 114 | }); 115 | 116 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 117 | // FAIL SCENARIO 3: Should fail if custom rule check will not fallow proper type for value 118 | 119 | interface F3 { 120 | a: string; 121 | } 122 | new Schema({ 123 | // $ExpectError 124 | a: StringType().addRule((v: number) => true) 125 | // TS2345: Argument of type '(v: number) => true' is not assignable to parameter of type '(value: string, data: any) => boolean | void | CheckResult | Promise | Promise | Promise>'. 126 | // Types of parameters 'v' and 'value' are incompatible. 127 | // Type 'string' is not assignable to type 'number'. 128 | }); 129 | 130 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 131 | // FAIL SCENARIO 4: Should fail if custom rule check will not fallow proper type for data 132 | 133 | interface F4 { 134 | a: string; 135 | } 136 | new Schema({ 137 | // $ExpectError 138 | a: StringType().addRule((v: string, d: number) => true) 139 | // TS2345: Argument of type '(v: string, d: number) => true' is not assignable to parameter of type '(value: string, data: F4) => boolean | void | CheckResult | Promise | Promise | Promise>'. 140 | // Types of parameters 'd' and 'data' are incompatible. 141 | // Type 'F4' is not assignable to type 'number'. 142 | }); 143 | 144 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 145 | // FAIL SCENARIO 5: Should fail if check and checkAsync function is called with not matching type 146 | 147 | interface F5 { 148 | a: string; 149 | } 150 | const schemaF5 = new Schema({ 151 | a: StringType() 152 | }); 153 | 154 | // $ExpectError 155 | schemaF5.check({ c: 12 }); 156 | // TS2345: Argument of type '{ c: number; }' is not assignable to parameter of type 'F5'. 157 | // Object literal may only specify known properties, and 'c' does not exist in type 'F5'. 158 | 159 | // $ExpectError 160 | schemaF5.checkAsync({ c: 12 }); 161 | // TS2345: Argument of type '{ c: number; }' is not assignable to parameter of type 'F5'. 162 | // Object literal may only specify known properties, and 'c' does not exist in type 'F5'. 163 | 164 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 165 | // FAIL SCENARIO 6: Should fail if checkForField function is called with non existing property name 166 | 167 | interface F6 { 168 | a: string; 169 | } 170 | const schemaF6 = new Schema({ 171 | a: StringType() 172 | }); 173 | 174 | // $ExpectError 175 | schemaF6.checkForField('c', 'a'); 176 | // TS2345: Argument of type '"c"' is not assignable to parameter of type '"a"'. 177 | 178 | // $ExpectError 179 | schemaF6.checkForFieldAsync('c', 'a'); 180 | // TS2345: Argument of type '"c"' is not assignable to parameter of type '"a"'. 181 | 182 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 183 | // FAIL SCENARIO 7: Should fail if check and checkAsync function is called with not matching type, when type is inferred 184 | 185 | const schemaF7 = new Schema({ 186 | a: StringType() 187 | }); 188 | 189 | // $ExpectError 190 | schemaF7.check({ c: 12 }); 191 | // TS2345: Argument of type '{ c: number; }' is not assignable to parameter of type '{ a: unknown; }'. 192 | // Object literal may only specify known properties, and 'c' does not exist in type '{ a: unknown; }'. 193 | 194 | // $ExpectError 195 | schemaF7.checkAsync({ c: 12 }); 196 | // TS2345: Argument of type '{ c: number; }' is not assignable to parameter of type '{ a: unknown; }'. 197 | // Object literal may only specify known properties, and 'c' does not exist in type '{ a: unknown; }'. 198 | 199 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 200 | // FAIL SCENARIO 8: Should fail if checkForField function is called with non existing property name, when type is inferred 201 | 202 | const schemaF8 = new Schema({ 203 | a: StringType() 204 | }); 205 | 206 | // $ExpectError 207 | schemaF8.checkForField('c', { a: 'str' }); 208 | // TS2345: Argument of type '"c"' is not assignable to parameter of type '"a"'. 209 | 210 | // $ExpectError 211 | schemaF8.checkForFieldAsync('c', { a: 'str' }); 212 | // TS2345: Argument of type '"c"' is not assignable to parameter of type '"a"'. 213 | 214 | // $ExpectError 215 | schemaF8.checkForField('a', { c: 'str' }); 216 | // TS2345: Argument of type '{ c: string; }' is not assignable to parameter of type '{ a: unknown; }'. 217 | 218 | // $ExpectError 219 | schemaF8.checkForFieldAsync('a', { c: 'str' }); 220 | // TS2345: Argument of type '{ c: string; }' is not assignable to parameter of type '{ a: unknown; }'. 221 | 222 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 223 | // FAIL SCENARIO 9: Should fail if ObjectType will get not matched shape 224 | 225 | interface F9 { 226 | a: string; 227 | } 228 | ObjectType().shape({ 229 | // $ExpectError 230 | a: BooleanType() 231 | // TS2322: Type 'BooleanType' is not assignable to type 'StringType | DateType | NumberType'. 232 | // Type 'BooleanType' is missing the following properties from type 'NumberType': isInteger, pattern, isOneOf, range, and 2 more. 233 | }); 234 | ObjectType().shape({ 235 | // $ExpectError 236 | b: NumberType() 237 | // TS2345: Argument of type '{ b: NumberType; }' is not assignable to parameter of type 'SchemaDeclaration'. 238 | // Object literal may only specify known properties, and 'b' does not exist in type 'SchemaDeclaration'. 239 | }); 240 | 241 | interface F10 { 242 | a: { 243 | b: number; 244 | }; 245 | } 246 | const schemaF10 = new Schema({ 247 | a: ObjectType().shape({ 248 | b: NumberType() 249 | }) 250 | }); 251 | 252 | schemaF10.check({ a: { b: 1 } }); 253 | 254 | // $ExpectError 255 | const checkResultF10 = schemaF10.check({ a: { b: '1' } }); 256 | 257 | checkResultF10.a.object?.b.errorMessage; 258 | checkResultF10.a.object?.b.hasError; 259 | -------------------------------------------------------------------------------- /test/StringTypeSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | require('chai').should(); 3 | const schema = require('../src'); 4 | const { StringType, SchemaModel } = schema; 5 | 6 | describe('#StringType', () => { 7 | it('Should check min string length', () => { 8 | const schema = SchemaModel({ 9 | str: StringType().minLength(5), 10 | cjkStr: StringType().minLength(5, ''), 11 | emojiStr: StringType().minLength(5, '') 12 | }); 13 | 14 | schema.checkForField('str', { str: 'abcde' }).hasError.should.equal(false); 15 | schema.checkForField('str', { str: 'abcd' }).hasError.should.equal(true); 16 | 17 | schema.checkForField('cjkStr', { cjkStr: '鲤鱼跃龙门' }).hasError.should.equal(false); 18 | schema.checkForField('cjkStr', { cjkStr: '岁寒三友' }).hasError.should.equal(true); 19 | schema.checkForField('emojiStr', { emojiStr: '👌👍🐱🐶🐸' }).hasError.should.equal(false); 20 | 21 | schema.checkForField('emojiStr', { emojiStr: '👌👍🐱🐶' }).hasError.should.equal(true); 22 | 23 | schema 24 | .checkForField('str', { str: 'a' }) 25 | .errorMessage.should.equal('str must be at least 5 characters'); 26 | }); 27 | 28 | it('Should check max string length', () => { 29 | const schema = SchemaModel({ 30 | str: StringType().maxLength(4), 31 | cjkStr: StringType().maxLength(4, ''), 32 | emojiStr: StringType().maxLength(4, '') 33 | }); 34 | 35 | schema.checkForField('str', { str: 'abcde' }).hasError.should.equal(true); 36 | schema.checkForField('str', { str: 'abcd' }).hasError.should.equal(false); 37 | schema.checkForField('cjkStr', { cjkStr: '鲤鱼跃龙门' }).hasError.should.equal(true); 38 | schema.checkForField('cjkStr', { cjkStr: '岁寒三友' }).hasError.should.equal(false); 39 | schema.checkForField('emojiStr', { emojiStr: '👌👍🐱🐶🐸' }).hasError.should.equal(true); 40 | schema.checkForField('emojiStr', { emojiStr: '👌👍🐱🐶' }).hasError.should.equal(false); 41 | 42 | schema 43 | .checkForField('str', { str: 'abcde' }) 44 | .errorMessage.should.equal('str must be at most 4 characters'); 45 | }); 46 | 47 | it('Should be required', () => { 48 | const schema = SchemaModel({ 49 | str: StringType().isRequired('isrequired'), 50 | str1: StringType().isRequired(), 51 | str2: StringType().isRequired('isrequired', false) 52 | }); 53 | 54 | schema.checkForField('str', { str: '' }).hasError.should.equal(true); 55 | schema.checkForField('str', { str: ' abcde ' }).hasError.should.equal(false); 56 | schema.checkForField('str', { str: ' ' }).hasError.should.equal(true); 57 | 58 | schema 59 | .checkForField('str1', { str1: '' }) 60 | .errorMessage.should.equal('str1 is a required field'); 61 | 62 | schema.checkForField('str2', { str2: '' }).hasError.should.equal(true); 63 | schema.checkForField('str2', { str2: ' abcde ' }).hasError.should.equal(false); 64 | schema.checkForField('str2', { str2: ' ' }).hasError.should.equal(false); 65 | }); 66 | 67 | it('Should be able to customize the rules', () => { 68 | const schema = SchemaModel({ 69 | str: StringType() 70 | .maxLength(4, 'error1') 71 | .addRule(value => value !== '123', 'error2') 72 | }); 73 | 74 | schema.checkForField('str', { str: '12' }).hasError.should.equal(false); 75 | 76 | schema.checkForField('str', { str: '123' }).hasError.should.equal(true); 77 | schema.checkForField('str', { str: '123' }).errorMessage.should.equal('error2'); 78 | schema.checkForField('str', { str: 'abcde' }).hasError.should.equal(true); 79 | schema.checkForField('str', { str: 'abcde' }).errorMessage.should.equal('error1'); 80 | }); 81 | 82 | it('Should be one of value in array', () => { 83 | const schema = SchemaModel({ 84 | str: StringType().isOneOf(['A', 'B', 'C'], 'error'), 85 | str1: StringType().isOneOf(['A', 'B', 'C']) 86 | }); 87 | schema.checkForField('str', { str: 'A' }).hasError.should.equal(false); 88 | schema.checkForField('str', { str: 'D' }).hasError.should.equal(true); 89 | schema.checkForField('str', { str: 'D' }).errorMessage.should.equal('error'); 90 | schema 91 | .checkForField('str1', { str1: 'D' }) 92 | .errorMessage.should.equal('str1 must be one of the following values: A,B,C'); 93 | }); 94 | 95 | it('Should contain letters', () => { 96 | const schema = SchemaModel({ 97 | str: StringType().containsLetter() 98 | }); 99 | schema.checkForField('str', { str: '12A' }).hasError.should.equal(false); 100 | schema.checkForField('str', { str: 'a12' }).hasError.should.equal(false); 101 | schema.checkForField('str', { str: '12' }).hasError.should.equal(true); 102 | schema 103 | .checkForField('str', { str: '-' }) 104 | .errorMessage.should.equal('str field must contain letters'); 105 | }); 106 | 107 | it('Should only contain letters', () => { 108 | const schema = SchemaModel({ 109 | str: StringType().containsLetterOnly() 110 | }); 111 | schema.checkForField('str', { str: 'aA' }).hasError.should.equal(false); 112 | schema.checkForField('str', { str: '12A' }).hasError.should.equal(true); 113 | schema.checkForField('str', { str: 'a12' }).hasError.should.equal(true); 114 | schema.checkForField('str', { str: '12' }).hasError.should.equal(true); 115 | schema.checkForField('str', { str: '1a' }).errorMessage.should.equal('str must all be letters'); 116 | }); 117 | 118 | it('Should contain uppercase letters', () => { 119 | const schema = SchemaModel({ 120 | str: StringType().containsUppercaseLetter() 121 | }); 122 | schema.checkForField('str', { str: '12A' }).hasError.should.equal(false); 123 | schema.checkForField('str', { str: 'a12' }).hasError.should.equal(true); 124 | schema.checkForField('str', { str: '12' }).hasError.should.equal(true); 125 | schema 126 | .checkForField('str', { str: '-' }) 127 | .errorMessage.should.equal('str must be a upper case string'); 128 | }); 129 | 130 | it('Should contain lowercase letters', () => { 131 | const schema = SchemaModel({ 132 | str: StringType().containsLowercaseLetter() 133 | }); 134 | schema.checkForField('str', { str: '12A' }).hasError.should.equal(true); 135 | schema.checkForField('str', { str: 'a12' }).hasError.should.equal(false); 136 | schema.checkForField('str', { str: '12' }).hasError.should.equal(true); 137 | schema 138 | .checkForField('str', { str: '-' }) 139 | .errorMessage.should.equal('str must be a lowercase string'); 140 | }); 141 | 142 | it('Should contain numbers', () => { 143 | const schema = SchemaModel({ 144 | str: StringType().containsNumber() 145 | }); 146 | schema.checkForField('str', { str: '12' }).hasError.should.equal(false); 147 | schema.checkForField('str', { str: 'a12' }).hasError.should.equal(false); 148 | schema.checkForField('str', { str: '12A' }).hasError.should.equal(false); 149 | schema 150 | .checkForField('str', { str: 'a' }) 151 | .errorMessage.should.equal('str field must contain numbers'); 152 | }); 153 | 154 | it('Should be a url', () => { 155 | const schema = SchemaModel({ 156 | str: StringType().isURL(), 157 | email: StringType().isURL('', { allowMailto: true }) 158 | }); 159 | schema.checkForField('str', { str: 'https://www.abc.com' }).hasError.should.equal(false); 160 | schema.checkForField('str', { str: 'http://www.abc.com' }).hasError.should.equal(false); 161 | schema.checkForField('str', { str: 'ftp://www.abc.com' }).hasError.should.equal(false); 162 | schema.checkForField('str', { str: 'http://127.0.0.1/home' }).hasError.should.equal(false); 163 | schema.checkForField('str', { str: 'www.abc.com' }).hasError.should.equal(true); 164 | schema.checkForField('str', { str: 'a' }).errorMessage.should.equal('str must be a valid URL'); 165 | schema.checkForField('str', { str: 'mailto:user@example.com' }).hasError.should.be.true; 166 | schema.checkForField('email', { email: 'mailto:user@example.com' }).hasError.should.be.false; 167 | }); 168 | 169 | it('Should be a hexadecimal character', () => { 170 | const schema = SchemaModel({ 171 | str: StringType().isHex() 172 | }); 173 | schema.checkForField('str', { str: '#fff000' }).hasError.should.equal(false); 174 | schema.checkForField('str', { str: 'fff000' }).hasError.should.equal(false); 175 | schema.checkForField('str', { str: '#fff' }).hasError.should.equal(false); 176 | schema.checkForField('str', { str: 'fff' }).hasError.should.equal(false); 177 | schema.checkForField('str', { str: '#000' }).hasError.should.equal(false); 178 | schema.checkForField('str', { str: '#00' }).hasError.should.equal(true); 179 | schema 180 | .checkForField('str', { str: 'a' }) 181 | .errorMessage.should.equal('str must be a valid hexadecimal'); 182 | }); 183 | 184 | it('Should allow custom rules', () => { 185 | let schema = SchemaModel({ data: StringType().pattern(/^-?1\d+$/) }); 186 | schema.checkForField('data', { data: '11' }).hasError.should.equal(false); 187 | schema.checkForField('data', { data: '12' }).hasError.should.equal(false); 188 | schema.checkForField('data', { data: '22' }).hasError.should.equal(true); 189 | schema.checkForField('data', { data: '22' }).errorMessage.should.equal('data is invalid'); 190 | }); 191 | 192 | it('Should be within the range of the number of characters', () => { 193 | let schema = SchemaModel({ data: StringType().rangeLength(5, 10) }); 194 | schema.checkForField('data', { data: '12345' }).hasError.should.equal(false); 195 | schema.checkForField('data', { data: '1234' }).hasError.should.equal(true); 196 | schema.checkForField('data', { data: '12345678910' }).hasError.should.equal(true); 197 | schema 198 | .checkForField('data', { data: '1234' }) 199 | .errorMessage.should.equal('data must contain 5 to 10 characters'); 200 | }); 201 | 202 | it('Should not be less than the minimum number of characters', () => { 203 | let schema = SchemaModel({ data: StringType().minLength(5) }); 204 | schema.checkForField('data', { data: '12345' }).hasError.should.equal(false); 205 | schema.checkForField('data', { data: '1234' }).hasError.should.equal(true); 206 | schema 207 | .checkForField('data', { data: '1234' }) 208 | .errorMessage.should.equal('data must be at least 5 characters'); 209 | }); 210 | 211 | it('Should not exceed the maximum number of characters', () => { 212 | let schema = SchemaModel({ data: StringType().maxLength(5) }); 213 | schema.checkForField('data', { data: '12345' }).hasError.should.equal(false); 214 | schema.checkForField('data', { data: '123456' }).hasError.should.equal(true); 215 | schema 216 | .checkForField('data', { data: '123456' }) 217 | .errorMessage.should.equal('data must be at most 5 characters'); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /test/SchemaSpec.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import * as schema from '../src'; 3 | 4 | const { StringType, NumberType, ObjectType, ArrayType, Schema, SchemaModel } = schema; 5 | 6 | chai.should(); 7 | 8 | describe('#Schema', () => { 9 | it('The schema should be saved as proporty', () => { 10 | const schemaData = { data: StringType() }; 11 | const schema = new Schema(schemaData); 12 | 13 | schema.$spec.should.equal(schemaData); 14 | }); 15 | 16 | it('Should be able to get the field value type for the given field name', () => { 17 | const schemaData = { data: NumberType() }; 18 | const schema = new Schema(schemaData); 19 | schema.getFieldType('data').should.equal(schemaData.data); 20 | }); 21 | 22 | it('Should return error information', () => { 23 | const schemaData = { data: NumberType() }; 24 | const schema = new Schema(schemaData); 25 | const checkResult = schema.checkForField('data', '2.22'); 26 | 27 | checkResult.should.have.property('hasError').be.a('boolean'); 28 | }); 29 | 30 | it('Should return error information', () => { 31 | const model = SchemaModel({ 32 | username: StringType().isRequired(), 33 | email: StringType().isEmail(), 34 | age: NumberType().range(18, 30) 35 | }); 36 | 37 | const checkResult = model.check({ 38 | username: 'foobar', 39 | email: 'foo@bar.com', 40 | age: 40 41 | }); 42 | 43 | expect(checkResult).to.deep.equal({ 44 | username: { hasError: false }, 45 | email: { hasError: false }, 46 | age: { hasError: true, errorMessage: 'age field must be between 18 and 30' } 47 | }); 48 | }); 49 | 50 | it('Should get the schema spec by calling getSchemaSpec', () => { 51 | const model = SchemaModel({ 52 | username: StringType().isRequired(), 53 | email: StringType().isEmail() 54 | }); 55 | 56 | model.getSchemaSpec().should.deep.equal(model.$spec); 57 | }); 58 | 59 | describe('## getKeys', () => { 60 | it('Should return keys', () => { 61 | const model = SchemaModel({ 62 | username: StringType(), 63 | email: StringType(), 64 | age: NumberType() 65 | }); 66 | 67 | model.getKeys().length.should.equals(3); 68 | model.getKeys()[0].should.equals('username'); 69 | model.getKeys()[1].should.equals('email'); 70 | model.getKeys()[2].should.equals('age'); 71 | }); 72 | }); 73 | 74 | describe('## getErrorMessages', () => { 75 | it('Should return error messages', () => { 76 | const model = SchemaModel({ 77 | username: StringType().isRequired(), 78 | email: StringType().isEmail(), 79 | age: NumberType().range(18, 30) 80 | }); 81 | 82 | model.check({ 83 | username: 'foobar', 84 | email: ' ', 85 | age: 40 86 | }); 87 | 88 | expect(model.getErrorMessages()).to.deep.equal([ 89 | 'email must be a valid email', 90 | 'age field must be between 18 and 30' 91 | ]); 92 | 93 | expect(model.getErrorMessages('age')).to.deep.equal(['age field must be between 18 and 30']); 94 | expect(model.getErrorMessages('username')).to.deep.equal([]); 95 | }); 96 | 97 | it('Should return error messages for array', () => { 98 | const model = SchemaModel({ 99 | a: ArrayType().of(StringType().isRequired()) 100 | }); 101 | 102 | model.check({ 103 | a: ['', 12] 104 | }); 105 | 106 | expect(model.getErrorMessages('a')).to.deep.equal([ 107 | 'a.[0] is a required field', 108 | 'a.[1] must be a string' 109 | ]); 110 | }); 111 | 112 | it('Should return error messages for nested object', () => { 113 | const model = SchemaModel({ 114 | a: StringType().isRequired(), 115 | b: StringType().isEmail(), 116 | c: NumberType().range(18, 30), 117 | d: ObjectType().shape({ 118 | e: StringType().isEmail().isRequired(), 119 | f: NumberType().range(50, 60) 120 | }) 121 | }); 122 | 123 | model.check({ 124 | a: 'foobar', 125 | b: 'a', 126 | c: 40, 127 | d: { e: ' ', f: 40 } 128 | }); 129 | 130 | expect(model.getErrorMessages()).to.deep.equal([ 131 | 'b must be a valid email', 132 | 'c field must be between 18 and 30' 133 | ]); 134 | 135 | expect(model.getErrorMessages('d')).to.deep.equal([ 136 | 'e is a required field', 137 | 'f field must be between 50 and 60' 138 | ]); 139 | 140 | expect(model.getErrorMessages('d.e')).to.deep.equal(['e is a required field']); 141 | }); 142 | 143 | it('Should return error messages for nested array', () => { 144 | const model = SchemaModel({ 145 | a: StringType().isRequired(), 146 | b: StringType().isEmail(), 147 | c: ArrayType() 148 | .of( 149 | ObjectType().shape({ 150 | d: StringType().isEmail().isRequired(), 151 | e: NumberType().range(50, 60) 152 | }) 153 | ) 154 | .isRequired() 155 | }); 156 | 157 | model.check({ 158 | a: 'foobar', 159 | b: 'a', 160 | c: [{}, { d: ' ', e: 40 }] 161 | }); 162 | 163 | expect(model.getErrorMessages()).to.deep.equal(['b must be a valid email']); 164 | expect(model.getErrorMessages('c.0.d')).to.deep.equal(['d is a required field']); 165 | }); 166 | 167 | it('Should return error messages', () => { 168 | const model = SchemaModel({ 169 | 'a.b': StringType().isRequired() 170 | }); 171 | 172 | model.check({ 173 | 'a.b': '' 174 | }); 175 | 176 | expect(model.getErrorMessages()).to.deep.equal(['a.b is a required field']); 177 | expect(model.getErrorMessages('a.b')).to.deep.equal(['a.b is a required field']); 178 | }); 179 | }); 180 | 181 | describe('## getCheckResult', () => { 182 | it('Should return check results', () => { 183 | const model = SchemaModel({ 184 | username: StringType().isRequired(), 185 | email: StringType().isEmail(), 186 | age: NumberType().range(18, 30) 187 | }); 188 | 189 | model.check({ 190 | username: 'foobar', 191 | email: ' ', 192 | age: 40 193 | }); 194 | 195 | expect(model.getCheckResult()).to.deep.equal({ 196 | username: { hasError: false }, 197 | email: { hasError: true, errorMessage: 'email must be a valid email' }, 198 | age: { hasError: true, errorMessage: 'age field must be between 18 and 30' } 199 | }); 200 | 201 | expect(model.getCheckResult('age')).to.deep.equal({ 202 | hasError: true, 203 | errorMessage: 'age field must be between 18 and 30' 204 | }); 205 | 206 | expect(model.getCheckResult('username')).to.deep.equal({ hasError: false }); 207 | }); 208 | 209 | it('Should return check results for nested object', () => { 210 | const model = SchemaModel({ 211 | a: StringType().isRequired(), 212 | b: StringType().isEmail(), 213 | c: NumberType().range(18, 30), 214 | d: ObjectType().shape({ 215 | e: StringType().isEmail().isRequired(), 216 | f: NumberType().range(50, 60) 217 | }) 218 | }); 219 | 220 | model.check({ 221 | a: 'foobar', 222 | b: 'a', 223 | c: 40, 224 | d: { e: ' ', f: 40 } 225 | }); 226 | 227 | expect(model.getCheckResult()).to.deep.equal({ 228 | a: { hasError: false }, 229 | b: { hasError: true, errorMessage: 'b must be a valid email' }, 230 | c: { hasError: true, errorMessage: 'c field must be between 18 and 30' }, 231 | d: { 232 | hasError: true, 233 | object: { 234 | e: { hasError: true, errorMessage: 'e is a required field' }, 235 | f: { hasError: true, errorMessage: 'f field must be between 50 and 60' } 236 | } 237 | } 238 | }); 239 | 240 | expect(model.getCheckResult('d')).to.deep.equal({ 241 | hasError: true, 242 | object: { 243 | e: { hasError: true, errorMessage: 'e is a required field' }, 244 | f: { hasError: true, errorMessage: 'f field must be between 50 and 60' } 245 | } 246 | }); 247 | 248 | expect(model.getCheckResult('d.e')).to.deep.equal({ 249 | hasError: true, 250 | errorMessage: 'e is a required field' 251 | }); 252 | }); 253 | 254 | it('Should return check results for nested array', () => { 255 | const model = SchemaModel({ 256 | a: StringType().isRequired(), 257 | b: StringType().isEmail(), 258 | c: ArrayType() 259 | .of( 260 | ObjectType().shape({ 261 | d: StringType().isEmail().isRequired(), 262 | e: NumberType().range(50, 60) 263 | }) 264 | ) 265 | .isRequired() 266 | }); 267 | 268 | model.check({ 269 | a: 'foobar', 270 | b: 'a', 271 | c: [{}, { d: ' ', e: 40 }] 272 | }); 273 | 274 | expect(model.getCheckResult()).to.deep.equal({ 275 | a: { hasError: false }, 276 | b: { hasError: true, errorMessage: 'b must be a valid email' }, 277 | c: { 278 | hasError: true, 279 | array: [ 280 | { 281 | hasError: true, 282 | object: { 283 | d: { hasError: true, errorMessage: 'd is a required field' }, 284 | e: { hasError: false } 285 | } 286 | }, 287 | { 288 | hasError: true, 289 | object: { 290 | d: { hasError: true, errorMessage: 'd is a required field' }, 291 | e: { hasError: true, errorMessage: 'e field must be between 50 and 60' } 292 | } 293 | } 294 | ] 295 | } 296 | }); 297 | 298 | expect(model.getCheckResult('c.0.d')).to.deep.equal({ 299 | hasError: true, 300 | errorMessage: 'd is a required field' 301 | }); 302 | }); 303 | }); 304 | 305 | describe('## static combine', () => { 306 | it('Should return a combined model. ', () => { 307 | const model1 = SchemaModel({ 308 | username: StringType().isRequired(), 309 | email: StringType().isEmail() 310 | }); 311 | 312 | const checkResult = model1.check({ 313 | username: 'foobar', 314 | email: 'foo@bar.com', 315 | age: 40 316 | }); 317 | 318 | expect(checkResult).to.deep.equal({ 319 | username: { hasError: false }, 320 | email: { hasError: false } 321 | }); 322 | 323 | const model2 = SchemaModel({ 324 | username: StringType().isRequired().minLength(7), 325 | age: NumberType().range(18, 30) 326 | }); 327 | 328 | const checkResult2 = SchemaModel.combine(model1, model2).check({ 329 | username: 'fooba', 330 | email: 'foo@bar.com', 331 | age: 40 332 | }); 333 | 334 | expect(checkResult2).to.deep.equal({ 335 | username: { hasError: true, errorMessage: 'username must be at least 7 characters' }, 336 | email: { hasError: false }, 337 | age: { hasError: true, errorMessage: 'age field must be between 18 and 30' } 338 | }); 339 | }); 340 | }); 341 | }); 342 | -------------------------------------------------------------------------------- /test/ObjectTypeSpec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { flaser } from 'object-flaser'; 3 | import * as schema from '../src'; 4 | 5 | const { ObjectType, StringType, NumberType, Schema } = schema; 6 | 7 | describe('#ObjectType', () => { 8 | it('Should be a valid object', () => { 9 | const schemaData = { 10 | url: StringType().isURL('Should be a url'), 11 | user: ObjectType().shape({ 12 | email: StringType().isEmail('Should be an email'), 13 | age: NumberType().min(18, 'Age should be greater than 18') 14 | }) 15 | }; 16 | 17 | const schema = new Schema(schemaData); 18 | 19 | const checkResult = schema.checkForField('user', { 20 | user: { email: 'simon.guo@hypers.com', age: 19 } 21 | }); 22 | 23 | expect(checkResult).to.deep.equal({ 24 | hasError: false, 25 | object: { 26 | email: { hasError: false }, 27 | age: { hasError: false } 28 | } 29 | }); 30 | 31 | const checkResult2 = schema.checkForField('user', { user: { email: 'simon.guo', age: 19 } }); 32 | 33 | expect(checkResult2).to.deep.equal({ 34 | hasError: true, 35 | object: { 36 | email: { hasError: true, errorMessage: 'Should be an email' }, 37 | age: { hasError: false } 38 | } 39 | }); 40 | 41 | const checkResult3 = schema.checkForField('user', { 42 | user: { 43 | email: 'simon.guo@hypers.com', 44 | age: 17 45 | } 46 | }); 47 | 48 | expect(checkResult3).to.deep.equal({ 49 | hasError: true, 50 | object: { 51 | email: { hasError: false }, 52 | age: { hasError: true, errorMessage: 'Age should be greater than 18' } 53 | } 54 | }); 55 | }); 56 | 57 | it('Should be checked for object nesting.', () => { 58 | const schemaData = { 59 | url: StringType().isURL('Should be a url'), 60 | user: ObjectType().shape({ 61 | email: StringType().isEmail('Should be an email'), 62 | age: NumberType().min(18, 'Age should be greater than 18'), 63 | parent: ObjectType().shape({ 64 | email: StringType().isEmail('Should be an email'), 65 | age: NumberType().min(50, 'Age should be greater than 50') 66 | }) 67 | }) 68 | }; 69 | 70 | const schema = new Schema(schemaData); 71 | 72 | const checkResult = schema.checkForField('user', { 73 | user: { 74 | email: 'simon.guo@hypers.com', 75 | age: 17, 76 | parent: { email: 'zicheng', age: 40 } 77 | } 78 | }); 79 | 80 | expect(checkResult).to.deep.equal({ 81 | hasError: true, 82 | object: { 83 | email: { hasError: false }, 84 | age: { hasError: true, errorMessage: 'Age should be greater than 18' }, 85 | parent: { 86 | hasError: true, 87 | object: { 88 | email: { hasError: true, errorMessage: 'Should be an email' }, 89 | age: { hasError: true, errorMessage: 'Age should be greater than 50' } 90 | } 91 | } 92 | } 93 | }); 94 | 95 | const checkResult2 = schema.checkForField('user', { 96 | user: { 97 | email: 'simon.guo@hypers.com', 98 | age: 18, 99 | parent: { email: 'zicheng@dd.com', age: 50 } 100 | } 101 | }); 102 | 103 | expect(checkResult2).to.deep.equal({ 104 | hasError: false, 105 | object: { 106 | email: { hasError: false }, 107 | age: { hasError: false }, 108 | parent: { 109 | hasError: false, 110 | object: { 111 | email: { hasError: false }, 112 | age: { hasError: false } 113 | } 114 | } 115 | } 116 | }); 117 | }); 118 | 119 | it('Should be a valid object by flaser', () => { 120 | const schemaData = { 121 | 'data.email': StringType().isEmail('Should be an email'), 122 | 'data.age': NumberType().min(18, 'Should be greater than 18') 123 | }; 124 | 125 | const data = { 126 | data: { email: 'simon.guo@hypers.com', age: 17 } 127 | }; 128 | 129 | const schema = new Schema(schemaData); 130 | const checkResult = schema.check(flaser(data)); 131 | 132 | expect(checkResult).to.deep.equal({ 133 | 'data.email': { hasError: false }, 134 | 'data.age': { hasError: true, errorMessage: 'Should be greater than 18' } 135 | }); 136 | }); 137 | 138 | it('Should aync check for object nesting', async () => { 139 | const schema = new Schema({ 140 | url: StringType().isURL('Should be a url'), 141 | user: ObjectType().shape({ 142 | email: StringType().addRule(() => { 143 | return new Promise(resolve => { 144 | setTimeout(() => resolve(false), 400); 145 | }); 146 | }, 'Should be an email'), 147 | age: NumberType().min(18, 'Should be greater than 18') 148 | }) 149 | }); 150 | 151 | const result = await schema.checkAsync({ url: 'url', user: { email: 'a', age: '10' } }); 152 | 153 | expect(result).to.deep.equal({ 154 | url: { hasError: true, errorMessage: 'Should be a url' }, 155 | user: { 156 | hasError: true, 157 | object: { 158 | email: { hasError: true, errorMessage: 'Should be an email' }, 159 | age: { hasError: true, errorMessage: 'Should be greater than 18' } 160 | } 161 | } 162 | }); 163 | }); 164 | 165 | it('Should be checked for object nesting with nestedObject option.', () => { 166 | const schema = new Schema({ 167 | url: StringType().isURL('Should be a url'), 168 | user: ObjectType().shape({ 169 | email: StringType().isEmail('Should be an email'), 170 | age: NumberType().min(18, 'Age should be greater than 18'), 171 | parent: ObjectType().shape({ 172 | email: StringType().isEmail('Should be an email').isRequired('Email is required'), 173 | age: NumberType().min(50, 'Age should be greater than 50') 174 | }) 175 | }) 176 | }); 177 | const options = { nestedObject: true }; 178 | 179 | const checkResult = schema.checkForField( 180 | 'user.parent.age', 181 | { user: { parent: { age: 40 } } }, 182 | options 183 | ); 184 | 185 | expect(checkResult).to.deep.equal({ 186 | hasError: true, 187 | errorMessage: 'Age should be greater than 50' 188 | }); 189 | 190 | expect(schema.getCheckResult()).to.deep.equal({ 191 | user: { 192 | object: { 193 | parent: { 194 | object: { age: { hasError: true, errorMessage: 'Age should be greater than 50' } } 195 | } 196 | } 197 | } 198 | }); 199 | 200 | const checkResult2 = schema.checkForField( 201 | 'user.parent.age', 202 | { user: { parent: { age: 60 } } }, 203 | options 204 | ); 205 | 206 | expect(checkResult2).to.deep.equal({ hasError: false }); 207 | 208 | expect(schema.getCheckResult()).to.deep.equal({ 209 | user: { object: { parent: { object: { age: { hasError: false } } } } } 210 | }); 211 | 212 | const checkResult3 = schema.checkForField( 213 | 'user.parent.email', 214 | { user: { parent: { age: 60 } } }, 215 | options 216 | ); 217 | 218 | expect(checkResult3).to.deep.equal({ hasError: true, errorMessage: 'Email is required' }); 219 | 220 | expect(schema.getCheckResult()).to.deep.equal({ 221 | user: { 222 | object: { 223 | parent: { 224 | object: { 225 | age: { hasError: false }, 226 | email: { hasError: true, errorMessage: 'Email is required' } 227 | } 228 | } 229 | } 230 | } 231 | }); 232 | }); 233 | 234 | it('Should aync check for object nesting', async () => { 235 | const schema = new Schema({ 236 | url: StringType().isURL('Should be a url'), 237 | user: ObjectType().shape({ 238 | email: StringType().isEmail('Should be an email'), 239 | age: NumberType().min(18, 'Should be greater than 18'), 240 | parent: ObjectType().shape({ 241 | email: StringType().addRule(value => { 242 | return new Promise(resolve => { 243 | setTimeout(() => { 244 | if (/@/.test(value)) { 245 | resolve(true); 246 | } 247 | resolve(false); 248 | }, 400); 249 | }); 250 | }, 'Should be an email'), 251 | age: NumberType().min(50, 'Age should be greater than 50') 252 | }) 253 | }) 254 | }); 255 | 256 | const options = { nestedObject: true }; 257 | 258 | const result = await schema.checkForFieldAsync( 259 | 'user.parent.email', 260 | { user: { parent: { email: 'a' } } }, 261 | options 262 | ); 263 | 264 | expect(result).to.deep.equal({ hasError: true, errorMessage: 'Should be an email' }); 265 | 266 | const result2 = await schema.checkForFieldAsync( 267 | 'user.parent.email', 268 | { user: { parent: { email: 'a@a.com' } } }, 269 | options 270 | ); 271 | 272 | expect(result2).to.deep.equal({ hasError: false }); 273 | }); 274 | 275 | it('Should not allow empty object', () => { 276 | const schema = new Schema({ 277 | user: ObjectType().isRequired('User is required') 278 | }); 279 | 280 | const result = schema.check({ user: null }); 281 | expect(result).to.deep.equal({ user: { hasError: true, errorMessage: 'User is required' } }); 282 | 283 | const result2 = schema.check({ user: undefined }); 284 | expect(result2).to.deep.equal({ user: { hasError: true, errorMessage: 'User is required' } }); 285 | 286 | const result3 = schema.check({ user: false }); 287 | expect(result3).to.deep.equal({ 288 | user: { hasError: true, errorMessage: 'user must be an object' } 289 | }); 290 | }); 291 | 292 | it('Should not allow empty object by async', async () => { 293 | const schema = new Schema({ 294 | user: ObjectType().isRequired('User is required') 295 | }); 296 | 297 | const result = await schema.checkAsync({ user: null }); 298 | expect(result).to.deep.equal({ user: { hasError: true, errorMessage: 'User is required' } }); 299 | 300 | const result2 = await schema.checkAsync({ user: undefined }); 301 | expect(result2).to.deep.equal({ user: { hasError: true, errorMessage: 'User is required' } }); 302 | 303 | const result3 = await schema.checkAsync({ user: false }); 304 | expect(result3).to.deep.equal({ 305 | user: { hasError: true, errorMessage: 'user must be an object' } 306 | }); 307 | }); 308 | 309 | it('Should allow empty object', () => { 310 | const schema = new Schema({ 311 | user: ObjectType() 312 | }); 313 | 314 | const result = schema.check({ user: null }); 315 | expect(result).to.deep.equal({ user: { hasError: false } }); 316 | }); 317 | 318 | it('Should allow empty object by async', async () => { 319 | const schema = new Schema({ 320 | user: ObjectType() 321 | }); 322 | 323 | const result = await schema.checkAsync({ user: null }); 324 | expect(result).to.deep.equal({ user: { hasError: false } }); 325 | }); 326 | 327 | it('Should replace default required message', () => { 328 | const schema = new Schema({ 329 | user: ObjectType().shape({ 330 | email1: StringType().isEmail().isRequired(), 331 | email2: StringType().isEmail().isRequired('Email is required') 332 | }) 333 | }); 334 | 335 | const result = schema.check({ user: { email1: '', email2: '' } }); 336 | 337 | expect(result.user.object.email1.errorMessage).to.equal('email1 is a required field'); 338 | expect(result.user.object.email2.errorMessage).to.equal('Email is required'); 339 | }); 340 | 341 | it('Should replace default required message with async', async () => { 342 | const schema = new Schema({ 343 | user: ObjectType().shape({ 344 | email1: StringType().isEmail().isRequired(), 345 | email2: StringType().isEmail().isRequired('Email is required') 346 | }) 347 | }); 348 | 349 | const result = await schema.checkAsync({ user: { email1: '', email2: '' } }); 350 | 351 | expect(result.user.object.email1.errorMessage).to.equal('email1 is a required field'); 352 | expect(result.user.object.email2.errorMessage).to.equal('Email is required'); 353 | }); 354 | 355 | describe('priority', () => { 356 | it('Should have the correct priority', () => { 357 | const schema = new Schema({ 358 | user: ObjectType().shape({ 359 | name: StringType() 360 | .isEmail('error1') 361 | .addRule(() => false, 'error2') 362 | }) 363 | }); 364 | 365 | const result = schema.check({ user: { name: 'a' } }); 366 | 367 | expect(result.user.object).to.deep.equal({ 368 | name: { hasError: true, errorMessage: 'error1' } 369 | }); 370 | 371 | const schema2 = new Schema({ 372 | user: ObjectType().shape({ 373 | name: StringType() 374 | .isEmail('error1') 375 | .addRule(() => false, 'error2', true) 376 | }) 377 | }); 378 | 379 | const result2 = schema2.check({ user: { name: 'a' } }); 380 | 381 | expect(result2.user.object).to.deep.equal({ 382 | name: { hasError: true, errorMessage: 'error2' } 383 | }); 384 | }); 385 | 386 | it('Should have the correct priority with async', async () => { 387 | const schema = new Schema({ 388 | user: ObjectType().shape({ 389 | name: StringType() 390 | .isEmail('error1') 391 | .addRule(() => false, 'error2') 392 | }) 393 | }); 394 | 395 | const result = await schema.checkAsync({ user: { name: 'a' } }); 396 | 397 | expect(result.user.object).to.deep.equal({ 398 | name: { hasError: true, errorMessage: 'error1' } 399 | }); 400 | 401 | const schema2 = new Schema({ 402 | user: ObjectType().shape({ 403 | name: StringType() 404 | .isEmail('error1') 405 | .addRule(() => false, 'error2', true) 406 | }) 407 | }); 408 | 409 | const result2 = await schema2.checkAsync({ user: { name: 'a' } }); 410 | 411 | expect(result2.user.object).to.deep.equal({ 412 | name: { hasError: true, errorMessage: 'error2' } 413 | }); 414 | }); 415 | }); 416 | }); 417 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # schema-typed 2 | 3 | Schema for data modeling & validation 4 | 5 | [![npm][npm-badge]][npm] [![GitHub Actions][actions-svg]][actions-home] [![Coverage Status][soverage-svg]][soverage] 6 | 7 | ## Table of Contents 8 | 9 | 10 | 11 | 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Getting Started](#getting-started) 15 | - [Multiple verification](#multiple-verification) 16 | - [Custom verification](#custom-verification) 17 | - [Field dependency validation](#field-dependency-validation) 18 | - [Asynchronous check](#asynchronous-check) 19 | - [Validate nested objects](#validate-nested-objects) 20 | - [Combine](#combine) 21 | - [API](#api) 22 | - [SchemaModel](#schemamodel) 23 | - [`static combine(...models)`](#static-combinemodels) 24 | - [`check(data: object)`](#checkdata-object) 25 | - [`checkAsync(data: object)`](#checkasyncdata-object) 26 | - [`checkForField(fieldName: string, data: object, options?: { nestedObject?: boolean })`](#checkforfieldfieldname-string-data-object-options--nestedobject-boolean-) 27 | - [`checkForFieldAsync(fieldName: string, data: object, options?: { nestedObject?: boolean })`](#checkforfieldasyncfieldname-string-data-object-options--nestedobject-boolean-) 28 | - [MixedType()](#mixedtype) 29 | - [`isRequired(errorMessage?: string, trim: boolean = true)`](#isrequirederrormessage-string-trim-boolean--true) 30 | - [`isRequiredOrEmpty(errorMessage?: string, trim: boolean = true)`](#isrequiredoremptyerrormessage-string-trim-boolean--true) 31 | - [`addRule(onValid: Function, errorMessage?: string, priority: boolean)`](#addruleonvalid-function-errormessage-string-priority-boolean) 32 | - [`addAsyncRule(onValid: Function, errorMessage?: string, priority: boolean)`](#addasyncruleonvalid-function-errormessage-string-priority-boolean) 33 | - [`when(condition: (schemaSpec: SchemaDeclaration) => Type)`](#whencondition-schemaspec-schemadeclarationdatatype-errormsgtype--type) 34 | - [`check(value: ValueType, data?: DataType):CheckResult`](#checkvalue-valuetype-data-datatypecheckresult) 35 | - [`checkAsync(value: ValueType, data?: DataType):Promise`](#checkasyncvalue-valuetype-data-datatypepromisecheckresult) 36 | - [`label(label: string)`](#labellabel-string) 37 | - [`equalTo(fieldName: string, errorMessage?: string)`](#equaltofieldname-string-errormessage-string) 38 | - [`proxy(fieldNames: string[], options?: { checkIfValueExists?: boolean })`](#proxyfieldnames-string-options--checkifvalueexists-boolean-) 39 | - [StringType(errorMessage?: string)](#stringtypeerrormessage-string) 40 | - [`isEmail(errorMessage?: string)`](#isemailerrormessage-string) 41 | - [`isURL(errorMessage?: string)`](#isurlerrormessage-string) 42 | - [`isOneOf(items: string[], errorMessage?: string)`](#isoneofitems-string-errormessage-string) 43 | - [`containsLetter(errorMessage?: string)`](#containslettererrormessage-string) 44 | - [`containsUppercaseLetter(errorMessage?: string)`](#containsuppercaselettererrormessage-string) 45 | - [`containsLowercaseLetter(errorMessage?: string)`](#containslowercaselettererrormessage-string) 46 | - [`containsLetterOnly(errorMessage?: string)`](#containsletteronlyerrormessage-string) 47 | - [`containsNumber(errorMessage?: string)`](#containsnumbererrormessage-string) 48 | - [`pattern(regExp: RegExp, errorMessage?: string)`](#patternregexp-regexp-errormessage-string) 49 | - [`rangeLength(minLength: number, maxLength: number, errorMessage?: string)`](#rangelengthminlength-number-maxlength-number-errormessage-string) 50 | - [`minLength(minLength: number, errorMessage?: string)`](#minlengthminlength-number-errormessage-string) 51 | - [`maxLength(maxLength: number, errorMessage?: string)`](#maxlengthmaxlength-number-errormessage-string) 52 | - [NumberType(errorMessage?: string)](#numbertypeerrormessage-string) 53 | - [`isInteger(errorMessage?: string)`](#isintegererrormessage-string) 54 | - [`isOneOf(items: number[], errorMessage?: string)`](#isoneofitems-number-errormessage-string) 55 | - [`pattern(regExp: RegExp, errorMessage?: string)`](#patternregexp-regexp-errormessage-string-1) 56 | - [`range(minLength: number, maxLength: number, errorMessage?: string)`](#rangeminlength-number-maxlength-number-errormessage-string) 57 | - [`min(min: number, errorMessage?: string)`](#minmin-number-errormessage-string) 58 | - [`max(max: number, errorMessage?: string)`](#maxmax-number-errormessage-string) 59 | - [ArrayType(errorMessage?: string)](#arraytypeerrormessage-string) 60 | - [`isRequiredOrEmpty(errorMessage?: string)`](#isrequiredoremptyerrormessage-string) 61 | - [`rangeLength(minLength: number, maxLength: number, errorMessage?: string)`](#rangelengthminlength-number-maxlength-number-errormessage-string-1) 62 | - [`minLength(minLength: number, errorMessage?: string)`](#minlengthminlength-number-errormessage-string-1) 63 | - [`maxLength(maxLength: number, errorMessage?: string)`](#maxlengthmaxlength-number-errormessage-string-1) 64 | - [`unrepeatable(errorMessage?: string)`](#unrepeatableerrormessage-string) 65 | - [`of()`](#of) 66 | - [DateType(errorMessage?: string)](#datetypeerrormessage-string) 67 | - [`range(min: Date, max: Date, errorMessage?: string)`](#rangemin-date-max-date-errormessage-string) 68 | - [`min(min: Date, errorMessage?: string)`](#minmin-date-errormessage-string) 69 | - [`max(max: Date, errorMessage?: string)`](#maxmax-date-errormessage-string) 70 | - [ObjectType(errorMessage?: string)](#objecttypeerrormessage-string) 71 | - [`shape(fields: object)`](#shapefields-object) 72 | - [BooleanType(errorMessage?: string)](#booleantypeerrormessage-string) 73 | - [⚠️ Notes](#-notes) 74 | 75 | 76 | 77 | ## Installation 78 | 79 | ``` 80 | npm install schema-typed --save 81 | ``` 82 | 83 | ## Usage 84 | 85 | ### Getting Started 86 | 87 | ```js 88 | import { SchemaModel, StringType, DateType, NumberType, ObjectType, ArrayType } from 'schema-typed'; 89 | 90 | const model = SchemaModel({ 91 | username: StringType().isRequired('Username required'), 92 | email: StringType().isEmail('Email required'), 93 | age: NumberType('Age should be a number').range(18, 30, 'Over the age limit'), 94 | tags: ArrayType().of(StringType('The tag should be a string').isRequired()), 95 | role: ObjectType().shape({ 96 | name: StringType().isRequired('Name required'), 97 | permissions: ArrayType().isRequired('Permissions required') 98 | }) 99 | }); 100 | 101 | const checkResult = model.check({ 102 | username: 'foobar', 103 | email: 'foo@bar.com', 104 | age: 40, 105 | tags: ['Sports', 'Games', 10], 106 | role: { name: 'administrator' } 107 | }); 108 | 109 | console.log(checkResult); 110 | ``` 111 | 112 | `checkResult` return structure is: 113 | 114 | ```js 115 | { 116 | username: { hasError: false }, 117 | email: { hasError: false }, 118 | age: { hasError: true, errorMessage: 'Over the age limit' }, 119 | tags: { 120 | hasError: true, 121 | array: [ 122 | { hasError: false }, 123 | { hasError: false }, 124 | { hasError: true, errorMessage: 'The tag should be a string' } 125 | ] 126 | }, 127 | role: { 128 | hasError: true, 129 | object: { 130 | name: { hasError: false }, 131 | permissions: { hasError: true, errorMessage: 'Permissions required' } 132 | } 133 | } 134 | }; 135 | ``` 136 | 137 | ### Multiple verification 138 | 139 | ```js 140 | StringType() 141 | .minLength(6, "Can't be less than 6 characters") 142 | .maxLength(30, 'Cannot be greater than 30 characters') 143 | .isRequired('This field required'); 144 | ``` 145 | 146 | ### Custom verification 147 | 148 | Customize a rule with the `addRule` function. 149 | 150 | If you are validating a string type of data, you can set a regular expression for custom validation by the `pattern` method. 151 | 152 | ```js 153 | const model = SchemaModel({ 154 | field1: StringType().addRule((value, data) => { 155 | return /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(value); 156 | }, 'Please enter legal characters'), 157 | field2: StringType().pattern(/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/, 'Please enter legal characters') 158 | }); 159 | 160 | model.check({ field1: '', field2: '' }); 161 | 162 | /** 163 | { 164 | field1: { 165 | hasError: true, 166 | errorMessage: 'Please enter legal characters' 167 | }, 168 | field2: { 169 | hasError: true, 170 | errorMessage: 'Please enter legal characters' 171 | } 172 | }; 173 | **/ 174 | ``` 175 | 176 | #### Field dependency validation 177 | 178 | 1. Use the `equalTo` method to verify that the values of two fields are equal. 179 | 180 | ```js 181 | const model = SchemaModel({ 182 | password: StringType().isRequired(), 183 | confirmPassword: StringType().equalTo('password') 184 | }); 185 | ``` 186 | 187 | 2. Use the `addRule` method to create a custom validation rule. 188 | 189 | ```js 190 | const model = SchemaModel({ 191 | password: StringType().isRequired(), 192 | confirmPassword: StringType().addRule( 193 | (value, data) => value === data.password, 194 | 'Confirm password must be the same as password' 195 | ) 196 | }); 197 | ``` 198 | 199 | 3. Use the `proxy` method to verify that a field passes, and then proxy verification of other fields. 200 | 201 | ```js 202 | const model = SchemaModel({ 203 | password: StringType().isRequired().proxy(['confirmPassword']), 204 | confirmPassword: StringType().equalTo('password') 205 | }); 206 | ``` 207 | 208 | #### Asynchronous check 209 | 210 | For example, verify that the mailbox is duplicated 211 | 212 | ```js 213 | function asyncCheckEmail(email) { 214 | return new Promise(resolve => { 215 | setTimeout(() => { 216 | if (email === 'foo@domain.com') { 217 | resolve(false); 218 | } else { 219 | resolve(true); 220 | } 221 | }, 500); 222 | }); 223 | } 224 | 225 | const model = SchemaModel({ 226 | email: StringType() 227 | .isEmail('Please input the correct email address') 228 | .addAsyncRule((value, data) => { 229 | return asyncCheckEmail(value); 230 | }, 'Email address already exists') 231 | .isRequired('This field cannot be empty') 232 | }); 233 | 234 | model.checkAsync({ email: 'foo@domain.com' }).then(checkResult => { 235 | console.log(checkResult); 236 | /** 237 | { 238 | email: { 239 | hasError: true, 240 | errorMessage: 'Email address already exists' 241 | } 242 | }; 243 | **/ 244 | }); 245 | ``` 246 | 247 | ### Validate nested objects 248 | 249 | Validate nested objects, which can be defined using the `ObjectType().shape` method. E.g: 250 | 251 | ```js 252 | const model = SchemaModel({ 253 | id: NumberType().isRequired('This field required'), 254 | name: StringType().isRequired('This field required'), 255 | info: ObjectType().shape({ 256 | email: StringType().isEmail('Should be an email'), 257 | age: NumberType().min(18, 'Age should be greater than 18 years old') 258 | }) 259 | }); 260 | 261 | const user = { 262 | id: 1, 263 | name: '', 264 | info: { email: 'schema-type', age: 17 } 265 | }; 266 | 267 | model.check(data); 268 | 269 | /** 270 | { 271 | "id": { "hasError": false }, 272 | "name": { "hasError": true, "errorMessage": "This field required" }, 273 | "info": { 274 | "hasError": true, 275 | "object": { 276 | "email": { "hasError": true, "errorMessage": "Should be an email" }, 277 | "age": { "hasError": true, "errorMessage": "Age should be greater than 18 years old" } 278 | } 279 | } 280 | } 281 | */ 282 | ``` 283 | 284 | ### Combine 285 | 286 | `SchemaModel` provides a static method `combine` that can be combined with multiple `SchemaModel` to return a new `SchemaModel`. 287 | 288 | ```js 289 | const model1 = SchemaModel({ 290 | username: StringType().isRequired('This field required'), 291 | email: StringType().isEmail('Should be an email') 292 | }); 293 | 294 | const model2 = SchemaModel({ 295 | username: StringType().minLength(7, "Can't be less than 7 characters"), 296 | age: NumberType().range(18, 30, 'Age should be greater than 18 years old') 297 | }); 298 | 299 | const model3 = SchemaModel({ 300 | groupId: NumberType().isRequired('This field required') 301 | }); 302 | 303 | const model4 = SchemaModel.combine(model1, model2, model3); 304 | 305 | model4.check({ 306 | username: 'foobar', 307 | email: 'foo@bar.com', 308 | age: 40, 309 | groupId: 1 310 | }); 311 | ``` 312 | 313 | ## API 314 | 315 | ### SchemaModel 316 | 317 | SchemaModel is a JavaScript schema builder for data model creation and validation. 318 | 319 | #### `static combine(...models)` 320 | 321 | A static method for merging multiple models. 322 | 323 | ```js 324 | const model1 = SchemaModel({ 325 | username: StringType().isRequired('This field required') 326 | }); 327 | 328 | const model2 = SchemaModel({ 329 | email: StringType().isEmail('Please input the correct email address') 330 | }); 331 | 332 | const model3 = SchemaModel.combine(model1, model2); 333 | ``` 334 | 335 | #### `check(data: object)` 336 | 337 | Check whether the data conforms to the model shape definition. Return a check result. 338 | 339 | ```js 340 | const model = SchemaModel({ 341 | username: StringType().isRequired('This field required'), 342 | email: StringType().isEmail('Please input the correct email address') 343 | }); 344 | 345 | model.check({ 346 | username: 'root', 347 | email: 'root@email.com' 348 | }); 349 | ``` 350 | 351 | #### `checkAsync(data: object)` 352 | 353 | Asynchronously check whether the data conforms to the model shape definition. Return a check result. 354 | 355 | ```js 356 | const model = SchemaModel({ 357 | username: StringType() 358 | .isRequired('This field required') 359 | .addRule(value => { 360 | return new Promise(resolve => { 361 | // Asynchronous processing logic 362 | }); 363 | }, 'Username already exists'), 364 | email: StringType().isEmail('Please input the correct email address') 365 | }); 366 | 367 | model 368 | .checkAsync({ 369 | username: 'root', 370 | email: 'root@email.com' 371 | }) 372 | .then(result => { 373 | // Data verification result 374 | }); 375 | ``` 376 | 377 | #### `checkForField(fieldName: string, data: object, options?: { nestedObject?: boolean })` 378 | 379 | Check whether a field in the data conforms to the model shape definition. Return a check result. 380 | 381 | ```js 382 | const model = SchemaModel({ 383 | username: StringType().isRequired('This field required'), 384 | email: StringType().isEmail('Please input the correct email address') 385 | }); 386 | 387 | const data = { 388 | username: 'root' 389 | }; 390 | 391 | model.checkForField('username', data); 392 | ``` 393 | 394 | #### `checkForFieldAsync(fieldName: string, data: object, options?: { nestedObject?: boolean })` 395 | 396 | Asynchronously check whether a field in the data conforms to the model shape definition. Return a check result. 397 | 398 | ```js 399 | const model = SchemaModel({ 400 | username: StringType() 401 | .isRequired('This field required') 402 | .addAsyncRule(value => { 403 | return new Promise(resolve => { 404 | // Asynchronous processing logic 405 | }); 406 | }, 'Username already exists'), 407 | email: StringType().isEmail('Please input the correct email address') 408 | }); 409 | 410 | const data = { 411 | username: 'root' 412 | }; 413 | 414 | model.checkForFieldAsync('username', data).then(result => { 415 | // Data verification result 416 | }); 417 | ``` 418 | 419 | ### MixedType() 420 | 421 | Creates a type that matches all types. All types inherit from this base type. 422 | 423 | #### `isRequired(errorMessage?: string, trim: boolean = true)` 424 | 425 | ```js 426 | MixedType().isRequired('This field required'); 427 | ``` 428 | 429 | #### `isRequiredOrEmpty(errorMessage?: string, trim: boolean = true)` 430 | 431 | ```js 432 | MixedType().isRequiredOrEmpty('This field required'); 433 | ``` 434 | 435 | #### `addRule(onValid: Function, errorMessage?: string, priority: boolean)` 436 | 437 | ```js 438 | MixedType().addRule((value, data) => { 439 | return /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(value); 440 | }, 'Please enter a legal character.'); 441 | ``` 442 | 443 | #### `addAsyncRule(onValid: Function, errorMessage?: string, priority: boolean)` 444 | 445 | ```js 446 | MixedType().addAsyncRule((value, data) => { 447 | return new Promise(resolve => { 448 | // Asynchronous processing logic 449 | }); 450 | }, 'Please enter a legal character.'); 451 | ``` 452 | 453 | #### `when(condition: (schemaSpec: SchemaDeclaration) => Type)` 454 | 455 | Conditional validation, the return value is a new type. 456 | 457 | ```ts 458 | const model = SchemaModel({ 459 | option: StringType().isOneOf(['a', 'b', 'other']), 460 | other: StringType().when(schema => { 461 | const { value } = schema.option; 462 | return value === 'other' ? StringType().isRequired('Other required') : StringType(); 463 | }) 464 | }); 465 | 466 | /** 467 | { 468 | option: { hasError: false }, 469 | other: { hasError: false } 470 | } 471 | */ 472 | model.check({ option: 'a', other: '' }); 473 | 474 | /* 475 | { 476 | option: { hasError: false }, 477 | other: { hasError: true, errorMessage: 'Other required' } 478 | } 479 | */ 480 | model.check({ option: 'other', other: '' }); 481 | ``` 482 | 483 | Check whether a field passes the validation to determine the validation rules of another field. 484 | 485 | ```js 486 | const model = SchemaModel({ 487 | password: StringType().isRequired('Password required'), 488 | confirmPassword: StringType().when(schema => { 489 | const { hasError } = schema.password.check(); 490 | return hasError 491 | ? StringType() 492 | : StringType().addRule( 493 | value => value === schema.password.value, 494 | 'The passwords are inconsistent twice' 495 | ); 496 | }) 497 | }); 498 | ``` 499 | 500 | #### `check(value: ValueType, data?: DataType):CheckResult` 501 | 502 | ```js 503 | const type = MixedType().addRule(v => { 504 | if (typeof v === 'number') { 505 | return true; 506 | } 507 | return false; 508 | }, 'Please enter a valid number'); 509 | 510 | type.check('1'); // { hasError: true, errorMessage: 'Please enter a valid number' } 511 | type.check(1); // { hasError: false } 512 | ``` 513 | 514 | #### `checkAsync(value: ValueType, data?: DataType):Promise` 515 | 516 | ```js 517 | const type = MixedType().addRule(v => { 518 | return new Promise(resolve => { 519 | setTimeout(() => { 520 | if (typeof v === 'number') { 521 | resolve(true); 522 | } else { 523 | resolve(false); 524 | } 525 | }, 500); 526 | }); 527 | }, 'Please enter a valid number'); 528 | 529 | type.checkAsync('1').then(checkResult => { 530 | // { hasError: true, errorMessage: 'Please enter a valid number' } 531 | }); 532 | type.checkAsync(1).then(checkResult => { 533 | // { hasError: false } 534 | }); 535 | ``` 536 | 537 | #### `label(label: string)` 538 | 539 | Overrides the key name in error messages. 540 | 541 | ```js 542 | MixedType().label('Username'); 543 | ``` 544 | 545 | Eg: 546 | 547 | ```js 548 | SchemaModel({ 549 | first_name: StringType().label('First name'), 550 | age: NumberType().label('Age') 551 | }); 552 | ``` 553 | 554 | #### `equalTo(fieldName: string, errorMessage?: string)` 555 | 556 | Check if the value is equal to the value of another field. 557 | 558 | ```js 559 | SchemaModel({ 560 | password: StringType().isRequired(), 561 | confirmPassword: StringType().equalTo('password') 562 | }); 563 | ``` 564 | 565 | #### `proxy(fieldNames: string[], options?: { checkIfValueExists?: boolean })` 566 | 567 | After the field verification passes, proxy verification of other fields. 568 | 569 | - `fieldNames`: The field name to be proxied. 570 | - `options.checkIfValueExists`: When the value of other fields exists, the verification is performed (default: false) 571 | 572 | ```js 573 | SchemaModel({ 574 | password: StringType().isRequired().proxy(['confirmPassword']), 575 | confirmPassword: StringType().equalTo('password') 576 | }); 577 | ``` 578 | 579 | ### StringType(errorMessage?: string) 580 | 581 | Define a string type. Supports all the same methods as [MixedType](#mixedtype). 582 | 583 | #### `isEmail(errorMessage?: string)` 584 | 585 | ```js 586 | StringType().isEmail('Please input the correct email address'); 587 | ``` 588 | 589 | #### `isURL(errorMessage?: string)` 590 | 591 | ```js 592 | StringType().isURL('Please enter the correct URL address'); 593 | ``` 594 | 595 | #### `isOneOf(items: string[], errorMessage?: string)` 596 | 597 | ```js 598 | StringType().isOneOf(['Javascript', 'CSS'], 'Can only type `Javascript` and `CSS`'); 599 | ``` 600 | 601 | #### `containsLetter(errorMessage?: string)` 602 | 603 | ```js 604 | StringType().containsLetter('Must contain English characters'); 605 | ``` 606 | 607 | #### `containsUppercaseLetter(errorMessage?: string)` 608 | 609 | ```js 610 | StringType().containsUppercaseLetter('Must contain uppercase English characters'); 611 | ``` 612 | 613 | #### `containsLowercaseLetter(errorMessage?: string)` 614 | 615 | ```js 616 | StringType().containsLowercaseLetter('Must contain lowercase English characters'); 617 | ``` 618 | 619 | #### `containsLetterOnly(errorMessage?: string)` 620 | 621 | ```js 622 | StringType().containsLetterOnly('English characters that can only be included'); 623 | ``` 624 | 625 | #### `containsNumber(errorMessage?: string)` 626 | 627 | ```js 628 | StringType().containsNumber('Must contain numbers'); 629 | ``` 630 | 631 | #### `pattern(regExp: RegExp, errorMessage?: string)` 632 | 633 | ```js 634 | StringType().pattern(/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/, 'Please enter legal characters'); 635 | ``` 636 | 637 | #### `rangeLength(minLength: number, maxLength: number, errorMessage?: string)` 638 | 639 | ```js 640 | StringType().rangeLength(6, 30, 'The number of characters can only be between 6 and 30'); 641 | ``` 642 | 643 | #### `minLength(minLength: number, errorMessage?: string)` 644 | 645 | ```js 646 | StringType().minLength(6, 'Minimum 6 characters required'); 647 | ``` 648 | 649 | #### `maxLength(maxLength: number, errorMessage?: string)` 650 | 651 | ```js 652 | StringType().maxLength(30, 'The maximum is only 30 characters.'); 653 | ``` 654 | 655 | ### NumberType(errorMessage?: string) 656 | 657 | Define a number type. Supports all the same methods as [MixedType](#mixedtype). 658 | 659 | #### `isInteger(errorMessage?: string)` 660 | 661 | ```js 662 | NumberType().isInteger('It can only be an integer'); 663 | ``` 664 | 665 | #### `isOneOf(items: number[], errorMessage?: string)` 666 | 667 | ```js 668 | NumberType().isOneOf([5, 10, 15], 'Can only be `5`, `10`, `15`'); 669 | ``` 670 | 671 | #### `pattern(regExp: RegExp, errorMessage?: string)` 672 | 673 | ```js 674 | NumberType().pattern(/^[1-9][0-9]{3}$/, 'Please enter a legal character.'); 675 | ``` 676 | 677 | #### `range(minLength: number, maxLength: number, errorMessage?: string)` 678 | 679 | ```js 680 | NumberType().range(18, 40, 'Please enter a number between 18 - 40'); 681 | ``` 682 | 683 | #### `min(min: number, errorMessage?: string)` 684 | 685 | ```js 686 | NumberType().min(18, 'Minimum 18'); 687 | ``` 688 | 689 | #### `max(max: number, errorMessage?: string)` 690 | 691 | ```js 692 | NumberType().max(40, 'Maximum 40'); 693 | ``` 694 | 695 | ### ArrayType(errorMessage?: string) 696 | 697 | Define a array type. Supports all the same methods as [MixedType](#mixedtype). 698 | 699 | #### `isRequiredOrEmpty(errorMessage?: string)` 700 | 701 | ```js 702 | ArrayType().isRequiredOrEmpty('This field required'); 703 | ``` 704 | 705 | #### `rangeLength(minLength: number, maxLength: number, errorMessage?: string)` 706 | 707 | ```js 708 | ArrayType().rangeLength(1, 3, 'Choose at least one, but no more than three'); 709 | ``` 710 | 711 | #### `minLength(minLength: number, errorMessage?: string)` 712 | 713 | ```js 714 | ArrayType().minLength(1, 'Choose at least one'); 715 | ``` 716 | 717 | #### `maxLength(maxLength: number, errorMessage?: string)` 718 | 719 | ```js 720 | ArrayType().maxLength(3, "Can't exceed three"); 721 | ``` 722 | 723 | #### `unrepeatable(errorMessage?: string)` 724 | 725 | ```js 726 | ArrayType().unrepeatable('Duplicate options cannot appear'); 727 | ``` 728 | 729 | #### `of()` 730 | 731 | ```js 732 | // for every element of array 733 | ArrayType().of(StringType('The tag should be a string').isRequired()); 734 | // for every element of array 735 | ArrayType().of( 736 | ObjectType().shape({ 737 | name: StringType().isEmail() 738 | }) 739 | ); 740 | // just specify the first and the second element 741 | ArrayType().of( 742 | StringType().isEmail(), 743 | ObjectType().shape({ 744 | name: StringType().isEmail() 745 | }) 746 | ); 747 | ``` 748 | 749 | ### DateType(errorMessage?: string) 750 | 751 | Define a date type. Supports all the same methods as [MixedType](#mixedtype). 752 | 753 | #### `range(min: Date, max: Date, errorMessage?: string)` 754 | 755 | ```js 756 | DateType().range( 757 | new Date('08/01/2017'), 758 | new Date('08/30/2017'), 759 | 'Date should be between 08/01/2017 - 08/30/2017' 760 | ); 761 | ``` 762 | 763 | #### `min(min: Date, errorMessage?: string)` 764 | 765 | ```js 766 | DateType().min(new Date('08/01/2017'), 'Minimum date 08/01/2017'); 767 | ``` 768 | 769 | #### `max(max: Date, errorMessage?: string)` 770 | 771 | ```js 772 | DateType().max(new Date('08/30/2017'), 'Maximum date 08/30/2017'); 773 | ``` 774 | 775 | ### ObjectType(errorMessage?: string) 776 | 777 | Define a object type. Supports all the same methods as [MixedType](#mixedtype). 778 | 779 | #### `shape(fields: object)` 780 | 781 | ```js 782 | ObjectType().shape({ 783 | email: StringType().isEmail('Should be an email'), 784 | age: NumberType().min(18, 'Age should be greater than 18 years old') 785 | }); 786 | ``` 787 | 788 | ### BooleanType(errorMessage?: string) 789 | 790 | Define a boolean type. Supports all the same methods as [MixedType](#mixedtype). 791 | 792 | ## ⚠️ Notes 793 | 794 | Default check priority: 795 | 796 | - 1.isRequired 797 | - 2.All other checks are executed in sequence 798 | 799 | If the third argument to addRule is `true`, the priority of the check is as follows: 800 | 801 | - 1.addRule 802 | - 2.isRequired 803 | - 3.Predefined rules (if there is no isRequired, value is empty, the rule is not executed) 804 | 805 | [npm-badge]: https://img.shields.io/npm/v/schema-typed.svg 806 | [npm]: https://www.npmjs.com/package/schema-typed 807 | [actions-svg]: https://github.com/rsuite/schema-typed/workflows/Node.js%20CI/badge.svg?branch=master 808 | [actions-home]: https://github.com/rsuite/schema-typed/actions/workflows/nodejs-ci.yml 809 | [soverage-svg]: https://coveralls.io/repos/github/rsuite/schema-typed/badge.svg?branch=master 810 | [soverage]: https://coveralls.io/github/rsuite/schema-typed?branch=master 811 | -------------------------------------------------------------------------------- /test/ArrayTypeSpec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as schema from '../src'; 3 | 4 | const { ArrayType, StringType, NumberType, ObjectType, Schema } = schema; 5 | 6 | describe('#ArrayType', () => { 7 | it('Should be a valid array', () => { 8 | const schemaData = { 9 | data: ArrayType().minLength(2, 'error1').of(StringType().isEmail('error2')), 10 | data2: ArrayType().minLength(2).of(StringType().isEmail()) 11 | }; 12 | 13 | const schema = new Schema(schemaData); 14 | 15 | const checkResult = schema.checkForField('data', { 16 | data: ['simon.guo@hypers.com', 'ddddd@d.com', 'ddd@bbb.com'] 17 | }); 18 | 19 | expect(checkResult).to.deep.equal({ 20 | hasError: false, 21 | array: [{ hasError: false }, { hasError: false }, { hasError: false }] 22 | }); 23 | 24 | const checkResult2 = schema.check({ 25 | data: ['simon.guo@hypers.com', 'error_email', 'ddd@bbb.com'] 26 | }); 27 | 28 | expect(checkResult2).to.deep.equal({ 29 | data: { 30 | hasError: true, 31 | array: [ 32 | { hasError: false }, 33 | { hasError: true, errorMessage: 'error2' }, 34 | { hasError: false } 35 | ] 36 | }, 37 | data2: { hasError: false } 38 | }); 39 | 40 | const checkResult3 = schema.check({ 41 | data2: [] 42 | }); 43 | 44 | expect(checkResult3).to.deep.equal({ 45 | data: { hasError: false }, 46 | data2: { hasError: true, errorMessage: 'data2 field must have at least 2 items' } 47 | }); 48 | 49 | const checkResult4 = schema.check({ 50 | data2: ['simon.guo@hypers.com', 'error_email', 'ddd@bbb.com'] 51 | }); 52 | 53 | expect(checkResult4).to.deep.equal({ 54 | data: { hasError: false }, 55 | data2: { 56 | hasError: true, 57 | array: [ 58 | { hasError: false }, 59 | { hasError: true, errorMessage: 'data2.[1] must be a valid email' }, 60 | { hasError: false } 61 | ] 62 | } 63 | }); 64 | }); 65 | 66 | it('Should output default error message ', () => { 67 | const schemaData = { data: ArrayType().of(StringType().isEmail()) }; 68 | const schema = new Schema(schemaData); 69 | const checkStatus = schema.checkForField('data', { 70 | data: ['simon.guo@hypers.com', 'error_email', 'ddd@bbb.com'] 71 | }); 72 | 73 | checkStatus.array[1].hasError.should.equal(true); 74 | checkStatus.array[1].errorMessage.should.equal('data.[1] must be a valid email'); 75 | }); 76 | 77 | it('Should be unrepeatable ', () => { 78 | const schemaData = { data: ArrayType().unrepeatable('error1') }; 79 | const schema = new Schema(schemaData); 80 | const checkStatus = schema.checkForField('data', { data: ['abc', '123', 'abc'] }); 81 | 82 | checkStatus.hasError.should.equal(true); 83 | checkStatus.errorMessage.should.equal('error1'); 84 | 85 | const schemaData2 = { data: ArrayType().unrepeatable() }; 86 | const schema2 = new Schema(schemaData2); 87 | const checkStatus2 = schema2.checkForField('data', { data: ['abc', '123', 'abc'] }); 88 | checkStatus2.errorMessage.should.equal('data must have non-repeatable items'); 89 | 90 | schema.checkForField('data', { data: ['1', '2', '3'] }).hasError.should.equal(false); 91 | }); 92 | 93 | it('Should be required ', () => { 94 | const schemaData = { 95 | data: ArrayType().isRequired('error1'), 96 | data2: ArrayType().isRequired() 97 | }; 98 | const schema = new Schema(schemaData); 99 | const checkStatus = schema.checkForField('data', { data: null }); 100 | const checkStatus2 = schema.checkForField('data2', { data2: null }); 101 | 102 | checkStatus.hasError.should.equal(true); 103 | checkStatus.errorMessage.should.equal('error1'); 104 | checkStatus2.errorMessage.should.equal('data2 is a required field'); 105 | 106 | schema.checkForField('data', { data: [] }).hasError.should.equal(true); 107 | schema.checkForField('data', { data: undefined }).hasError.should.equal(true); 108 | }); 109 | 110 | it('Should be within the number of items', () => { 111 | const schemaData = { 112 | data: ArrayType().rangeLength(2, 4) 113 | }; 114 | const schema = new Schema(schemaData); 115 | schema.checkForField('data', { data: [1, 2] }).hasError.should.equal(false); 116 | schema.checkForField('data', { data: [1] }).hasError.should.equal(true); 117 | schema.checkForField('data', { data: [1, 2, 3, 4, 5] }).hasError.should.equal(true); 118 | schema 119 | .checkForField('data', { data: [1] }) 120 | .errorMessage.should.equal('data must contain 2 to 4 items'); 121 | }); 122 | 123 | it('Should not exceed the maximum number of items', () => { 124 | const schemaData = { 125 | data: ArrayType().maxLength(2) 126 | }; 127 | const schema = new Schema(schemaData); 128 | schema.checkForField('data', { data: [1, 2, 3] }).hasError.should.equal(true); 129 | schema 130 | .checkForField('data', { data: [1, 2, 3] }) 131 | .errorMessage.should.equal('data field must have less than or equal to 2 items'); 132 | }); 133 | 134 | it('Should not be less than the maximum number of items', () => { 135 | const schemaData = { 136 | data: ArrayType().minLength(2) 137 | }; 138 | const schema = new Schema(schemaData); 139 | schema.checkForField('data', { data: [1] }).hasError.should.equal(true); 140 | schema 141 | .checkForField('data', { data: [1] }) 142 | .errorMessage.should.equal('data field must have at least 2 items'); 143 | }); 144 | 145 | describe('Nested Object', () => { 146 | const options = { 147 | nestedObject: true 148 | }; 149 | 150 | it('Should support array nested objects', () => { 151 | const schemaData = { 152 | users: ArrayType().of( 153 | ObjectType('error1').shape({ 154 | email: StringType().isEmail('error2'), 155 | age: NumberType().min(18, 'error3') 156 | }) 157 | ), 158 | users2: ArrayType().of( 159 | ObjectType().shape({ 160 | email: StringType().isEmail(), 161 | age: NumberType().min(18) 162 | }) 163 | ) 164 | }; 165 | const schema = new Schema(schemaData); 166 | const checkResult = schema.check({ 167 | users: [ 168 | 'simon.guo@hypers.com', 169 | { email: 'error_email', age: 19 }, 170 | { email: 'error_email', age: 17 } 171 | ] 172 | }); 173 | 174 | expect(checkResult).to.deep.equal({ 175 | users: { 176 | hasError: true, 177 | array: [ 178 | { hasError: true, errorMessage: 'error1' }, 179 | { 180 | hasError: true, 181 | object: { 182 | email: { hasError: true, errorMessage: 'error2' }, 183 | age: { hasError: false } 184 | } 185 | }, 186 | { 187 | hasError: true, 188 | object: { 189 | email: { hasError: true, errorMessage: 'error2' }, 190 | age: { hasError: true, errorMessage: 'error3' } 191 | } 192 | } 193 | ] 194 | }, 195 | users2: { hasError: false } 196 | }); 197 | 198 | const schema2 = new Schema(schemaData); 199 | const checkResult2 = schema2.check({ 200 | users2: [ 201 | 'simon.guo@hypers.com', 202 | { email: 'error_email', age: 19 }, 203 | { email: 'error_email', age: 17 } 204 | ] 205 | }); 206 | 207 | expect(checkResult2).to.deep.equal({ 208 | users: { hasError: false }, 209 | users2: { 210 | hasError: true, 211 | array: [ 212 | { hasError: true, errorMessage: 'users2.[0] must be an object' }, 213 | { 214 | hasError: true, 215 | object: { 216 | email: { hasError: true, errorMessage: 'email must be a valid email' }, 217 | age: { hasError: false } 218 | } 219 | }, 220 | { 221 | hasError: true, 222 | object: { 223 | email: { hasError: true, errorMessage: 'email must be a valid email' }, 224 | age: { hasError: true, errorMessage: 'age must be greater than or equal to 18' } 225 | } 226 | } 227 | ] 228 | } 229 | }); 230 | }); 231 | 232 | it('Should validate nested array with required fields', () => { 233 | const schema = new Schema({ 234 | address: ArrayType().of( 235 | ObjectType().shape({ 236 | city: StringType().isRequired('City is required'), 237 | postCode: StringType().isRequired('Post code is required') 238 | }) 239 | ) 240 | }); 241 | 242 | const checkResult = schema.check({ 243 | address: [ 244 | { city: 'Shanghai', postCode: '200000' }, 245 | { city: 'Beijing', postCode: '100000' } 246 | ] 247 | }); 248 | 249 | expect(checkResult).to.deep.equal({ 250 | address: { 251 | hasError: false, 252 | array: [ 253 | { 254 | hasError: false, 255 | object: { city: { hasError: false }, postCode: { hasError: false } } 256 | }, 257 | { 258 | hasError: false, 259 | object: { city: { hasError: false }, postCode: { hasError: false } } 260 | } 261 | ] 262 | } 263 | }); 264 | 265 | const checkResult2 = schema.check({ 266 | address: [{ postCode: '200000' }, { city: 'Beijing' }] 267 | }); 268 | 269 | expect(checkResult2).to.deep.equal({ 270 | address: { 271 | hasError: true, 272 | array: [ 273 | { 274 | hasError: true, 275 | object: { 276 | city: { 277 | hasError: true, 278 | errorMessage: 'City is required' 279 | }, 280 | postCode: { 281 | hasError: false 282 | } 283 | } 284 | }, 285 | { 286 | hasError: true, 287 | object: { 288 | city: { 289 | hasError: false 290 | }, 291 | postCode: { 292 | hasError: true, 293 | errorMessage: 'Post code is required' 294 | } 295 | } 296 | } 297 | ] 298 | } 299 | }); 300 | }); 301 | 302 | it('Should check a field in an array', () => { 303 | const schema = new Schema({ 304 | address: ArrayType().of( 305 | ObjectType().shape({ 306 | city: StringType().isRequired('City is required'), 307 | postCode: StringType().isRequired('Post code is required') 308 | }) 309 | ) 310 | }); 311 | 312 | const checkResult = schema.checkForField( 313 | 'address[0].city', 314 | { address: [{ city: 'Shanghai' }] }, 315 | options 316 | ); 317 | 318 | expect(checkResult).to.deep.equal({ 319 | hasError: false 320 | }); 321 | 322 | const checkResult2 = schema.checkForField( 323 | 'address[1].postCode', 324 | { address: [{ postCode: '' }] }, 325 | options 326 | ); 327 | 328 | expect(checkResult2).to.deep.equal({ 329 | hasError: true, 330 | errorMessage: 'Post code is required' 331 | }); 332 | }); 333 | 334 | it('Should check primitive type array items', () => { 335 | const schema = new Schema({ 336 | emails: ArrayType().of(StringType().isEmail('Invalid email')), 337 | numbers: ArrayType().of(NumberType().min(0, 'Must be positive')) 338 | }); 339 | 340 | // Test valid email 341 | expect( 342 | schema.checkForField('emails[0]', { emails: ['test@example.com'] }, options) 343 | ).to.deep.equal({ 344 | hasError: false 345 | }); 346 | 347 | // Test invalid email 348 | expect( 349 | schema.checkForField('emails[0]', { emails: ['invalid-email'] }, options) 350 | ).to.deep.equal({ 351 | hasError: true, 352 | errorMessage: 'Invalid email' 353 | }); 354 | 355 | // Test negative number 356 | expect(schema.checkForField('numbers[0]', { numbers: [-1] }, options)).to.deep.equal({ 357 | hasError: true, 358 | errorMessage: 'Must be positive' 359 | }); 360 | }); 361 | 362 | it('Should support nested arrays', () => { 363 | const schema = new Schema({ 364 | matrix: ArrayType().of(ArrayType().of(NumberType().min(0, 'Must be positive'))) 365 | }); 366 | 367 | // Test negative number in nested array 368 | expect( 369 | schema.checkForField( 370 | 'matrix[0][1]', 371 | { 372 | matrix: [[0, -1]] 373 | }, 374 | options 375 | ) 376 | ).to.deep.equal({ 377 | hasError: true, 378 | errorMessage: 'Must be positive' 379 | }); 380 | }); 381 | 382 | it('Should support nested arrays in check', () => { 383 | const schema = new Schema({ 384 | matrix: ArrayType().of(ArrayType().of(NumberType().min(0, 'Must be positive'))) 385 | }); 386 | 387 | // Test negative number in nested array 388 | expect( 389 | schema.check({ 390 | matrix: [[0, -1]] 391 | }) 392 | ).to.deep.equal({ 393 | matrix: { 394 | array: [ 395 | { 396 | array: [ 397 | { 398 | hasError: false 399 | }, 400 | { 401 | errorMessage: 'Must be positive', 402 | hasError: true 403 | } 404 | ], 405 | hasError: true 406 | } 407 | ], 408 | hasError: true 409 | } 410 | }); 411 | }); 412 | 413 | it('Should validate array elements with complex validation rules', () => { 414 | const schema = new Schema({ 415 | users: ArrayType().of( 416 | ObjectType().shape({ 417 | name: StringType().isRequired('Name is required'), 418 | age: NumberType().min(18, 'Must be an adult'), 419 | email: StringType().isEmail('Invalid email format') 420 | }) 421 | ) 422 | }); 423 | 424 | // Test valid name 425 | expect( 426 | schema.checkForField( 427 | 'users[0].name', 428 | { 429 | users: [{ name: 'John Doe' }] 430 | }, 431 | { 432 | nestedObject: true 433 | } 434 | ) 435 | ).to.deep.equal({ 436 | hasError: false 437 | }); 438 | 439 | // Test required field in array object 440 | expect( 441 | schema.checkForField( 442 | 'users[0].name', 443 | { 444 | users: [{ name: '' }] 445 | }, 446 | { 447 | nestedObject: true 448 | } 449 | ) 450 | ).to.deep.equal({ 451 | hasError: true, 452 | errorMessage: 'Name is required' 453 | }); 454 | 455 | // Test minimum value in array object 456 | expect( 457 | schema.checkForField( 458 | 'users[0].age', 459 | { 460 | users: [{ age: 16 }] 461 | }, 462 | { 463 | nestedObject: true 464 | } 465 | ) 466 | ).to.deep.equal({ 467 | hasError: true, 468 | errorMessage: 'Must be an adult' 469 | }); 470 | 471 | // Test email format in array object 472 | expect( 473 | schema.checkForField( 474 | 'users[0].email', 475 | { 476 | users: [{ email: 'invalid-email' }] 477 | }, 478 | { 479 | nestedObject: true 480 | } 481 | ) 482 | ).to.deep.equal({ 483 | hasError: true, 484 | errorMessage: 'Invalid email format' 485 | }); 486 | }); 487 | 488 | it('Should validate nested array objects (max 3 levels)', () => { 489 | const schema = new Schema({ 490 | users: ArrayType().of( 491 | ObjectType().shape({ 492 | name: StringType().isRequired('Name required'), 493 | tasks: ArrayType().of( 494 | ObjectType().shape({ 495 | title: StringType().isRequired('Task title required'), 496 | assignees: ArrayType().of( 497 | ObjectType().shape({ 498 | email: StringType().isEmail('Invalid email format'), 499 | role: StringType() 500 | .isOneOf(['owner', 'admin', 'member'], 'Invalid role') 501 | .isRequired('Role required'), 502 | priority: NumberType() 503 | .min(1, 'Priority too low') 504 | .max(5, 'Priority too high') 505 | .isRequired('Priority required') 506 | }) 507 | ) 508 | }) 509 | ) 510 | }) 511 | ) 512 | }); 513 | 514 | // Test valid email 515 | expect( 516 | schema.checkForField( 517 | 'users[0].tasks[0].assignees[0].email', 518 | { 519 | users: [ 520 | { 521 | name: 'John Doe', 522 | tasks: [ 523 | { 524 | title: 'Frontend Development', 525 | assignees: [ 526 | { 527 | email: 'test@example.com', 528 | role: 'owner', 529 | priority: 3 530 | } 531 | ] 532 | } 533 | ] 534 | } 535 | ] 536 | }, 537 | { 538 | nestedObject: true 539 | } 540 | ) 541 | ).to.deep.equal({ 542 | hasError: false 543 | }); 544 | 545 | // Test invalid email 546 | expect( 547 | schema.checkForField( 548 | 'users[0].tasks[0].assignees[0].email', 549 | { 550 | users: [ 551 | { 552 | name: 'John Doe', 553 | tasks: [ 554 | { 555 | title: 'Frontend Development', 556 | assignees: [ 557 | { 558 | email: 'invalid-email', 559 | role: 'owner', 560 | priority: 3 561 | } 562 | ] 563 | } 564 | ] 565 | } 566 | ] 567 | }, 568 | { 569 | nestedObject: true 570 | } 571 | ) 572 | ).to.deep.equal({ 573 | hasError: true, 574 | errorMessage: 'Invalid email format' 575 | }); 576 | 577 | // Test valid role 578 | expect( 579 | schema.checkForField( 580 | 'users[0].tasks[0].assignees[0].role', 581 | { 582 | users: [ 583 | { 584 | name: 'John Doe', 585 | tasks: [ 586 | { 587 | title: 'Frontend Development', 588 | assignees: [ 589 | { 590 | email: 'test@example.com', 591 | role: 'owner', 592 | priority: 3 593 | } 594 | ] 595 | } 596 | ] 597 | } 598 | ] 599 | }, 600 | { 601 | nestedObject: true 602 | } 603 | ) 604 | ).to.deep.equal({ 605 | hasError: false 606 | }); 607 | 608 | // Test invalid role 609 | expect( 610 | schema.checkForField( 611 | 'users[0].tasks[0].assignees[0].role', 612 | { 613 | users: [ 614 | { 615 | name: 'John Doe', 616 | tasks: [ 617 | { 618 | title: 'Frontend Development', 619 | assignees: [ 620 | { 621 | email: 'test@example.com', 622 | role: 'guest', 623 | priority: 3 624 | } 625 | ] 626 | } 627 | ] 628 | } 629 | ] 630 | }, 631 | { 632 | nestedObject: true 633 | } 634 | ) 635 | ).to.deep.equal({ 636 | hasError: true, 637 | errorMessage: 'Invalid role' 638 | }); 639 | 640 | // Test valid priority 641 | expect( 642 | schema.checkForField( 643 | 'users[0].tasks[0].assignees[0].priority', 644 | { 645 | users: [ 646 | { 647 | name: 'John Doe', 648 | tasks: [ 649 | { 650 | title: 'Frontend Development', 651 | assignees: [ 652 | { 653 | email: 'test@example.com', 654 | role: 'owner', 655 | priority: 3 656 | } 657 | ] 658 | } 659 | ] 660 | } 661 | ] 662 | }, 663 | { 664 | nestedObject: true 665 | } 666 | ) 667 | ).to.deep.equal({ 668 | hasError: false 669 | }); 670 | 671 | // Test invalid priority (too high) 672 | expect( 673 | schema.checkForField( 674 | 'users[0].tasks[0].assignees[0].priority', 675 | { 676 | users: [ 677 | { 678 | name: 'John Doe', 679 | tasks: [ 680 | { 681 | title: 'Frontend Development', 682 | assignees: [ 683 | { 684 | email: 'test@example.com', 685 | role: 'owner', 686 | priority: 6 687 | } 688 | ] 689 | } 690 | ] 691 | } 692 | ] 693 | }, 694 | { 695 | nestedObject: true 696 | } 697 | ) 698 | ).to.deep.equal({ 699 | hasError: true, 700 | errorMessage: 'Priority too high' 701 | }); 702 | 703 | // Test invalid priority (too low) 704 | expect( 705 | schema.checkForField( 706 | 'users[0].tasks[0].assignees[0].priority', 707 | { 708 | users: [ 709 | { 710 | name: 'John Doe', 711 | tasks: [ 712 | { 713 | title: 'Frontend Development', 714 | assignees: [ 715 | { 716 | email: 'test@example.com', 717 | role: 'owner', 718 | priority: 0 719 | } 720 | ] 721 | } 722 | ] 723 | } 724 | ] 725 | }, 726 | { 727 | nestedObject: true 728 | } 729 | ) 730 | ).to.deep.equal({ 731 | hasError: true, 732 | errorMessage: 'Priority too low' 733 | }); 734 | 735 | // Test required field present 736 | expect( 737 | schema.checkForField( 738 | 'users[0].tasks[0].title', 739 | { 740 | users: [ 741 | { 742 | name: 'John Doe', 743 | tasks: [ 744 | { 745 | title: 'Frontend Development', 746 | assignees: [ 747 | { 748 | email: 'test@example.com', 749 | role: 'owner', 750 | priority: 3 751 | } 752 | ] 753 | } 754 | ] 755 | } 756 | ] 757 | }, 758 | { 759 | nestedObject: true 760 | } 761 | ) 762 | ).to.deep.equal({ 763 | hasError: false 764 | }); 765 | 766 | // Test required field missing 767 | expect( 768 | schema.checkForField( 769 | 'users[0].tasks[0].title', 770 | { 771 | users: [ 772 | { 773 | name: 'John Doe', 774 | tasks: [ 775 | { 776 | title: null, 777 | assignees: [ 778 | { 779 | email: 'test@example.com', 780 | role: 'owner', 781 | priority: 3 782 | } 783 | ] 784 | } 785 | ] 786 | } 787 | ] 788 | }, 789 | { 790 | nestedObject: true 791 | } 792 | ) 793 | ).to.deep.equal({ 794 | hasError: true, 795 | errorMessage: 'Task title required' 796 | }); 797 | }); 798 | 799 | it('Should validate explicit nested array type', () => { 800 | const schema = new Schema({ 801 | users: ArrayType().of( 802 | StringType().isRequired().isEmail(), 803 | ObjectType().shape({ 804 | name: StringType().isEmail(), 805 | email: StringType().isEmail() 806 | }) 807 | ) 808 | }); 809 | 810 | expect( 811 | schema.checkForField( 812 | 'users[0]', 813 | { 814 | users: ['xx'] 815 | }, 816 | options 817 | ) 818 | ).to.deep.equal({ 819 | hasError: true, 820 | errorMessage: 'users[0] must be a valid email' 821 | }); 822 | 823 | expect( 824 | schema.checkForField( 825 | 'users[0]', 826 | { 827 | users: ['ddd@bbb.com'] 828 | }, 829 | options 830 | ) 831 | ).to.deep.equal({ 832 | hasError: false 833 | }); 834 | 835 | expect( 836 | schema.checkForField( 837 | 'users[1].name', 838 | { 839 | users: ['ddd@bbb.com', { name: 'xxx' }] 840 | }, 841 | options 842 | ) 843 | ).to.deep.equal({ 844 | hasError: true, 845 | errorMessage: 'users[1].name must be a valid email' 846 | }); 847 | 848 | expect( 849 | schema.checkForField( 850 | 'users[1].name', 851 | { 852 | users: ['ddd@bbb.com', { name: 'ddd@bbb.com' }] 853 | }, 854 | options 855 | ) 856 | ).to.deep.equal({ 857 | hasError: false 858 | }); 859 | 860 | expect( 861 | schema.check({ 862 | users: [ 863 | 'xxx', 864 | { 865 | name: 'xx', 866 | email: 'xx' 867 | } 868 | ] 869 | }) 870 | ).to.deep.equal({ 871 | users: { 872 | hasError: true, 873 | array: [ 874 | { 875 | hasError: true, 876 | errorMessage: 'users.[0] must be a valid email' 877 | }, 878 | { 879 | hasError: true, 880 | object: { 881 | name: { 882 | hasError: true, 883 | errorMessage: 'name must be a valid email' 884 | }, 885 | email: { 886 | hasError: true, 887 | errorMessage: 'email must be a valid email' 888 | } 889 | } 890 | } 891 | ] 892 | } 893 | }); 894 | }); 895 | 896 | it('Should validate nested array within an object', () => { 897 | const schema = new Schema({ 898 | user: ObjectType().shape({ 899 | emails: ArrayType().of( 900 | StringType().isEmail(), 901 | ObjectType().shape({ 902 | name: StringType().isEmail() 903 | }) 904 | ) 905 | }) 906 | }); 907 | 908 | expect( 909 | schema.checkForField( 910 | 'user.emails[0]', 911 | { 912 | user: { 913 | emails: ['xxx'] 914 | } 915 | }, 916 | options 917 | ) 918 | ).to.deep.equal({ 919 | hasError: true, 920 | errorMessage: 'user.emails[0] must be a valid email' 921 | }); 922 | 923 | expect( 924 | schema.check({ 925 | user: { 926 | emails: [ 927 | 'xxx', 928 | { 929 | name: 'xxx' 930 | } 931 | ] 932 | } 933 | }) 934 | ).to.deep.equal({ 935 | user: { 936 | hasError: true, 937 | object: { 938 | emails: { 939 | hasError: true, 940 | array: [ 941 | { 942 | hasError: true, 943 | errorMessage: 'emails.[0] must be a valid email' 944 | }, 945 | { 946 | hasError: true, 947 | object: { 948 | name: { 949 | hasError: true, 950 | errorMessage: 'name must be a valid email' 951 | } 952 | } 953 | } 954 | ] 955 | } 956 | } 957 | } 958 | }); 959 | }); 960 | }); 961 | }); 962 | --------------------------------------------------------------------------------