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' } + }); + }); + }); }); }); });