Skip to content

Commit

Permalink
feat: add support for equalTo and proxy (#78)
Browse files Browse the repository at this point in the history
* feat: add support for `equalTo` and `proxy`

* docs: update README.md

* docs: update README.md

* test: fix error tests

* test: fix error tests

* feat: add support for getErrorMessages and getCheckResult

* feat: add support for getErrorMessages and getCheckResult

* test: update tests

* fix: use lodash
  • Loading branch information
simonguo authored Apr 11, 2024
1 parent 9ff16c3 commit d9f0e55
Show file tree
Hide file tree
Showing 20 changed files with 1,684 additions and 543 deletions.
69 changes: 50 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<CheckResult>`](#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)
Expand Down Expand Up @@ -171,32 +173,36 @@ model.check({ field1: '', field2: '' });
**/
```

#### Multi-field cross validation
#### Field dependency validation

E.g: verify that the two passwords are the same.
1. Use the `equalTo` method to verify that the values of two fields are equal.

```js
const model = SchemaModel({
password1: StringType().isRequired('This field required'),
password2: StringType().addRule((value, data) => {
if (value !== data.password1) {
return false;
}
return true;
}, 'The passwords are inconsistent twice')
password: StringType().isRequired(),
confirmPassword: StringType().equalTo('password')
});
```

model.check({ password1: '123456', password2: 'root' });
2. Use the `addRule` method to create a custom validation rule.

/**
{
password1: { hasError: false },
password2: {
hasError: true,
errorMessage: 'The passwords are inconsistent twice'
}
}
**/
```js
const model = SchemaModel({
password: StringType().isRequired(),
confirmPassword: StringType().addRule(
(value, data) => value === data.password,
'Confirm password must be the same as password'
)
});
```

3. Use the `proxy` method to verify that a field passes, and then proxy verification of other fields.

```js
const model = SchemaModel({
password: StringType().isRequired().proxy(['confirmPassword']),
confirmPassword: StringType().equalTo('password')
});
```

#### Asynchronous check
Expand Down Expand Up @@ -545,6 +551,31 @@ SchemaModel({
});
```

#### `equalTo(fieldName: string, errorMessage?: string)`

Check if the value is equal to the value of another field.

```js
SchemaModel({
password: StringType().isRequired(),
confirmPassword: StringType().equalTo('password')
});
```

#### `proxy(fieldNames: string[], options?: { checkIfValueExists?: boolean })`

After the field verification passes, proxy verification of other fields.

- `fieldNames`: The field name to be proxied.
- `options.checkIfValueExists`: When the value of other fields exists, the verification is performed (default: false)

```js
SchemaModel({
password: StringType().isRequired().proxy(['confirmPassword']),
confirmPassword: StringType().equalTo('password')
});
```

### StringType(errorMessage?: string)

Define a string type. Supports all the same methods as [MixedType](#mixedtype).
Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
131 changes: 114 additions & 17 deletions src/MixedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,48 @@ import {
AsyncValidCallbackType,
RuleType,
ErrorMessageType,
TypeName
TypeName,
PlainObject
} from './types';
import {
checkRequired,
createValidator,
createValidatorAsync,
isEmpty,
formatErrorMessage
shallowEqual,
formatErrorMessage,
get
} from './utils';
import { joinName } from './utils/formatErrorMessage';
import locales, { MixedTypeLocale } from './locales';

type ProxyOptions = {
// Check if the value exists
checkIfValueExists?: boolean;
};

export const schemaSpecKey = 'objectTypeSchemaSpec';

/**
* Get the field type from the schema object
*/
export function getFieldType(schemaSpec: any, fieldName: string, nestedObject?: boolean) {
if (nestedObject) {
const namePath = fieldName.split('.').join(`.${schemaSpecKey}.`);
return get(schemaSpec, namePath);
}
return schemaSpec?.[fieldName];
}

/**
* Get the field value from the data object
*/
export function getFieldValue(data: PlainObject, fieldName: string, nestedObject?: boolean) {
return nestedObject ? get(data, fieldName) : data?.[fieldName];
}

export class MixedType<ValueType = any, DataType = any, E = ErrorMessageType, L = any> {
readonly typeName?: string;
readonly $typeName?: string;
protected required = false;
protected requiredMessage: E | string = '';
protected trim = false;
Expand All @@ -26,31 +55,39 @@ export class MixedType<ValueType = any, DataType = any, E = ErrorMessageType, L
protected priorityRules: RuleType<ValueType, DataType, E | string>[] = [];
protected fieldLabel?: string;

schemaSpec: SchemaDeclaration<DataType, E>;
$schemaSpec: SchemaDeclaration<DataType, E>;
value: any;
locale: L & MixedTypeLocale;

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

constructor(name?: TypeName) {
this.typeName = name;
this.$typeName = name;
this.locale = Object.assign(name ? locales[name] : {}, locales.mixed) as L & MixedTypeLocale;
}

setSchemaOptions(schemaSpec: SchemaDeclaration<DataType, E>, value: any) {
this.schemaSpec = schemaSpec;
this.$schemaSpec = schemaSpec;
this.value = value;
}

check(value: ValueType = this.value, data?: DataType, fieldName?: string | string[]) {
check(value: any = this.value, data?: DataType, fieldName?: string | string[]) {
if (this.required && !checkRequired(value, this.trim, this.emptyAllowed)) {
return {
hasError: true,
errorMessage: formatErrorMessage(this.requiredMessage, {
name: this.fieldLabel || fieldName
name: this.fieldLabel || joinName(fieldName)
})
};
}

const validator = createValidator<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,20 +103,24 @@ 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>> {
if (this.required && !checkRequired(value, this.trim, this.emptyAllowed)) {
return Promise.resolve({
hasError: true,
errorMessage: formatErrorMessage(this.requiredMessage, {
name: this.fieldLabel || fieldName
name: this.fieldLabel || joinName(fieldName)
})
});
}

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 +160,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,25 +190,81 @@ 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(
(value, data, fieldName) => {
return condition(this.schemaSpec).check(value, data, fieldName);
return condition(this.$schemaSpec).check(value, data, fieldName);
},
undefined,
true
);
return this;
}

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

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

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

/**
* Overrides the key name in error messages.
*
* @example
* ```js
* SchemaModel({
* first_name: StringType().label('First name'),
* age: NumberType().label('Age')
* });
* ```
*/
label(label: string) {
this.fieldLabel = label;
Expand Down
Loading

0 comments on commit d9f0e55

Please sign in to comment.