Repository: rsuite/schema-typed Branch: master Commit: 3776457b1a6c Files: 52 Total size: 199.5 KB Directory structure: gitextract_y06uf7xz/ ├── .codesandbox/ │ └── ci.json ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── 1.bug_report.md │ │ ├── 2.feature_request.md │ │ └── config.yml │ └── workflows/ │ ├── nodejs-ci.yml │ └── nodejs-publish.yml ├── .gitignore ├── .mocharc.js ├── .npmignore ├── .npmrc ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src/ │ ├── ArrayType.ts │ ├── BooleanType.ts │ ├── DateType.ts │ ├── MixedType.ts │ ├── NumberType.ts │ ├── ObjectType.ts │ ├── Schema.ts │ ├── StringType.ts │ ├── index.ts │ ├── locales/ │ │ ├── default.ts │ │ └── index.ts │ ├── types.ts │ └── utils/ │ ├── basicEmptyCheck.ts │ ├── checkRequired.ts │ ├── createValidator.ts │ ├── createValidatorAsync.ts │ ├── formatErrorMessage.ts │ ├── index.ts │ ├── isEmpty.ts │ ├── pathTransform.ts │ └── shallowEqual.ts ├── test/ │ ├── ArrayTypeSpec.js │ ├── BooleanTypeSpec.js │ ├── DateTypeSpec.js │ ├── MixedTypeSpec.js │ ├── NumberTypeSpec.js │ ├── ObjectTypeSpec.js │ ├── SchemaSpec.js │ ├── StringTypeSpec.js │ └── utilsSpec.js ├── tsconfig-es.json ├── tsconfig.json └── types/ ├── index.d.ts ├── test.ts ├── tsconfig.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codesandbox/ci.json ================================================ { "sandboxes": ["new"] } ================================================ FILE: .eslintrc.js ================================================ const OFF = 0; const WARNING = 1; const ERROR = 2; module.exports = { env: { browser: true, es6: true }, parser: '@typescript-eslint/parser', extends: [ 'plugin:@typescript-eslint/recommended', 'prettier/@typescript-eslint', 'plugin:prettier/recommended' ], parserOptions: {}, plugins: ['@typescript-eslint'], rules: { quotes: [ERROR, 'single'], semi: [ERROR, 'always'], 'space-infix-ops': ERROR, 'prefer-spread': ERROR, 'no-multi-spaces': ERROR, 'class-methods-use-this': WARNING, 'arrow-parens': [ERROR, 'as-needed'], '@typescript-eslint/no-unused-vars': ERROR, '@typescript-eslint/no-explicit-any': OFF, '@typescript-eslint/explicit-function-return-type': OFF, '@typescript-eslint/explicit-member-accessibility': OFF, '@typescript-eslint/no-namespace': OFF, '@typescript-eslint/explicit-module-boundary-types': OFF } }; ================================================ FILE: .github/ISSUE_TEMPLATE/1.bug_report.md ================================================ --- name: 🐛 Bug report about: Report a reproducible bug or regression title: '' labels: '' assignees: '' --- ### What version of schema-typed are you using? ### Describe the Bug ### Expected Behavior ### To Reproduce ================================================ FILE: .github/ISSUE_TEMPLATE/2.feature_request.md ================================================ --- name: 💄 Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- ### What problem does this feature solve? ### What does the proposed API look like? ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 🤔 Ask a question url: https://github.com/rsuite/rsuite/discussions about: Ask questions and discuss with other community members ================================================ FILE: .github/workflows/nodejs-ci.yml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run build --if-present - run: npm test - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/nodejs-publish.yml ================================================ # see https://help.github.com/cn/actions/language-and-framework-guides/publishing-nodejs-packages name: Node.js Package on: push: tags: ['*'] jobs: publish: name: 'Publish' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 # Setup .npmrc file to publish to npm - uses: actions/setup-node@v3 with: node-version: '12.x' registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: npm install - run: npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules jspm_packages # Optional npm cache directory .npm # Optional REPL history .node_repl_history lib es .vscode yarn.lock .DS_Store ================================================ FILE: .mocharc.js ================================================ 'use strict'; // https://github.com/mochajs/mocha-examples/tree/master/packages/typescript-babel const config = { diff: true, extension: ['js', 'ts'], package: './package.json', reporter: 'spec', slow: 75, timeout: 2000, ui: 'bdd', require: 'ts-node/register', 'watch-files': ['src/**/*.ts', 'test/**/*.js'] }; const M = process.env.M; /** * @example: * M=ObjectType npm run tdd */ if (M) { config.spec = 'test/' + M + 'Spec.js'; } module.exports = config; ================================================ FILE: .npmignore ================================================ src/ types/*.json types/test.ts ================================================ FILE: .npmrc ================================================ message="build: bump %s" ================================================ FILE: .prettierrc.js ================================================ module.exports = { printWidth: 100, tabWidth: 2, singleQuote: true, arrowParens: 'avoid', trailingComma: 'none' }; ================================================ FILE: CHANGELOG.md ================================================ ## [2.4.2](https://github.com/rsuite/schema-typed/compare/v2.4.1...v2.4.2) (2025-04-11) ### Bug Fixes * 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)) ## [2.4.1](https://github.com/rsuite/schema-typed/compare/v2.4.0...v2.4.1) (2025-03-24) ### Bug Fixes * 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)) # [2.4.0](https://github.com/rsuite/schema-typed/compare/v2.3.0...v2.4.0) (2025-03-24) ### Features * Array support explicit type ([#83](https://github.com/rsuite/schema-typed/issues/83)) ([e716ab4](https://github.com/rsuite/schema-typed/commit/e716ab4b9f20d66da8e3b6b1d2ae46181c59641a)) # [2.3.0](https://github.com/rsuite/schema-typed/compare/v2.2.2...v2.3.0) (2025-02-06) ### Features * **ArrayType:** enhance nested array validation ([#82](https://github.com/rsuite/schema-typed/issues/82)) ([db389b9](https://github.com/rsuite/schema-typed/commit/db389b90a016627982202214dee23f7ee34d6de4)) ## [2.2.2](https://github.com/rsuite/schema-typed/compare/2.2.1...2.2.2) (2024-04-12) ## [2.2.1](https://github.com/rsuite/schema-typed/compare/2.2.0...2.2.1) (2024-04-12) ### Bug Fixes * **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)) # [2.2.0](https://github.com/rsuite/schema-typed/compare/2.1.3...2.2.0) (2024-04-11) ### Features * add support for `equalTo` and `proxy` ([#78](https://github.com/rsuite/schema-typed/issues/78)) ([d9f0e55](https://github.com/rsuite/schema-typed/commit/d9f0e555cf532731839584b0c036648001fe0503)) * add support for `label` method ([#77](https://github.com/rsuite/schema-typed/issues/77)) ([9ff16c3](https://github.com/rsuite/schema-typed/commit/9ff16c346d6f13caabd4910a7d920c1c11eced18)) * **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)) * **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)) ## [2.1.3](https://github.com/rsuite/schema-typed/compare/2.1.2...2.1.3) (2023-05-06) ### Bug Fixes * 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)) ## [2.1.2](https://github.com/rsuite/schema-typed/compare/2.1.1...2.1.2) (2023-03-10) ### Bug Fixes * **build:** fix unpublished source code ([#67](https://github.com/rsuite/schema-typed/issues/67)) ([c21ae0a](https://github.com/rsuite/schema-typed/commit/c21ae0a94578907e3fdd0467e5d1a1e3ec7c4d85)) ## [2.1.1](https://github.com/rsuite/schema-typed/compare/2.1.0...2.1.1) (2023-03-08) - chore: change the compilation target of TypeScript from esnext to es2019 # [2.1.0](https://github.com/rsuite/schema-typed/compare/2.0.4...2.1.0) (2023-03-02) ### Features - 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)) ## [2.0.4](https://github.com/rsuite/schema-typed/compare/2.0.3...2.0.4) (2023-03-01) ### Bug Fixes - promises where not allowed by type ([#61](https://github.com/rsuite/schema-typed/issues/61)) ([9cc665c](https://github.com/rsuite/schema-typed/commit/9cc665c4f72b5a22942d351c961263c179888a7a)) ## [2.0.3](https://github.com/rsuite/schema-typed/compare/2.0.2...2.0.3) (2022-06-30) ### Bug Fixes - **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)) # 2.0.2 - build(deps): add @babel/runtime #37 # 2.0.1 - fix ArrayType.of type error #35 # 2.0.0 - 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)) - refactor: refactor the project through typescript. - feat(MixedType): Added support for `when` method on all types - feat(MixedType): Replace Type with MixedType. - feat(ObjectType): Support nested objects in the `shape` method of ObjectType. # 1.5.1 - Update the typescript definition of `addRule` # 1.5.0 - Added support for `isRequiredOrEmpty` in StringType and ArrayType # 1.4.0 - Adding the typescript types declaration in to package # 1.3.1 - Fixed an issue where `isOneOf` was not valid in `StringType` (#18) # 1.3.0 - Added support for ESM # 1.2.2 > Aug 30, 2019 - **Bugfix**: Fix an issue where addRule is not called # 1.2.0 > Aug 20, 2019 - **Feature**: Support for async check. ([#14]) --- [#14]: https://github.com/rsuite/rsuite/pull/14 ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 RSuite Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # schema-typed Schema for data modeling & validation [![npm][npm-badge]][npm] [![GitHub Actions][actions-svg]][actions-home] [![Coverage Status][soverage-svg]][soverage] ## Table of Contents - [Installation](#installation) - [Usage](#usage) - [Getting Started](#getting-started) - [Multiple verification](#multiple-verification) - [Custom verification](#custom-verification) - [Field dependency validation](#field-dependency-validation) - [Asynchronous check](#asynchronous-check) - [Validate nested objects](#validate-nested-objects) - [Combine](#combine) - [API](#api) - [SchemaModel](#schemamodel) - [`static combine(...models)`](#static-combinemodels) - [`check(data: object)`](#checkdata-object) - [`checkAsync(data: object)`](#checkasyncdata-object) - [`checkForField(fieldName: string, data: object, options?: { nestedObject?: boolean })`](#checkforfieldfieldname-string-data-object-options--nestedobject-boolean-) - [`checkForFieldAsync(fieldName: string, data: object, options?: { nestedObject?: boolean })`](#checkforfieldasyncfieldname-string-data-object-options--nestedobject-boolean-) - [MixedType()](#mixedtype) - [`isRequired(errorMessage?: string, trim: boolean = true)`](#isrequirederrormessage-string-trim-boolean--true) - [`isRequiredOrEmpty(errorMessage?: string, trim: boolean = true)`](#isrequiredoremptyerrormessage-string-trim-boolean--true) - [`addRule(onValid: Function, errorMessage?: string, priority: boolean)`](#addruleonvalid-function-errormessage-string-priority-boolean) - [`addAsyncRule(onValid: Function, errorMessage?: string, priority: boolean)`](#addasyncruleonvalid-function-errormessage-string-priority-boolean) - [`when(condition: (schemaSpec: SchemaDeclaration) => Type)`](#whencondition-schemaspec-schemadeclarationdatatype-errormsgtype--type) - [`check(value: ValueType, data?: DataType):CheckResult`](#checkvalue-valuetype-data-datatypecheckresult) - [`checkAsync(value: ValueType, data?: DataType):Promise`](#checkasyncvalue-valuetype-data-datatypepromisecheckresult) - [`label(label: string)`](#labellabel-string) - [`equalTo(fieldName: string, errorMessage?: string)`](#equaltofieldname-string-errormessage-string) - [`proxy(fieldNames: string[], options?: { checkIfValueExists?: boolean })`](#proxyfieldnames-string-options--checkifvalueexists-boolean-) - [StringType(errorMessage?: string)](#stringtypeerrormessage-string) - [`isEmail(errorMessage?: string)`](#isemailerrormessage-string) - [`isURL(errorMessage?: string)`](#isurlerrormessage-string) - [`isOneOf(items: string[], errorMessage?: string)`](#isoneofitems-string-errormessage-string) - [`containsLetter(errorMessage?: string)`](#containslettererrormessage-string) - [`containsUppercaseLetter(errorMessage?: string)`](#containsuppercaselettererrormessage-string) - [`containsLowercaseLetter(errorMessage?: string)`](#containslowercaselettererrormessage-string) - [`containsLetterOnly(errorMessage?: string)`](#containsletteronlyerrormessage-string) - [`containsNumber(errorMessage?: string)`](#containsnumbererrormessage-string) - [`pattern(regExp: RegExp, errorMessage?: string)`](#patternregexp-regexp-errormessage-string) - [`rangeLength(minLength: number, maxLength: number, errorMessage?: string)`](#rangelengthminlength-number-maxlength-number-errormessage-string) - [`minLength(minLength: number, errorMessage?: string)`](#minlengthminlength-number-errormessage-string) - [`maxLength(maxLength: number, errorMessage?: string)`](#maxlengthmaxlength-number-errormessage-string) - [NumberType(errorMessage?: string)](#numbertypeerrormessage-string) - [`isInteger(errorMessage?: string)`](#isintegererrormessage-string) - [`isOneOf(items: number[], errorMessage?: string)`](#isoneofitems-number-errormessage-string) - [`pattern(regExp: RegExp, errorMessage?: string)`](#patternregexp-regexp-errormessage-string-1) - [`range(minLength: number, maxLength: number, errorMessage?: string)`](#rangeminlength-number-maxlength-number-errormessage-string) - [`min(min: number, errorMessage?: string)`](#minmin-number-errormessage-string) - [`max(max: number, errorMessage?: string)`](#maxmax-number-errormessage-string) - [ArrayType(errorMessage?: string)](#arraytypeerrormessage-string) - [`isRequiredOrEmpty(errorMessage?: string)`](#isrequiredoremptyerrormessage-string) - [`rangeLength(minLength: number, maxLength: number, errorMessage?: string)`](#rangelengthminlength-number-maxlength-number-errormessage-string-1) - [`minLength(minLength: number, errorMessage?: string)`](#minlengthminlength-number-errormessage-string-1) - [`maxLength(maxLength: number, errorMessage?: string)`](#maxlengthmaxlength-number-errormessage-string-1) - [`unrepeatable(errorMessage?: string)`](#unrepeatableerrormessage-string) - [`of()`](#of) - [DateType(errorMessage?: string)](#datetypeerrormessage-string) - [`range(min: Date, max: Date, errorMessage?: string)`](#rangemin-date-max-date-errormessage-string) - [`min(min: Date, errorMessage?: string)`](#minmin-date-errormessage-string) - [`max(max: Date, errorMessage?: string)`](#maxmax-date-errormessage-string) - [ObjectType(errorMessage?: string)](#objecttypeerrormessage-string) - [`shape(fields: object)`](#shapefields-object) - [BooleanType(errorMessage?: string)](#booleantypeerrormessage-string) - [⚠️ Notes](#-notes) ## Installation ``` npm install schema-typed --save ``` ## Usage ### Getting Started ```js import { SchemaModel, StringType, DateType, NumberType, ObjectType, ArrayType } from 'schema-typed'; const model = SchemaModel({ username: StringType().isRequired('Username required'), email: StringType().isEmail('Email required'), age: NumberType('Age should be a number').range(18, 30, 'Over the age limit'), tags: ArrayType().of(StringType('The tag should be a string').isRequired()), role: ObjectType().shape({ name: StringType().isRequired('Name required'), permissions: ArrayType().isRequired('Permissions required') }) }); const checkResult = model.check({ username: 'foobar', email: 'foo@bar.com', age: 40, tags: ['Sports', 'Games', 10], role: { name: 'administrator' } }); console.log(checkResult); ``` `checkResult` return structure is: ```js { username: { hasError: false }, email: { hasError: false }, age: { hasError: true, errorMessage: 'Over the age limit' }, tags: { hasError: true, array: [ { hasError: false }, { hasError: false }, { hasError: true, errorMessage: 'The tag should be a string' } ] }, role: { hasError: true, object: { name: { hasError: false }, permissions: { hasError: true, errorMessage: 'Permissions required' } } } }; ``` ### Multiple verification ```js StringType() .minLength(6, "Can't be less than 6 characters") .maxLength(30, 'Cannot be greater than 30 characters') .isRequired('This field required'); ``` ### Custom verification Customize a rule with the `addRule` function. If you are validating a string type of data, you can set a regular expression for custom validation by the `pattern` method. ```js const model = SchemaModel({ field1: StringType().addRule((value, data) => { return /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(value); }, 'Please enter legal characters'), field2: StringType().pattern(/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/, 'Please enter legal characters') }); model.check({ field1: '', field2: '' }); /** { field1: { hasError: true, errorMessage: 'Please enter legal characters' }, field2: { hasError: true, errorMessage: 'Please enter legal characters' } }; **/ ``` #### Field dependency validation 1. Use the `equalTo` method to verify that the values of two fields are equal. ```js const model = SchemaModel({ password: StringType().isRequired(), confirmPassword: StringType().equalTo('password') }); ``` 2. Use the `addRule` method to create a custom validation rule. ```js const model = SchemaModel({ password: StringType().isRequired(), confirmPassword: StringType().addRule( (value, data) => value === data.password, 'Confirm password must be the same as password' ) }); ``` 3. Use the `proxy` method to verify that a field passes, and then proxy verification of other fields. ```js const model = SchemaModel({ password: StringType().isRequired().proxy(['confirmPassword']), confirmPassword: StringType().equalTo('password') }); ``` #### Asynchronous check For example, verify that the mailbox is duplicated ```js function asyncCheckEmail(email) { return new Promise(resolve => { setTimeout(() => { if (email === 'foo@domain.com') { resolve(false); } else { resolve(true); } }, 500); }); } const model = SchemaModel({ email: StringType() .isEmail('Please input the correct email address') .addAsyncRule((value, data) => { return asyncCheckEmail(value); }, 'Email address already exists') .isRequired('This field cannot be empty') }); model.checkAsync({ email: 'foo@domain.com' }).then(checkResult => { console.log(checkResult); /** { email: { hasError: true, errorMessage: 'Email address already exists' } }; **/ }); ``` ### Validate nested objects Validate nested objects, which can be defined using the `ObjectType().shape` method. E.g: ```js const model = SchemaModel({ id: NumberType().isRequired('This field required'), name: StringType().isRequired('This field required'), info: ObjectType().shape({ email: StringType().isEmail('Should be an email'), age: NumberType().min(18, 'Age should be greater than 18 years old') }) }); const user = { id: 1, name: '', info: { email: 'schema-type', age: 17 } }; model.check(data); /** { "id": { "hasError": false }, "name": { "hasError": true, "errorMessage": "This field required" }, "info": { "hasError": true, "object": { "email": { "hasError": true, "errorMessage": "Should be an email" }, "age": { "hasError": true, "errorMessage": "Age should be greater than 18 years old" } } } } */ ``` ### Combine `SchemaModel` provides a static method `combine` that can be combined with multiple `SchemaModel` to return a new `SchemaModel`. ```js const model1 = SchemaModel({ username: StringType().isRequired('This field required'), email: StringType().isEmail('Should be an email') }); const model2 = SchemaModel({ username: StringType().minLength(7, "Can't be less than 7 characters"), age: NumberType().range(18, 30, 'Age should be greater than 18 years old') }); const model3 = SchemaModel({ groupId: NumberType().isRequired('This field required') }); const model4 = SchemaModel.combine(model1, model2, model3); model4.check({ username: 'foobar', email: 'foo@bar.com', age: 40, groupId: 1 }); ``` ## API ### SchemaModel SchemaModel is a JavaScript schema builder for data model creation and validation. #### `static combine(...models)` A static method for merging multiple models. ```js const model1 = SchemaModel({ username: StringType().isRequired('This field required') }); const model2 = SchemaModel({ email: StringType().isEmail('Please input the correct email address') }); const model3 = SchemaModel.combine(model1, model2); ``` #### `check(data: object)` Check whether the data conforms to the model shape definition. Return a check result. ```js const model = SchemaModel({ username: StringType().isRequired('This field required'), email: StringType().isEmail('Please input the correct email address') }); model.check({ username: 'root', email: 'root@email.com' }); ``` #### `checkAsync(data: object)` Asynchronously check whether the data conforms to the model shape definition. Return a check result. ```js const model = SchemaModel({ username: StringType() .isRequired('This field required') .addRule(value => { return new Promise(resolve => { // Asynchronous processing logic }); }, 'Username already exists'), email: StringType().isEmail('Please input the correct email address') }); model .checkAsync({ username: 'root', email: 'root@email.com' }) .then(result => { // Data verification result }); ``` #### `checkForField(fieldName: string, data: object, options?: { nestedObject?: boolean })` Check whether a field in the data conforms to the model shape definition. Return a check result. ```js const model = SchemaModel({ username: StringType().isRequired('This field required'), email: StringType().isEmail('Please input the correct email address') }); const data = { username: 'root' }; model.checkForField('username', data); ``` #### `checkForFieldAsync(fieldName: string, data: object, options?: { nestedObject?: boolean })` Asynchronously check whether a field in the data conforms to the model shape definition. Return a check result. ```js const model = SchemaModel({ username: StringType() .isRequired('This field required') .addAsyncRule(value => { return new Promise(resolve => { // Asynchronous processing logic }); }, 'Username already exists'), email: StringType().isEmail('Please input the correct email address') }); const data = { username: 'root' }; model.checkForFieldAsync('username', data).then(result => { // Data verification result }); ``` ### MixedType() Creates a type that matches all types. All types inherit from this base type. #### `isRequired(errorMessage?: string, trim: boolean = true)` ```js MixedType().isRequired('This field required'); ``` #### `isRequiredOrEmpty(errorMessage?: string, trim: boolean = true)` ```js MixedType().isRequiredOrEmpty('This field required'); ``` #### `addRule(onValid: Function, errorMessage?: string, priority: boolean)` ```js MixedType().addRule((value, data) => { return /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(value); }, 'Please enter a legal character.'); ``` #### `addAsyncRule(onValid: Function, errorMessage?: string, priority: boolean)` ```js MixedType().addAsyncRule((value, data) => { return new Promise(resolve => { // Asynchronous processing logic }); }, 'Please enter a legal character.'); ``` #### `when(condition: (schemaSpec: SchemaDeclaration) => Type)` Conditional validation, the return value is a new type. ```ts const model = SchemaModel({ option: StringType().isOneOf(['a', 'b', 'other']), other: StringType().when(schema => { const { value } = schema.option; return value === 'other' ? StringType().isRequired('Other required') : StringType(); }) }); /** { option: { hasError: false }, other: { hasError: false } } */ model.check({ option: 'a', other: '' }); /* { option: { hasError: false }, other: { hasError: true, errorMessage: 'Other required' } } */ model.check({ option: 'other', other: '' }); ``` Check whether a field passes the validation to determine the validation rules of another field. ```js const model = SchemaModel({ password: StringType().isRequired('Password required'), confirmPassword: StringType().when(schema => { const { hasError } = schema.password.check(); return hasError ? StringType() : StringType().addRule( value => value === schema.password.value, 'The passwords are inconsistent twice' ); }) }); ``` #### `check(value: ValueType, data?: DataType):CheckResult` ```js const type = MixedType().addRule(v => { if (typeof v === 'number') { return true; } return false; }, 'Please enter a valid number'); type.check('1'); // { hasError: true, errorMessage: 'Please enter a valid number' } type.check(1); // { hasError: false } ``` #### `checkAsync(value: ValueType, data?: DataType):Promise` ```js const type = MixedType().addRule(v => { return new Promise(resolve => { setTimeout(() => { if (typeof v === 'number') { resolve(true); } else { resolve(false); } }, 500); }); }, 'Please enter a valid number'); type.checkAsync('1').then(checkResult => { // { hasError: true, errorMessage: 'Please enter a valid number' } }); type.checkAsync(1).then(checkResult => { // { hasError: false } }); ``` #### `label(label: string)` Overrides the key name in error messages. ```js MixedType().label('Username'); ``` Eg: ```js SchemaModel({ first_name: StringType().label('First name'), age: NumberType().label('Age') }); ``` #### `equalTo(fieldName: string, errorMessage?: string)` Check if the value is equal to the value of another field. ```js SchemaModel({ password: StringType().isRequired(), confirmPassword: StringType().equalTo('password') }); ``` #### `proxy(fieldNames: string[], options?: { checkIfValueExists?: boolean })` After the field verification passes, proxy verification of other fields. - `fieldNames`: The field name to be proxied. - `options.checkIfValueExists`: When the value of other fields exists, the verification is performed (default: false) ```js SchemaModel({ password: StringType().isRequired().proxy(['confirmPassword']), confirmPassword: StringType().equalTo('password') }); ``` ### StringType(errorMessage?: string) Define a string type. Supports all the same methods as [MixedType](#mixedtype). #### `isEmail(errorMessage?: string)` ```js StringType().isEmail('Please input the correct email address'); ``` #### `isURL(errorMessage?: string)` ```js StringType().isURL('Please enter the correct URL address'); ``` #### `isOneOf(items: string[], errorMessage?: string)` ```js StringType().isOneOf(['Javascript', 'CSS'], 'Can only type `Javascript` and `CSS`'); ``` #### `containsLetter(errorMessage?: string)` ```js StringType().containsLetter('Must contain English characters'); ``` #### `containsUppercaseLetter(errorMessage?: string)` ```js StringType().containsUppercaseLetter('Must contain uppercase English characters'); ``` #### `containsLowercaseLetter(errorMessage?: string)` ```js StringType().containsLowercaseLetter('Must contain lowercase English characters'); ``` #### `containsLetterOnly(errorMessage?: string)` ```js StringType().containsLetterOnly('English characters that can only be included'); ``` #### `containsNumber(errorMessage?: string)` ```js StringType().containsNumber('Must contain numbers'); ``` #### `pattern(regExp: RegExp, errorMessage?: string)` ```js StringType().pattern(/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/, 'Please enter legal characters'); ``` #### `rangeLength(minLength: number, maxLength: number, errorMessage?: string)` ```js StringType().rangeLength(6, 30, 'The number of characters can only be between 6 and 30'); ``` #### `minLength(minLength: number, errorMessage?: string)` ```js StringType().minLength(6, 'Minimum 6 characters required'); ``` #### `maxLength(maxLength: number, errorMessage?: string)` ```js StringType().maxLength(30, 'The maximum is only 30 characters.'); ``` ### NumberType(errorMessage?: string) Define a number type. Supports all the same methods as [MixedType](#mixedtype). #### `isInteger(errorMessage?: string)` ```js NumberType().isInteger('It can only be an integer'); ``` #### `isOneOf(items: number[], errorMessage?: string)` ```js NumberType().isOneOf([5, 10, 15], 'Can only be `5`, `10`, `15`'); ``` #### `pattern(regExp: RegExp, errorMessage?: string)` ```js NumberType().pattern(/^[1-9][0-9]{3}$/, 'Please enter a legal character.'); ``` #### `range(minLength: number, maxLength: number, errorMessage?: string)` ```js NumberType().range(18, 40, 'Please enter a number between 18 - 40'); ``` #### `min(min: number, errorMessage?: string)` ```js NumberType().min(18, 'Minimum 18'); ``` #### `max(max: number, errorMessage?: string)` ```js NumberType().max(40, 'Maximum 40'); ``` ### ArrayType(errorMessage?: string) Define a array type. Supports all the same methods as [MixedType](#mixedtype). #### `isRequiredOrEmpty(errorMessage?: string)` ```js ArrayType().isRequiredOrEmpty('This field required'); ``` #### `rangeLength(minLength: number, maxLength: number, errorMessage?: string)` ```js ArrayType().rangeLength(1, 3, 'Choose at least one, but no more than three'); ``` #### `minLength(minLength: number, errorMessage?: string)` ```js ArrayType().minLength(1, 'Choose at least one'); ``` #### `maxLength(maxLength: number, errorMessage?: string)` ```js ArrayType().maxLength(3, "Can't exceed three"); ``` #### `unrepeatable(errorMessage?: string)` ```js ArrayType().unrepeatable('Duplicate options cannot appear'); ``` #### `of()` ```js // for every element of array ArrayType().of(StringType('The tag should be a string').isRequired()); // for every element of array ArrayType().of( ObjectType().shape({ name: StringType().isEmail() }) ); // just specify the first and the second element ArrayType().of( StringType().isEmail(), ObjectType().shape({ name: StringType().isEmail() }) ); ``` ### DateType(errorMessage?: string) Define a date type. Supports all the same methods as [MixedType](#mixedtype). #### `range(min: Date, max: Date, errorMessage?: string)` ```js DateType().range( new Date('08/01/2017'), new Date('08/30/2017'), 'Date should be between 08/01/2017 - 08/30/2017' ); ``` #### `min(min: Date, errorMessage?: string)` ```js DateType().min(new Date('08/01/2017'), 'Minimum date 08/01/2017'); ``` #### `max(max: Date, errorMessage?: string)` ```js DateType().max(new Date('08/30/2017'), 'Maximum date 08/30/2017'); ``` ### ObjectType(errorMessage?: string) Define a object type. Supports all the same methods as [MixedType](#mixedtype). #### `shape(fields: object)` ```js ObjectType().shape({ email: StringType().isEmail('Should be an email'), age: NumberType().min(18, 'Age should be greater than 18 years old') }); ``` ### BooleanType(errorMessage?: string) Define a boolean type. Supports all the same methods as [MixedType](#mixedtype). ## ⚠️ Notes Default check priority: - 1.isRequired - 2.All other checks are executed in sequence If the third argument to addRule is `true`, the priority of the check is as follows: - 1.addRule - 2.isRequired - 3.Predefined rules (if there is no isRequired, value is empty, the rule is not executed) [npm-badge]: https://img.shields.io/npm/v/schema-typed.svg [npm]: https://www.npmjs.com/package/schema-typed [actions-svg]: https://github.com/rsuite/schema-typed/workflows/Node.js%20CI/badge.svg?branch=master [actions-home]: https://github.com/rsuite/schema-typed/actions/workflows/nodejs-ci.yml [soverage-svg]: https://coveralls.io/repos/github/rsuite/schema-typed/badge.svg?branch=master [soverage]: https://coveralls.io/github/rsuite/schema-typed?branch=master ================================================ FILE: package.json ================================================ { "name": "schema-typed", "version": "2.4.2", "description": "Schema for data modeling & validation", "main": "lib/index.js", "module": "es/index.js", "types": "lib/index.d.ts", "scripts": { "lint": "eslint src/**/*.ts", "build": "tsc --outDir lib && tsc -p tsconfig-es.json --outDir es", "prepublishOnly": "npm run test && npm run build", "tdd": "mocha --watch", "test": "npm run lint && npm run test:once", "test:once": "nyc --reporter=lcovonly --reporter=html mocha", "doctoc:": "doctoc README.md", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "version": "npm run changelog && git add -A" }, "repository": { "type": "git", "url": "git+https://github.com/rsuite/schema-typed.git" }, "keywords": [ "schema", "validation" ], "contributors": [ "A2ZH", "Simon Guo " ], "license": "MIT", "bugs": { "url": "https://github.com/rsuite/schema-typed/issues" }, "files": [ "lib", "es", "src", "types" ], "homepage": "https://github.com/rsuite/schema-typed#readme", "dependencies": { "lodash": "^4.17.21" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/node": "^20.12.5", "@typescript-eslint/eslint-plugin": "^4.29.3", "@typescript-eslint/parser": "^4.29.3", "chai": "^3.5.0", "conventional-changelog-cli": "^2.1.1", "coveralls": "^3.1.0", "cross-env": "^6.0.3", "del": "^6.0.0", "eslint": "^6.7.2", "eslint-config-prettier": "^6.11.0", "eslint-plugin-import": "^2.19.1", "eslint-plugin-prettier": "^3.1.4", "istanbul": "^0.4.5", "mocha": "^10.2.0", "nyc": "^15.1.0", "object-flaser": "^0.1.1", "prettier": "^2.2.1", "ts-node": "^10.9.2", "typescript": "^4.2.2" } } ================================================ FILE: src/ArrayType.ts ================================================ import { MixedType, arrayTypeSchemaSpec } from './MixedType'; import { PlainObject, CheckResult, ErrorMessageType } from './types'; import { ArrayTypeLocale } from './locales'; export class ArrayType extends MixedType< any[], DataType, E, ArrayTypeLocale > { [arrayTypeSchemaSpec]: MixedType | MixedType[]; private isArrayTypeNested = false; constructor(errorMessage?: E | string) { super('array'); super.pushRule({ onValid: v => { // Skip array type check for nested array elements if (this.isArrayTypeNested) { return true; } return Array.isArray(v); }, errorMessage: errorMessage || this.locale.type }); } rangeLength( minLength: number, maxLength: number, errorMessage: E | string = this.locale.rangeLength ) { super.pushRule({ onValid: (value: string[]) => value.length >= minLength && value.length <= maxLength, errorMessage, params: { minLength, maxLength } }); return this; } minLength(minLength: number, errorMessage: E | string = this.locale.minLength) { super.pushRule({ onValid: value => value.length >= minLength, errorMessage, params: { minLength } }); return this; } maxLength(maxLength: number, errorMessage: E | string = this.locale.maxLength) { super.pushRule({ onValid: value => value.length <= maxLength, errorMessage, params: { maxLength } }); return this; } unrepeatable(errorMessage: E | string = this.locale.unrepeatable) { super.pushRule({ onValid: items => { const hash: PlainObject = {}; for (const i in items) { if (hash[items[i]]) { return false; } hash[items[i]] = true; } return true; }, errorMessage }); return this; } of(...types: MixedType[]) { if (types.length === 1) { const type = types[0]; this[arrayTypeSchemaSpec] = type; // Mark inner ArrayType as nested when dealing with nested arrays if (type instanceof ArrayType) { type.isArrayTypeNested = true; } super.pushRule({ onValid: (items, data, fieldName) => { // For non-array values in nested arrays, pass directly to inner type validation if (!Array.isArray(items) && this.isArrayTypeNested) { return type.check(items, data, fieldName); } // For non-array values in non-nested arrays, return array type error if (!Array.isArray(items)) { return { hasError: true, errorMessage: this.locale.type }; } const checkResults = items.map((value, index) => { const name = Array.isArray(fieldName) ? [...fieldName, `[${index}]`] : [fieldName, `[${index}]`]; return type.check(value, data, name as string[]); }); const hasError = !!checkResults.find(item => item?.hasError); return { hasError, array: checkResults } as CheckResult; } }); } else { this[arrayTypeSchemaSpec] = types; super.pushRule({ onValid: (items, data, fieldName) => { const checkResults = items.map((value, index) => { const name = Array.isArray(fieldName) ? [...fieldName, `[${index}]`] : [fieldName, `[${index}]`]; return types[index].check(value, data, name as string[]); }); const hasError = !!checkResults.find(item => item?.hasError); return { hasError, array: checkResults } as CheckResult; } }); } return this; } } export default function getArrayType(errorMessage?: E) { return new ArrayType(errorMessage); } ================================================ FILE: src/BooleanType.ts ================================================ import { MixedType } from './MixedType'; import { ErrorMessageType } from './types'; import { BooleanTypeLocale } from './locales'; export class BooleanType extends MixedType< boolean, DataType, E, BooleanTypeLocale > { constructor(errorMessage?: E | string) { super('boolean'); super.pushRule({ onValid: v => typeof v === 'boolean', errorMessage: errorMessage || this.locale.type }); } } export default function getBooleanType(errorMessage?: E) { return new BooleanType(errorMessage); } ================================================ FILE: src/DateType.ts ================================================ import { MixedType } from './MixedType'; import { ErrorMessageType } from './types'; import { DateTypeLocale } from './locales'; export class DateType extends MixedType< string | Date, DataType, E, DateTypeLocale > { constructor(errorMessage?: E | string) { super('date'); super.pushRule({ onValid: value => !/Invalid|NaN/.test(new Date(value).toString()), errorMessage: errorMessage || this.locale.type }); } range(min: string | Date, max: string | Date, errorMessage: E | string = this.locale.range) { super.pushRule({ onValid: value => new Date(value) >= new Date(min) && new Date(value) <= new Date(max), errorMessage, params: { min, max } }); return this; } min(min: string | Date, errorMessage: E | string = this.locale.min) { super.pushRule({ onValid: value => new Date(value) >= new Date(min), errorMessage, params: { min } }); return this; } max(max: string | Date, errorMessage: E | string = this.locale.max) { super.pushRule({ onValid: value => new Date(value) <= new Date(max), errorMessage, params: { max } }); return this; } } export default function getDateType(errorMessage?: E) { return new DateType(errorMessage); } ================================================ FILE: src/MixedType.ts ================================================ import { SchemaDeclaration, CheckResult, ValidCallbackType, AsyncValidCallbackType, RuleType, ErrorMessageType, TypeName, PlainObject } from './types'; import { checkRequired, createValidator, createValidatorAsync, isEmpty, shallowEqual, formatErrorMessage, get } from './utils'; import { joinName } from './utils/formatErrorMessage'; import locales, { MixedTypeLocale } from './locales'; type ProxyOptions = { // Check if the value exists checkIfValueExists?: boolean; }; export const schemaSpecKey = 'objectTypeSchemaSpec'; export const arrayTypeSchemaSpec = 'arrayTypeSchemaSpec'; /** * Get the field type from the schema object */ export function getFieldType(schemaSpec: any, fieldName: string, nestedObject?: boolean) { if (schemaSpec) { if (nestedObject) { const namePath = fieldName.split('.'); const currentField = namePath[0]; const arrayMatch = currentField.match(/(\w+)\[(\d+)\]/); if (arrayMatch) { const [, arrayField, arrayIndex] = arrayMatch; const type = schemaSpec[arrayField]; if (type?.[arrayTypeSchemaSpec]) { const arrayType = type[arrayTypeSchemaSpec]; if (namePath.length > 1) { if (arrayType[schemaSpecKey]) { return getFieldType(arrayType[schemaSpecKey], namePath.slice(1).join('.'), true); } if (Array.isArray(arrayType) && arrayType[parseInt(arrayIndex)][schemaSpecKey]) { return getFieldType( arrayType[parseInt(arrayIndex)][schemaSpecKey], namePath.slice(1).join('.'), true ); } } if (Array.isArray(arrayType)) { return arrayType[parseInt(arrayIndex)]; } // Otherwise return the array element type directly return arrayType; } return type; } else { const type = schemaSpec[currentField]; if (namePath.length === 1) { return type; } if (namePath.length > 1 && type && type[schemaSpecKey]) { return getFieldType(type[schemaSpecKey], namePath.slice(1).join('.'), true); } } } return schemaSpec?.[fieldName]; } } /** * Get the field value from the data object */ export function getFieldValue(data: PlainObject, fieldName: string, nestedObject?: boolean) { return nestedObject ? get(data, fieldName) : data?.[fieldName]; } export class MixedType { readonly $typeName?: string; protected required = false; protected requiredMessage: E | string = ''; protected trim = false; protected emptyAllowed = false; protected rules: RuleType[] = []; protected priorityRules: RuleType[] = []; protected fieldLabel?: string; $schemaSpec: SchemaDeclaration; value: any; locale: L & MixedTypeLocale; // The field name that depends on the verification of other fields otherFields: string[] = []; proxyOptions: ProxyOptions = {}; constructor(name?: TypeName) { this.$typeName = name; this.locale = Object.assign(name ? locales[name] : {}, locales.mixed) as L & MixedTypeLocale; } setSchemaOptions(schemaSpec: SchemaDeclaration, value: any) { this.$schemaSpec = schemaSpec; this.value = value; } check(value: any = this.value, data?: DataType, fieldName?: string | string[]) { if (this.required && !checkRequired(value, this.trim, this.emptyAllowed)) { return { hasError: true, errorMessage: formatErrorMessage(this.requiredMessage, { name: this.fieldLabel || joinName(fieldName) }) }; } const validator = createValidator( data, fieldName, this.fieldLabel ); const checkResult = validator(value, this.priorityRules); // If the priority rule fails, return the result directly if (checkResult) { return checkResult; } if (!this.required && isEmpty(value)) { return { hasError: false }; } return validator(value, this.rules) || { hasError: false }; } checkAsync( value: any = this.value, data?: DataType, fieldName?: string | string[] ): Promise> { if (this.required && !checkRequired(value, this.trim, this.emptyAllowed)) { return Promise.resolve({ hasError: true, errorMessage: formatErrorMessage(this.requiredMessage, { name: this.fieldLabel || joinName(fieldName) }) }); } const validator = createValidatorAsync( data, fieldName, this.fieldLabel ); return new Promise(resolve => validator(value, this.priorityRules) .then((checkResult: CheckResult | void | null) => { // If the priority rule fails, return the result directly if (checkResult) { resolve(checkResult); } }) .then(() => { if (!this.required && isEmpty(value)) { resolve({ hasError: false }); } }) .then(() => validator(value, this.rules)) .then((checkResult: CheckResult | void | null) => { if (checkResult) { resolve(checkResult); } resolve({ hasError: false }); }) ); } protected pushRule(rule: RuleType) { const { onValid, errorMessage, priority, params } = rule; const nextRule = { onValid, params, isAsync: rule.isAsync, errorMessage: errorMessage || this.rules?.[0]?.errorMessage }; if (priority) { this.priorityRules.push(nextRule); } else { this.rules.push(nextRule); } } addRule( onValid: ValidCallbackType, errorMessage?: E | string | (() => E | string), priority?: boolean ) { this.pushRule({ onValid, errorMessage, priority }); return this; } addAsyncRule( onValid: AsyncValidCallbackType, errorMessage?: E | string, priority?: boolean ) { this.pushRule({ onValid, isAsync: true, errorMessage, priority }); return this; } isRequired(errorMessage: E | string = this.locale.isRequired, trim = true) { this.required = true; this.trim = trim; this.requiredMessage = errorMessage; return this; } isRequiredOrEmpty(errorMessage: E | string = this.locale.isRequiredOrEmpty, trim = true) { this.required = true; this.trim = trim; this.emptyAllowed = true; this.requiredMessage = errorMessage; return this; } /** * Define data verification rules based on conditions. * @param condition * @example * * ```js * SchemaModel({ * option: StringType().isOneOf(['a', 'b', 'other']), * other: StringType().when(schema => { * const { value } = schema.option; * return value === 'other' ? StringType().isRequired('Other required') : StringType(); * }) * }); * ``` */ when(condition: (schemaSpec: SchemaDeclaration) => MixedType) { this.addRule( (value, data, fieldName) => { return condition(this.$schemaSpec).check(value, data, fieldName); }, undefined, true ); return this; } /** * Check if the value is equal to the value of another field. * @example * * ```js * SchemaModel({ * password: StringType().isRequired(), * confirmPassword: StringType().equalTo('password').isRequired() * }); * ``` */ equalTo(fieldName: string, errorMessage: E | string = this.locale.equalTo) { const errorMessageFunc = () => { const type = getFieldType(this.$schemaSpec, fieldName, true); return formatErrorMessage(errorMessage, { toFieldName: type?.fieldLabel || fieldName }); }; this.addRule((value, data) => { return shallowEqual(value, get(data, fieldName)); }, errorMessageFunc); return this; } /** * After the field verification passes, proxy verification of other fields. * @param options.checkIfValueExists When the value of other fields exists, the verification is performed (default: false) * @example * * ```js * SchemaModel({ * password: StringType().isRequired().proxy(['confirmPassword']), * confirmPassword: StringType().equalTo('password').isRequired() * }); * ``` */ proxy(fieldNames: string[], options?: ProxyOptions) { this.otherFields = fieldNames; this.proxyOptions = options || {}; return this; } /** * Overrides the key name in error messages. * * @example * ```js * SchemaModel({ * first_name: StringType().label('First name'), * age: NumberType().label('Age') * }); * ``` */ label(label: string) { this.fieldLabel = label; return this; } } export default function getMixedType() { return new MixedType(); } ================================================ FILE: src/NumberType.ts ================================================ import { MixedType } from './MixedType'; import { ErrorMessageType } from './types'; import { NumberTypeLocale } from './locales'; function toNumber(value: string | number) { return +value; } export class NumberType extends MixedType< number | string, DataType, E, NumberTypeLocale > { constructor(errorMessage?: E | string) { super('number'); super.pushRule({ onValid: value => /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(value + ''), errorMessage: errorMessage || this.locale.type }); } isInteger(errorMessage: E | string = this.locale.isInteger) { super.pushRule({ onValid: value => /^-?\d+$/.test(value + ''), errorMessage }); return this; } pattern(regexp: RegExp, errorMessage: E | string = this.locale.pattern) { super.pushRule({ onValid: value => regexp.test(value + ''), errorMessage, params: { regexp } }); return this; } isOneOf(values: number[], errorMessage: E | string = this.locale.isOneOf) { super.pushRule({ onValid: value => values.includes(toNumber(value)), errorMessage, params: { values } }); return this; } range(min: number, max: number, errorMessage: E | string = this.locale.range) { super.pushRule({ onValid: value => toNumber(value) >= min && toNumber(value) <= max, errorMessage, params: { min, max } }); return this; } min(min: number, errorMessage: E | string = this.locale.min) { super.pushRule({ onValid: value => toNumber(value) >= min, errorMessage, params: { min } }); return this; } max(max: number, errorMessage: E | string = this.locale.max) { super.pushRule({ onValid: value => toNumber(value) <= max, errorMessage, params: { max } }); return this; } } export default function getNumberType(errorMessage?: E) { return new NumberType(errorMessage); } ================================================ FILE: src/ObjectType.ts ================================================ import { MixedType, schemaSpecKey } from './MixedType'; import { createValidator, createValidatorAsync, checkRequired, isEmpty, formatErrorMessage } from './utils'; import { PlainObject, SchemaDeclaration, CheckResult, ErrorMessageType } from './types'; import { ObjectTypeLocale } from './locales'; export class ObjectType extends MixedType< PlainObject, DataType, E, ObjectTypeLocale > { [schemaSpecKey]: SchemaDeclaration; constructor(errorMessage?: E | string) { super('object'); super.pushRule({ onValid: v => typeof v === 'object', errorMessage: errorMessage || this.locale.type }); } check(value: PlainObject = this.value, data?: DataType, fieldName?: string | string[]) { const check = (value: any, data: any, type: any, childFieldKey?: string) => { if (type.required && !checkRequired(value, type.trim, type.emptyAllowed)) { return { hasError: true, errorMessage: formatErrorMessage(type.requiredMessage || type.locale?.isRequired, { name: type.fieldLabel || childFieldKey || fieldName }) }; } if (type[schemaSpecKey] && typeof value === 'object') { const checkResultObject: any = {}; let hasError = false; Object.entries(type[schemaSpecKey]).forEach(([k, v]) => { const checkResult = check(value[k], value, v, k); if (checkResult?.hasError) { hasError = true; } checkResultObject[k] = checkResult; }); return { hasError, object: checkResultObject }; } const validator = createValidator( data, childFieldKey || fieldName, type.fieldLabel ); const checkStatus = validator(value, type.priorityRules); if (checkStatus) { return checkStatus; } if (!type.required && isEmpty(value)) { return { hasError: false }; } return validator(value, type.rules) || { hasError: false }; }; return check(value, data, this) as CheckResult; } checkAsync(value: PlainObject = this.value, data?: DataType, fieldName?: string | string[]) { const check = (value: any, data: any, type: any, childFieldKey?: string) => { if (type.required && !checkRequired(value, type.trim, type.emptyAllowed)) { return Promise.resolve({ hasError: true, errorMessage: formatErrorMessage(type.requiredMessage || type.locale?.isRequired, { name: type.fieldLabel || childFieldKey || fieldName }) }); } const validator = createValidatorAsync( data, childFieldKey || fieldName, type.fieldLabel ); return new Promise(resolve => { if (type[schemaSpecKey] && typeof value === 'object') { const checkResult: any = {}; const checkAll: Promise[] = []; const keys: string[] = []; Object.entries(type[schemaSpecKey]).forEach(([k, v]) => { checkAll.push(check(value[k], value, v, k)); keys.push(k); }); return Promise.all(checkAll).then(values => { let hasError = false; values.forEach((v: any, index: number) => { if (v?.hasError) { hasError = true; } checkResult[keys[index]] = v; }); resolve({ hasError, object: checkResult }); }); } return validator(value, type.priorityRules) .then((checkStatus: CheckResult | void | null) => { if (checkStatus) { resolve(checkStatus); } }) .then(() => { if (!type.required && isEmpty(value)) { resolve({ hasError: false }); } }) .then(() => validator(value, type.rules)) .then((checkStatus: CheckResult | void | null) => { if (checkStatus) { resolve(checkStatus); } resolve({ hasError: false }); }); }); }; return check(value, data, this) as Promise>; } /** * @example * ObjectType().shape({ * name: StringType(), * age: NumberType() * }) */ shape(fields: SchemaDeclaration) { this[schemaSpecKey] = fields; return this; } } export default function getObjectType(errorMessage?: E) { return new ObjectType(errorMessage); } ================================================ FILE: src/Schema.ts ================================================ import { SchemaDeclaration, SchemaCheckResult, CheckResult, PlainObject } from './types'; import { MixedType, getFieldType, getFieldValue } from './MixedType'; import { set, get, isEmpty, pathTransform } from './utils'; interface CheckOptions { /** * Check for nested object */ nestedObject?: boolean; } export class Schema { readonly $spec: SchemaDeclaration; private data: PlainObject; private checkedFields: string[] = []; private checkResult: SchemaCheckResult = {}; constructor(schema: SchemaDeclaration) { this.$spec = schema; } private getFieldType( fieldName: T, nestedObject?: boolean ): SchemaDeclaration[T] { return getFieldType(this.$spec, fieldName as string, nestedObject); } private setFieldCheckResult( fieldName: string, checkResult: CheckResult, nestedObject?: boolean ) { if (nestedObject) { const namePath = fieldName.split('.').join('.object.'); set(this.checkResult, namePath, checkResult); return; } this.checkResult[fieldName] = checkResult; } private setSchemaOptionsForAllType(data: PlainObject) { if (data === this.data) { return; } Object.entries(this.$spec).forEach(([key, type]) => { (type as MixedType).setSchemaOptions(this.$spec as any, data?.[key]); }); this.data = data; } /** * Get the check result of the schema * @returns CheckResult */ getCheckResult(path?: string, result = this.checkResult): CheckResult { if (path) { return result?.[path] || get(result, pathTransform(path)) || { hasError: false }; } return result; } /** * Get the error messages of the schema */ getErrorMessages(path?: string, result = this.checkResult): (string | ErrorMsgType)[] { let messages: (string | ErrorMsgType)[] = []; if (path) { const { errorMessage, object, array } = result?.[path] || get(result, pathTransform(path)) || {}; if (errorMessage) { messages = [errorMessage]; } else if (object) { messages = Object.keys(object).map(key => object[key]?.errorMessage); } else if (array) { messages = array.map(item => item?.errorMessage); } } else { messages = Object.keys(result).map(key => result[key]?.errorMessage); } return messages.filter(Boolean); } /** * Get all the keys of the schema */ getKeys() { return Object.keys(this.$spec); } /** * Get the schema specification */ getSchemaSpec() { return this.$spec; } _checkForField( fieldName: T, data: DataType, options: CheckOptions = {} ): CheckResult { this.setSchemaOptionsForAllType(data); const { nestedObject } = options; // Add current field to checked list this.checkedFields = [...this.checkedFields, fieldName as string]; const fieldChecker = this.getFieldType(fieldName, nestedObject); if (!fieldChecker) { return { hasError: false }; } const fieldValue = getFieldValue(data, fieldName as string, nestedObject); const checkResult = fieldChecker.check(fieldValue, data, fieldName as string); this.setFieldCheckResult(fieldName as string, checkResult, nestedObject); if (!checkResult.hasError) { const { checkIfValueExists } = fieldChecker.proxyOptions; fieldChecker.otherFields?.forEach((field: string) => { if (!this.checkedFields.includes(field)) { if (checkIfValueExists) { if (!isEmpty(getFieldValue(data, field, nestedObject))) { this._checkForField(field as T, data, { ...options }); } return; } this._checkForField(field as T, data, { ...options }); } }); } return checkResult; } checkForField( fieldName: T, data: DataType, options: CheckOptions = {} ): CheckResult { const result = this._checkForField(fieldName, data, options); // clean checked fields after check finished this.checkedFields = []; return result; } checkForFieldAsync( fieldName: T, data: DataType, options: CheckOptions = {} ): Promise> { this.setSchemaOptionsForAllType(data); const { nestedObject } = options; const fieldChecker = this.getFieldType(fieldName, nestedObject); if (!fieldChecker) { // fieldValue can be anything if no schema defined return Promise.resolve({ hasError: false }); } const fieldValue = getFieldValue(data, fieldName as string, nestedObject); const checkResult = fieldChecker.checkAsync(fieldValue, data, fieldName as string); return checkResult.then(async result => { this.setFieldCheckResult(fieldName as string, result, nestedObject); if (!result.hasError) { const { checkIfValueExists } = fieldChecker.proxyOptions; const checkAll: Promise>[] = []; // Check other fields if the field depends on them for validation fieldChecker.otherFields?.forEach((field: string) => { if (checkIfValueExists) { if (!isEmpty(getFieldValue(data, field, nestedObject))) { checkAll.push(this.checkForFieldAsync(field as T, data, options)); } return; } checkAll.push(this.checkForFieldAsync(field as T, data, options)); }); await Promise.all(checkAll); } return result; }); } check(data: DataType) { const checkResult: SchemaCheckResult = {}; Object.keys(this.$spec).forEach(key => { if (typeof data === 'object') { checkResult[key] = this.checkForField(key as T, data); } }); return checkResult; } checkAsync(data: DataType) { const checkResult: SchemaCheckResult = {}; const promises: Promise>[] = []; const keys: string[] = []; Object.keys(this.$spec).forEach((key: string) => { keys.push(key); promises.push(this.checkForFieldAsync(key as T, data)); }); return Promise.all(promises).then(values => { for (let i = 0; i < values.length; i += 1) { checkResult[keys[i]] = values[i]; } return checkResult; }); } } export function SchemaModel( o: SchemaDeclaration ) { return new Schema(o); } SchemaModel.combine = function combine( ...specs: Schema[] ) { return new Schema( specs .map(model => model.$spec) .reduce((accumulator, currentValue) => Object.assign(accumulator, currentValue), {} as any) ); }; ================================================ FILE: src/StringType.ts ================================================ import { MixedType } from './MixedType'; import { ErrorMessageType } from './types'; import { StringTypeLocale } from './locales'; export class StringType extends MixedType< string, DataType, E, StringTypeLocale > { constructor(errorMessage?: E | string) { super('string'); super.pushRule({ onValid: v => typeof v === 'string', errorMessage: errorMessage || this.locale.type }); } containsLetter(errorMessage: E | string = this.locale.containsLetter) { super.pushRule({ onValid: v => /[a-zA-Z]/.test(v), errorMessage }); return this; } containsUppercaseLetter(errorMessage: E | string = this.locale.containsUppercaseLetter) { super.pushRule({ onValid: v => /[A-Z]/.test(v), errorMessage }); return this; } containsLowercaseLetter(errorMessage: E | string = this.locale.containsLowercaseLetter) { super.pushRule({ onValid: v => /[a-z]/.test(v), errorMessage }); return this; } containsLetterOnly(errorMessage: E | string = this.locale.containsLetterOnly) { super.pushRule({ onValid: v => /^[a-zA-Z]+$/.test(v), errorMessage }); return this; } containsNumber(errorMessage: E | string = this.locale.containsNumber) { super.pushRule({ onValid: v => /[0-9]/.test(v), errorMessage }); return this; } isOneOf(values: string[], errorMessage: E | string = this.locale.isOneOf) { super.pushRule({ onValid: v => !!~values.indexOf(v), errorMessage, params: { values } }); return this; } isEmail(errorMessage: E | string = this.locale.isEmail) { // http://emailregex.com/ const regexp = /^(([^<>()\[\]\\.,;:\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,}))$/; super.pushRule({ onValid: v => regexp.test(v), errorMessage }); return this; } isURL( errorMessage: E | string = this.locale.isURL, options?: { allowMailto?: boolean; } ) { const regexp = new RegExp( options?.allowMailto ?? false ? '^(?: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]*)?$' : '^(?!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]*)?$', 'i' ); super.pushRule({ onValid: v => regexp.test(v), errorMessage }); return this; } isHex(errorMessage: E | string = this.locale.isHex) { const regexp = /^#?([a-f0-9]{6}|[a-f0-9]{3})$/i; super.pushRule({ onValid: v => regexp.test(v), errorMessage }); return this; } pattern(regexp: RegExp, errorMessage: E | string = this.locale.pattern) { super.pushRule({ onValid: v => regexp.test(v), errorMessage, params: { regexp } }); return this; } rangeLength( minLength: number, maxLength: number, errorMessage: E | string = this.locale.rangeLength ) { super.pushRule({ onValid: value => value.length >= minLength && value.length <= maxLength, errorMessage, params: { minLength, maxLength } }); return this; } minLength(minLength: number, errorMessage: E | string = this.locale.minLength) { super.pushRule({ onValid: value => Array.from(value).length >= minLength, errorMessage, params: { minLength } }); return this; } maxLength(maxLength: number, errorMessage: E | string = this.locale.maxLength) { super.pushRule({ onValid: value => Array.from(value).length <= maxLength, errorMessage, params: { maxLength } }); return this; } } export default function getStringType(errorMessage?: E) { return new StringType(errorMessage); } ================================================ FILE: src/index.ts ================================================ import { SchemaModel, Schema } from './Schema'; import { default as MixedType } from './MixedType'; import { default as StringType } from './StringType'; import { default as NumberType } from './NumberType'; import { default as ArrayType } from './ArrayType'; import { default as DateType } from './DateType'; import { default as ObjectType } from './ObjectType'; import { default as BooleanType } from './BooleanType'; export type { CheckResult, SchemaCheckResult, SchemaDeclaration, CheckType, RuleType, ValidCallbackType } from './types'; export { SchemaModel, Schema, MixedType, StringType, NumberType, ArrayType, DateType, ObjectType, BooleanType }; ================================================ FILE: src/locales/default.ts ================================================ export default { mixed: { isRequired: '${name} is a required field', isRequiredOrEmpty: '${name} is a required field', equalTo: '${name} must be the same as ${toFieldName}' }, array: { type: '${name} must be an array', rangeLength: '${name} must contain ${minLength} to ${maxLength} items', minLength: '${name} field must have at least ${minLength} items', maxLength: '${name} field must have less than or equal to ${maxLength} items', unrepeatable: '${name} must have non-repeatable items' }, boolean: { type: '${name} must be a boolean' }, date: { type: '${name} must be a date', min: '${name} field must be later than ${min}', max: '${name} field must be at earlier than ${max}', range: '${name} field must be between ${min} and ${max}' }, number: { type: '${name} must be a number', isInteger: '${name} must be an integer', pattern: '${name} is invalid', isOneOf: '${name} must be one of the following values: ${values}', range: '${name} field must be between ${min} and ${max}', min: '${name} must be greater than or equal to ${min}', max: '${name} must be less than or equal to ${max}' }, string: { type: '${name} must be a string', containsLetter: '${name} field must contain letters', containsUppercaseLetter: '${name} must be a upper case string', containsLowercaseLetter: '${name} must be a lowercase string', containsLetterOnly: '${name} must all be letters', containsNumber: '${name} field must contain numbers', isOneOf: '${name} must be one of the following values: ${values}', isEmail: '${name} must be a valid email', isURL: '${name} must be a valid URL', isHex: '${name} must be a valid hexadecimal', pattern: '${name} is invalid', rangeLength: '${name} must contain ${minLength} to ${maxLength} characters', minLength: '${name} must be at least ${minLength} characters', maxLength: '${name} must be at most ${maxLength} characters' }, object: { type: '${name} must be an object' } }; ================================================ FILE: src/locales/index.ts ================================================ import defaultLocale from './default'; export type PickKeys = { [keys in keyof T]: T[keys]; }; export type Locale = PickKeys; export type MixedTypeLocale = PickKeys; export type ArrayTypeLocale = PickKeys & MixedTypeLocale; export type ObjectTypeLocale = PickKeys & MixedTypeLocale; export type BooleanTypeLocale = PickKeys & MixedTypeLocale; export type StringTypeLocale = PickKeys & MixedTypeLocale; export type NumberTypeLocale = PickKeys & MixedTypeLocale; export type DateTypeLocale = PickKeys & MixedTypeLocale; export default defaultLocale; ================================================ FILE: src/types.ts ================================================ import { ArrayType } from './ArrayType'; import { BooleanType } from './BooleanType'; import { DateType } from './DateType'; import { NumberType } from './NumberType'; import { StringType } from './StringType'; import { ObjectType } from './ObjectType'; export type TypeName = 'array' | 'string' | 'boolean' | 'number' | 'object' | 'date'; export interface CheckResult { hasError?: boolean; errorMessage?: E | string; object?: { [P in keyof DataType]: CheckResult; }; array?: CheckResult[]; } export type ErrorMessageType = string; export type ValidCallbackType = ( value: V, data?: D, fieldName?: string | string[] ) => CheckResult | boolean; export type AsyncValidCallbackType = ( value: V, data?: D, fieldName?: string | string[] ) => CheckResult | boolean | Promise>; export type PlainObject = any> = { [P in keyof T]: T; }; export interface RuleType { onValid: AsyncValidCallbackType; errorMessage?: any; priority?: boolean; params?: any; isAsync?: boolean; } export type CheckType = X extends string ? StringType | DateType | NumberType : X extends number ? NumberType : X extends boolean ? BooleanType : X extends Date ? DateType : X extends Array ? ArrayType : X extends Record ? ObjectType : | StringType | NumberType | BooleanType | ArrayType | DateType | ObjectType; export type SchemaDeclaration = { [P in keyof T]: CheckType; }; export type SchemaCheckResult = { [P in keyof T]?: CheckResult; }; ================================================ FILE: src/utils/basicEmptyCheck.ts ================================================ function basicEmptyCheck(value?: any) { return typeof value === 'undefined' || value === null; } export default basicEmptyCheck; ================================================ FILE: src/utils/checkRequired.ts ================================================ import basicEmptyCheck from './basicEmptyCheck'; import isEmpty from './isEmpty'; function checkRequired(value: any, trim: boolean, emptyAllowed: boolean) { // String trim if (trim && typeof value === 'string') { value = value.replace(/(^\s*)|(\s*$)/g, ''); } if (emptyAllowed) { return !basicEmptyCheck(value); } // Array if (Array.isArray(value)) { return !!value.length; } return !isEmpty(value); } export default checkRequired; ================================================ FILE: src/utils/createValidator.ts ================================================ import { CheckResult, RuleType } from '../types'; import formatErrorMessage from './formatErrorMessage'; function isObj(o: unknown): o is Record { return o != null && (typeof o === 'object' || typeof o == 'function'); } function isPromiseLike(v: unknown): v is Promise { return v instanceof Promise || (isObj(v) && typeof v.then === 'function'); } /** * Create a data validator * @param data */ export function createValidator(data?: D, name?: string | string[], label?: string) { return (value: V, rules: RuleType[]): CheckResult | null => { for (let i = 0; i < rules.length; i += 1) { const { onValid, errorMessage, params, isAsync } = rules[i]; if (isAsync) continue; const checkResult = onValid(value, data, name); const errorMsg = typeof errorMessage === 'function' ? errorMessage() : errorMessage; if (checkResult === false) { return { hasError: true, errorMessage: formatErrorMessage(errorMsg, { ...params, name: label || (Array.isArray(name) ? name.join('.') : name) }) }; } else if (isPromiseLike(checkResult)) { throw new Error( 'synchronous validator had an async result, you should probably call "checkAsync()"' ); } else if (typeof checkResult === 'object' && (checkResult.hasError || checkResult.array)) { return checkResult; } } return null; }; } export default createValidator; ================================================ FILE: src/utils/createValidatorAsync.ts ================================================ import { CheckResult, RuleType } from '../types'; import formatErrorMessage, { joinName } from './formatErrorMessage'; /** * Create a data asynchronous validator * @param data */ export function createValidatorAsync(data?: D, name?: string | string[], label?: string) { function check(errorMessage?: E | string) { return (checkResult: CheckResult | boolean): CheckResult | null => { if (checkResult === false) { return { hasError: true, errorMessage }; } else if (typeof checkResult === 'object' && (checkResult.hasError || checkResult.array)) { return checkResult; } return null; }; } return (value: V, rules: RuleType[]) => { const promises = rules.map(rule => { const { onValid, errorMessage, params } = rule; const errorMsg = typeof errorMessage === 'function' ? errorMessage() : errorMessage; return Promise.resolve(onValid(value, data, name)).then( check( formatErrorMessage(errorMsg, { ...params, name: label || joinName(name) }) ) ); }); return Promise.all(promises).then(results => results.find((item: CheckResult | null) => item && item?.hasError) ); }; } export default createValidatorAsync; ================================================ FILE: src/utils/formatErrorMessage.ts ================================================ import isEmpty from './isEmpty'; export function joinName(name: string | string[]) { return Array.isArray(name) ? name.join('.') : name; } /** * formatErrorMessage('${name} is a required field', {name: 'email'}); * output: 'email is a required field' */ export default function formatErrorMessage(errorMessage?: string | E, params?: any) { if (typeof errorMessage === 'string') { return errorMessage.replace(/\$\{\s*(\w+)\s*\}/g, (_, key) => { return isEmpty(params?.[key]) ? `$\{${key}\}` : params?.[key]; }); } return errorMessage; } ================================================ FILE: src/utils/index.ts ================================================ export { default as get } from 'lodash/get'; export { default as set } from 'lodash/set'; export { default as basicEmptyCheck } from './basicEmptyCheck'; export { default as checkRequired } from './checkRequired'; export { default as createValidator } from './createValidator'; export { default as createValidatorAsync } from './createValidatorAsync'; export { default as isEmpty } from './isEmpty'; export { default as formatErrorMessage } from './formatErrorMessage'; export { default as shallowEqual } from './shallowEqual'; export { default as pathTransform } from './pathTransform'; ================================================ FILE: src/utils/isEmpty.ts ================================================ function isEmpty(value?: any) { return typeof value === 'undefined' || value === null || value === ''; } export default isEmpty; ================================================ FILE: src/utils/pathTransform.ts ================================================ export default function pathTransform(path: string) { const arr = path.split('.'); if (arr.length === 1) { return path; } return path .split('.') .map((item, index) => { if (index === 0) { return item; } // Check if the item is a number, e.g. `list.0` return /^\d+$/.test(item) ? `array.${item}` : `object.${item}`; }) .join('.'); } ================================================ FILE: src/utils/shallowEqual.ts ================================================ /** * From: https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/shallowEqual.js * @providesModule shallowEqual * @typechecks * @flow */ const hasOwnProperty = Object.prototype.hasOwnProperty; /** * inlined Object.is polyfill to avoid requiring consumers ship their own * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is */ function is(x: any, y: any): boolean { // SameValue algorithm if (x === y) { // Steps 1-5, 7-10 // Steps 6.b-6.e: +0 != -0 // Added the nonzero y check to make Flow happy, but it is redundant return x !== 0 || y !== 0 || 1 / x === 1 / y; } // Step 6.a: NaN == NaN return x !== x && y !== y; } /** * Performs equality by iterating through keys on an object and returning false * when any key has values which are not strictly equal between the arguments. * Returns true when the values of all keys are strictly equal. */ function shallowEqual(objA: any, objB: any): boolean { if (is(objA, objB)) { return true; } if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. for (let i = 0; i < keysA.length; i += 1) { if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { return false; } } return true; } export default shallowEqual; ================================================ FILE: test/ArrayTypeSpec.js ================================================ import { expect } from 'chai'; import * as schema from '../src'; const { ArrayType, StringType, NumberType, ObjectType, Schema } = schema; describe('#ArrayType', () => { it('Should be a valid array', () => { const schemaData = { data: ArrayType().minLength(2, 'error1').of(StringType().isEmail('error2')), data2: ArrayType().minLength(2).of(StringType().isEmail()) }; const schema = new Schema(schemaData); const checkResult = schema.checkForField('data', { data: ['simon.guo@hypers.com', 'ddddd@d.com', 'ddd@bbb.com'] }); expect(checkResult).to.deep.equal({ hasError: false, array: [{ hasError: false }, { hasError: false }, { hasError: false }] }); const checkResult2 = schema.check({ data: ['simon.guo@hypers.com', 'error_email', 'ddd@bbb.com'] }); expect(checkResult2).to.deep.equal({ data: { hasError: true, array: [ { hasError: false }, { hasError: true, errorMessage: 'error2' }, { hasError: false } ] }, data2: { hasError: false } }); const checkResult3 = schema.check({ data2: [] }); expect(checkResult3).to.deep.equal({ data: { hasError: false }, data2: { hasError: true, errorMessage: 'data2 field must have at least 2 items' } }); const checkResult4 = schema.check({ data2: ['simon.guo@hypers.com', 'error_email', 'ddd@bbb.com'] }); expect(checkResult4).to.deep.equal({ data: { hasError: false }, data2: { hasError: true, array: [ { hasError: false }, { hasError: true, errorMessage: 'data2.[1] must be a valid email' }, { hasError: false } ] } }); }); it('Should output default error message ', () => { const schemaData = { data: ArrayType().of(StringType().isEmail()) }; const schema = new Schema(schemaData); const checkStatus = schema.checkForField('data', { data: ['simon.guo@hypers.com', 'error_email', 'ddd@bbb.com'] }); checkStatus.array[1].hasError.should.equal(true); checkStatus.array[1].errorMessage.should.equal('data.[1] must be a valid email'); }); it('Should be unrepeatable ', () => { const schemaData = { data: ArrayType().unrepeatable('error1') }; const schema = new Schema(schemaData); const checkStatus = schema.checkForField('data', { data: ['abc', '123', 'abc'] }); checkStatus.hasError.should.equal(true); checkStatus.errorMessage.should.equal('error1'); const schemaData2 = { data: ArrayType().unrepeatable() }; const schema2 = new Schema(schemaData2); const checkStatus2 = schema2.checkForField('data', { data: ['abc', '123', 'abc'] }); checkStatus2.errorMessage.should.equal('data must have non-repeatable items'); schema.checkForField('data', { data: ['1', '2', '3'] }).hasError.should.equal(false); }); it('Should be required ', () => { const schemaData = { data: ArrayType().isRequired('error1'), data2: ArrayType().isRequired() }; const schema = new Schema(schemaData); const checkStatus = schema.checkForField('data', { data: null }); const checkStatus2 = schema.checkForField('data2', { data2: null }); checkStatus.hasError.should.equal(true); checkStatus.errorMessage.should.equal('error1'); checkStatus2.errorMessage.should.equal('data2 is a required field'); schema.checkForField('data', { data: [] }).hasError.should.equal(true); schema.checkForField('data', { data: undefined }).hasError.should.equal(true); }); it('Should be within the number of items', () => { const schemaData = { data: ArrayType().rangeLength(2, 4) }; const schema = new Schema(schemaData); schema.checkForField('data', { data: [1, 2] }).hasError.should.equal(false); schema.checkForField('data', { data: [1] }).hasError.should.equal(true); schema.checkForField('data', { data: [1, 2, 3, 4, 5] }).hasError.should.equal(true); schema .checkForField('data', { data: [1] }) .errorMessage.should.equal('data must contain 2 to 4 items'); }); it('Should not exceed the maximum number of items', () => { const schemaData = { data: ArrayType().maxLength(2) }; const schema = new Schema(schemaData); schema.checkForField('data', { data: [1, 2, 3] }).hasError.should.equal(true); schema .checkForField('data', { data: [1, 2, 3] }) .errorMessage.should.equal('data field must have less than or equal to 2 items'); }); it('Should not be less than the maximum number of items', () => { const schemaData = { data: ArrayType().minLength(2) }; const schema = new Schema(schemaData); schema.checkForField('data', { data: [1] }).hasError.should.equal(true); schema .checkForField('data', { data: [1] }) .errorMessage.should.equal('data field must have at least 2 items'); }); describe('Nested Object', () => { const options = { nestedObject: true }; it('Should support array nested objects', () => { const schemaData = { users: ArrayType().of( ObjectType('error1').shape({ email: StringType().isEmail('error2'), age: NumberType().min(18, 'error3') }) ), users2: ArrayType().of( ObjectType().shape({ email: StringType().isEmail(), age: NumberType().min(18) }) ) }; const schema = new Schema(schemaData); const checkResult = schema.check({ users: [ 'simon.guo@hypers.com', { email: 'error_email', age: 19 }, { email: 'error_email', age: 17 } ] }); expect(checkResult).to.deep.equal({ users: { hasError: true, array: [ { hasError: true, errorMessage: 'error1' }, { hasError: true, object: { email: { hasError: true, errorMessage: 'error2' }, age: { hasError: false } } }, { hasError: true, object: { email: { hasError: true, errorMessage: 'error2' }, age: { hasError: true, errorMessage: 'error3' } } } ] }, users2: { hasError: false } }); const schema2 = new Schema(schemaData); const checkResult2 = schema2.check({ users2: [ 'simon.guo@hypers.com', { email: 'error_email', age: 19 }, { email: 'error_email', age: 17 } ] }); expect(checkResult2).to.deep.equal({ users: { hasError: false }, users2: { hasError: true, array: [ { hasError: true, errorMessage: 'users2.[0] must be an object' }, { hasError: true, object: { email: { hasError: true, errorMessage: 'email must be a valid email' }, age: { hasError: false } } }, { hasError: true, object: { email: { hasError: true, errorMessage: 'email must be a valid email' }, age: { hasError: true, errorMessage: 'age must be greater than or equal to 18' } } } ] } }); }); it('Should validate nested array with required fields', () => { const schema = new Schema({ address: ArrayType().of( ObjectType().shape({ city: StringType().isRequired('City is required'), postCode: StringType().isRequired('Post code is required') }) ) }); const checkResult = schema.check({ address: [ { city: 'Shanghai', postCode: '200000' }, { city: 'Beijing', postCode: '100000' } ] }); expect(checkResult).to.deep.equal({ address: { hasError: false, array: [ { hasError: false, object: { city: { hasError: false }, postCode: { hasError: false } } }, { hasError: false, object: { city: { hasError: false }, postCode: { hasError: false } } } ] } }); const checkResult2 = schema.check({ address: [{ postCode: '200000' }, { city: 'Beijing' }] }); expect(checkResult2).to.deep.equal({ address: { hasError: true, array: [ { hasError: true, object: { city: { hasError: true, errorMessage: 'City is required' }, postCode: { hasError: false } } }, { hasError: true, object: { city: { hasError: false }, postCode: { hasError: true, errorMessage: 'Post code is required' } } } ] } }); }); it('Should check a field in an array', () => { const schema = new Schema({ address: ArrayType().of( ObjectType().shape({ city: StringType().isRequired('City is required'), postCode: StringType().isRequired('Post code is required') }) ) }); const checkResult = schema.checkForField( 'address[0].city', { address: [{ city: 'Shanghai' }] }, options ); expect(checkResult).to.deep.equal({ hasError: false }); const checkResult2 = schema.checkForField( 'address[1].postCode', { address: [{ postCode: '' }] }, options ); expect(checkResult2).to.deep.equal({ hasError: true, errorMessage: 'Post code is required' }); }); it('Should check primitive type array items', () => { const schema = new Schema({ emails: ArrayType().of(StringType().isEmail('Invalid email')), numbers: ArrayType().of(NumberType().min(0, 'Must be positive')) }); // Test valid email expect( schema.checkForField('emails[0]', { emails: ['test@example.com'] }, options) ).to.deep.equal({ hasError: false }); // Test invalid email expect( schema.checkForField('emails[0]', { emails: ['invalid-email'] }, options) ).to.deep.equal({ hasError: true, errorMessage: 'Invalid email' }); // Test negative number expect(schema.checkForField('numbers[0]', { numbers: [-1] }, options)).to.deep.equal({ hasError: true, errorMessage: 'Must be positive' }); }); it('Should support nested arrays', () => { const schema = new Schema({ matrix: ArrayType().of(ArrayType().of(NumberType().min(0, 'Must be positive'))) }); // Test negative number in nested array expect( schema.checkForField( 'matrix[0][1]', { matrix: [[0, -1]] }, options ) ).to.deep.equal({ hasError: true, errorMessage: 'Must be positive' }); }); it('Should support nested arrays in check', () => { const schema = new Schema({ matrix: ArrayType().of(ArrayType().of(NumberType().min(0, 'Must be positive'))) }); // Test negative number in nested array expect( schema.check({ matrix: [[0, -1]] }) ).to.deep.equal({ matrix: { array: [ { array: [ { hasError: false }, { errorMessage: 'Must be positive', hasError: true } ], hasError: true } ], hasError: true } }); }); it('Should validate array elements with complex validation rules', () => { const schema = new Schema({ users: ArrayType().of( ObjectType().shape({ name: StringType().isRequired('Name is required'), age: NumberType().min(18, 'Must be an adult'), email: StringType().isEmail('Invalid email format') }) ) }); // Test valid name expect( schema.checkForField( 'users[0].name', { users: [{ name: 'John Doe' }] }, { nestedObject: true } ) ).to.deep.equal({ hasError: false }); // Test required field in array object expect( schema.checkForField( 'users[0].name', { users: [{ name: '' }] }, { nestedObject: true } ) ).to.deep.equal({ hasError: true, errorMessage: 'Name is required' }); // Test minimum value in array object expect( schema.checkForField( 'users[0].age', { users: [{ age: 16 }] }, { nestedObject: true } ) ).to.deep.equal({ hasError: true, errorMessage: 'Must be an adult' }); // Test email format in array object expect( schema.checkForField( 'users[0].email', { users: [{ email: 'invalid-email' }] }, { nestedObject: true } ) ).to.deep.equal({ hasError: true, errorMessage: 'Invalid email format' }); }); it('Should validate nested array objects (max 3 levels)', () => { const schema = new Schema({ users: ArrayType().of( ObjectType().shape({ name: StringType().isRequired('Name required'), tasks: ArrayType().of( ObjectType().shape({ title: StringType().isRequired('Task title required'), assignees: ArrayType().of( ObjectType().shape({ email: StringType().isEmail('Invalid email format'), role: StringType() .isOneOf(['owner', 'admin', 'member'], 'Invalid role') .isRequired('Role required'), priority: NumberType() .min(1, 'Priority too low') .max(5, 'Priority too high') .isRequired('Priority required') }) ) }) ) }) ) }); // Test valid email expect( schema.checkForField( 'users[0].tasks[0].assignees[0].email', { users: [ { name: 'John Doe', tasks: [ { title: 'Frontend Development', assignees: [ { email: 'test@example.com', role: 'owner', priority: 3 } ] } ] } ] }, { nestedObject: true } ) ).to.deep.equal({ hasError: false }); // Test invalid email expect( schema.checkForField( 'users[0].tasks[0].assignees[0].email', { users: [ { name: 'John Doe', tasks: [ { title: 'Frontend Development', assignees: [ { email: 'invalid-email', role: 'owner', priority: 3 } ] } ] } ] }, { nestedObject: true } ) ).to.deep.equal({ hasError: true, errorMessage: 'Invalid email format' }); // Test valid role expect( schema.checkForField( 'users[0].tasks[0].assignees[0].role', { users: [ { name: 'John Doe', tasks: [ { title: 'Frontend Development', assignees: [ { email: 'test@example.com', role: 'owner', priority: 3 } ] } ] } ] }, { nestedObject: true } ) ).to.deep.equal({ hasError: false }); // Test invalid role expect( schema.checkForField( 'users[0].tasks[0].assignees[0].role', { users: [ { name: 'John Doe', tasks: [ { title: 'Frontend Development', assignees: [ { email: 'test@example.com', role: 'guest', priority: 3 } ] } ] } ] }, { nestedObject: true } ) ).to.deep.equal({ hasError: true, errorMessage: 'Invalid role' }); // Test valid priority expect( schema.checkForField( 'users[0].tasks[0].assignees[0].priority', { users: [ { name: 'John Doe', tasks: [ { title: 'Frontend Development', assignees: [ { email: 'test@example.com', role: 'owner', priority: 3 } ] } ] } ] }, { nestedObject: true } ) ).to.deep.equal({ hasError: false }); // Test invalid priority (too high) expect( schema.checkForField( 'users[0].tasks[0].assignees[0].priority', { users: [ { name: 'John Doe', tasks: [ { title: 'Frontend Development', assignees: [ { email: 'test@example.com', role: 'owner', priority: 6 } ] } ] } ] }, { nestedObject: true } ) ).to.deep.equal({ hasError: true, errorMessage: 'Priority too high' }); // Test invalid priority (too low) expect( schema.checkForField( 'users[0].tasks[0].assignees[0].priority', { users: [ { name: 'John Doe', tasks: [ { title: 'Frontend Development', assignees: [ { email: 'test@example.com', role: 'owner', priority: 0 } ] } ] } ] }, { nestedObject: true } ) ).to.deep.equal({ hasError: true, errorMessage: 'Priority too low' }); // Test required field present expect( schema.checkForField( 'users[0].tasks[0].title', { users: [ { name: 'John Doe', tasks: [ { title: 'Frontend Development', assignees: [ { email: 'test@example.com', role: 'owner', priority: 3 } ] } ] } ] }, { nestedObject: true } ) ).to.deep.equal({ hasError: false }); // Test required field missing expect( schema.checkForField( 'users[0].tasks[0].title', { users: [ { name: 'John Doe', tasks: [ { title: null, assignees: [ { email: 'test@example.com', role: 'owner', priority: 3 } ] } ] } ] }, { nestedObject: true } ) ).to.deep.equal({ hasError: true, errorMessage: 'Task title required' }); }); it('Should validate explicit nested array type', () => { const schema = new Schema({ users: ArrayType().of( StringType().isRequired().isEmail(), ObjectType().shape({ name: StringType().isEmail(), email: StringType().isEmail() }) ) }); expect( schema.checkForField( 'users[0]', { users: ['xx'] }, options ) ).to.deep.equal({ hasError: true, errorMessage: 'users[0] must be a valid email' }); expect( schema.checkForField( 'users[0]', { users: ['ddd@bbb.com'] }, options ) ).to.deep.equal({ hasError: false }); expect( schema.checkForField( 'users[1].name', { users: ['ddd@bbb.com', { name: 'xxx' }] }, options ) ).to.deep.equal({ hasError: true, errorMessage: 'users[1].name must be a valid email' }); expect( schema.checkForField( 'users[1].name', { users: ['ddd@bbb.com', { name: 'ddd@bbb.com' }] }, options ) ).to.deep.equal({ hasError: false }); expect( schema.check({ users: [ 'xxx', { name: 'xx', email: 'xx' } ] }) ).to.deep.equal({ users: { hasError: true, array: [ { hasError: true, errorMessage: 'users.[0] must be a valid email' }, { hasError: true, object: { name: { hasError: true, errorMessage: 'name must be a valid email' }, email: { hasError: true, errorMessage: 'email must be a valid email' } } } ] } }); }); it('Should validate nested array within an object', () => { const schema = new Schema({ user: ObjectType().shape({ emails: ArrayType().of( StringType().isEmail(), ObjectType().shape({ name: StringType().isEmail() }) ) }) }); expect( schema.checkForField( 'user.emails[0]', { user: { emails: ['xxx'] } }, options ) ).to.deep.equal({ hasError: true, errorMessage: 'user.emails[0] must be a valid email' }); expect( schema.check({ user: { emails: [ 'xxx', { name: 'xxx' } ] } }) ).to.deep.equal({ user: { hasError: true, object: { emails: { hasError: true, array: [ { hasError: true, errorMessage: 'emails.[0] must be a valid email' }, { hasError: true, object: { name: { hasError: true, errorMessage: 'name must be a valid email' } } } ] } } } }); }); }); }); ================================================ FILE: test/BooleanTypeSpec.js ================================================ /* eslint-disable @typescript-eslint/no-var-requires */ require('chai').should(); const schema = require('../src'); const { BooleanType, Schema } = schema; describe('#BooleanType', () => { it('Should be a valid boolean', () => { const schemaData = { data: BooleanType(), data2: BooleanType().isRequired() }; const schema = new Schema(schemaData); schema.checkForField('data', { data: true }).hasError.should.equal(false); schema.checkForField('data', { data: false }).hasError.should.equal(false); schema.checkForField('data', { data: 0 }).hasError.should.equal(true); schema.checkForField('data', { data: '' }).hasError.should.equal(false); schema.checkForField('data', { data: null }).hasError.should.equal(false); schema.checkForField('data', { data: undefined }).hasError.should.equal(false); schema.checkForField('data', { data: 0 }).errorMessage.should.equal('data must be a boolean'); schema.checkForField('data2', { data2: '' }).hasError.should.equal(true); schema.checkForField('data2', { data2: null }).hasError.should.equal(true); schema.checkForField('data2', { data2: undefined }).hasError.should.equal(true); schema .checkForField('data2', { data2: '' }) .errorMessage.should.equal('data2 is a required field'); }); }); ================================================ FILE: test/DateTypeSpec.js ================================================ /* eslint-disable @typescript-eslint/no-var-requires */ require('chai').should(); const schema = require('../src'); const { DateType, Schema } = schema; describe('#DateType', () => { it('Should be a valid date', () => { const schemaData = { data: DateType(), data2: DateType().isRequired() }; const schema = new Schema(schemaData); schema.checkForField('data', { data: new Date() }).hasError.should.equal(false); schema.checkForField('data', { data: 'date' }).hasError.should.equal(true); schema.checkForField('data', { data: '' }).hasError.should.equal(false); schema.checkForField('data', { data: null }).hasError.should.equal(false); schema.checkForField('data', { data: undefined }).hasError.should.equal(false); schema.checkForField('data', { data: 'date' }).errorMessage.should.equal('data must be a date'); schema.checkForField('data2', { data2: '' }).hasError.should.equal(true); schema.checkForField('data2', { data2: null }).hasError.should.equal(true); schema.checkForField('data2', { data2: undefined }).hasError.should.equal(true); schema .checkForField('data2', { data2: '' }) .errorMessage.should.equal('data2 is a required field'); }); it('Should be within the date range', () => { const schemaData = { data: DateType().range('2020-01-01', '2020-02-01') }; const schema = new Schema(schemaData); schema.checkForField('data', { data: '2020-01-02' }).hasError.should.equal(false); schema.checkForField('data', { data: '2020-02-02' }).hasError.should.equal(true); schema .checkForField('data', { data: '2020-02-02' }) .errorMessage.should.equal('data field must be between 2020-01-01 and 2020-02-01'); }); it('Should not be less than the minimum date', () => { const schemaData = { data: DateType().min('2020-01-01') }; const schema = new Schema(schemaData); schema.checkForField('data', { data: '2020-01-02' }).hasError.should.equal(false); schema.checkForField('data', { data: '2019-12-30' }).hasError.should.equal(true); schema .checkForField('data', { data: '2019-12-30' }) .errorMessage.should.equal('data field must be later than 2020-01-01'); }); it('Should not exceed the maximum date', () => { const schemaData = { data: DateType().max('2020-01-01') }; const schema = new Schema(schemaData); schema.checkForField('data', { data: '2019-12-30' }).hasError.should.equal(false); schema.checkForField('data', { data: '2020-01-02' }).hasError.should.equal(true); schema .checkForField('data', { data: '2020-01-02' }) .errorMessage.should.equal('data field must be at earlier than 2020-01-01'); }); }); ================================================ FILE: test/MixedTypeSpec.js ================================================ import chai, { expect } from 'chai'; import * as schema from '../src'; import { getFieldType, schemaSpecKey, arrayTypeSchemaSpec } from '../src/MixedType'; chai.should(); const { StringType, SchemaModel, NumberType, ArrayType, MixedType, ObjectType } = schema; describe('#MixedType', () => { describe('addRule', () => { it('Should check if two fields are the same by addRule', () => { const schema = SchemaModel({ a: StringType().isRequired(), b: StringType() .addRule((value, data) => value === data.a, 'The two fields are not the same') .isRequired() }); expect(schema.check({ a: '123', b: '123' })).to.deep.equal({ a: { hasError: false }, b: { hasError: false } }); expect(schema.check({ a: '123', b: '456' })).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'The two fields are not the same' } }); expect(schema.check({ a: '123', b: '' })).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'b is a required field' } }); }); it('Should check if two fields are the same and the field value is not root', () => { const schema = SchemaModel({ a: StringType().isRequired(), b: StringType() .addRule(value => value !== 'root', 'The value is root') .addRule((value, data) => value === data.a, 'The two fields are not the same') .isRequired() }); expect(schema.check({ a: 'root', b: 'root' })).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'The value is root' } }); expect(schema.check({ a: '123', b: '456' })).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'The two fields are not the same' } }); }); }); describe('priority', () => { it('Should have the correct priority', () => { const schema = SchemaModel({ name: StringType() .isEmail('error1') .addRule(() => false, 'error2') }); schema.check({ name: 'a' }).name.hasError.should.equal(true); schema.check({ name: 'a' }).name.errorMessage.should.equal('error1'); const schema2 = SchemaModel({ name: StringType() .isEmail('error1') .addRule(() => false, 'error2', true) }); schema2.check({ name: 'a' }).name.hasError.should.equal(true); schema2.check({ name: 'a' }).name.errorMessage.should.equal('error2'); const schema3 = SchemaModel({ name: StringType().addRule(() => true, 'error2', true) }); schema3.check({ name: 'a' }).name.hasError.should.equal(false); }); it('Should be isRequired with a higher priority than addRule', () => { const schema = SchemaModel({ str: StringType() .isRequired('required') .addRule(value => value === '', 'error') }); schema.checkForField('str', { str: '' }).hasError.should.equal(true); schema.checkForField('str', { str: '' }).errorMessage.should.equal('required'); schema.checkForField('str', { str: '12' }).hasError.should.equal(true); schema.checkForField('str', { str: '12' }).errorMessage.should.equal('error'); const schema2 = SchemaModel({ str: StringType().addRule(value => value === '', 'error') }); schema2.checkForField('str', { str: '12' }).hasError.should.equal(true); schema2.checkForField('str', { str: '12' }).errorMessage.should.equal('error'); }); describe('priority - async', () => { it('Should have the correct priority', async () => { const schema = SchemaModel({ name: StringType() .isEmail('error1') .addRule(() => false, 'error2') }); const result = await schema.checkAsync({ name: 'a' }); expect(result).to.deep.equal({ name: { hasError: true, errorMessage: 'error1' } }); const schema2 = SchemaModel({ name: StringType() .isEmail('error1') .addRule(() => false, 'error2', true) }); const result2 = await schema2.checkAsync({ name: 'a' }); expect(result2).to.deep.equal({ name: { hasError: true, errorMessage: 'error2' } }); const schema3 = SchemaModel({ name: StringType().addRule(() => true, 'error2', true) }); const result3 = await schema3.checkAsync({ name: 'a' }); expect(result3).to.deep.equal({ name: { hasError: false } }); }); it('Should be isRequired with a higher priority than addRule', async () => { const schema = SchemaModel({ str: StringType() .isRequired('required') .addRule(value => value === '', 'error') }); const result = await schema.checkAsync({ str: '' }); expect(result).to.deep.equal({ str: { hasError: true, errorMessage: 'required' } }); const result2 = await schema.checkAsync({ str: '12' }); expect(result2).to.deep.equal({ str: { hasError: true, errorMessage: 'error' } }); const schema2 = SchemaModel({ str: StringType().addRule(value => value === '', 'error') }); const result3 = await schema2.checkAsync({ str: '12' }); expect(result3).to.deep.equal({ str: { hasError: true, errorMessage: 'error' } }); }); }); }); describe('required', () => { it('Should be error for undefined string with isRequired', () => { const schema = SchemaModel({ str: StringType().isRequired('required') }); const result = schema.check({ str: undefined }); result.str.hasError.should.equal(true); }); it('Should be error for empty string with isRequired', () => { const schema = SchemaModel({ str: StringType().isRequired('required') }); const result = schema.check({ str: '' }); result.str.hasError.should.equal(true); }); it('Should be error for empty array with isRequired', () => { const schema = SchemaModel({ arr: ArrayType().isRequired('required') }); let obj = { arr: [] }; let result = schema.check(obj); result.arr.hasError.should.equal(true); }); it('Should be without error for empty string with isRequiredOrEmpty', () => { const schema = SchemaModel({ str: StringType().isRequiredOrEmpty('required'), str2: StringType().isRequiredOrEmpty() }); let obj = { str: '', str2: null }; let result = schema.check(obj); result.str.hasError.should.equal(false); result.str2.hasError.should.equal(true); result.str2.errorMessage.should.equal('str2 is a required field'); }); it('Should be without error for empty array with isRequiredOrEmpty', () => { const schema = SchemaModel({ arr: ArrayType().isRequiredOrEmpty('required') }); let obj = { arr: [] }; let result = schema.check(obj); result.arr.hasError.should.equal(false); }); it('Should be error for undefined string with isRequiredOrEmpty', () => { const schema = SchemaModel({ str: StringType().isRequiredOrEmpty('required') }); let obj = { str: undefined }; let result = schema.check(obj); result.str.hasError.should.equal(true); }); }); describe('async', () => { it('Should call async check', done => { const schema = SchemaModel({ email: StringType('error1').isEmail('error2'), name: StringType().addRule(() => { return new Promise(resolve => { setTimeout(() => { resolve(false); }, 1000); }); }, 'error1') }); schema.checkAsync({ name: 'a', email: 'a' }).then(status => { if ( status.name.hasError && status.name.errorMessage === 'error1' && status.email.hasError && status.email.errorMessage === 'error2' ) { done(); } }); }); it('Should call async check', done => { const schema = SchemaModel({ email: StringType('error1').isEmail('error2') }); schema.checkAsync({ name: 'a', email: 'a' }).then(status => { if (status.email.hasError && status.email.errorMessage === 'error2') { done(); } }); }); it('Should call async checkForFieldAsync and verify pass', done => { const schema = SchemaModel({ name: StringType().addRule(() => { return new Promise(resolve => { setTimeout(() => { resolve(false); }, 500); }); }, 'error1') }); schema.checkForFieldAsync('name', { name: 'a' }).then(status => { if (status.hasError && status.errorMessage === 'error1') { done(); } }); }); it('Should call async checkForFieldAsync and the validation fails', done => { const schema = SchemaModel({ email: StringType('error1').isEmail('error2') }); schema.checkForFieldAsync('email', { email: 'a' }).then(status => { if (status.hasError && status.errorMessage === 'error2') { done(); } }); }); it('Should call async checkForFieldAsync and the validation fails', done => { const schema = SchemaModel({ name: StringType().addRule(() => { return new Promise(resolve => { setTimeout(() => { resolve(true); }, 200); }); }, 'error1') }); schema.checkForFieldAsync('name', { name: 'a' }).then(status => { if (status.hasError === false) { done(); } }); }); it('Should call async checkForFieldAsync and the validation fails', done => { const schema = SchemaModel({ name: StringType() .addRule(() => { return new Promise(resolve => { setTimeout(() => { resolve(false); }, 200); }); }, 'error1') .addRule(() => { return new Promise(resolve => { resolve(false); }); }, 'error2') }); schema.checkForFieldAsync('name', { name: 'a' }).then(status => { if (status.hasError && status.errorMessage === 'error1') { done(); } }); }); }); describe('when', () => { it('Should type be changed by condition', () => { const model = SchemaModel({ field1: NumberType().min(10), field2: MixedType().when(schema => { const checkResult = schema.field1.check(); return checkResult.hasError ? NumberType().min(10, 'error1') : NumberType().min(2, 'error2'); }) }); const checkResult1 = model.check({ field1: 20, field2: 2 }); expect(checkResult1).to.deep.equal({ field1: { hasError: false }, field2: { hasError: false } }); const checkResult2 = model.check({ field1: 1, field2: 1 }); expect(checkResult2).to.deep.equal({ field1: { hasError: true, errorMessage: 'field1 must be greater than or equal to 10' }, field2: { hasError: true, errorMessage: 'error1' } }); const checkResult3 = model.check({ field1: 10, field2: 1 }); expect(checkResult3).to.deep.equal({ field1: { hasError: false }, field2: { hasError: true, errorMessage: 'error2' } }); const checkResult4 = model.checkForField('field2', { field1: 20, field2: 1 }); checkResult4.errorMessage.should.equal('error2'); expect(checkResult4).to.deep.equal({ hasError: true, errorMessage: 'error2' }); const checkResult5 = model.checkForField('field2', { field1: 9, field2: 1 }); expect(checkResult5).to.deep.equal({ hasError: true, errorMessage: 'error1' }); }); it('Should change the type by getting the value of other fields in the schema', () => { const model = SchemaModel({ option: StringType().isOneOf(['a', 'b', 'other']), other: StringType().when(schema => { const { value } = schema.option; return value === 'other' ? StringType().isRequired('Other required') : StringType(); }) }); const checkResult = model.check({ option: 'a', other: '' }); expect(checkResult).to.deep.equal({ option: { hasError: false }, other: { hasError: false } }); const checkResult2 = model.check({ option: 'other', other: '' }); expect(checkResult2).to.deep.equal({ option: { hasError: false }, other: { hasError: true, errorMessage: 'Other required' } }); }); it('Should change the type by verifying the value of other fields in the schema', () => { const model = SchemaModel({ password: StringType().isRequired('Password required'), confirmPassword: StringType().when(schema => { const { hasError } = schema.password.check(); return hasError ? StringType() : StringType() .addRule( value => value === schema.password.value, 'The passwords are inconsistent twice' ) .isRequired() .label('Confirm password'); }) }); const checkResult = model.check({ password: '', confirmPassword: '123' }); expect(checkResult).to.deep.equal({ password: { hasError: true, errorMessage: 'Password required' }, confirmPassword: { hasError: false } }); const checkResult2 = model.check({ password: '123', confirmPassword: '123' }); expect(checkResult2).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: false } }); const checkResult3 = model.check({ password: '123', confirmPassword: '1234' }); expect(checkResult3).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: true, errorMessage: 'The passwords are inconsistent twice' } }); const checkResult4 = model.check({ password: '123', confirmPassword: '' }); expect(checkResult4).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: true, errorMessage: 'Confirm password is a required field' } }); }); }); describe('proxy - checkForField', () => { it('Should verify the dependent field through proxy', () => { const schema = SchemaModel({ password: StringType().isRequired().proxy(['confirmPassword']), confirmPassword: StringType() .isRequired() .addRule((value, data) => { if (value !== data?.password) { return false; } return true; }, 'The passwords are inconsistent twice') }); expect( schema.checkForField('password', { password: '123', confirmPassword: '13' }) ).to.deep.equal({ hasError: false }); expect(schema.getCheckResult()).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: true, errorMessage: 'The passwords are inconsistent twice' } }); expect(schema.check({ password: '123', confirmPassword: '13' })).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: true, errorMessage: 'The passwords are inconsistent twice' } }); expect(schema.check({ password: '123', confirmPassword: '123' })).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: false } }); expect(schema.getCheckResult()).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: false } }); }); it('Should not verify the dependent field when field validation fails', () => { const schema = SchemaModel({ a: StringType().isRequired().proxy(['b']), b: StringType().isRequired() }); expect(schema.checkForField('a', { a: '' })).to.deep.equal({ hasError: true, errorMessage: 'a is a required field' }); expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: true, errorMessage: 'a is a required field' } }); }); it('Should verify the dependent field through proxy with nestedObject', () => { const schema = SchemaModel({ a: StringType().isRequired().proxy(['b.c']), b: ObjectType().shape({ c: StringType().isRequired() }) }); expect(schema.checkForField('a', { a: 'd' }, { nestedObject: true })).to.deep.equal({ hasError: false }); expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { object: { c: { hasError: true, errorMessage: 'b.c is a required field' } } } }); }); it('Should not verify the dependent field when field validation fails', () => { const schema = SchemaModel({ a: StringType().isRequired().proxy(['b', 'd']), b: StringType().isRequired(), c: StringType().isRequired(), d: StringType().isRequired() }); expect(schema.checkForField('a', { a: 'a' })).to.deep.equal({ hasError: false }); expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'b is a required field' }, d: { hasError: true, errorMessage: 'd is a required field' } }); }); it('Should verify the dependent field through proxy with checkIfValueExists', () => { const schema = SchemaModel({ a: StringType().isRequired().proxy(['b'], { checkIfValueExists: true }), b: StringType().isRequired() }); expect(schema.checkForField('a', { a: 'a' })).to.deep.equal({ hasError: false }); expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false } }); expect(schema.checkForField('a', { a: 'a', b: 1 })).to.deep.equal({ hasError: false }); expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'b must be a string' } }); }); }); describe('proxy - checkForFieldAsync', () => { it('Should verify the dependent field through proxy', async () => { const schema = SchemaModel({ password: StringType().isRequired().proxy(['confirmPassword']), confirmPassword: StringType() .isRequired() .addRule((value, data) => { if (value !== data?.password) { return false; } return true; }, 'The passwords are inconsistent twice') }); await schema .checkForFieldAsync('password', { password: '123', confirmPassword: '12' }) .then(result => { expect(result).to.deep.equal({ hasError: false }); return result; }); expect(schema.getCheckResult()).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: true, errorMessage: 'The passwords are inconsistent twice' } }); await schema.checkAsync({ password: '123', confirmPassword: '13' }).then(result => { expect(result).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: true, errorMessage: 'The passwords are inconsistent twice' } }); }); await schema.checkAsync({ password: '123', confirmPassword: '123' }).then(result => { expect(result).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: false } }); }); expect(schema.getCheckResult()).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: false } }); }); it('Should not verify the dependent field when field validation fails', async () => { const schema = SchemaModel({ a: StringType().isRequired().proxy(['b']), b: StringType().isRequired() }); await schema.checkForFieldAsync('a', { a: '' }).then(result => { expect(result).to.deep.equal({ hasError: true, errorMessage: 'a is a required field' }); }); expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: true, errorMessage: 'a is a required field' } }); }); it('Should verify the dependent field through proxy with nestedObject', async () => { const schema = SchemaModel({ a: StringType().isRequired().proxy(['b.c']), b: ObjectType().shape({ c: StringType().isRequired() }) }); await schema.checkForFieldAsync('a', { a: 'd' }, { nestedObject: true }).then(result => { expect(result).to.deep.equal({ hasError: false }); }); expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { object: { c: { hasError: true, errorMessage: 'b.c is a required field' } } } }); }); it('Should not verify the dependent field when field validation fails', async () => { const schema = SchemaModel({ a: StringType().isRequired().proxy(['b', 'd']), b: StringType().isRequired(), c: StringType().isRequired(), d: StringType().isRequired() }); await schema.checkForFieldAsync('a', { a: 'a' }).then(result => { expect(result).to.deep.equal({ hasError: false }); }); expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'b is a required field' }, d: { hasError: true, errorMessage: 'd is a required field' } }); }); it('Should verify the dependent field through proxy with checkIfValueExists', async () => { const schema = SchemaModel({ a: StringType().isRequired().proxy(['b'], { checkIfValueExists: true }), b: StringType().isRequired() }); await schema.checkForFieldAsync('a', { a: 'a' }).then(result => { expect(result).to.deep.equal({ hasError: false }); }); expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false } }); await schema.checkForFieldAsync('a', { a: 'a', b: 1 }).then(result => { expect(result).to.deep.equal({ hasError: false }); }); expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'b must be a string' } }); }); it('Should handle circular proxy dependencies', () => { const model = SchemaModel({ username: StringType().minLength(6, 'min 6').proxy(['password']), password: StringType().minLength(6, 'min 6').proxy(['username']) }); model.checkForField('username', { username: '123456', password: '123456' }); const checkResult = model.getCheckResult(); expect(checkResult.username.hasError).to.equal(false); expect(checkResult.password.hasError).to.equal(false); }); it('Should clean checked Field inside', () => { const model = SchemaModel({ username: StringType().minLength(6, 'min 6').proxy(['password']), password: StringType().minLength(6, 'min 6').proxy(['username']) }); model.checkForField('username', { username: '123456', password: '123456' }); expect(model.getCheckResult().password.hasError).to.equal(false); model.checkForField('username', { username: '123456', password: '12' }); expect(model.getCheckResult().password.hasError).to.equal(true); }); }); it('Should check the wrong verification object', () => { const schema = SchemaModel({ name: StringType() .isRequired('This field is required.') .addRule(() => ({ hasError: false, errorMessage: 'No Error' })) .addRule(() => ({ hasError: true, errorMessage: 'Error!!' })) }); const checkResult = schema.checkForField('name', { name: 'a' }); checkResult.hasError.should.equal(true); checkResult.errorMessage.should.equal('Error!!'); }); it('Should check the wrong verification object by Async', done => { const schema = SchemaModel({ name: StringType() .isRequired('This field is required.') .addRule(() => ({ hasError: false, errorMessage: 'No Error' })) .addRule(() => ({ hasError: true, errorMessage: 'Error!!' })) }); schema.checkForFieldAsync('name', { name: 'a' }).then(checkResult => { if (checkResult.hasError && checkResult.errorMessage === 'Error!!') { done(); } }); }); it('Should be able to check by `check` ', () => { const type = MixedType() .addRule(v => { if (typeof v === 'number') { return true; } return false; }, 'error1') .isRequired('error2'); type.check('').hasError.should.equal(true); type.check('').errorMessage.should.equal('error2'); type.check('1').hasError.should.equal(true); type.check('1').errorMessage.should.equal('error1'); type.check(1).hasError.should.equal(false); }); it('Should be able to check by `checkAsync` ', done => { const type = MixedType() .addRule(v => { return new Promise(resolve => { setTimeout(() => { if (typeof v === 'number') { resolve(true); } else { resolve(false); } }, 500); }); }, 'error1') .isRequired('error2'); Promise.all([type.checkAsync(''), type.checkAsync('1'), type.checkAsync(1)]).then(res => { if (res[0].hasError && res[1].hasError && !res[2].hasError) { done(); } }); }); it('should error when an async rule is executed by the sync validator', () => { const m = MixedType().addRule(async () => { return true; }, 'An async error'); let err; try { m.check({}); } catch (e) { err = e; } chai .expect(err?.message) .to.eql('synchronous validator had an async result, you should probably call "checkAsync()"'); }); it('Should be able to check by `checkAsync` with `addAsyncRule`', done => { const type = MixedType() .addAsyncRule(v => { return new Promise(resolve => { setTimeout(() => { if (typeof v === 'number') { resolve(true); } else { resolve(false); } }, 500); }); }, 'error1') .isRequired('error2'); Promise.all([type.checkAsync(''), type.checkAsync('1'), type.checkAsync(1)]).then(res => { if (res[0].hasError && res[1].hasError && !res[2].hasError) { done(); } }); }); it('Should be able to check by `check` with `addAsyncRule` and skip the async ', done => { let called = false; const type = MixedType() .addRule(v => { return typeof v === 'number'; }, 'This is not async') .addAsyncRule(async () => { called = true; return false; }, 'error1') .isRequired('error2'); setTimeout(() => { try { expect(called).to.eq(false); expect(type.check('').hasError).to.eq(true); expect(type.check('1').hasError).to.eq(true); expect(type.check(1).hasError).to.eq(false); done(); } catch (e) { done(e); } }, 100); }); describe('equalTo', () => { it('Should check if two fields are the same by equalTo', () => { const schema = SchemaModel({ a: StringType().isRequired(), b: StringType().equalTo('a').isRequired() }); expect(schema.check({ a: '123', b: '123' })).to.deep.equal({ a: { hasError: false }, b: { hasError: false } }); expect(schema.check({ a: '123', b: '456' })).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'b must be the same as a' } }); expect(schema.check({ a: '123', b: '' })).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'b is a required field' } }); }); it('Should check if two fields are the same with custom message', () => { const schema = SchemaModel({ a: StringType().isRequired(), b: StringType().equalTo('a', 'The two fields are not the same').isRequired() }); expect(schema.check({ a: '123', b: '456' })).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'The two fields are not the same' } }); }); it('Should check if two fields are the same when the field is an object', () => { const schema = SchemaModel({ a: ObjectType(), b: ObjectType().equalTo('a'), c: ArrayType(), d: ArrayType().equalTo('c') }); expect(schema.check({ a: { A: '1' }, b: { A: '2' } })).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'b must be the same as a' }, c: { hasError: false }, d: { hasError: false } }); expect(schema.check({ a: { A: '1' }, b: { A: '1' } })).to.deep.equal({ a: { hasError: false }, b: { hasError: false }, c: { hasError: false }, d: { hasError: false } }); expect(schema.check({ c: [1, 2, 3], d: [4, 5, 6] })).to.deep.equal({ a: { hasError: false }, b: { hasError: false }, c: { hasError: false }, d: { hasError: true, errorMessage: 'd must be the same as c' } }); expect(schema.check({ c: [1, 2, 3], d: [1, 2, 3] })).to.deep.equal({ a: { hasError: false }, b: { hasError: false }, c: { hasError: false }, d: { hasError: false } }); }); it('Should check if two fields are the same when the field is a nested object', () => { const schema = SchemaModel({ a: ObjectType().shape({ a1: StringType(), a2: StringType().equalTo('a1') }), c: StringType().equalTo('a.a2').isRequired() }); expect(schema.check({ a: { a1: '1', a2: '1' }, c: '1' })).to.deep.equal({ a: { hasError: false, object: { a1: { hasError: false }, a2: { hasError: false } } }, c: { hasError: false } }); expect(schema.check({ a: { a1: '1', a2: '2' }, c: '2' })).to.deep.equal({ a: { hasError: true, object: { a1: { hasError: false }, a2: { hasError: true, errorMessage: 'a2 must be the same as a1' } } }, c: { hasError: false } }); expect(schema.check({ a: { a1: '1', a2: '1' }, c: '2' })).to.deep.equal({ a: { hasError: false, object: { a1: { hasError: false }, a2: { hasError: false } } }, c: { hasError: true, errorMessage: 'c must be the same as a.a2' } }); }); }); describe('label', () => { it('Should use label to override the field name in the error message', () => { const schema = SchemaModel({ first_name: StringType().label('First Name').isRequired(), age: NumberType().label('Age').isRequired().range(18, 60) }); expect(schema.check({})).to.deep.equal({ first_name: { hasError: true, errorMessage: 'First Name is a required field' }, age: { hasError: true, errorMessage: 'Age is a required field' } }); expect(schema.checkForField('age', { first_name: 'a', age: 5 })).to.deep.equal({ hasError: true, errorMessage: 'Age field must be between 18 and 60' }); }); it('Should use label to override the field name in the error message when the field is an object', () => { const schema = SchemaModel({ user: ObjectType().shape({ first_name: StringType().label('First Name').isRequired(), age: NumberType().label('Age').isRequired().isRequired().range(18, 60) }) }); expect(schema.check({ user: {} })).to.deep.equal({ user: { hasError: true, object: { first_name: { hasError: true, errorMessage: 'First Name is a required field' }, age: { hasError: true, errorMessage: 'Age is a required field' } } } }); expect(schema.checkForField('user', { user: { first_name: 'a', age: 5 } })).to.deep.equal({ hasError: true, object: { first_name: { hasError: false }, age: { hasError: true, errorMessage: 'Age field must be between 18 and 60' } } }); }); it('Should check if two fields are the same by equalTo', () => { const schema = SchemaModel({ a: StringType().isRequired().label('A'), b: StringType().equalTo('a').isRequired().label('B') }); expect(schema.check({ a: '123', b: '456' })).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'B must be the same as A' } }); }); describe('label - async', () => { it('Should use label to override the field name in the error message', async () => { const schema = SchemaModel({ first_name: StringType().label('First Name').isRequired(), age: NumberType().label('Age').isRequired().range(18, 60) }); await schema.checkAsync({}).then(result => { expect(result).to.deep.equal({ first_name: { hasError: true, errorMessage: 'First Name is a required field' }, age: { hasError: true, errorMessage: 'Age is a required field' } }); }); await schema.checkForFieldAsync('age', { first_name: 'a', age: 5 }).then(result => { expect(result).to.deep.equal({ hasError: true, errorMessage: 'Age field must be between 18 and 60' }); }); }); it('Should use label to override the field name in the error message when the field is an object', async () => { const schema = SchemaModel({ user: ObjectType().shape({ first_name: StringType().label('First Name').isRequired(), age: NumberType().label('Age').isRequired().isRequired().range(18, 60) }) }); await schema.checkAsync({ user: {} }).then(result => { expect(result).to.deep.equal({ user: { hasError: true, object: { first_name: { hasError: true, errorMessage: 'First Name is a required field' }, age: { hasError: true, errorMessage: 'Age is a required field' } } } }); }); await schema .checkForFieldAsync('user', { user: { first_name: 'a', age: 5 } }) .then(result => { expect(result).to.deep.equal({ hasError: true, object: { first_name: { hasError: false }, age: { hasError: true, errorMessage: 'Age field must be between 18 and 60' } } }); }); }); it('Should check if two fields are the same by equalTo', async () => { const schema = SchemaModel({ a: StringType().isRequired().label('A'), b: StringType().equalTo('a').isRequired().label('B') }); await schema.checkAsync({ a: '123', b: '456' }).then(result => { expect(result).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'B must be the same as A' } }); }); }); }); }); describe('getFieldType', () => { it('Should return the field type directly', () => { const schema = { username: StringType().isRequired(), email: StringType().isEmail(), age: NumberType().range(18, 30) }; expect(getFieldType(schema, 'username')).to.equal(schema.username); expect(getFieldType(schema, 'email')).to.equal(schema.email); expect(getFieldType(schema, 'age')).to.equal(schema.age); expect(getFieldType(schema, 'nonexistent')).to.be.undefined; }); it('Should return the nested field type when nestedObject is true', () => { const schema = { user: ObjectType().shape({ name: StringType().isRequired(), profile: ObjectType().shape({ age: NumberType().range(18, 30) }) }) }; expect(getFieldType(schema, 'user.name', true)).to.equal(schema.user[schemaSpecKey].name); expect(getFieldType(schema, 'user.profile.age', true)).to.equal( schema.user[schemaSpecKey].profile[schemaSpecKey].age ); expect(getFieldType(schema, 'user.nonexistent', true)).to.be.undefined; }); it('Should return array element type when dealing with arrays', () => { const itemType = StringType().isRequired(); const schema = { tags: ArrayType().of(itemType) }; expect(getFieldType(schema, 'tags[0]', true)).to.equal(itemType); expect(getFieldType(schema, 'nonexistent[0]', true)).to.be.undefined; }); it('Should return nested field type within array objects', () => { const schema = { users: ArrayType().of( ObjectType().shape({ name: StringType().isRequired(), age: NumberType().range(18, 30) }) ) }; expect(getFieldType(schema, 'users[0].name', true)).to.equal( schema.users[arrayTypeSchemaSpec][schemaSpecKey].name ); expect(getFieldType(schema, 'users[0].age', true)).to.equal( schema.users[arrayTypeSchemaSpec][schemaSpecKey].age ); expect(getFieldType(schema, 'users[0].nonexistent', true)).to.be.undefined; }); it('Should handle complex nested structures', () => { const schema = { company: ObjectType().shape({ departments: ArrayType().of( ObjectType().shape({ name: StringType().isRequired(), employees: ArrayType().of( ObjectType().shape({ name: StringType().isRequired(), email: StringType().isEmail() }) ) }) ) }) }; expect(getFieldType(schema, 'company.departments[0].name', true)).to.equal( schema.company[schemaSpecKey].departments[arrayTypeSchemaSpec][schemaSpecKey].name ); expect(getFieldType(schema, 'company.departments[0].employees[0].email', true)).to.equal( schema.company[schemaSpecKey].departments[arrayTypeSchemaSpec][schemaSpecKey].employees[ arrayTypeSchemaSpec ][schemaSpecKey].email ); }); it('Should return explicit filed of ArrayType().of', () => { const schema = { users: ArrayType().of( StringType().isRequired(), ObjectType().shape({ name: StringType().isRequired(), email: StringType().isEmail() }) ) }; expect(getFieldType(schema, 'users[0]', true)).to.equal(schema.users[arrayTypeSchemaSpec][0]); expect(getFieldType(schema, 'users[1].name', true)).to.equal( schema.users[arrayTypeSchemaSpec][1][schemaSpecKey].name ); }); it('Should handle edge cases', () => { const schema = { a: StringType() }; expect(getFieldType(null, 'a')).to.be.undefined; expect(getFieldType(undefined, 'a')).to.be.undefined; expect(getFieldType({}, 'a')).to.be.undefined; expect(getFieldType({}, 'a.a[1]', true)).to.be.undefined; expect(getFieldType(schema, '')).to.be.undefined; expect(getFieldType(schema, '..')).to.be.undefined; expect(getFieldType(schema, 'a.b.c[0].d', true)).to.be.undefined; }); }); }); ================================================ FILE: test/NumberTypeSpec.js ================================================ /* eslint-disable @typescript-eslint/no-var-requires */ require('chai').should(); const schema = require('../src'); const { NumberType, Schema } = schema; describe('#NumberType', () => { let schemaData = { data: NumberType() }; let schema = new Schema(schemaData); it('Should be a valid number', () => { schema.checkForField('data', { data: '2.22' }).hasError.should.equal(false); schema.checkForField('data', { data: 2.22 }).hasError.should.equal(false); schema.checkForField('data', { data: 2 }).hasError.should.equal(false); schema.checkForField('data', { data: -222 }).hasError.should.equal(false); }); it('Should not be checked', () => { schema.checkForField('data', { data: null }).hasError.should.equal(false); schema.checkForField('data', { data: undefined }).hasError.should.equal(false); schema.checkForField('data', { data: '' }).hasError.should.equal(false); }); it('Should be a invalid number', () => { schema.checkForField('data', { data: 'abc' }).hasError.should.equal(true); schema.checkForField('data', { data: '1abc' }).hasError.should.equal(true); schema.checkForField('data', { data: {} }).hasError.should.equal(true); schema.checkForField('data', { data: [] }).hasError.should.equal(true); schema.checkForField('data', { data: [] }).errorMessage.should.equal('data must be a number'); }); it('True should be a invalid number', () => { schema.checkForField('data', { data: true }).hasError.should.equal(true); }); it('Function should be a invalid number', () => { schema.checkForField('data', { data: () => 0 }).hasError.should.equal(true); }); it('Null and Undefined should be a invalid number', () => { let schemaData = { data: NumberType().isRequired() }; let schema = new Schema(schemaData); schema.checkForField('data', { data: null }).hasError.should.equal(true); schema.checkForField('data', { data: undefined }).hasError.should.equal(true); }); it('Should be an integer', () => { let schemaData = { data: NumberType().isInteger() }; let schema = new Schema(schemaData); schema.checkForField('data', { data: 1 }).hasError.should.equal(false); schema.checkForField('data', { data: '1' }).hasError.should.equal(false); schema.checkForField('data', { data: -1 }).hasError.should.equal(false); schema.checkForField('data', { data: 1.1 }).hasError.should.equal(true); schema .checkForField('data', { data: 1.1 }) .errorMessage.should.equal('data must be an integer'); }); it('Should not be lower than the minimum', () => { let schemaData = { data: NumberType().min(10) }; let schema = new Schema(schemaData); schema.checkForField('data', { data: 10 }).hasError.should.equal(false); schema.checkForField('data', { data: 9 }).hasError.should.equal(true); schema .checkForField('data', { data: 9 }) .errorMessage.should.equal('data must be greater than or equal to 10'); }); it('Should not exceed the maximum', () => { let schemaData = { data: NumberType().max(10) }; let schema = new Schema(schemaData); schema.checkForField('data', { data: 10 }).hasError.should.equal(false); schema.checkForField('data', { data: 11 }).hasError.should.equal(true); schema .checkForField('data', { data: 11 }) .errorMessage.should.equal('data must be less than or equal to 10'); }); it('Should be within the range of optional values', () => { let schemaData = { data: NumberType().range(0, 20) }; let schema = new Schema(schemaData); schema.checkForField('data', { data: 0 }).hasError.should.equal(false); schema.checkForField('data', { data: 20 }).hasError.should.equal(false); schema.checkForField('data', { data: -1 }).hasError.should.equal(true); schema.checkForField('data', { data: 21 }).hasError.should.equal(true); schema .checkForField('data', { data: -1 }) .errorMessage.should.equal('data field must be between 0 and 20'); schema .checkForField('data', { data: 21 }) .errorMessage.should.equal('data field must be between 0 and 20'); }); it('Should be within the following value range: 1,2,3,4', () => { let schemaData = { data: NumberType().isOneOf([1, 2, 3, 4]) }; let schema = new Schema(schemaData); schema.checkForField('data', { data: 1 }).hasError.should.equal(false); schema.checkForField('data', { data: 2 }).hasError.should.equal(false); schema.checkForField('data', { data: 5 }).hasError.should.equal(true); schema .checkForField('data', { data: 5 }) .errorMessage.should.equal('data must be one of the following values: 1,2,3,4'); }); it('Should allow custom rules', () => { let schemaData = { data: NumberType().pattern(/^-?1\d+$/) }; let schema = new Schema(schemaData); schema.checkForField('data', { data: 11 }).hasError.should.equal(false); schema.checkForField('data', { data: 12 }).hasError.should.equal(false); schema.checkForField('data', { data: 22 }).hasError.should.equal(true); schema.checkForField('data', { data: 22 }).errorMessage.should.equal('data is invalid'); }); }); ================================================ FILE: test/ObjectTypeSpec.js ================================================ import { expect } from 'chai'; import { flaser } from 'object-flaser'; import * as schema from '../src'; const { ObjectType, StringType, NumberType, Schema } = schema; describe('#ObjectType', () => { it('Should be a valid object', () => { const schemaData = { url: StringType().isURL('Should be a url'), user: ObjectType().shape({ email: StringType().isEmail('Should be an email'), age: NumberType().min(18, 'Age should be greater than 18') }) }; const schema = new Schema(schemaData); const checkResult = schema.checkForField('user', { user: { email: 'simon.guo@hypers.com', age: 19 } }); expect(checkResult).to.deep.equal({ hasError: false, object: { email: { hasError: false }, age: { hasError: false } } }); const checkResult2 = schema.checkForField('user', { user: { email: 'simon.guo', age: 19 } }); expect(checkResult2).to.deep.equal({ hasError: true, object: { email: { hasError: true, errorMessage: 'Should be an email' }, age: { hasError: false } } }); const checkResult3 = schema.checkForField('user', { user: { email: 'simon.guo@hypers.com', age: 17 } }); expect(checkResult3).to.deep.equal({ hasError: true, object: { email: { hasError: false }, age: { hasError: true, errorMessage: 'Age should be greater than 18' } } }); }); it('Should be checked for object nesting.', () => { const schemaData = { url: StringType().isURL('Should be a url'), user: ObjectType().shape({ email: StringType().isEmail('Should be an email'), age: NumberType().min(18, 'Age should be greater than 18'), parent: ObjectType().shape({ email: StringType().isEmail('Should be an email'), age: NumberType().min(50, 'Age should be greater than 50') }) }) }; const schema = new Schema(schemaData); const checkResult = schema.checkForField('user', { user: { email: 'simon.guo@hypers.com', age: 17, parent: { email: 'zicheng', age: 40 } } }); expect(checkResult).to.deep.equal({ hasError: true, object: { email: { hasError: false }, age: { hasError: true, errorMessage: 'Age should be greater than 18' }, parent: { hasError: true, object: { email: { hasError: true, errorMessage: 'Should be an email' }, age: { hasError: true, errorMessage: 'Age should be greater than 50' } } } } }); const checkResult2 = schema.checkForField('user', { user: { email: 'simon.guo@hypers.com', age: 18, parent: { email: 'zicheng@dd.com', age: 50 } } }); expect(checkResult2).to.deep.equal({ hasError: false, object: { email: { hasError: false }, age: { hasError: false }, parent: { hasError: false, object: { email: { hasError: false }, age: { hasError: false } } } } }); }); it('Should be a valid object by flaser', () => { const schemaData = { 'data.email': StringType().isEmail('Should be an email'), 'data.age': NumberType().min(18, 'Should be greater than 18') }; const data = { data: { email: 'simon.guo@hypers.com', age: 17 } }; const schema = new Schema(schemaData); const checkResult = schema.check(flaser(data)); expect(checkResult).to.deep.equal({ 'data.email': { hasError: false }, 'data.age': { hasError: true, errorMessage: 'Should be greater than 18' } }); }); it('Should aync check for object nesting', async () => { const schema = new Schema({ url: StringType().isURL('Should be a url'), user: ObjectType().shape({ email: StringType().addRule(() => { return new Promise(resolve => { setTimeout(() => resolve(false), 400); }); }, 'Should be an email'), age: NumberType().min(18, 'Should be greater than 18') }) }); const result = await schema.checkAsync({ url: 'url', user: { email: 'a', age: '10' } }); expect(result).to.deep.equal({ url: { hasError: true, errorMessage: 'Should be a url' }, user: { hasError: true, object: { email: { hasError: true, errorMessage: 'Should be an email' }, age: { hasError: true, errorMessage: 'Should be greater than 18' } } } }); }); it('Should be checked for object nesting with nestedObject option.', () => { const schema = new Schema({ url: StringType().isURL('Should be a url'), user: ObjectType().shape({ email: StringType().isEmail('Should be an email'), age: NumberType().min(18, 'Age should be greater than 18'), parent: ObjectType().shape({ email: StringType().isEmail('Should be an email').isRequired('Email is required'), age: NumberType().min(50, 'Age should be greater than 50') }) }) }); const options = { nestedObject: true }; const checkResult = schema.checkForField( 'user.parent.age', { user: { parent: { age: 40 } } }, options ); expect(checkResult).to.deep.equal({ hasError: true, errorMessage: 'Age should be greater than 50' }); expect(schema.getCheckResult()).to.deep.equal({ user: { object: { parent: { object: { age: { hasError: true, errorMessage: 'Age should be greater than 50' } } } } } }); const checkResult2 = schema.checkForField( 'user.parent.age', { user: { parent: { age: 60 } } }, options ); expect(checkResult2).to.deep.equal({ hasError: false }); expect(schema.getCheckResult()).to.deep.equal({ user: { object: { parent: { object: { age: { hasError: false } } } } } }); const checkResult3 = schema.checkForField( 'user.parent.email', { user: { parent: { age: 60 } } }, options ); expect(checkResult3).to.deep.equal({ hasError: true, errorMessage: 'Email is required' }); expect(schema.getCheckResult()).to.deep.equal({ user: { object: { parent: { object: { age: { hasError: false }, email: { hasError: true, errorMessage: 'Email is required' } } } } } }); }); it('Should aync check for object nesting', async () => { const schema = new Schema({ url: StringType().isURL('Should be a url'), user: ObjectType().shape({ email: StringType().isEmail('Should be an email'), age: NumberType().min(18, 'Should be greater than 18'), parent: ObjectType().shape({ email: StringType().addRule(value => { return new Promise(resolve => { setTimeout(() => { if (/@/.test(value)) { resolve(true); } resolve(false); }, 400); }); }, 'Should be an email'), age: NumberType().min(50, 'Age should be greater than 50') }) }) }); const options = { nestedObject: true }; const result = await schema.checkForFieldAsync( 'user.parent.email', { user: { parent: { email: 'a' } } }, options ); expect(result).to.deep.equal({ hasError: true, errorMessage: 'Should be an email' }); const result2 = await schema.checkForFieldAsync( 'user.parent.email', { user: { parent: { email: 'a@a.com' } } }, options ); expect(result2).to.deep.equal({ hasError: false }); }); it('Should not allow empty object', () => { const schema = new Schema({ user: ObjectType().isRequired('User is required') }); const result = schema.check({ user: null }); expect(result).to.deep.equal({ user: { hasError: true, errorMessage: 'User is required' } }); const result2 = schema.check({ user: undefined }); expect(result2).to.deep.equal({ user: { hasError: true, errorMessage: 'User is required' } }); const result3 = schema.check({ user: false }); expect(result3).to.deep.equal({ user: { hasError: true, errorMessage: 'user must be an object' } }); }); it('Should not allow empty object by async', async () => { const schema = new Schema({ user: ObjectType().isRequired('User is required') }); const result = await schema.checkAsync({ user: null }); expect(result).to.deep.equal({ user: { hasError: true, errorMessage: 'User is required' } }); const result2 = await schema.checkAsync({ user: undefined }); expect(result2).to.deep.equal({ user: { hasError: true, errorMessage: 'User is required' } }); const result3 = await schema.checkAsync({ user: false }); expect(result3).to.deep.equal({ user: { hasError: true, errorMessage: 'user must be an object' } }); }); it('Should allow empty object', () => { const schema = new Schema({ user: ObjectType() }); const result = schema.check({ user: null }); expect(result).to.deep.equal({ user: { hasError: false } }); }); it('Should allow empty object by async', async () => { const schema = new Schema({ user: ObjectType() }); const result = await schema.checkAsync({ user: null }); expect(result).to.deep.equal({ user: { hasError: false } }); }); it('Should replace default required message', () => { const schema = new Schema({ user: ObjectType().shape({ email1: StringType().isEmail().isRequired(), email2: StringType().isEmail().isRequired('Email is required') }) }); const result = schema.check({ user: { email1: '', email2: '' } }); expect(result.user.object.email1.errorMessage).to.equal('email1 is a required field'); expect(result.user.object.email2.errorMessage).to.equal('Email is required'); }); it('Should replace default required message with async', async () => { const schema = new Schema({ user: ObjectType().shape({ email1: StringType().isEmail().isRequired(), email2: StringType().isEmail().isRequired('Email is required') }) }); const result = await schema.checkAsync({ user: { email1: '', email2: '' } }); expect(result.user.object.email1.errorMessage).to.equal('email1 is a required field'); expect(result.user.object.email2.errorMessage).to.equal('Email is required'); }); describe('priority', () => { it('Should have the correct priority', () => { const schema = new Schema({ user: ObjectType().shape({ name: StringType() .isEmail('error1') .addRule(() => false, 'error2') }) }); const result = schema.check({ user: { name: 'a' } }); expect(result.user.object).to.deep.equal({ name: { hasError: true, errorMessage: 'error1' } }); const schema2 = new Schema({ user: ObjectType().shape({ name: StringType() .isEmail('error1') .addRule(() => false, 'error2', true) }) }); const result2 = schema2.check({ user: { name: 'a' } }); expect(result2.user.object).to.deep.equal({ name: { hasError: true, errorMessage: 'error2' } }); }); it('Should have the correct priority with async', async () => { const schema = new Schema({ user: ObjectType().shape({ name: StringType() .isEmail('error1') .addRule(() => false, 'error2') }) }); const result = await schema.checkAsync({ user: { name: 'a' } }); expect(result.user.object).to.deep.equal({ name: { hasError: true, errorMessage: 'error1' } }); const schema2 = new Schema({ user: ObjectType().shape({ name: StringType() .isEmail('error1') .addRule(() => false, 'error2', true) }) }); const result2 = await schema2.checkAsync({ user: { name: 'a' } }); expect(result2.user.object).to.deep.equal({ name: { hasError: true, errorMessage: 'error2' } }); }); }); }); ================================================ FILE: test/SchemaSpec.js ================================================ import chai, { expect } from 'chai'; import * as schema from '../src'; const { StringType, NumberType, ObjectType, ArrayType, Schema, SchemaModel } = schema; chai.should(); describe('#Schema', () => { it('The schema should be saved as proporty', () => { const schemaData = { data: StringType() }; const schema = new Schema(schemaData); schema.$spec.should.equal(schemaData); }); it('Should be able to get the field value type for the given field name', () => { const schemaData = { data: NumberType() }; const schema = new Schema(schemaData); schema.getFieldType('data').should.equal(schemaData.data); }); it('Should return error information', () => { const schemaData = { data: NumberType() }; const schema = new Schema(schemaData); const checkResult = schema.checkForField('data', '2.22'); checkResult.should.have.property('hasError').be.a('boolean'); }); it('Should return error information', () => { const model = SchemaModel({ username: StringType().isRequired(), email: StringType().isEmail(), age: NumberType().range(18, 30) }); const checkResult = model.check({ username: 'foobar', email: 'foo@bar.com', age: 40 }); expect(checkResult).to.deep.equal({ username: { hasError: false }, email: { hasError: false }, age: { hasError: true, errorMessage: 'age field must be between 18 and 30' } }); }); it('Should get the schema spec by calling getSchemaSpec', () => { const model = SchemaModel({ username: StringType().isRequired(), email: StringType().isEmail() }); model.getSchemaSpec().should.deep.equal(model.$spec); }); describe('## getKeys', () => { it('Should return keys', () => { const model = SchemaModel({ username: StringType(), email: StringType(), age: NumberType() }); model.getKeys().length.should.equals(3); model.getKeys()[0].should.equals('username'); model.getKeys()[1].should.equals('email'); model.getKeys()[2].should.equals('age'); }); }); describe('## getErrorMessages', () => { it('Should return error messages', () => { const model = SchemaModel({ username: StringType().isRequired(), email: StringType().isEmail(), age: NumberType().range(18, 30) }); model.check({ username: 'foobar', email: ' ', age: 40 }); expect(model.getErrorMessages()).to.deep.equal([ 'email must be a valid email', 'age field must be between 18 and 30' ]); expect(model.getErrorMessages('age')).to.deep.equal(['age field must be between 18 and 30']); expect(model.getErrorMessages('username')).to.deep.equal([]); }); it('Should return error messages for array', () => { const model = SchemaModel({ a: ArrayType().of(StringType().isRequired()) }); model.check({ a: ['', 12] }); expect(model.getErrorMessages('a')).to.deep.equal([ 'a.[0] is a required field', 'a.[1] must be a string' ]); }); it('Should return error messages for nested object', () => { const model = SchemaModel({ a: StringType().isRequired(), b: StringType().isEmail(), c: NumberType().range(18, 30), d: ObjectType().shape({ e: StringType().isEmail().isRequired(), f: NumberType().range(50, 60) }) }); model.check({ a: 'foobar', b: 'a', c: 40, d: { e: ' ', f: 40 } }); expect(model.getErrorMessages()).to.deep.equal([ 'b must be a valid email', 'c field must be between 18 and 30' ]); expect(model.getErrorMessages('d')).to.deep.equal([ 'e is a required field', 'f field must be between 50 and 60' ]); expect(model.getErrorMessages('d.e')).to.deep.equal(['e is a required field']); }); it('Should return error messages for nested array', () => { const model = SchemaModel({ a: StringType().isRequired(), b: StringType().isEmail(), c: ArrayType() .of( ObjectType().shape({ d: StringType().isEmail().isRequired(), e: NumberType().range(50, 60) }) ) .isRequired() }); model.check({ a: 'foobar', b: 'a', c: [{}, { d: ' ', e: 40 }] }); expect(model.getErrorMessages()).to.deep.equal(['b must be a valid email']); expect(model.getErrorMessages('c.0.d')).to.deep.equal(['d is a required field']); }); it('Should return error messages', () => { const model = SchemaModel({ 'a.b': StringType().isRequired() }); model.check({ 'a.b': '' }); expect(model.getErrorMessages()).to.deep.equal(['a.b is a required field']); expect(model.getErrorMessages('a.b')).to.deep.equal(['a.b is a required field']); }); }); describe('## getCheckResult', () => { it('Should return check results', () => { const model = SchemaModel({ username: StringType().isRequired(), email: StringType().isEmail(), age: NumberType().range(18, 30) }); model.check({ username: 'foobar', email: ' ', age: 40 }); expect(model.getCheckResult()).to.deep.equal({ username: { hasError: false }, email: { hasError: true, errorMessage: 'email must be a valid email' }, age: { hasError: true, errorMessage: 'age field must be between 18 and 30' } }); expect(model.getCheckResult('age')).to.deep.equal({ hasError: true, errorMessage: 'age field must be between 18 and 30' }); expect(model.getCheckResult('username')).to.deep.equal({ hasError: false }); }); it('Should return check results for nested object', () => { const model = SchemaModel({ a: StringType().isRequired(), b: StringType().isEmail(), c: NumberType().range(18, 30), d: ObjectType().shape({ e: StringType().isEmail().isRequired(), f: NumberType().range(50, 60) }) }); model.check({ a: 'foobar', b: 'a', c: 40, d: { e: ' ', f: 40 } }); expect(model.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'b must be a valid email' }, c: { hasError: true, errorMessage: 'c field must be between 18 and 30' }, d: { hasError: true, object: { e: { hasError: true, errorMessage: 'e is a required field' }, f: { hasError: true, errorMessage: 'f field must be between 50 and 60' } } } }); expect(model.getCheckResult('d')).to.deep.equal({ hasError: true, object: { e: { hasError: true, errorMessage: 'e is a required field' }, f: { hasError: true, errorMessage: 'f field must be between 50 and 60' } } }); expect(model.getCheckResult('d.e')).to.deep.equal({ hasError: true, errorMessage: 'e is a required field' }); }); it('Should return check results for nested array', () => { const model = SchemaModel({ a: StringType().isRequired(), b: StringType().isEmail(), c: ArrayType() .of( ObjectType().shape({ d: StringType().isEmail().isRequired(), e: NumberType().range(50, 60) }) ) .isRequired() }); model.check({ a: 'foobar', b: 'a', c: [{}, { d: ' ', e: 40 }] }); expect(model.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { hasError: true, errorMessage: 'b must be a valid email' }, c: { hasError: true, array: [ { hasError: true, object: { d: { hasError: true, errorMessage: 'd is a required field' }, e: { hasError: false } } }, { hasError: true, object: { d: { hasError: true, errorMessage: 'd is a required field' }, e: { hasError: true, errorMessage: 'e field must be between 50 and 60' } } } ] } }); expect(model.getCheckResult('c.0.d')).to.deep.equal({ hasError: true, errorMessage: 'd is a required field' }); }); }); describe('## static combine', () => { it('Should return a combined model. ', () => { const model1 = SchemaModel({ username: StringType().isRequired(), email: StringType().isEmail() }); const checkResult = model1.check({ username: 'foobar', email: 'foo@bar.com', age: 40 }); expect(checkResult).to.deep.equal({ username: { hasError: false }, email: { hasError: false } }); const model2 = SchemaModel({ username: StringType().isRequired().minLength(7), age: NumberType().range(18, 30) }); const checkResult2 = SchemaModel.combine(model1, model2).check({ username: 'fooba', email: 'foo@bar.com', age: 40 }); expect(checkResult2).to.deep.equal({ username: { hasError: true, errorMessage: 'username must be at least 7 characters' }, email: { hasError: false }, age: { hasError: true, errorMessage: 'age field must be between 18 and 30' } }); }); }); }); ================================================ FILE: test/StringTypeSpec.js ================================================ /* eslint-disable @typescript-eslint/no-var-requires */ require('chai').should(); const schema = require('../src'); const { StringType, SchemaModel } = schema; describe('#StringType', () => { it('Should check min string length', () => { const schema = SchemaModel({ str: StringType().minLength(5), cjkStr: StringType().minLength(5, ''), emojiStr: StringType().minLength(5, '') }); schema.checkForField('str', { str: 'abcde' }).hasError.should.equal(false); schema.checkForField('str', { str: 'abcd' }).hasError.should.equal(true); schema.checkForField('cjkStr', { cjkStr: '鲤鱼跃龙门' }).hasError.should.equal(false); schema.checkForField('cjkStr', { cjkStr: '岁寒三友' }).hasError.should.equal(true); schema.checkForField('emojiStr', { emojiStr: '👌👍🐱🐶🐸' }).hasError.should.equal(false); schema.checkForField('emojiStr', { emojiStr: '👌👍🐱🐶' }).hasError.should.equal(true); schema .checkForField('str', { str: 'a' }) .errorMessage.should.equal('str must be at least 5 characters'); }); it('Should check max string length', () => { const schema = SchemaModel({ str: StringType().maxLength(4), cjkStr: StringType().maxLength(4, ''), emojiStr: StringType().maxLength(4, '') }); schema.checkForField('str', { str: 'abcde' }).hasError.should.equal(true); schema.checkForField('str', { str: 'abcd' }).hasError.should.equal(false); schema.checkForField('cjkStr', { cjkStr: '鲤鱼跃龙门' }).hasError.should.equal(true); schema.checkForField('cjkStr', { cjkStr: '岁寒三友' }).hasError.should.equal(false); schema.checkForField('emojiStr', { emojiStr: '👌👍🐱🐶🐸' }).hasError.should.equal(true); schema.checkForField('emojiStr', { emojiStr: '👌👍🐱🐶' }).hasError.should.equal(false); schema .checkForField('str', { str: 'abcde' }) .errorMessage.should.equal('str must be at most 4 characters'); }); it('Should be required', () => { const schema = SchemaModel({ str: StringType().isRequired('isrequired'), str1: StringType().isRequired(), str2: StringType().isRequired('isrequired', false) }); schema.checkForField('str', { str: '' }).hasError.should.equal(true); schema.checkForField('str', { str: ' abcde ' }).hasError.should.equal(false); schema.checkForField('str', { str: ' ' }).hasError.should.equal(true); schema .checkForField('str1', { str1: '' }) .errorMessage.should.equal('str1 is a required field'); schema.checkForField('str2', { str2: '' }).hasError.should.equal(true); schema.checkForField('str2', { str2: ' abcde ' }).hasError.should.equal(false); schema.checkForField('str2', { str2: ' ' }).hasError.should.equal(false); }); it('Should be able to customize the rules', () => { const schema = SchemaModel({ str: StringType() .maxLength(4, 'error1') .addRule(value => value !== '123', 'error2') }); schema.checkForField('str', { str: '12' }).hasError.should.equal(false); schema.checkForField('str', { str: '123' }).hasError.should.equal(true); schema.checkForField('str', { str: '123' }).errorMessage.should.equal('error2'); schema.checkForField('str', { str: 'abcde' }).hasError.should.equal(true); schema.checkForField('str', { str: 'abcde' }).errorMessage.should.equal('error1'); }); it('Should be one of value in array', () => { const schema = SchemaModel({ str: StringType().isOneOf(['A', 'B', 'C'], 'error'), str1: StringType().isOneOf(['A', 'B', 'C']) }); schema.checkForField('str', { str: 'A' }).hasError.should.equal(false); schema.checkForField('str', { str: 'D' }).hasError.should.equal(true); schema.checkForField('str', { str: 'D' }).errorMessage.should.equal('error'); schema .checkForField('str1', { str1: 'D' }) .errorMessage.should.equal('str1 must be one of the following values: A,B,C'); }); it('Should contain letters', () => { const schema = SchemaModel({ str: StringType().containsLetter() }); schema.checkForField('str', { str: '12A' }).hasError.should.equal(false); schema.checkForField('str', { str: 'a12' }).hasError.should.equal(false); schema.checkForField('str', { str: '12' }).hasError.should.equal(true); schema .checkForField('str', { str: '-' }) .errorMessage.should.equal('str field must contain letters'); }); it('Should only contain letters', () => { const schema = SchemaModel({ str: StringType().containsLetterOnly() }); schema.checkForField('str', { str: 'aA' }).hasError.should.equal(false); schema.checkForField('str', { str: '12A' }).hasError.should.equal(true); schema.checkForField('str', { str: 'a12' }).hasError.should.equal(true); schema.checkForField('str', { str: '12' }).hasError.should.equal(true); schema.checkForField('str', { str: '1a' }).errorMessage.should.equal('str must all be letters'); }); it('Should contain uppercase letters', () => { const schema = SchemaModel({ str: StringType().containsUppercaseLetter() }); schema.checkForField('str', { str: '12A' }).hasError.should.equal(false); schema.checkForField('str', { str: 'a12' }).hasError.should.equal(true); schema.checkForField('str', { str: '12' }).hasError.should.equal(true); schema .checkForField('str', { str: '-' }) .errorMessage.should.equal('str must be a upper case string'); }); it('Should contain lowercase letters', () => { const schema = SchemaModel({ str: StringType().containsLowercaseLetter() }); schema.checkForField('str', { str: '12A' }).hasError.should.equal(true); schema.checkForField('str', { str: 'a12' }).hasError.should.equal(false); schema.checkForField('str', { str: '12' }).hasError.should.equal(true); schema .checkForField('str', { str: '-' }) .errorMessage.should.equal('str must be a lowercase string'); }); it('Should contain numbers', () => { const schema = SchemaModel({ str: StringType().containsNumber() }); schema.checkForField('str', { str: '12' }).hasError.should.equal(false); schema.checkForField('str', { str: 'a12' }).hasError.should.equal(false); schema.checkForField('str', { str: '12A' }).hasError.should.equal(false); schema .checkForField('str', { str: 'a' }) .errorMessage.should.equal('str field must contain numbers'); }); it('Should be a url', () => { const schema = SchemaModel({ str: StringType().isURL(), email: StringType().isURL('', { allowMailto: true }) }); schema.checkForField('str', { str: 'https://www.abc.com' }).hasError.should.equal(false); schema.checkForField('str', { str: 'http://www.abc.com' }).hasError.should.equal(false); schema.checkForField('str', { str: 'ftp://www.abc.com' }).hasError.should.equal(false); schema.checkForField('str', { str: 'http://127.0.0.1/home' }).hasError.should.equal(false); schema.checkForField('str', { str: 'www.abc.com' }).hasError.should.equal(true); schema.checkForField('str', { str: 'a' }).errorMessage.should.equal('str must be a valid URL'); schema.checkForField('str', { str: 'mailto:user@example.com' }).hasError.should.be.true; schema.checkForField('email', { email: 'mailto:user@example.com' }).hasError.should.be.false; }); it('Should be a hexadecimal character', () => { const schema = SchemaModel({ str: StringType().isHex() }); schema.checkForField('str', { str: '#fff000' }).hasError.should.equal(false); schema.checkForField('str', { str: 'fff000' }).hasError.should.equal(false); schema.checkForField('str', { str: '#fff' }).hasError.should.equal(false); schema.checkForField('str', { str: 'fff' }).hasError.should.equal(false); schema.checkForField('str', { str: '#000' }).hasError.should.equal(false); schema.checkForField('str', { str: '#00' }).hasError.should.equal(true); schema .checkForField('str', { str: 'a' }) .errorMessage.should.equal('str must be a valid hexadecimal'); }); it('Should allow custom rules', () => { let schema = SchemaModel({ data: StringType().pattern(/^-?1\d+$/) }); schema.checkForField('data', { data: '11' }).hasError.should.equal(false); schema.checkForField('data', { data: '12' }).hasError.should.equal(false); schema.checkForField('data', { data: '22' }).hasError.should.equal(true); schema.checkForField('data', { data: '22' }).errorMessage.should.equal('data is invalid'); }); it('Should be within the range of the number of characters', () => { let schema = SchemaModel({ data: StringType().rangeLength(5, 10) }); schema.checkForField('data', { data: '12345' }).hasError.should.equal(false); schema.checkForField('data', { data: '1234' }).hasError.should.equal(true); schema.checkForField('data', { data: '12345678910' }).hasError.should.equal(true); schema .checkForField('data', { data: '1234' }) .errorMessage.should.equal('data must contain 5 to 10 characters'); }); it('Should not be less than the minimum number of characters', () => { let schema = SchemaModel({ data: StringType().minLength(5) }); schema.checkForField('data', { data: '12345' }).hasError.should.equal(false); schema.checkForField('data', { data: '1234' }).hasError.should.equal(true); schema .checkForField('data', { data: '1234' }) .errorMessage.should.equal('data must be at least 5 characters'); }); it('Should not exceed the maximum number of characters', () => { let schema = SchemaModel({ data: StringType().maxLength(5) }); schema.checkForField('data', { data: '12345' }).hasError.should.equal(false); schema.checkForField('data', { data: '123456' }).hasError.should.equal(true); schema .checkForField('data', { data: '123456' }) .errorMessage.should.equal('data must be at most 5 characters'); }); }); ================================================ FILE: test/utilsSpec.js ================================================ import chai from 'chai'; import { formatErrorMessage, checkRequired, shallowEqual, pathTransform } from '../src/utils'; chai.should(); describe('#utils', () => { describe('## formatErrorMessage', () => { it('Should output the parameter `email`', () => { const str = formatErrorMessage('${name} is a required field', { name: 'email' }); const str2 = formatErrorMessage('${name} is a required field', { name1: 'email' }); str.should.equal('email is a required field'); str2.should.equal('${name} is a required field'); }); it('Should output multiple parameters', () => { const str = formatErrorMessage('${name} must contain ${minLength} to ${maxLength} items', { name: 'tag', minLength: 3, maxLength: 10 }); const str2 = formatErrorMessage('${name} must contain ${minLength} to ${maxLength} items', { name: 'tag', minLength1: 3, maxLength: 10 }); str.should.equal('tag must contain 3 to 10 items'); str2.should.equal('tag must contain ${minLength} to 10 items'); }); it('Should not replace parameters', () => { const str = formatErrorMessage('name is a required field'); str.should.equal('name is a required field'); }); it('Should return unprocessed parameters', () => { const str = formatErrorMessage(true); str.should.equal(true); }); }); describe('## checkRequired', () => { it('Should check string, null and undefined', () => { checkRequired('1').should.equal(true); checkRequired(0).should.equal(true); checkRequired(' ').should.equal(true); checkRequired(' ', true).should.equal(false); checkRequired('').should.equal(false); checkRequired().should.equal(false); checkRequired(null).should.equal(false); checkRequired('', false, true).should.equal(true); checkRequired(undefined, false, true).should.equal(false); checkRequired(null, false, true).should.equal(false); }); it('Should check array', () => { checkRequired([]).should.equal(false); checkRequired([1]).should.equal(true); checkRequired([undefined]).should.equal(true); checkRequired(['']).should.equal(true); }); }); describe('## shallowEqual', () => { it('Should compare the object', () => { const obj1 = { a: 1, b: 2 }; const obj2 = { a: 1, b: 2 }; const obj3 = { a: 1, b: 3 }; const obj4 = { a: 1, b: 2, c: 3 }; shallowEqual(obj1, obj2).should.equal(true); shallowEqual(obj1, obj3).should.equal(false); shallowEqual(obj1, obj4).should.equal(false); }); it('Should compare the array', () => { const arr1 = [1, 2]; const arr2 = [1, 2]; const arr3 = [1, 3]; const arr4 = [1, 2, 3]; shallowEqual(arr1, arr2).should.equal(true); shallowEqual(arr1, arr3).should.equal(false); shallowEqual(arr1, arr4).should.equal(false); }); it('Should compare the object and array', () => { const obj = { a: 1, b: [1, 2] }; const obj1 = { a: 1, b: [1, 2] }; const obj2 = { a: 1, b: [1, 3] }; const obj3 = { a: 1, b: [1, 2, 3] }; shallowEqual(obj, obj1).should.equal(false); shallowEqual(obj, obj2).should.equal(false); shallowEqual(obj, obj3).should.equal(false); }); }); describe('## pathTransform', () => { it('Should transform the path', () => { pathTransform('a').should.equal('a'); pathTransform('a.b').should.equal('a.object.b'); pathTransform('a.0').should.equal('a.array.0'); pathTransform('a.0.1').should.equal('a.array.0.array.1'); pathTransform('a.b.c').should.equal('a.object.b.object.c'); pathTransform('a.0.b').should.equal('a.array.0.object.b'); }); }); }); ================================================ FILE: tsconfig-es.json ================================================ { "compilerOptions": { "declaration": true, "allowJs": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "noImplicitAny": false, "noUnusedParameters": true, "noUnusedLocals": true, "sourceMap": true, "moduleResolution": "node", "target": "ES2019" }, "include": ["./src/**/*.ts"] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "declaration": true, "allowJs": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "noImplicitAny": false, "noUnusedParameters": true, "noUnusedLocals": true, "sourceMap": true, "moduleResolution": "node", "lib": ["ES2019"], "module": "commonjs", "target": "ES2019" }, "include": ["./src/**/*.ts"] } ================================================ FILE: types/index.d.ts ================================================ ================================================ FILE: types/test.ts ================================================ import { BooleanType, NumberType, StringType, DateType, ArrayType, ObjectType, Schema, SchemaModel } from '../src'; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // PASSING SCENARIO 1: Should not fail if proper check types are used interface PassObj { s: string; } interface Pass { n?: number; b?: boolean; s?: string; d?: Date; a?: Array; o: PassObj; } const passSchema = new Schema({ n: NumberType(), b: BooleanType(), s: StringType(), d: DateType(), a: ArrayType(), o: ObjectType() }); passSchema.check({ a: ['a'], b: false, d: new Date(), n: 0, o: { s: '' }, s: '' }); passSchema.checkAsync({ a: ['a'], b: false, d: new Date(), n: 0, o: { s: '' }, s: '' }); passSchema.checkForField('o', { o: { s: '1' } }); passSchema.checkForFieldAsync('o', { o: { s: '1' } }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // PASSING SCENARIO 2: Should allows combine proper schemas SchemaModel.combine<{ x: string; y: string }>( new Schema<{ x: string }>({ x: StringType() }), new Schema<{ y: string }>({ y: StringType() }) ); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // PASSING SCENARIO 3: Should allows adding custom rules which have proper types on callback SchemaModel<{ password1: string; password2: string }>({ password1: StringType(), password2: StringType().addRule( (value, data) => value.toLowerCase() === data.password1.toLowerCase() ) }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // PASSING SCENARIO 4: Should allows to use custom error message type SchemaModel<{ a: string }, number>({ a: StringType<{ a: string }, number>().addRule(() => ({ hasError: true, errorMessage: 500 })) }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // PASSING SCENARIO 5: Should allows to use NumberType on string field SchemaModel<{ a: string }>({ a: NumberType<{ a: string }>() }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // PASSING SCENARIO 6: Should allows to use DataType on string field SchemaModel<{ a: string }>({ a: DateType<{ a: string }>() }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // PASSING SCENARIO 7: Should allow other types to be included in ArrayType ArrayType().of(NumberType()); ArrayType().of(ObjectType()); ArrayType().of(ArrayType()); ArrayType().of(BooleanType()); ArrayType().of(StringType()); ArrayType().of(DateType()); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FAIL SCENARIO 1: Should fail if type check is not matching declared type interface F1 { a: string; } new Schema({ // $ExpectError a: BooleanType() // TS2322: Type 'BooleanType' is not assignable to type 'StringType | DateType | NumberType'. // Type 'BooleanType' is missing the following properties from type 'NumberType': isInteger, pattern, isOneOf, range, and 2 more. }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FAIL SCENARIO 2: Should fail if checks declaration provides check for undeclared property interface F2 { a: string; } new Schema({ // $ExpectError b: NumberType() // TS2345: Argument of type '{ b: NumberType; }' is not assignable to parameter of type 'SchemaDeclaration'. // Object literal may only specify known properties, and 'b' does not exist in type 'SchemaDeclaration'. }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FAIL SCENARIO 3: Should fail if custom rule check will not fallow proper type for value interface F3 { a: string; } new Schema({ // $ExpectError a: StringType().addRule((v: number) => true) // TS2345: Argument of type '(v: number) => true' is not assignable to parameter of type '(value: string, data: any) => boolean | void | CheckResult | Promise | Promise | Promise>'. // Types of parameters 'v' and 'value' are incompatible. // Type 'string' is not assignable to type 'number'. }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FAIL SCENARIO 4: Should fail if custom rule check will not fallow proper type for data interface F4 { a: string; } new Schema({ // $ExpectError a: StringType().addRule((v: string, d: number) => true) // 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>'. // Types of parameters 'd' and 'data' are incompatible. // Type 'F4' is not assignable to type 'number'. }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FAIL SCENARIO 5: Should fail if check and checkAsync function is called with not matching type interface F5 { a: string; } const schemaF5 = new Schema({ a: StringType() }); // $ExpectError schemaF5.check({ c: 12 }); // TS2345: Argument of type '{ c: number; }' is not assignable to parameter of type 'F5'. // Object literal may only specify known properties, and 'c' does not exist in type 'F5'. // $ExpectError schemaF5.checkAsync({ c: 12 }); // TS2345: Argument of type '{ c: number; }' is not assignable to parameter of type 'F5'. // Object literal may only specify known properties, and 'c' does not exist in type 'F5'. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FAIL SCENARIO 6: Should fail if checkForField function is called with non existing property name interface F6 { a: string; } const schemaF6 = new Schema({ a: StringType() }); // $ExpectError schemaF6.checkForField('c', 'a'); // TS2345: Argument of type '"c"' is not assignable to parameter of type '"a"'. // $ExpectError schemaF6.checkForFieldAsync('c', 'a'); // TS2345: Argument of type '"c"' is not assignable to parameter of type '"a"'. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FAIL SCENARIO 7: Should fail if check and checkAsync function is called with not matching type, when type is inferred const schemaF7 = new Schema({ a: StringType() }); // $ExpectError schemaF7.check({ c: 12 }); // TS2345: Argument of type '{ c: number; }' is not assignable to parameter of type '{ a: unknown; }'. // Object literal may only specify known properties, and 'c' does not exist in type '{ a: unknown; }'. // $ExpectError schemaF7.checkAsync({ c: 12 }); // TS2345: Argument of type '{ c: number; }' is not assignable to parameter of type '{ a: unknown; }'. // Object literal may only specify known properties, and 'c' does not exist in type '{ a: unknown; }'. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FAIL SCENARIO 8: Should fail if checkForField function is called with non existing property name, when type is inferred const schemaF8 = new Schema({ a: StringType() }); // $ExpectError schemaF8.checkForField('c', { a: 'str' }); // TS2345: Argument of type '"c"' is not assignable to parameter of type '"a"'. // $ExpectError schemaF8.checkForFieldAsync('c', { a: 'str' }); // TS2345: Argument of type '"c"' is not assignable to parameter of type '"a"'. // $ExpectError schemaF8.checkForField('a', { c: 'str' }); // TS2345: Argument of type '{ c: string; }' is not assignable to parameter of type '{ a: unknown; }'. // $ExpectError schemaF8.checkForFieldAsync('a', { c: 'str' }); // TS2345: Argument of type '{ c: string; }' is not assignable to parameter of type '{ a: unknown; }'. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // FAIL SCENARIO 9: Should fail if ObjectType will get not matched shape interface F9 { a: string; } ObjectType().shape({ // $ExpectError a: BooleanType() // TS2322: Type 'BooleanType' is not assignable to type 'StringType | DateType | NumberType'. // Type 'BooleanType' is missing the following properties from type 'NumberType': isInteger, pattern, isOneOf, range, and 2 more. }); ObjectType().shape({ // $ExpectError b: NumberType() // TS2345: Argument of type '{ b: NumberType; }' is not assignable to parameter of type 'SchemaDeclaration'. // Object literal may only specify known properties, and 'b' does not exist in type 'SchemaDeclaration'. }); interface F10 { a: { b: number; }; } const schemaF10 = new Schema({ a: ObjectType().shape({ b: NumberType() }) }); schemaF10.check({ a: { b: 1 } }); // $ExpectError const checkResultF10 = schemaF10.check({ a: { b: '1' } }); checkResultF10.a.object?.b.errorMessage; checkResultF10.a.object?.b.hasError; ================================================ FILE: types/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "lib": ["es6", "dom", "es2017"], "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, "strictFunctionTypes": true, "downlevelIteration": true, "noEmit": true, "baseUrl": ".", "paths": { "schema-typed": ["."] } } } ================================================ FILE: types/tslint.json ================================================ { "extends": "dtslint/dtslint.json", // Or "dtslint/dt.json" if on DefinitelyTyped "rules": { "semicolon": false, "indent": [true, "tabs"] } }