-
-
Notifications
You must be signed in to change notification settings - Fork 147
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(validation): link two or more properties' validation #2015
base: master
Are you sure you want to change the base?
Conversation
* possibility to connect properties if they are related to each other
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2015 +/- ##
==========================================
+ Coverage 88.97% 88.99% +0.02%
==========================================
Files 280 280
Lines 23232 23317 +85
Branches 5350 5379 +29
==========================================
+ Hits 20670 20752 +82
- Misses 2562 2565 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @XmasCarola, thanks for the contribution. This is an interesting change. Left a couple of suggestions.
Coming to bike-shading now :D
The method name linkProperties
seems a bit ambiguous to me. The purpose is not immediately clear whether the validation of the current property is dependent on the other properties, or is it the other way around. As your test shows, it is the other way around. From that perspective, a .dependsOn
method will be more intuitive.
As an example from your tests
const rules = validationRules
.on(defineRuleOnClass ? Person : obj)
.ensure('name')
.required()
.ensure('age')
.dependsOn('name')
.required()
.ensure('address.line1')
.dependsOn('name')
.required()
.withMessage('Address is required.')
It don't have to be dependsOn
, any other intuitive name should also work.
packages/validation/src/validator.ts
Outdated
if (propertyRule !== void 0 && propertyRule.linkedProperties.length > 0) { | ||
const additionalRules = propertyRule.linkedProperties.map(lp => rules.find(r => r.property.name === lp)).filter(Boolean) as PropertyRule[]; | ||
return (await Promise.all([propertyRule?.validate(object, propertyTag, scope), ...additionalRules.map(async (rule) => rule.validate(object, propertyTag, scope))])).flat(); | ||
} | ||
return (await rules.find((r) => r.property.name === propertyName)?.validate(object, propertyTag, scope)) ?? []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
propertyRule
should be reused here.
packages/validation/src/validator.ts
Outdated
const propertyRule: PropertyRule | undefined = rules.find((r) => r.property.name === propertyName); | ||
if (propertyRule !== void 0 && propertyRule.linkedProperties.length > 0) { | ||
const additionalRules = propertyRule.linkedProperties.map(lp => rules.find(r => r.property.name === lp)).filter(Boolean) as PropertyRule[]; | ||
return (await Promise.all([propertyRule?.validate(object, propertyTag, scope), ...additionalRules.map(async (rule) => rule.validate(object, propertyTag, scope))])).flat(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the .filter
is needed? Are we expecting the rules to be falsy
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There could be undefined values in case .find() does not detect a match.
.ensure('name') | ||
.linkProperties(['age', 'address']); | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a bit more details. To me at least, after reading the docs, the purpose was not clear.
@@ -123,6 +123,7 @@ export class PropertyRule<TObject extends IValidateable = IValidateable, TValue | |||
private latestRule?: IValidationRule; | |||
/** @internal */ | |||
public readonly l: IServiceLocator; | |||
public linkedProperties: PropertyKey[] = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it would be great if these linked properties can be serialized; refer model based validation.
To that end, the accept
method(s) and the rule hydrators needs to be refactored.
* Links the PropertyRule to other key values. | ||
* @param properties - Array of property keys to link. | ||
*/ | ||
public linkProperties(properties: PropertyKey[]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that we should also support lambda expressions as input along with PropertyKey
, like we also do for the .ensure
.
@@ -57,6 +57,11 @@ export class StandardValidator implements IValidator { | |||
const scope = Scope.create({ [rootObjectSymbol]: object }); | |||
|
|||
if (propertyName !== void 0) { | |||
const propertyRule: PropertyRule | undefined = rules.find((r) => r.property.name === propertyName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When validating multiple properties, we might want to do that when the validation instruction involves validating a single property or a single binding. We might want to avoid repetitive executions of the same rules when the validation is triggered for the entire object or for all bindings.
Maybe slightly differently, we can mark a set of properties as one group. When one of those properties is validated, all the properties in the set are validated. For example: validationRules
.on(defineRuleOnClass ? Person : obj)
.group('name', 'age', 'address.line1') // <-- validate all 3 properties when one of those are validated.
.ensure('name')
.required()
.ensure('age')
.required()
.ensure('address.line1')
.required()
.withMessage('Address is required.') @bigopon What do you think about that? |
If we had a group of
I think I want to express something like this .ensureGroup('direction', 'departureDate', 'returnDate', (direction, departureDate, returnDate) => {
if (direction === '1 way') {
if (returnDate != null) { throw new Error('1 way flight has no return date'); }
if (startDate < Date.now()) { throw new Error('No time travel possible'); }
} else {
if (startDate > returnDate) { throw new Error('not possible to go back in time'); }
if (!hasFlight(returnDate)) { throw new Error('no flights on the date back, boat?'); }
}
}); and the trigger point will probably be after the radio and a date (either departure or return) has been selected. I wouldn't want to trigger any date independently I think. |
@bigopon I like your example a lot. Only instead of throwing error, it needs to return |
Hi guys, thank you for your input! :) |
Just an idea, maybe a serializable version of the .ensureGroup(
['direction', 'departureDate', 'returnDate'],
`$direction === '1-way' && $returnDate == null && $departureDate > Date.now()
|| $returnDate > $departureDate
`
) |
1353ece
to
84ebba7
Compare
Hi guys, I've made some additional integrations to the code. After analyzing the structure of ensureGroup(), I realized that what @bigopon is aiming for can be achieved with the current setup. As a result, I designed a test to emulate his request (you can refer to "can validate linked properties w/ custom logic" inside validator.spec.ts). Let me know your thoughts. Thanks! |
79b0f79
to
6b8044a
Compare
const propertyRule: PropertyRule | undefined = rules.find((r) => r.property.name === propertyName); | ||
if (propertyRule !== void 0 && propertyRule.linkedProperties.length > 0) { | ||
const additionalRules = propertyRule.linkedProperties.map(lp => rules.find(r => r.property.name === lp)).filter(Boolean) as PropertyRule[]; | ||
return (await Promise.all([propertyRule.validate(object, propertyTag, scope), ...additionalRules.map(async (rule) => rule.validate(object, propertyTag, scope))])).flat(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the .flat()
necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's needed to flatten the validation results, because there might be multiple validation results from a single property. This depends on how the rules are defined (if .then
is used).
packages/validation/src/validator.ts
Outdated
@@ -57,7 +57,12 @@ export class StandardValidator implements IValidator { | |||
const scope = Scope.create({ [rootObjectSymbol]: object }); | |||
|
|||
if (propertyName !== void 0) { | |||
return (await rules.find((r) => r.property.name === propertyName)?.validate(object, propertyTag, scope)) ?? []; | |||
const propertyRule: PropertyRule | undefined = rules.find((r) => r.property.name === propertyName); | |||
if (propertyRule !== void 0 && propertyRule.linkedProperties.length > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think with this addition, what we have is the ability to trigger a set of rules together with another rule. I also can't answer whether the depended rules will run first or in parallel with the dependent rule. For those reasons, I think the .dependsOn()
is inappropriate. Maybe let's try to come up with a more accurate name?
What about:
afterRules(...)
withRules(...)
whenRules(...)
with(...)
useRules(...)
withGroup(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After revisiting the code, I also agree that the .dependsOn
might not be a correct name for this. If we have the following:
rules
.on(object)
.ensure('prop1')
.rule1()
.dependsOn(['prop2', 'prop3'])
then the rules associated with prop2
and prop3
are executed when the prop1
is validated. So, in other words, the validation of prop2
and prop3
are triggered by the validation of prop1
, making prop2
and prop3
dependent on prop1
and not the other way around. I am unsure if that is the goal? If yes, then .dependsOn
for that might be a confusing name.
I was under the impression that the goal is to validate a set of properties as a group. Therefore, I am inclined towards the .ensureGroup
idea from @bigopon. @XmasCarola Do you think, implementing that satisfies your use-case as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a blocking comment, but on this note, we should also evaluate the UX aspect for this. My preference is that when one property is changed and therefore validated, only the rules associated with that property are executed, and the errors are shown accordingly.
However, with the introduction of this change, many properties will potentially be validated when the value of one property is changed, showing validation errors for potentially many properties. IMO, that increases the cognitive load for the end user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for my PR is that validating multiple properties simultaneously is sometimes necessary. It happened to me to encounter interdependent properties, such as booleans that need adjustment when one changes, or dates, like start and end date. Implementing date comparisons using Aurelia 1.0's validation plugin was challenging and led me to search online, where I found some related issues => aurelia/validation#279 (see the comment at the bottom). When two dates must follow a sequence, it’s helpful to indicate that adjusting either value can resolve the error.
Other frameworks validate all 'touched' fields whenever one is modified, which is another way to address this problem. However, your main solution - @Sayan751 - offers developers more flexibility, as some find it frustrating when parts of a form are validated unnecessarily. We could have related grouped properties with a specific validation order, as you both suggested. ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@XmasCarola I too have experience such need for validating multiple properties as a group. Hence, there is no question about the legitimate need of this feature. The point on the UX is more or less an emphasis on seeing it in action and evaluating how it is affecting the UX for a legitimate use-case. I appreciate you working on it!
btw, the benchmark jobs can be fixed by simply updating this branch with latest master @XmasCarola |
* Links the PropertyRule to other key values. | ||
* @param properties - Array of properties to link (accepts strings, object keys as well as property accessor functions). | ||
*/ | ||
public dependsOn(properties: string[] | PropertyAccessor[]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the intellisense might not work as expected with this. We might need something like PropertyAccessor<TObject, TValue>
, like it is done for .ensure
.
const propertyRule: PropertyRule | undefined = rules.find((r) => r.property.name === propertyName); | ||
if (propertyRule !== void 0 && propertyRule.linkedProperties.length > 0) { | ||
const additionalRules = propertyRule.linkedProperties.map(lp => rules.find(r => r.property.name === lp)).filter(Boolean) as PropertyRule[]; | ||
return (await Promise.all([propertyRule.validate(object, propertyTag, scope), ...additionalRules.map(async (rule) => rule.validate(object, propertyTag, scope))])).flat(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's needed to flatten the validation results, because there might be multiple validation results from a single property. This depends on how the rules are defined (if .then
is used).
packages/validation/src/validator.ts
Outdated
@@ -57,7 +57,12 @@ export class StandardValidator implements IValidator { | |||
const scope = Scope.create({ [rootObjectSymbol]: object }); | |||
|
|||
if (propertyName !== void 0) { | |||
return (await rules.find((r) => r.property.name === propertyName)?.validate(object, propertyTag, scope)) ?? []; | |||
const propertyRule: PropertyRule | undefined = rules.find((r) => r.property.name === propertyName); | |||
if (propertyRule !== void 0 && propertyRule.linkedProperties.length > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After revisiting the code, I also agree that the .dependsOn
might not be a correct name for this. If we have the following:
rules
.on(object)
.ensure('prop1')
.rule1()
.dependsOn(['prop2', 'prop3'])
then the rules associated with prop2
and prop3
are executed when the prop1
is validated. So, in other words, the validation of prop2
and prop3
are triggered by the validation of prop1
, making prop2
and prop3
dependent on prop1
and not the other way around. I am unsure if that is the goal? If yes, then .dependsOn
for that might be a confusing name.
I was under the impression that the goal is to validate a set of properties as a group. Therefore, I am inclined towards the .ensureGroup
idea from @bigopon. @XmasCarola Do you think, implementing that satisfies your use-case as well?
One thing that is possible if we declare all properties of a group together, is the ability to explicitly declare the sequence. Similar to the way Gulp enable sequence in it task running. For example: // all at once
.group(['direction', 'returnDate', 'departureDate'], ([direction, returnDate, departureDate]) => ...)
// direction then departure and return dates at once
.group('direction', ['departureDate', 'returnDate'], (direction, [departureDate, returnDate]) => ...)
// direction then departure date then return date
.group('direction', 'departureDate', 'returnDate', (direction, departureDate, returnDate) => ...) We can also allow |
@XmasCarola @bigopon I think that if we agree on the .ensureGroup(
['property', or => accessor],
(value1, value2, object) => boolean | Promise<boolean>
) The rule is executed when any one of the property value is changed. The order of the rule execution is determined by how the rule is registered. I mean that that is the default order of executing the rules. We should maintain that here as well, without treating this rule too specially. My mental model is as follows: when we encounter an |
I don't feel like this is an acceptable default at all. Adding a group validation and suddenly I won't be able to figure out where it went wrong in my tests is an undesirable scenario to be in. In a group validation, issues may not always be with a single property. Like in my example, sometimes it's the direction, sometimes it's the departure, sometimes it's the return date. Do we want to always notify all those direction departure and return dates when only one of those fails? .ensureGroup(
['property', or => accessor],
(value1, value2, object) =>
| { valid: true }
| Promise<{ valid: true }>
+ | { valid: false; property: string }
+ | Promise<{ valid: false; property: string }>
) |
@bigopon Like the idea. I would suggest an extension of that to the following: .ensureGroup(
['property', or => accessor],
(value1, value2, object) =>
+ | { valid: boolean, { property: string, valid: boolean, messageOrMessageKey?: string }[] }
+ | Promise<{ valid: boolean, { property: string, valid: boolean, messageOrMessageKey?: string }[] }>
) If no message or message key is provided in the result, then the usual workflow of resolving the message should take over the task. We need to remember that it should work with i18n as well. |
…oup - update test coverage
…oup - remove duplicated rules
…oup - remove prettier format
Remove latest rule reassignment
Pull Request
📖 Description
Possibility to connect properties if they are related to each other, so that the validation of one property acts as a trigger for the other(s). This is useful when two variables are interconnected, for example start and end date that are supposed to function as comparison values. Providing multiple checks with subsequent highlights can point out all the values that need to be modified to properly fill out a form.
🎫 Issues
👩💻 Reviewer Notes
📑 Test Plan
⏭ Next Steps