From d9f0e555cf532731839584b0c036648001fe0503 Mon Sep 17 00:00:00 2001 From: Simon Guo Date: Thu, 11 Apr 2024 10:29:55 +0800 Subject: [PATCH] feat: add support for `equalTo` and `proxy` (#78) * feat: add support for `equalTo` and `proxy` * docs: update README.md * docs: update README.md * test: fix error tests * test: fix error tests * feat: add support for getErrorMessages and getCheckResult * feat: add support for getErrorMessages and getCheckResult * test: update tests * fix: use lodash --- README.md | 69 +- package-lock.json | 24 + package.json | 4 + src/MixedType.ts | 131 +++- src/ObjectType.ts | 56 +- src/Schema.ts | 150 +++- src/locales/default.ts | 3 +- src/types.ts | 4 +- src/utils/createValidator.ts | 7 +- src/utils/createValidatorAsync.ts | 10 +- src/utils/formatErrorMessage.ts | 6 +- src/utils/get.ts | 21 - src/utils/index.ts | 5 +- src/utils/pathTransform.ts | 19 + src/utils/shallowEqual.ts | 57 ++ test/ArrayTypeSpec.js | 118 ++- test/MixedTypeSpec.js | 1139 +++++++++++++++++++++-------- test/ObjectTypeSpec.js | 33 +- test/SchemaSpec.js | 300 +++++++- test/utilsSpec.js | 71 +- 20 files changed, 1684 insertions(+), 543 deletions(-) delete mode 100644 src/utils/get.ts create mode 100644 src/utils/pathTransform.ts create mode 100644 src/utils/shallowEqual.ts diff --git a/README.md b/README.md index e7e5d9a..b22e341 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ Schema for data modeling & validation - [`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) @@ -171,32 +173,36 @@ model.check({ field1: '', field2: '' }); **/ ``` -#### Multi-field cross validation +#### Field dependency validation -E.g: verify that the two passwords are the same. +1. Use the `equalTo` method to verify that the values of two fields are equal. ```js const model = SchemaModel({ - password1: StringType().isRequired('This field required'), - password2: StringType().addRule((value, data) => { - if (value !== data.password1) { - return false; - } - return true; - }, 'The passwords are inconsistent twice') + password: StringType().isRequired(), + confirmPassword: StringType().equalTo('password') }); +``` -model.check({ password1: '123456', password2: 'root' }); +2. Use the `addRule` method to create a custom validation rule. -/** -{ - password1: { hasError: false }, - password2: { - hasError: true, - errorMessage: 'The passwords are inconsistent twice' - } -} -**/ +```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 @@ -545,6 +551,31 @@ SchemaModel({ }); ``` +#### `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). diff --git a/package-lock.json b/package-lock.json index 9d8764c..a73fd17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "schema-typed", "version": "2.1.3", "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2" + }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/node": "^20.12.5", @@ -4236,12 +4240,22 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==" + }, "node_modules/log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", @@ -10177,12 +10191,22 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==" + }, "log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", diff --git a/package.json b/package.json index b191c2b..84f68b0 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,10 @@ "types" ], "homepage": "https://github.com/rsuite/schema-typed#readme", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2" + }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/node": "^20.12.5", diff --git a/src/MixedType.ts b/src/MixedType.ts index 9d146cf..d91cae5 100644 --- a/src/MixedType.ts +++ b/src/MixedType.ts @@ -5,19 +5,48 @@ import { AsyncValidCallbackType, RuleType, ErrorMessageType, - TypeName + TypeName, + PlainObject } from './types'; import { checkRequired, createValidator, createValidatorAsync, isEmpty, - formatErrorMessage + 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'; + +/** + * Get the field type from the schema object + */ +export function getFieldType(schemaSpec: any, fieldName: string, nestedObject?: boolean) { + if (nestedObject) { + const namePath = fieldName.split('.').join(`.${schemaSpecKey}.`); + return get(schemaSpec, namePath); + } + 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; + readonly $typeName?: string; protected required = false; protected requiredMessage: E | string = ''; protected trim = false; @@ -26,31 +55,39 @@ export class MixedType[] = []; protected fieldLabel?: string; - schemaSpec: SchemaDeclaration; + $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.$typeName = name; this.locale = Object.assign(name ? locales[name] : {}, locales.mixed) as L & MixedTypeLocale; } setSchemaOptions(schemaSpec: SchemaDeclaration, value: any) { - this.schemaSpec = schemaSpec; + this.$schemaSpec = schemaSpec; this.value = value; } - check(value: ValueType = this.value, data?: DataType, fieldName?: string | string[]) { + 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 || fieldName + name: this.fieldLabel || joinName(fieldName) }) }; } - const validator = createValidator(data, fieldName); + const validator = createValidator( + data, + fieldName, + this.fieldLabel + ); const checkStatus = validator(value, this.priorityRules); @@ -66,7 +103,7 @@ export class MixedType> { @@ -74,12 +111,16 @@ export class MixedType(data, fieldName); + const validator = createValidatorAsync( + data, + fieldName, + this.fieldLabel + ); return new Promise(resolve => validator(value, this.priorityRules) @@ -119,7 +160,7 @@ export class MixedType, - errorMessage?: E | string, + errorMessage?: E | string | (() => E | string), priority?: boolean ) { this.pushRule({ onValid, errorMessage, priority }); @@ -149,16 +190,23 @@ export class MixedType { - * return schema.field1.check() ? NumberType().min(5) : NumberType().min(0); + * + * ```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); + return condition(this.$schemaSpec).check(value, data, fieldName); }, undefined, true @@ -166,8 +214,57 @@ export class MixedType { + 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; diff --git a/src/ObjectType.ts b/src/ObjectType.ts index eb033b2..69bf17d 100644 --- a/src/ObjectType.ts +++ b/src/ObjectType.ts @@ -1,5 +1,11 @@ -import { MixedType } from './MixedType'; -import { createValidator, createValidatorAsync, checkRequired, isEmpty } from './utils'; +import { MixedType, schemaSpecKey } from './MixedType'; +import { + createValidator, + createValidatorAsync, + checkRequired, + isEmpty, + formatErrorMessage +} from './utils'; import { PlainObject, SchemaDeclaration, CheckResult, ErrorMessageType } from './types'; import { ObjectTypeLocale } from './locales'; @@ -9,7 +15,7 @@ export class ObjectType extends MixedType< E, ObjectTypeLocale > { - objectTypeSchemaSpec: SchemaDeclaration; + [schemaSpecKey]: SchemaDeclaration; constructor(errorMessage?: E | string) { super('object'); super.pushRule({ @@ -19,16 +25,21 @@ export class ObjectType extends MixedType< } check(value: PlainObject = this.value, data?: DataType, fieldName?: string | string[]) { - const check = (value: any, data: any, type: any) => { + const check = (value: any, data: any, type: any, childFieldKey?: string) => { if (type.required && !checkRequired(value, type.trim, type.emptyAllowed)) { - return { hasError: true, errorMessage: type.requiredMessage }; + return { + hasError: true, + errorMessage: formatErrorMessage(this.requiredMessage || this.locale.isRequired, { + name: type.fieldLabel || childFieldKey || fieldName + }) + }; } - if (type.objectTypeSchemaSpec && typeof value === 'object') { + if (type[schemaSpecKey] && typeof value === 'object') { const checkResultObject: any = {}; let hasError = false; - Object.entries(type.objectTypeSchemaSpec).forEach(([k, v]) => { - const checkResult = check(value[k], value, v); + Object.entries(type[schemaSpecKey]).forEach(([k, v]) => { + const checkResult = check(value[k], value, v, k); if (checkResult?.hasError) { hasError = true; } @@ -38,7 +49,11 @@ export class ObjectType extends MixedType< return { hasError, object: checkResultObject }; } - const validator = createValidator(data, fieldName); + const validator = createValidator( + data, + childFieldKey || fieldName, + type.fieldLabel + ); const checkStatus = validator(value, type.priorityRules); if (checkStatus) { @@ -56,20 +71,29 @@ export class ObjectType extends MixedType< } checkAsync(value: PlainObject = this.value, data?: DataType, fieldName?: string | string[]) { - const check = (value: any, data: any, type: any) => { + 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: this.requiredMessage }); + return Promise.resolve({ + hasError: true, + errorMessage: formatErrorMessage(this.requiredMessage || this.locale.isRequired, { + name: type.fieldLabel || childFieldKey || fieldName + }) + }); } - const validator = createValidatorAsync(data, fieldName); + const validator = createValidatorAsync( + data, + childFieldKey || fieldName, + type.fieldLabel + ); return new Promise(resolve => { - if (type.objectTypeSchemaSpec && typeof value === 'object') { + if (type[schemaSpecKey] && typeof value === 'object') { const checkResult: any = {}; const checkAll: Promise[] = []; const keys: string[] = []; - Object.entries(type.objectTypeSchemaSpec).forEach(([k, v]) => { - checkAll.push(check(value[k], value, v)); + Object.entries(type[schemaSpecKey]).forEach(([k, v]) => { + checkAll.push(check(value[k], value, v, k)); keys.push(k); }); @@ -118,7 +142,7 @@ export class ObjectType extends MixedType< * }) */ shape(fields: SchemaDeclaration) { - this.objectTypeSchemaSpec = fields; + this[schemaSpecKey] = fields; return this; } } diff --git a/src/Schema.ts b/src/Schema.ts index 86d21eb..ae8555c 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -1,6 +1,6 @@ import { SchemaDeclaration, SchemaCheckResult, CheckResult, PlainObject } from './types'; -import { MixedType } from './MixedType'; -import get from './utils/get'; +import { MixedType, getFieldType, getFieldValue } from './MixedType'; +import { set, get, isEmpty, pathTransform } from './utils'; interface CheckOptions { /** @@ -9,47 +9,92 @@ interface CheckOptions { nestedObject?: boolean; } -/** - * Get the field value from the data object - */ -function getFieldValue(data: PlainObject, fieldName: string, nestedObject?: boolean) { - return nestedObject ? get(data, fieldName) : data?.[fieldName]; -} - export class Schema { - readonly spec: SchemaDeclaration; + readonly $spec: SchemaDeclaration; private data: PlainObject; + private checkResult: SchemaCheckResult = {}; constructor(schema: SchemaDeclaration) { - this.spec = schema; + this.$spec = schema; + } + + private getFieldType( + fieldName: T, + nestedObject?: boolean + ): SchemaDeclaration[T] { + return getFieldType(this.$spec, fieldName as string, nestedObject); } - getFieldType(fieldName: T, nestedObject?: boolean) { + private setFieldCheckResult( + fieldName: string, + checkResult: CheckResult, + nestedObject?: boolean + ) { if (nestedObject) { - const namePath = (fieldName as string).split('.').join('.objectTypeSchemaSpec.'); + const namePath = fieldName.split('.').join('.object.'); + set(this.checkResult, namePath, checkResult); - return get(this.spec, namePath); + return; } - return this.spec?.[fieldName]; - } - - getKeys() { - return Object.keys(this.spec); + this.checkResult[fieldName as string] = checkResult; } - setSchemaOptionsForAllType(data: PlainObject) { + 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]); + 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); + } + checkForField( fieldName: T, data: DataType, @@ -66,8 +111,26 @@ export class Schema { } 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; + + // Check other fields if the field depends on them for validation + fieldChecker.otherFields?.forEach((field: string) => { + if (checkIfValueExists) { + if (!isEmpty(getFieldValue(data, field, nestedObject))) { + this.checkForField(field as T, data, options); + } + return; + } + this.checkForField(field as T, data, options); + }); + } - return fieldChecker.check(fieldValue, data, fieldName as string); + return checkResult; } checkForFieldAsync( @@ -86,27 +149,51 @@ export class Schema { } 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)); + }); - return fieldChecker.checkAsync(fieldValue, data, fieldName as string); + await Promise.all(checkAll); + } + + return result; + }); } check(data: DataType) { - const checkResult: PlainObject = {}; - Object.keys(this.spec).forEach(key => { + const checkResult: SchemaCheckResult = {}; + Object.keys(this.$spec).forEach(key => { if (typeof data === 'object') { checkResult[key] = this.checkForField(key as T, data); } }); - return checkResult as SchemaCheckResult; + return checkResult; } checkAsync(data: DataType) { - const checkResult: PlainObject = {}; + const checkResult: SchemaCheckResult = {}; const promises: Promise>[] = []; const keys: string[] = []; - Object.keys(this.spec).forEach((key: string) => { + Object.keys(this.$spec).forEach((key: string) => { keys.push(key); promises.push(this.checkForFieldAsync(key as T, data)); }); @@ -115,7 +202,8 @@ export class Schema { for (let i = 0; i < values.length; i += 1) { checkResult[keys[i]] = values[i]; } - return checkResult as SchemaCheckResult; + + return checkResult; }); } } @@ -131,7 +219,7 @@ SchemaModel.combine = function combine( ) { return new Schema( specs - .map(model => model.spec) + .map(model => model.$spec) .reduce((accumulator, currentValue) => Object.assign(accumulator, currentValue), {} as any) ); }; diff --git a/src/locales/default.ts b/src/locales/default.ts index b88e84b..0b8df82 100644 --- a/src/locales/default.ts +++ b/src/locales/default.ts @@ -1,7 +1,8 @@ export default { mixed: { isRequired: '${name} is a required field', - isRequiredOrEmpty: '${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', diff --git a/src/types.ts b/src/types.ts index 5a8a735..eb6f559 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,7 +34,7 @@ export type PlainObject = any> = { export interface RuleType { onValid: AsyncValidCallbackType; - errorMessage?: E; + errorMessage?: any; priority?: boolean; params?: any; isAsync?: boolean; @@ -65,5 +65,5 @@ export type SchemaDeclaration = { }; export type SchemaCheckResult = { - [P in keyof T]: CheckResult; + [P in keyof T]?: CheckResult; }; diff --git a/src/utils/createValidator.ts b/src/utils/createValidator.ts index 3bf9b58..6686084 100644 --- a/src/utils/createValidator.ts +++ b/src/utils/createValidator.ts @@ -10,19 +10,20 @@ function isPromiseLike(v: unknown): v is Promise { * Create a data validator * @param data */ -export function createValidator(data?: D, name?: string | string[]) { +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(errorMessage, { + errorMessage: formatErrorMessage(errorMsg, { ...params, - name: Array.isArray(name) ? name.join('.') : name + name: label || (Array.isArray(name) ? name.join('.') : name) }) }; } else if (isPromiseLike(checkResult)) { diff --git a/src/utils/createValidatorAsync.ts b/src/utils/createValidatorAsync.ts index 4b38059..382ab20 100644 --- a/src/utils/createValidatorAsync.ts +++ b/src/utils/createValidatorAsync.ts @@ -1,11 +1,11 @@ import { CheckResult, RuleType } from '../types'; -import formatErrorMessage from './formatErrorMessage'; +import formatErrorMessage, { joinName } from './formatErrorMessage'; /** * Create a data asynchronous validator * @param data */ -export function createValidatorAsync(data?: D, name?: string | string[]) { +export function createValidatorAsync(data?: D, name?: string | string[], label?: string) { function check(errorMessage?: E | string) { return (checkResult: CheckResult | boolean): CheckResult | null => { if (checkResult === false) { @@ -20,11 +20,13 @@ export function createValidatorAsync(data?: D, name?: string | string[] 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(errorMessage, { + formatErrorMessage(errorMsg, { ...params, - name: Array.isArray(name) ? name.join('.') : name + name: label || joinName(name) }) ) ); diff --git a/src/utils/formatErrorMessage.ts b/src/utils/formatErrorMessage.ts index f6a7b97..98ca616 100644 --- a/src/utils/formatErrorMessage.ts +++ b/src/utils/formatErrorMessage.ts @@ -1,5 +1,9 @@ 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' @@ -7,7 +11,7 @@ import isEmpty from './isEmpty'; 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 isEmpty(params?.[key]) ? `$\{${key}\}` : params?.[key]; }); } diff --git a/src/utils/get.ts b/src/utils/get.ts deleted file mode 100644 index 1014fca..0000000 --- a/src/utils/get.ts +++ /dev/null @@ -1,21 +0,0 @@ -type Key = string | number | symbol; -type Path = Array | string; - -export default function get(object: any, path: Path, defaultValue?: any): any { - if (!object) { - return defaultValue; - } - - const keys = Array.isArray(path) ? path : path.split('.'); - let result = object; - - for (const key of keys) { - if (result && typeof result === 'object') { - result = result[key]; - } else { - return defaultValue; - } - } - - return result !== undefined ? result : defaultValue; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 0247ee4..45b14eb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,10 @@ +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 get } from './get'; +export { default as shallowEqual } from './shallowEqual'; +export { default as pathTransform } from './pathTransform'; diff --git a/src/utils/pathTransform.ts b/src/utils/pathTransform.ts new file mode 100644 index 0000000..2e3e37f --- /dev/null +++ b/src/utils/pathTransform.ts @@ -0,0 +1,19 @@ +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('.'); +} diff --git a/src/utils/shallowEqual.ts b/src/utils/shallowEqual.ts new file mode 100644 index 0000000..ac6b182 --- /dev/null +++ b/src/utils/shallowEqual.ts @@ -0,0 +1,57 @@ +/** + * 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; diff --git a/test/ArrayTypeSpec.js b/test/ArrayTypeSpec.js index 5d75b8f..fc517c1 100644 --- a/test/ArrayTypeSpec.js +++ b/test/ArrayTypeSpec.js @@ -1,8 +1,6 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +import { expect } from 'chai'; +import * as schema from '../src'; -require('chai').should(); - -const schema = require('../src'); const { ArrayType, StringType, NumberType, ObjectType, Schema } = schema; describe('#ArrayType', () => { @@ -14,34 +12,55 @@ describe('#ArrayType', () => { const schema = new Schema(schemaData); - const check1 = schema.checkForField('data', { + const checkResult = schema.checkForField('data', { data: ['simon.guo@hypers.com', 'ddddd@d.com', 'ddd@bbb.com'] }); - check1.hasError.should.equal(false); - check1.array[0].hasError.should.equal(false); - check1.array[1].hasError.should.equal(false); - check1.array[2].hasError.should.equal(false); + expect(checkResult).to.deep.equal({ + hasError: false, + array: [{ hasError: false }, { hasError: false }, { hasError: false }] + }); - const check2 = schema.check({ + const checkResult2 = schema.check({ data: ['simon.guo@hypers.com', 'error_email', 'ddd@bbb.com'] }); - check2.data.hasError.should.equal(true); - check2.data.array[1].hasError.should.equal(true); - check2.data.array[1].errorMessage.should.equal('error2'); + expect(checkResult2).to.deep.equal({ + data: { + hasError: true, + array: [ + { hasError: false }, + { hasError: true, errorMessage: 'error2' }, + { hasError: false } + ] + }, + data2: { hasError: false } + }); - const check3 = schema.check({ + const checkResult3 = schema.check({ data2: [] }); - check3.data2.errorMessage.should.equal('data2 field must have at least 2 items'); + expect(checkResult3).to.deep.equal({ + data: { hasError: false }, + data2: { hasError: true, errorMessage: 'data2 field must have at least 2 items' } + }); - const check4 = schema.check({ + const checkResult4 = schema.check({ data2: ['simon.guo@hypers.com', 'error_email', 'ddd@bbb.com'] }); - check4.data2.array[1].errorMessage.should.equal('data2.[1] must be a valid email'); + 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 ', () => { @@ -71,7 +90,7 @@ describe('#ArrayType', () => { ) }; const schema = new Schema(schemaData); - const checkStatus = schema.check({ + const checkResult = schema.check({ users: [ 'simon.guo@hypers.com', { email: 'error_email', age: 19 }, @@ -79,20 +98,29 @@ describe('#ArrayType', () => { ] }); - checkStatus.users.hasError.should.equal(true); - checkStatus.users.array[0].hasError.should.equal(true); - checkStatus.users.array[0].errorMessage.should.equal('error1'); - checkStatus.users.array[1].object.email.hasError.should.equal(true); - checkStatus.users.array[1].object.email.errorMessage.should.equal('error2'); - checkStatus.users.array[1].object.age.hasError.should.equal(false); - - checkStatus.users.array[2].object.email.hasError.should.equal(true); - checkStatus.users.array[2].object.email.errorMessage.should.equal('error2'); - checkStatus.users.array[2].object.age.hasError.should.equal(true); - checkStatus.users.array[2].object.age.errorMessage.should.equal('error3'); + 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 checkStatus2 = schema2.check({ + const checkResult2 = schema2.check({ users2: [ 'simon.guo@hypers.com', { email: 'error_email', age: 19 }, @@ -100,13 +128,29 @@ describe('#ArrayType', () => { ] }); - checkStatus2.users2.array[0].errorMessage.should.equal('users2.[0] must be an object'); - checkStatus2.users2.array[1].object.email.errorMessage.should.equal( - 'users2.[1] must be a valid email' - ); - checkStatus2.users2.array[2].object.age.errorMessage.should.equal( - 'users2.[2] must be greater than or equal to 18' - ); + 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 be unrepeatable ', () => { diff --git a/test/MixedTypeSpec.js b/test/MixedTypeSpec.js index 78af3f5..83beaa2 100644 --- a/test/MixedTypeSpec.js +++ b/test/MixedTypeSpec.js @@ -3,272 +3,647 @@ import * as schema from '../src'; chai.should(); -const { StringType, SchemaModel, NumberType, ArrayType, MixedType } = schema; +const { StringType, SchemaModel, NumberType, ArrayType, MixedType, ObjectType } = schema; describe('#MixedType', () => { - it('Should be the same password twice', () => { - const schema = SchemaModel({ - password1: StringType().isRequired('Password is required'), - password2: StringType() - .addRule((value, data) => value === data.password1, 'The two passwords do not match') - .isRequired('Password is required') + 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' } + }); }); - schema - .check({ password1: '123456', password2: '123456' }) - .password2.hasError.should.equal(false); - schema - .check({ password1: '123456', password2: 'abcdedf' }) - .password2.hasError.should.equal(true); - - schema.check({ password1: '123456', password2: '' }).password2.hasError.should.equal(true); + 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' } + }); + }); }); - it('Should be the same password twice and the password cannot be `root`', () => { - const schema = SchemaModel({ - password1: StringType().isRequired('Password is required'), - password2: StringType() - .addRule(value => value !== 'root', 'Password cannot be root') - .addRule((value, data) => value === data.password1, 'The two passwords do not match') - .isRequired('Password is required') - }); - - schema.check({ password1: 'root', password2: 'root' }).password2.hasError.should.equal(true); - schema - .check({ password1: 'root', password2: 'root' }) - .password2.errorMessage.should.equal('Password cannot be root'); - - schema - .check({ password1: '123456', password2: '' }) - .password2.errorMessage.should.equal('Password is required'); - schema - .check({ password1: '123456', password2: '123' }) - .password2.errorMessage.should.equal('The two passwords do not match'); - }); + describe('priority', () => { + it('Should have the correct priority', () => { + const schema = SchemaModel({ + name: StringType() + .isEmail('error1') + .addRule(() => false, 'error2') + }); - 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'); - 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) + }); - 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'); - 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) + }); - const schema3 = SchemaModel({ - name: StringType().addRule(() => true, 'error2', true) + schema3.check({ name: 'a' }).name.hasError.should.equal(false); }); - 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'); + }); }); - it('Should be isRequired with a higher priority than addRule', () => { - const schema = SchemaModel({ - str: StringType() - .isRequired('required') - .addRule(value => value === '', '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); }); - schema.checkForField('str', { str: '' }).hasError.should.equal(true); - schema.checkForField('str', { str: '' }).errorMessage.should.equal('required'); + 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); + }); - schema.checkForField('str', { str: '12' }).hasError.should.equal(true); - schema.checkForField('str', { str: '12' }).errorMessage.should.equal('error'); + 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); + }); - const schema2 = SchemaModel({ - str: StringType().addRule(value => value === '', 'error') + 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'); }); - schema2.checkForField('str', { str: '12' }).hasError.should.equal(true); - schema2.checkForField('str', { str: '12' }).errorMessage.should.equal('error'); - }); + 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 isRequired', () => { - const schema = SchemaModel({ - str: StringType().isRequired('required') + 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); }); - let obj = { - str: undefined - }; - let result = schema.check(obj); - result.str.hasError.should.equal(true); }); - it('Should be error for empty string with isRequired', () => { - const schema = SchemaModel({ - str: StringType().isRequired('required') + 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(); + } + }); }); - let obj = { - str: '' - }; - let result = schema.check(obj); - result.str.hasError.should.equal(true); - }); - it('Should be error for empty array with isRequired', () => { - const schema = SchemaModel({ - arr: ArrayType().isRequired('required') + 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(); + } + }); }); - 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 call async checkForFieldAsync and verify pass', done => { + const schema = SchemaModel({ + name: StringType().addRule(() => { + return new Promise(resolve => { + setTimeout(() => { + resolve(false); + }, 500); + }); + }, 'error1') + }); - it('Should be without error for empty array with isRequiredOrEmpty', () => { - const schema = SchemaModel({ - arr: ArrayType().isRequiredOrEmpty('required') + schema.checkForFieldAsync('name', { name: 'a' }).then(status => { + if (status.hasError && status.errorMessage === 'error1') { + done(); + } + }); }); - 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') + 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(); + } + }); }); - let obj = { - str: undefined - }; - let result = schema.check(obj); - result.str.hasError.should.equal(true); - }); - 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') + 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(); + } + }); }); - 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 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(); + } + }); }); }); - it('Should call async check', done => { - const schema = SchemaModel({ - email: StringType('error1').isEmail('error2') + 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' }); }); - schema.checkAsync({ name: 'a', email: 'a' }).then(status => { - if (status.email.hasError && status.email.errorMessage === 'error2') { - done(); - } + 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' } + }); }); }); - it('Should call async checkForFieldAsync and verify pass', done => { - const schema = SchemaModel({ - name: StringType().addRule(() => { - return new Promise(resolve => { - setTimeout(() => { - resolve(false); - }, 500); - }); - }, 'error1') + 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 } + }); }); - schema.checkForFieldAsync('name', { name: 'a' }).then(status => { - if (status.hasError && status.errorMessage === 'error1') { - done(); - } + 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 call async checkForFieldAsync and the validation fails', done => { - const schema = SchemaModel({ - email: StringType('error1').isEmail('error2') + 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' } } } + }); }); - schema.checkForFieldAsync('email', { email: 'a' }).then(status => { - if (status.hasError && status.errorMessage === 'error2') { - done(); - } + 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' + } + }); }); }); - it('Should call async checkForFieldAsync and the validation fails', done => { - const schema = SchemaModel({ - name: StringType().addRule(() => { - return new Promise(resolve => { - setTimeout(() => { - resolve(true); - }, 200); + 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; }); - }, 'error1') + + 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 } + }); }); - schema.checkForFieldAsync('name', { name: 'a' }).then(status => { - if (status.hasError === false) { - done(); - } + 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 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') + 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' } } } + }); }); - schema.checkForFieldAsync('name', { name: 'a' }).then(status => { - if (status.hasError && status.errorMessage === 'error1') { - done(); - } + 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' + } + }); }); }); @@ -352,118 +727,6 @@ describe('#MixedType', () => { }); }); - 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 type be changed by condition', () => { - 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 type be changed by condition', () => { - 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' } - }); - }); - it('should error when an async rule is executed by the sync validator', () => { const m = MixedType().addRule(async () => { return true; @@ -524,15 +787,241 @@ describe('#MixedType', () => { }, 100); }); - 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() + 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' } + } + }); }); - 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' } + 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' } + }); + }); + }); }); }); }); diff --git a/test/ObjectTypeSpec.js b/test/ObjectTypeSpec.js index bfc3eb0..f846ec6 100644 --- a/test/ObjectTypeSpec.js +++ b/test/ObjectTypeSpec.js @@ -163,7 +163,7 @@ describe('#ObjectType', () => { }); it('Should be checked for object nesting with nestedObject option.', () => { - const schemaData = { + const schema = new Schema({ url: StringType().isURL('Should be a url'), user: ObjectType().shape({ email: StringType().isEmail('Should be an email'), @@ -173,9 +173,7 @@ describe('#ObjectType', () => { age: NumberType().min(50, 'Age should be greater than 50') }) }) - }; - - const schema = new Schema(schemaData); + }); const options = { nestedObject: true }; const checkResult = schema.checkForField( @@ -189,6 +187,16 @@ describe('#ObjectType', () => { 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 } } }, @@ -197,6 +205,10 @@ describe('#ObjectType', () => { 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 } } }, @@ -204,6 +216,19 @@ describe('#ObjectType', () => { ); 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 () => { diff --git a/test/SchemaSpec.js b/test/SchemaSpec.js index 6000413..1c2b15f 100644 --- a/test/SchemaSpec.js +++ b/test/SchemaSpec.js @@ -1,47 +1,50 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -require('chai').should(); -const schema = require('../src'); +import chai, { expect } from 'chai'; +import * as schema from '../src'; -const { StringType, NumberType, Schema, SchemaModel } = schema; +const { StringType, NumberType, ObjectType, ArrayType, Schema, SchemaModel } = schema; + +chai.should(); describe('#Schema', () => { it('The schema should be saved as proporty', () => { - let schemaData = { data: StringType() }; - let schema = new Schema(schemaData); + const schemaData = { data: StringType() }; + const schema = new Schema(schemaData); - schema.spec.should.equal(schemaData); + schema.$spec.should.equal(schemaData); }); it('Should be able to get the field value type for the given field name', () => { - let schemaData = { data: NumberType() }; - let schema = new Schema(schemaData); + const schemaData = { data: NumberType() }; + const schema = new Schema(schemaData); schema.getFieldType('data').should.equal(schemaData.data); }); it('Should return error information', () => { - let schemaData = { data: NumberType() }; - let schema = new Schema(schemaData); - let checkResult = schema.checkForField('data', '2.22'); + 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, '年应该在 18 到 30 岁') + username: StringType().isRequired(), + email: StringType().isEmail(), + age: NumberType().range(18, 30) }); - const checkStatus = model.check({ + const checkResult = model.check({ username: 'foobar', email: 'foo@bar.com', age: 40 }); - checkStatus.username.hasError.should.equal(false); - checkStatus.email.hasError.should.equal(false); - checkStatus.age.hasError.should.equal(true); + expect(checkResult).to.deep.equal({ + username: { hasError: false }, + email: { hasError: false }, + age: { hasError: true, errorMessage: 'age field must be between 18 and 30' } + }); }); describe('## getKeys', () => { @@ -59,38 +62,271 @@ describe('#Schema', () => { }); }); + 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('请输入正确的邮箱') + username: StringType().isRequired(), + email: StringType().isEmail() }); - const model1CheckStatus = model1.check({ + const checkResult = model1.check({ username: 'foobar', email: 'foo@bar.com', age: 40 }); - model1CheckStatus.username.hasError.should.equal(false); - model1CheckStatus.email.hasError.should.equal(false); + expect(checkResult).to.deep.equal({ + username: { hasError: false }, + email: { hasError: false } + }); const model2 = SchemaModel({ - username: StringType().isRequired('用户名不能为空').minLength(7, '最少7个字符'), - age: NumberType().range(18, 30, '年应该在 18 到 30 岁') + username: StringType().isRequired().minLength(7), + age: NumberType().range(18, 30) }); - const model3 = SchemaModel.combine(model1, model2); - - const checkStatus = model3.check({ + const checkResult2 = SchemaModel.combine(model1, model2).check({ username: 'fooba', email: 'foo@bar.com', age: 40 }); - checkStatus.username.hasError.should.equal(true); - checkStatus.email.hasError.should.equal(false); - checkStatus.age.hasError.should.equal(true); + 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' } + }); }); }); }); diff --git a/test/utilsSpec.js b/test/utilsSpec.js index ea09a5b..f953a92 100644 --- a/test/utilsSpec.js +++ b/test/utilsSpec.js @@ -1,7 +1,7 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -require('chai').should(); -const { expect } = require('chai'); -const { formatErrorMessage, checkRequired, get } = require('../src/utils'); +import chai from 'chai'; +import { formatErrorMessage, checkRequired, shallowEqual, pathTransform } from '../src/utils'; + +chai.should(); describe('#utils', () => { describe('## formatErrorMessage', () => { @@ -9,7 +9,7 @@ describe('#utils', () => { 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'); + str2.should.equal('${name} is a required field'); }); it('Should output multiple parameters', () => { @@ -25,7 +25,7 @@ describe('#utils', () => { maxLength: 10 }); str.should.equal('tag must contain 3 to 10 items'); - str2.should.equal('tag must contain [minLength] to 10 items'); + str2.should.equal('tag must contain ${minLength} to 10 items'); }); it('Should not replace parameters', () => { @@ -63,36 +63,49 @@ describe('#utils', () => { }); }); - describe('## get', () => { - it('Should get the value of the object', () => { - const obj = { a: { b: { c: 1 } } }; - get(obj, 'a.b.c').should.equal(1); - get(obj, 'a.b').should.deep.equal({ c: 1 }); - get(obj, 'a').should.deep.equal({ b: { c: 1 } }); + 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 }; - expect(get(obj, 'a.b.d')).to.be.undefined; - expect(get(obj, 'a.b.d.e')).to.be.undefined; - expect(get(obj, 'a.b.d.e.f')).to.be.undefined; + shallowEqual(obj1, obj2).should.equal(true); + shallowEqual(obj1, obj3).should.equal(false); + shallowEqual(obj1, obj4).should.equal(false); }); - it('Should get the value of the array', () => { - const obj = { a: [{ b: 1 }, { b: 2 }] }; - get(obj, 'a.0.b').should.equal(1); - get(obj, 'a.1.b').should.equal(2); - expect(get(obj, 'a.2.b')).to.be.undefined; + 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 get the value of the array and object', () => { - const obj = { a: [{ b: { c: 1 } }, { b: { c: 2 } }] }; - get(obj, 'a.0.b.c').should.equal(1); - get(obj, 'a.1.b.c').should.equal(2); - expect(get(obj, 'a.2.b.c')).to.be.undefined; + 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); }); + }); - it('Should return the default value', () => { - const obj = { a: { b: [{ c: 1 }, { c: 2 }] } }; - expect(get(obj, 'a.b.2.c', 10)).to.equal(10); - expect(get(undefined, 'a.b', 10)).to.equal(10); + 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'); }); }); });