From 0de62aa3c405c8d775dce4c6a3b8b7dda97a7128 Mon Sep 17 00:00:00 2001 From: simonguo Date: Wed, 10 Apr 2024 15:44:46 +0800 Subject: [PATCH 1/9] feat: add support for `equalTo` and `proxy` --- README.md | 25 + src/MixedType.ts | 103 ++- src/ObjectType.ts | 56 +- src/Schema.ts | 84 ++- src/locales/default.ts | 3 +- src/types.ts | 4 +- src/utils/createValidator.ts | 7 +- src/utils/createValidatorAsync.ts | 8 +- src/utils/formatErrorMessage.ts | 2 +- src/utils/index.ts | 2 + src/utils/set.ts | 22 + src/utils/shallowEqual.ts | 57 ++ test/MixedTypeSpec.js | 1139 +++++++++++++++++++++-------- 13 files changed, 1141 insertions(+), 371 deletions(-) create mode 100644 src/utils/set.ts create mode 100644 src/utils/shallowEqual.ts diff --git a/README.md b/README.md index e7e5d9a..19aeb51 100644 --- a/README.md +++ b/README.md @@ -545,6 +545,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/src/MixedType.ts b/src/MixedType.ts index 9d146cf..7a6a538 100644 --- a/src/MixedType.ts +++ b/src/MixedType.ts @@ -12,10 +12,27 @@ import { createValidator, createValidatorAsync, isEmpty, - formatErrorMessage + shallowEqual, + formatErrorMessage, + get } from './utils'; import locales, { MixedTypeLocale } from './locales'; +type ProxyOptions = { + // Check if the value exists + checkIfValueExists?: boolean; +}; + +export const schemaSpecKey = 'objectTypeSchemaSpec'; + +export function getFieldType(schemaSpec: any, fieldName: string, nestedObject?: boolean) { + if (nestedObject) { + const namePath = fieldName.split('.').join(`.${schemaSpecKey}.`); + return get(schemaSpec, namePath); + } + return schemaSpec?.[fieldName]; +} + export class MixedType { readonly typeName?: string; protected required = false; @@ -30,6 +47,10 @@ export class MixedType(data, fieldName); + const validator = createValidator( + data, + fieldName, + this.fieldLabel + ); const checkStatus = validator(value, this.priorityRules); @@ -66,7 +91,7 @@ export class MixedType> { @@ -79,7 +104,11 @@ export class MixedType(data, fieldName); + const validator = createValidatorAsync( + data, + fieldName, + this.fieldLabel + ); return new Promise(resolve => validator(value, this.priorityRules) @@ -119,7 +148,7 @@ export class MixedType, - errorMessage?: E | string, + errorMessage?: E | string | (() => E | string), priority?: boolean ) { this.pushRule({ onValid, errorMessage, priority }); @@ -149,11 +178,18 @@ 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( @@ -166,8 +202,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..dbe5b00 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -1,6 +1,8 @@ import { SchemaDeclaration, SchemaCheckResult, CheckResult, PlainObject } from './types'; -import { MixedType } from './MixedType'; +import { MixedType, getFieldType } from './MixedType'; import get from './utils/get'; +import set from './utils/set'; +import isEmpty from './utils/isEmpty'; interface CheckOptions { /** @@ -19,19 +21,36 @@ function getFieldValue(data: PlainObject, fieldName: string, nestedObject?: bool export class Schema { readonly spec: SchemaDeclaration; private data: PlainObject; + private state: SchemaCheckResult = {}; constructor(schema: SchemaDeclaration) { this.spec = schema; } - getFieldType(fieldName: T, nestedObject?: boolean) { + 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 as string).split('.').join('.objectTypeSchemaSpec.'); + const namePath = fieldName.split('.').join('.object.'); + set(this.state, namePath, checkResult); - return get(this.spec, namePath); + return; } - return this.spec?.[fieldName]; + this.state[fieldName as string] = checkResult; + } + + getState() { + return this.state; } getKeys() { @@ -66,8 +85,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,23 +123,47 @@ 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); - return fieldChecker.checkAsync(fieldValue, data, fieldName as string); + 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: PlainObject = {}; + 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[] = []; @@ -115,7 +176,8 @@ export class Schema { for (let i = 0; i < values.length; i += 1) { checkResult[keys[i]] = values[i]; } - return checkResult as SchemaCheckResult; + + return checkResult; }); } } 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..fcfa5c4 100644 --- a/src/utils/createValidatorAsync.ts +++ b/src/utils/createValidatorAsync.ts @@ -5,7 +5,7 @@ import formatErrorMessage 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 || (Array.isArray(name) ? name.join('.') : name) }) ) ); diff --git a/src/utils/formatErrorMessage.ts b/src/utils/formatErrorMessage.ts index f6a7b97..5f3303c 100644 --- a/src/utils/formatErrorMessage.ts +++ b/src/utils/formatErrorMessage.ts @@ -7,7 +7,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/index.ts b/src/utils/index.ts index 0247ee4..75d2750 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,3 +5,5 @@ 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 set } from './set'; +export { default as shallowEqual } from './shallowEqual'; diff --git a/src/utils/set.ts b/src/utils/set.ts new file mode 100644 index 0000000..a535401 --- /dev/null +++ b/src/utils/set.ts @@ -0,0 +1,22 @@ +type Key = string | number | symbol; +type Path = Array | string; + +export default function set(object: any, path: Path, value: any): any { + if (!object) { + return object; + } + + const keys = Array.isArray(path) ? path : path.split('.'); + const length = keys.length; + + for (let i = 0; i < length - 1; i++) { + const key = keys[i]; + if (!object[key] || typeof object[key] !== 'object') { + object[key] = {}; + } + object = object[key]; + } + + object[keys[length - 1]] = value; + return object; +} 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/MixedTypeSpec.js b/test/MixedTypeSpec.js index 78af3f5..db1ae04 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 filed 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.getState()).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.getState()).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.getState()).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.getState()).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.getState()).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.getState()).to.deep.equal({ a: { hasError: false } }); + + expect(schema.checkForField('a', { a: 'a', b: 1 })).to.deep.equal({ + hasError: false + }); + + expect(schema.getState()).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.getState()).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.getState()).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.getState()).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.getState()).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.getState()).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.getState()).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.getState()).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' } + }); + }); + }); }); }); }); From e96dc319eb0838ce4fc89a773f650b60229bccab Mon Sep 17 00:00:00 2001 From: simonguo Date: Wed, 10 Apr 2024 15:46:11 +0800 Subject: [PATCH 2/9] docs: update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 19aeb51..b1a3223 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) From 15e28a690372820fd304041e970a24b6e2d72b6c Mon Sep 17 00:00:00 2001 From: simonguo Date: Wed, 10 Apr 2024 15:57:27 +0800 Subject: [PATCH 3/9] docs: update README.md --- README.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index b1a3223..b22e341 100644 --- a/README.md +++ b/README.md @@ -173,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 From 5d3e50477f102d244ce345a48abf066ef27ce0c4 Mon Sep 17 00:00:00 2001 From: simonguo Date: Wed, 10 Apr 2024 16:17:19 +0800 Subject: [PATCH 4/9] test: fix error tests --- test/ArrayTypeSpec.js | 118 +++++++++++++++++++++++++++++------------- test/utilsSpec.js | 4 +- 2 files changed, 83 insertions(+), 39 deletions(-) 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/utilsSpec.js b/test/utilsSpec.js index ea09a5b..becba1d 100644 --- a/test/utilsSpec.js +++ b/test/utilsSpec.js @@ -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', () => { From 53a0b37cf928a53e9509e215627b7fb1eaf5ff9a Mon Sep 17 00:00:00 2001 From: simonguo Date: Wed, 10 Apr 2024 16:32:35 +0800 Subject: [PATCH 5/9] test: fix error tests --- test/utilsSpec.js | 69 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/test/utilsSpec.js b/test/utilsSpec.js index becba1d..e28226f 100644 --- a/test/utilsSpec.js +++ b/test/utilsSpec.js @@ -1,7 +1,5 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -require('chai').should(); -const { expect } = require('chai'); -const { formatErrorMessage, checkRequired, get } = require('../src/utils'); +import { expect } from 'chai'; +import { formatErrorMessage, checkRequired, get, set, shallowEqual } from '../src/utils'; describe('#utils', () => { describe('## formatErrorMessage', () => { @@ -95,4 +93,67 @@ describe('#utils', () => { expect(get(undefined, 'a.b', 10)).to.equal(10); }); }); + + describe('## set', () => { + it('Should set the value of the object', () => { + const obj = { a: { b: { c: 1 } } }; + set(obj, 'a.b.c', 2); + obj.a.b.c.should.equal(2); + set(obj, 'a.b', { c: 3 }); + obj.a.b.should.deep.equal({ c: 3 }); + set(obj, 'a', { b: { c: 4 } }); + obj.a.should.deep.equal({ b: { c: 4 } }); + }); + + it('Should set the value of the array', () => { + const obj = { a: [{ b: 1 }, { b: 2 }] }; + set(obj, 'a.0.b', 3); + obj.a[0].b.should.equal(3); + set(obj, 'a.1.b', 4); + obj.a[1].b.should.equal(4); + }); + + it('Should set the value of the array and object', () => { + const obj = { a: [{ b: { c: 1 } }, { b: { c: 2 } }] }; + set(obj, 'a.0.b.c', 3); + obj.a[0].b.c.should.equal(3); + set(obj, 'a.1.b.c', 4); + obj.a[1].b.c.should.equal(4); + }); + }); + + 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); + }); + }); }); From e7b94ea960066bff8fb61381bbe03460ae3be785 Mon Sep 17 00:00:00 2001 From: simonguo Date: Wed, 10 Apr 2024 22:11:10 +0800 Subject: [PATCH 6/9] feat: add support for getErrorMessages and getCheckResult --- src/MixedType.ts | 25 ++-- src/Schema.ts | 104 +++++++++++----- test/MixedTypeSpec.js | 30 ++--- test/ObjectTypeSpec.js | 33 ++++- test/SchemaSpec.js | 272 ++++++++++++++++++++++++++++++++++++----- 5 files changed, 377 insertions(+), 87 deletions(-) diff --git a/src/MixedType.ts b/src/MixedType.ts index 7a6a538..5e43732 100644 --- a/src/MixedType.ts +++ b/src/MixedType.ts @@ -5,7 +5,8 @@ import { AsyncValidCallbackType, RuleType, ErrorMessageType, - TypeName + TypeName, + PlainObject } from './types'; import { checkRequired, @@ -25,6 +26,9 @@ type ProxyOptions = { 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}.`); @@ -33,8 +37,15 @@ export function getFieldType(schemaSpec: any, fieldName: string, nestedObject?: 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; @@ -43,7 +54,7 @@ export class MixedType[] = []; protected fieldLabel?: string; - schemaSpec: SchemaDeclaration; + $schemaSpec: SchemaDeclaration; value: any; locale: L & MixedTypeLocale; @@ -52,12 +63,12 @@ export class MixedType, value: any) { - this.schemaSpec = schemaSpec; + this.$schemaSpec = schemaSpec; this.value = value; } @@ -194,7 +205,7 @@ export class MixedType) => MixedType) { this.addRule( (value, data, fieldName) => { - return condition(this.schemaSpec).check(value, data, fieldName); + return condition(this.$schemaSpec).check(value, data, fieldName); }, undefined, true @@ -215,7 +226,7 @@ export class MixedType { - const type = getFieldType(this.schemaSpec, fieldName, true); + const type = getFieldType(this.$schemaSpec, fieldName, true); return formatErrorMessage(errorMessage, { toFieldName: type?.fieldLabel || fieldName }); }; diff --git a/src/Schema.ts b/src/Schema.ts index dbe5b00..808aafb 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -1,8 +1,6 @@ import { SchemaDeclaration, SchemaCheckResult, CheckResult, PlainObject } from './types'; -import { MixedType, getFieldType } from './MixedType'; -import get from './utils/get'; -import set from './utils/set'; -import isEmpty from './utils/isEmpty'; +import { MixedType, getFieldType, getFieldValue } from './MixedType'; +import { set, get, isEmpty } from './utils'; interface CheckOptions { /** @@ -11,27 +9,40 @@ 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]; +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('.'); } export class Schema { - readonly spec: SchemaDeclaration; + readonly $spec: SchemaDeclaration; private data: PlainObject; - private state: SchemaCheckResult = {}; + 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); + return getFieldType(this.$spec, fieldName as string, nestedObject); } private setFieldCheckResult( @@ -41,34 +52,69 @@ export class Schema { ) { if (nestedObject) { const namePath = fieldName.split('.').join('.object.'); - set(this.state, namePath, checkResult); + set(this.checkResult, namePath, checkResult); return; } - this.state[fieldName as string] = checkResult; - } - - getState() { - return this.state; + this.checkResult[fieldName as string] = checkResult; } - getKeys() { - return Object.keys(this.spec); - } - - 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): CheckResult { + if (path) { + return get(this.checkResult, pathTransform(path)) || { hasError: false }; + } + + return this.checkResult; + } + + /** + * Get the error messages of the schema + * + */ + getErrorMessages(path?: string): (string | ErrorMsgType)[] { + let messages: (string | ErrorMsgType)[] = []; + + if (path) { + const { errorMessage, object, array } = get(this.checkResult, 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(this.checkResult).map(key => this.checkResult[key]?.errorMessage); + } + + return messages.filter(Boolean); + } + + /** + * Get all the keys of the schema + */ + getKeys() { + return Object.keys(this.$spec); + } + checkForField( fieldName: T, data: DataType, @@ -153,7 +199,7 @@ export class Schema { check(data: DataType) { const checkResult: SchemaCheckResult = {}; - Object.keys(this.spec).forEach(key => { + Object.keys(this.$spec).forEach(key => { if (typeof data === 'object') { checkResult[key] = this.checkForField(key as T, data); } @@ -167,7 +213,7 @@ export class Schema { 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)); }); @@ -193,7 +239,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/test/MixedTypeSpec.js b/test/MixedTypeSpec.js index db1ae04..83beaa2 100644 --- a/test/MixedTypeSpec.js +++ b/test/MixedTypeSpec.js @@ -31,7 +31,7 @@ describe('#MixedType', () => { }); }); - it('Should check if two fields are the same and the filed value is not root', () => { + it('Should check if two fields are the same and the field value is not root', () => { const schema = SchemaModel({ a: StringType().isRequired(), b: StringType() @@ -411,7 +411,7 @@ describe('#MixedType', () => { schema.checkForField('password', { password: '123', confirmPassword: '13' }) ).to.deep.equal({ hasError: false }); - expect(schema.getState()).to.deep.equal({ + expect(schema.getCheckResult()).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: true, @@ -429,7 +429,7 @@ describe('#MixedType', () => { confirmPassword: { hasError: false } }); - expect(schema.getState()).to.deep.equal({ + expect(schema.getCheckResult()).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: false } }); @@ -446,7 +446,7 @@ describe('#MixedType', () => { errorMessage: 'a is a required field' }); - expect(schema.getState()).to.deep.equal({ + expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: true, errorMessage: 'a is a required field' } }); }); @@ -463,7 +463,7 @@ describe('#MixedType', () => { hasError: false }); - expect(schema.getState()).to.deep.equal({ + expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { object: { c: { hasError: true, errorMessage: 'b.c is a required field' } } } }); @@ -481,7 +481,7 @@ describe('#MixedType', () => { hasError: false }); - expect(schema.getState()).to.deep.equal({ + 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' } @@ -498,13 +498,13 @@ describe('#MixedType', () => { hasError: false }); - expect(schema.getState()).to.deep.equal({ a: { 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.getState()).to.deep.equal({ + expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { hasError: true, @@ -536,7 +536,7 @@ describe('#MixedType', () => { return result; }); - expect(schema.getState()).to.deep.equal({ + expect(schema.getCheckResult()).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: true, @@ -558,7 +558,7 @@ describe('#MixedType', () => { }); }); - expect(schema.getState()).to.deep.equal({ + expect(schema.getCheckResult()).to.deep.equal({ password: { hasError: false }, confirmPassword: { hasError: false } }); @@ -577,7 +577,7 @@ describe('#MixedType', () => { }); }); - expect(schema.getState()).to.deep.equal({ + expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: true, errorMessage: 'a is a required field' } }); }); @@ -596,7 +596,7 @@ describe('#MixedType', () => { }); }); - expect(schema.getState()).to.deep.equal({ + expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { object: { c: { hasError: true, errorMessage: 'b.c is a required field' } } } }); @@ -614,7 +614,7 @@ describe('#MixedType', () => { expect(result).to.deep.equal({ hasError: false }); }); - expect(schema.getState()).to.deep.equal({ + 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' } @@ -631,13 +631,13 @@ describe('#MixedType', () => { expect(result).to.deep.equal({ hasError: false }); }); - expect(schema.getState()).to.deep.equal({ a: { 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.getState()).to.deep.equal({ + expect(schema.getCheckResult()).to.deep.equal({ a: { hasError: false }, b: { hasError: true, 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..e5dbe8a 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,243 @@ 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 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']); + }); + }); + + 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' } + }); }); }); }); From 7b49076a61a0aadb94845f7f87c039def562c851 Mon Sep 17 00:00:00 2001 From: simonguo Date: Wed, 10 Apr 2024 23:02:36 +0800 Subject: [PATCH 7/9] feat: add support for getErrorMessages and getCheckResult --- src/Schema.ts | 14 +++++++------- test/SchemaSpec.js | 13 +++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Schema.ts b/src/Schema.ts index 808aafb..fac2a4d 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -76,23 +76,23 @@ export class Schema { * Get the check result of the schema * @returns CheckResult */ - getCheckResult(path?: string): CheckResult { + getCheckResult(path?: string, result = this.checkResult): CheckResult { if (path) { - return get(this.checkResult, pathTransform(path)) || { hasError: false }; + return result?.[path] || get(result, pathTransform(path)) || { hasError: false }; } - return this.checkResult; + return result; } /** * Get the error messages of the schema - * */ - getErrorMessages(path?: string): (string | ErrorMsgType)[] { + getErrorMessages(path?: string, result = this.checkResult): (string | ErrorMsgType)[] { let messages: (string | ErrorMsgType)[] = []; if (path) { - const { errorMessage, object, array } = get(this.checkResult, pathTransform(path)) || {}; + const { errorMessage, object, array } = + result?.[path] || get(result, pathTransform(path)) || {}; if (errorMessage) { messages = [errorMessage]; @@ -102,7 +102,7 @@ export class Schema { messages = array.map(item => item?.errorMessage); } } else { - messages = Object.keys(this.checkResult).map(key => this.checkResult[key]?.errorMessage); + messages = Object.keys(result).map(key => result[key]?.errorMessage); } return messages.filter(Boolean); diff --git a/test/SchemaSpec.js b/test/SchemaSpec.js index e5dbe8a..dc6590c 100644 --- a/test/SchemaSpec.js +++ b/test/SchemaSpec.js @@ -139,6 +139,19 @@ describe('#Schema', () => { 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', () => { From 3dc29619afda562206c4da7d3c18b402b7e5bb0c Mon Sep 17 00:00:00 2001 From: simonguo Date: Wed, 10 Apr 2024 23:35:08 +0800 Subject: [PATCH 8/9] test: update tests --- src/MixedType.ts | 5 +++-- src/Schema.ts | 22 +--------------------- src/utils/createValidatorAsync.ts | 4 ++-- src/utils/formatErrorMessage.ts | 4 ++++ src/utils/index.ts | 1 + src/utils/pathTransform.ts | 19 +++++++++++++++++++ test/SchemaSpec.js | 15 +++++++++++++++ test/utilsSpec.js | 24 ++++++++++++++++++++++-- 8 files changed, 67 insertions(+), 27 deletions(-) create mode 100644 src/utils/pathTransform.ts diff --git a/src/MixedType.ts b/src/MixedType.ts index 5e43732..d91cae5 100644 --- a/src/MixedType.ts +++ b/src/MixedType.ts @@ -17,6 +17,7 @@ import { formatErrorMessage, get } from './utils'; +import { joinName } from './utils/formatErrorMessage'; import locales, { MixedTypeLocale } from './locales'; type ProxyOptions = { @@ -77,7 +78,7 @@ export class MixedType { - 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('.'); -} - export class Schema { readonly $spec: SchemaDeclaration; private data: PlainObject; diff --git a/src/utils/createValidatorAsync.ts b/src/utils/createValidatorAsync.ts index fcfa5c4..382ab20 100644 --- a/src/utils/createValidatorAsync.ts +++ b/src/utils/createValidatorAsync.ts @@ -1,5 +1,5 @@ import { CheckResult, RuleType } from '../types'; -import formatErrorMessage from './formatErrorMessage'; +import formatErrorMessage, { joinName } from './formatErrorMessage'; /** * Create a data asynchronous validator @@ -26,7 +26,7 @@ export function createValidatorAsync(data?: D, name?: string | string[] check( formatErrorMessage(errorMsg, { ...params, - name: label || (Array.isArray(name) ? name.join('.') : name) + name: label || joinName(name) }) ) ); diff --git a/src/utils/formatErrorMessage.ts b/src/utils/formatErrorMessage.ts index 5f3303c..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' diff --git a/src/utils/index.ts b/src/utils/index.ts index 75d2750..e8fad32 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,3 +7,4 @@ export { default as formatErrorMessage } from './formatErrorMessage'; export { default as get } from './get'; export { default as set } from './set'; 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/test/SchemaSpec.js b/test/SchemaSpec.js index dc6590c..1c2b15f 100644 --- a/test/SchemaSpec.js +++ b/test/SchemaSpec.js @@ -85,6 +85,21 @@ describe('#Schema', () => { 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(), diff --git a/test/utilsSpec.js b/test/utilsSpec.js index e28226f..1ad4ca7 100644 --- a/test/utilsSpec.js +++ b/test/utilsSpec.js @@ -1,5 +1,14 @@ -import { expect } from 'chai'; -import { formatErrorMessage, checkRequired, get, set, shallowEqual } from '../src/utils'; +import chai, { expect } from 'chai'; +import { + formatErrorMessage, + checkRequired, + get, + set, + shallowEqual, + pathTransform +} from '../src/utils'; + +chai.should(); describe('#utils', () => { describe('## formatErrorMessage', () => { @@ -156,4 +165,15 @@ describe('#utils', () => { 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'); + }); + }); }); From eb6212736e71f2c7f049ad741c5c8278a71ecf60 Mon Sep 17 00:00:00 2001 From: simonguo Date: Thu, 11 Apr 2024 10:21:57 +0800 Subject: [PATCH 9/9] fix: use lodash --- package-lock.json | 24 ++++++++++++++++ package.json | 4 +++ src/utils/get.ts | 21 -------------- src/utils/index.ts | 4 +-- src/utils/set.ts | 22 -------------- test/utilsSpec.js | 72 ++-------------------------------------------- 6 files changed, 32 insertions(+), 115 deletions(-) delete mode 100644 src/utils/get.ts delete mode 100644 src/utils/set.ts 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/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 e8fad32..45b14eb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,10 +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 set } from './set'; export { default as shallowEqual } from './shallowEqual'; export { default as pathTransform } from './pathTransform'; diff --git a/src/utils/set.ts b/src/utils/set.ts deleted file mode 100644 index a535401..0000000 --- a/src/utils/set.ts +++ /dev/null @@ -1,22 +0,0 @@ -type Key = string | number | symbol; -type Path = Array | string; - -export default function set(object: any, path: Path, value: any): any { - if (!object) { - return object; - } - - const keys = Array.isArray(path) ? path : path.split('.'); - const length = keys.length; - - for (let i = 0; i < length - 1; i++) { - const key = keys[i]; - if (!object[key] || typeof object[key] !== 'object') { - object[key] = {}; - } - object = object[key]; - } - - object[keys[length - 1]] = value; - return object; -} diff --git a/test/utilsSpec.js b/test/utilsSpec.js index 1ad4ca7..f953a92 100644 --- a/test/utilsSpec.js +++ b/test/utilsSpec.js @@ -1,12 +1,5 @@ -import chai, { expect } from 'chai'; -import { - formatErrorMessage, - checkRequired, - get, - set, - shallowEqual, - pathTransform -} from '../src/utils'; +import chai from 'chai'; +import { formatErrorMessage, checkRequired, shallowEqual, pathTransform } from '../src/utils'; chai.should(); @@ -70,67 +63,6 @@ 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 } }); - - 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; - }); - - 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 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 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('## set', () => { - it('Should set the value of the object', () => { - const obj = { a: { b: { c: 1 } } }; - set(obj, 'a.b.c', 2); - obj.a.b.c.should.equal(2); - set(obj, 'a.b', { c: 3 }); - obj.a.b.should.deep.equal({ c: 3 }); - set(obj, 'a', { b: { c: 4 } }); - obj.a.should.deep.equal({ b: { c: 4 } }); - }); - - it('Should set the value of the array', () => { - const obj = { a: [{ b: 1 }, { b: 2 }] }; - set(obj, 'a.0.b', 3); - obj.a[0].b.should.equal(3); - set(obj, 'a.1.b', 4); - obj.a[1].b.should.equal(4); - }); - - it('Should set the value of the array and object', () => { - const obj = { a: [{ b: { c: 1 } }, { b: { c: 2 } }] }; - set(obj, 'a.0.b.c', 3); - obj.a[0].b.c.should.equal(3); - set(obj, 'a.1.b.c', 4); - obj.a[1].b.c.should.equal(4); - }); - }); - describe('## shallowEqual', () => { it('Should compare the object', () => { const obj1 = { a: 1, b: 2 };