Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for equalTo and proxy #78

Merged
merged 9 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading