Skip to content

Commit

Permalink
feat: add support for equalTo and proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
simonguo committed Apr 10, 2024
1 parent 9ff16c3 commit 0de62aa
Show file tree
Hide file tree
Showing 13 changed files with 1,141 additions and 371 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
103 changes: 94 additions & 9 deletions src/MixedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValueType = any, DataType = any, E = ErrorMessageType, L = any> {
readonly typeName?: string;
protected required = false;
Expand All @@ -30,6 +47,10 @@ export class MixedType<ValueType = any, DataType = any, E = ErrorMessageType, L
value: any;
locale: L & MixedTypeLocale;

// The field name that depends on the verification of other fields
otherFields: string[] = [];
proxyOptions: ProxyOptions = {};

constructor(name?: TypeName) {
this.typeName = name;
this.locale = Object.assign(name ? locales[name] : {}, locales.mixed) as L & MixedTypeLocale;
Expand All @@ -40,7 +61,7 @@ export class MixedType<ValueType = any, DataType = any, E = ErrorMessageType, L
this.value = value;
}

check(value: ValueType = this.value, data?: DataType, fieldName?: string | string[]) {
check(value: any = this.value, data?: DataType, fieldName?: string | string[]) {
if (this.required && !checkRequired(value, this.trim, this.emptyAllowed)) {
return {
hasError: true,
Expand All @@ -50,7 +71,11 @@ export class MixedType<ValueType = any, DataType = any, E = ErrorMessageType, L
};
}

const validator = createValidator<ValueType, DataType, E | string>(data, fieldName);
const validator = createValidator<ValueType, DataType, E | string>(
data,
fieldName,
this.fieldLabel
);

const checkStatus = validator(value, this.priorityRules);

Expand All @@ -66,7 +91,7 @@ export class MixedType<ValueType = any, DataType = any, E = ErrorMessageType, L
}

checkAsync(
value: ValueType = this.value,
value: any = this.value,
data?: DataType,
fieldName?: string | string[]
): Promise<CheckResult<E | string>> {
Expand All @@ -79,7 +104,11 @@ export class MixedType<ValueType = any, DataType = any, E = ErrorMessageType, L
});
}

const validator = createValidatorAsync<ValueType, DataType, E | string>(data, fieldName);
const validator = createValidatorAsync<ValueType, DataType, E | string>(
data,
fieldName,
this.fieldLabel
);

return new Promise(resolve =>
validator(value, this.priorityRules)
Expand Down Expand Up @@ -119,7 +148,7 @@ export class MixedType<ValueType = any, DataType = any, E = ErrorMessageType, L
}
addRule(
onValid: ValidCallbackType<ValueType, DataType, E | string>,
errorMessage?: E | string,
errorMessage?: E | string | (() => E | string),
priority?: boolean
) {
this.pushRule({ onValid, errorMessage, priority });
Expand Down Expand Up @@ -149,11 +178,18 @@ export class MixedType<ValueType = any, DataType = any, E = ErrorMessageType, L

/**
* Define data verification rules based on conditions.
* @param validator
* @param condition
* @example
* MixedType().when(schema => {
* 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<DataType, E>) => MixedType) {
this.addRule(
Expand All @@ -166,8 +202,57 @@ export class MixedType<ValueType = any, DataType = any, E = ErrorMessageType, L
return this;
}

/**
* Check if the value is equal to the value of another field.
* @example
*
* ```js
* SchemaModel({
* password: StringType().isRequired(),
* confirmPassword: StringType().equalTo('password').isRequired()
* });
* ```
*/
equalTo(fieldName: string, errorMessage: E | string = this.locale.equalTo) {
const errorMessageFunc = () => {
const type = getFieldType(this.schemaSpec, fieldName, true);
return formatErrorMessage(errorMessage, { toFieldName: type?.fieldLabel || fieldName });
};

this.addRule((value, data) => {
return shallowEqual(value, get(data, fieldName));
}, errorMessageFunc);
return this;
}

/**
* After the field verification passes, proxy verification of other fields.
* @param options.checkIfValueExists When the value of other fields exists, the verification is performed (default: false)
* @example
*
* ```js
* SchemaModel({
* password: StringType().isRequired().proxy(['confirmPassword']),
* confirmPassword: StringType().equalTo('password').isRequired()
* });
* ```
*/
proxy(fieldNames: string[], options?: ProxyOptions) {
this.otherFields = fieldNames;
this.proxyOptions = options || {};
return this;
}

/**
* Overrides the key name in error messages.
*
* @example
* ```js
* SchemaModel({
* first_name: StringType().label('First name'),
* age: NumberType().label('Age')
* });
* ```
*/
label(label: string) {
this.fieldLabel = label;
Expand Down
56 changes: 40 additions & 16 deletions src/ObjectType.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -9,7 +15,7 @@ export class ObjectType<DataType = any, E = ErrorMessageType> extends MixedType<
E,
ObjectTypeLocale
> {
objectTypeSchemaSpec: SchemaDeclaration<DataType, E>;
[schemaSpecKey]: SchemaDeclaration<DataType, E>;
constructor(errorMessage?: E | string) {
super('object');
super.pushRule({
Expand All @@ -19,16 +25,21 @@ export class ObjectType<DataType = any, E = ErrorMessageType> 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<E>(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;
}
Expand All @@ -38,7 +49,11 @@ export class ObjectType<DataType = any, E = ErrorMessageType> extends MixedType<
return { hasError, object: checkResultObject };
}

const validator = createValidator<PlainObject, DataType, E | string>(data, fieldName);
const validator = createValidator<PlainObject, DataType, E | string>(
data,
childFieldKey || fieldName,
type.fieldLabel
);
const checkStatus = validator(value, type.priorityRules);

if (checkStatus) {
Expand All @@ -56,20 +71,29 @@ export class ObjectType<DataType = any, E = ErrorMessageType> 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<E>(this.requiredMessage || this.locale.isRequired, {
name: type.fieldLabel || childFieldKey || fieldName
})
});
}

const validator = createValidatorAsync<PlainObject, DataType, E | string>(data, fieldName);
const validator = createValidatorAsync<PlainObject, DataType, E | string>(
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<unknown>[] = [];
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);
});

Expand Down Expand Up @@ -118,7 +142,7 @@ export class ObjectType<DataType = any, E = ErrorMessageType> extends MixedType<
* })
*/
shape(fields: SchemaDeclaration<DataType, E>) {
this.objectTypeSchemaSpec = fields;
this[schemaSpecKey] = fields;
return this;
}
}
Expand Down
Loading

0 comments on commit 0de62aa

Please sign in to comment.