Skip to content

Commit 4d147b9

Browse files
authored
fix: prevent infinite loops in circular proxy validation (#85)
1 parent c21468e commit 4d147b9

File tree

2 files changed

+58
-10
lines changed

2 files changed

+58
-10
lines changed

src/Schema.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ interface CheckOptions {
1111

1212
export class Schema<DataType = any, ErrorMsgType = string> {
1313
readonly $spec: SchemaDeclaration<DataType, ErrorMsgType>;
14-
1514
private data: PlainObject;
15+
private checkedFields: string[] = [];
1616
private checkResult: SchemaCheckResult<DataType, ErrorMsgType> = {};
1717

1818
constructor(schema: SchemaDeclaration<DataType, ErrorMsgType>) {
@@ -102,19 +102,21 @@ export class Schema<DataType = any, ErrorMsgType = string> {
102102
getSchemaSpec() {
103103
return this.$spec;
104104
}
105-
106-
checkForField<T extends keyof DataType>(
105+
_checkForField<T extends keyof DataType>(
107106
fieldName: T,
108107
data: DataType,
109108
options: CheckOptions = {}
110109
): CheckResult<ErrorMsgType | string> {
111110
this.setSchemaOptionsForAllType(data);
112111

113112
const { nestedObject } = options;
113+
114+
// Add current field to checked list
115+
this.checkedFields = [...this.checkedFields, fieldName as string];
116+
114117
const fieldChecker = this.getFieldType(fieldName, nestedObject);
115118

116119
if (!fieldChecker) {
117-
// fieldValue can be anything if no schema defined
118120
return { hasError: false };
119121
}
120122

@@ -126,21 +128,33 @@ export class Schema<DataType = any, ErrorMsgType = string> {
126128
if (!checkResult.hasError) {
127129
const { checkIfValueExists } = fieldChecker.proxyOptions;
128130

129-
// Check other fields if the field depends on them for validation
130131
fieldChecker.otherFields?.forEach((field: string) => {
131-
if (checkIfValueExists) {
132-
if (!isEmpty(getFieldValue(data, field, nestedObject))) {
133-
this.checkForField(field as T, data, options);
132+
if (!this.checkedFields.includes(field)) {
133+
if (checkIfValueExists) {
134+
if (!isEmpty(getFieldValue(data, field, nestedObject))) {
135+
this._checkForField(field as T, data, { ...options });
136+
}
137+
return;
134138
}
135-
return;
139+
this._checkForField(field as T, data, { ...options });
136140
}
137-
this.checkForField(field as T, data, options);
138141
});
139142
}
140143

141144
return checkResult;
142145
}
143146

147+
checkForField<T extends keyof DataType>(
148+
fieldName: T,
149+
data: DataType,
150+
options: CheckOptions = {}
151+
): CheckResult<ErrorMsgType | string> {
152+
const result = this._checkForField(fieldName, data, options);
153+
// clean checked fields after check finished
154+
this.checkedFields = [];
155+
return result;
156+
}
157+
144158
checkForFieldAsync<T extends keyof DataType>(
145159
fieldName: T,
146160
data: DataType,

test/MixedTypeSpec.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,40 @@ describe('#MixedType', () => {
714714
}
715715
});
716716
});
717+
718+
it('Should handle circular proxy dependencies', () => {
719+
const model = SchemaModel({
720+
username: StringType().minLength(6, 'min 6').proxy(['password']),
721+
password: StringType().minLength(6, 'min 6').proxy(['username'])
722+
});
723+
724+
model.checkForField('username', {
725+
username: '123456',
726+
password: '123456'
727+
});
728+
const checkResult = model.getCheckResult();
729+
expect(checkResult.username.hasError).to.equal(false);
730+
expect(checkResult.password.hasError).to.equal(false);
731+
});
732+
733+
it('Should clean checked Field inside', () => {
734+
const model = SchemaModel({
735+
username: StringType().minLength(6, 'min 6').proxy(['password']),
736+
password: StringType().minLength(6, 'min 6').proxy(['username'])
737+
});
738+
model.checkForField('username', {
739+
username: '123456',
740+
password: '123456'
741+
});
742+
expect(model.getCheckResult().password.hasError).to.equal(false);
743+
744+
model.checkForField('username', {
745+
username: '123456',
746+
password: '12'
747+
});
748+
749+
expect(model.getCheckResult().password.hasError).to.equal(true);
750+
});
717751
});
718752

719753
it('Should check the wrong verification object', () => {

0 commit comments

Comments
 (0)