@platecms/delta-validation 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/index.ts +8 -7
- package/src/lib/enums/validation-rule-update-impact.enum.ts +5 -0
- package/src/lib/helpers/fields-to-schema.spec.ts +184 -0
- package/src/lib/helpers/fields-to-schema.ts +141 -0
- package/src/lib/helpers/get-content-value-type-name.ts +26 -0
- package/src/lib/helpers/is-superset.helper.ts +4 -0
- package/src/lib/helpers/schema-builder.ts +5 -0
- package/src/lib/schemas/content-value.schema.ts +27 -2
- package/src/lib/schemas/object.schema.spec.ts +223 -0
- package/src/lib/schemas/object.schema.ts +106 -0
- package/src/lib/types/validation-schema.interface.ts +1 -1
- package/src/lib/validation-rules/allowed-values.validation-rule.spec.ts +25 -0
- package/src/lib/validation-rules/allowed-values.validation-rule.ts +15 -0
- package/src/lib/validation-rules/base.validation-rule.ts +21 -4
- package/src/lib/validation-rules/count.validation-rule.spec.ts +38 -0
- package/src/lib/validation-rules/count.validation-rule.ts +35 -0
- package/src/lib/validation-rules/date-between.validation-rule.spec.ts +83 -0
- package/src/lib/validation-rules/date-between.validation-rule.ts +15 -0
- package/src/lib/validation-rules/decimal-count.validation-rule.spec.ts +26 -0
- package/src/lib/validation-rules/decimal-count.validation-rule.ts +12 -0
- package/src/lib/validation-rules/number-between.validation-rule.spec.ts +44 -0
- package/src/lib/validation-rules/number-between.validation-rule.ts +14 -0
- package/src/lib/validation-rules/relatable-content-types.validation-rule.spec.ts +33 -8
- package/src/lib/validation-rules/relatable-content-types.validation-rule.ts +17 -0
- package/src/lib/validation-rules/string-format.validation-rule.spec.ts +47 -21
- package/src/lib/validation-rules/string-format.validation-rule.ts +14 -0
- package/src/lib/validation-rules/value-type.validation-rule.spec.ts +104 -74
- package/src/lib/validation-rules/value-type.validation-rule.ts +17 -2
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { PRN } from "@platecms/delta-plate-resource-notation";
|
|
2
|
+
import { RuleType } from "../enums/rule-types.enum";
|
|
3
|
+
import { NumberValidationSchema } from "./number.schema";
|
|
4
|
+
import { ObjectValidationSchema } from "./object.schema";
|
|
5
|
+
import { StringValidationSchema } from "./string.schema";
|
|
6
|
+
|
|
7
|
+
type TestObject = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
describe("ObjectValidationSchema", () => {
|
|
10
|
+
let objectSchema: ObjectValidationSchema<TestObject>;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
objectSchema = new ObjectValidationSchema<TestObject>();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("type checking", () => {
|
|
17
|
+
it("passes validation when value is an object", () => {
|
|
18
|
+
const validationResult = objectSchema.validate({ alpha: 1, beta: 2 }, { path: ["test"] });
|
|
19
|
+
|
|
20
|
+
expect(validationResult.isValid).toBe(true);
|
|
21
|
+
expect(validationResult.errors).toHaveLength(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("fails validation when value is not an object", () => {
|
|
25
|
+
const validationResult = objectSchema.validate("not an object" as unknown as TestObject, {
|
|
26
|
+
path: ["test"],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(validationResult.isValid).toBe(false);
|
|
30
|
+
expect(validationResult.errors).toHaveLength(1);
|
|
31
|
+
expect(validationResult.errors[0].message).toBe("Value must be an object");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("fails validation when value is an array", () => {
|
|
35
|
+
const validationResult = objectSchema.validate([1, 2, 3] as unknown as TestObject, {
|
|
36
|
+
path: ["test"],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(validationResult.isValid).toBe(false);
|
|
40
|
+
expect(validationResult.errors).toHaveLength(1);
|
|
41
|
+
expect(validationResult.errors[0].message).toBe("Value must be an object");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("fails validation when value is a number", () => {
|
|
45
|
+
const validationResult = objectSchema.validate(42 as unknown as TestObject, {
|
|
46
|
+
path: ["test"],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(validationResult.isValid).toBe(false);
|
|
50
|
+
expect(validationResult.errors).toHaveLength(1);
|
|
51
|
+
expect(validationResult.errors[0].message).toBe("Value must be an object");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("passes validation when value is null (allowed by default)", () => {
|
|
55
|
+
const validationResult = objectSchema.validate(null as unknown as TestObject, {
|
|
56
|
+
path: ["test"],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(validationResult.isValid).toBe(true);
|
|
60
|
+
expect(validationResult.errors).toHaveLength(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("passes validation when value is undefined (allowed by default)", () => {
|
|
64
|
+
const validationResult = objectSchema.validate(undefined as unknown as TestObject, {
|
|
65
|
+
path: ["test"],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(validationResult.isValid).toBe(true);
|
|
69
|
+
expect(validationResult.errors).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("count validation", () => {
|
|
74
|
+
it("passes validation when object keys count is within min and max bounds", () => {
|
|
75
|
+
objectSchema.min(2).max(4);
|
|
76
|
+
|
|
77
|
+
const validationResult = objectSchema.validate({ alpha: 1, beta: 2, gamma: 3 }, { path: ["test"] });
|
|
78
|
+
|
|
79
|
+
expect(validationResult.isValid).toBe(true);
|
|
80
|
+
expect(validationResult.errors).toHaveLength(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("fails validation when object keys count is below minimum", () => {
|
|
84
|
+
objectSchema.min(3);
|
|
85
|
+
|
|
86
|
+
const validationResult = objectSchema.validate({ alpha: 1, beta: 2 }, { path: ["test"] });
|
|
87
|
+
|
|
88
|
+
expect(validationResult.isValid).toBe(false);
|
|
89
|
+
expect(validationResult.errors).toHaveLength(1);
|
|
90
|
+
expect(validationResult.errors[0].message).toContain("object keys");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("fails validation when object keys count is above maximum", () => {
|
|
94
|
+
objectSchema.max(2);
|
|
95
|
+
|
|
96
|
+
const validationResult = objectSchema.validate({ alpha: 1, beta: 2, gamma: 3 }, { path: ["test"] });
|
|
97
|
+
|
|
98
|
+
expect(validationResult.isValid).toBe(false);
|
|
99
|
+
expect(validationResult.errors).toHaveLength(1);
|
|
100
|
+
expect(validationResult.errors[0].message).toContain("object keys");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("passes validation when no min/max constraints are set", () => {
|
|
104
|
+
const validationResult = objectSchema.validate(
|
|
105
|
+
{ alpha: 1, beta: 2, gamma: 3, delta: 4, epsilon: 5 },
|
|
106
|
+
{ path: ["test"] },
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(validationResult.isValid).toBe(true);
|
|
110
|
+
expect(validationResult.errors).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("handles empty object with min constraint", () => {
|
|
114
|
+
objectSchema.min(1);
|
|
115
|
+
|
|
116
|
+
const validationResult = objectSchema.validate({}, { path: ["test"] });
|
|
117
|
+
|
|
118
|
+
expect(validationResult.isValid).toBe(false);
|
|
119
|
+
expect(validationResult.errors).toHaveLength(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("handles empty object with max constraint", () => {
|
|
123
|
+
objectSchema.max(2);
|
|
124
|
+
|
|
125
|
+
const validationResult = objectSchema.validate({}, { path: ["test"] });
|
|
126
|
+
|
|
127
|
+
expect(validationResult.isValid).toBe(true);
|
|
128
|
+
expect(validationResult.errors).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("shape validation", () => {
|
|
133
|
+
it("validates object keys against shape schema", () => {
|
|
134
|
+
const stringValidationSchema = new StringValidationSchema();
|
|
135
|
+
objectSchema.of({ name: stringValidationSchema });
|
|
136
|
+
|
|
137
|
+
const validationResult = objectSchema.validate({ name: "John" }, { path: ["test"] });
|
|
138
|
+
|
|
139
|
+
expect(validationResult.isValid).toBe(true);
|
|
140
|
+
expect(validationResult.errors).toHaveLength(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("fails when shape property fails validation", () => {
|
|
144
|
+
const stringValidationSchema = new StringValidationSchema();
|
|
145
|
+
objectSchema.of({ name: stringValidationSchema });
|
|
146
|
+
|
|
147
|
+
const validationResult = objectSchema.validate({ name: 123 }, { path: ["test"] });
|
|
148
|
+
|
|
149
|
+
expect(validationResult.isValid).toBe(false);
|
|
150
|
+
expect(validationResult.errors.length).toBeGreaterThan(0);
|
|
151
|
+
expect(validationResult.errors[0].path).toEqual(["test", "name"]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("does not validate shape when no shape is set", () => {
|
|
155
|
+
const valueWithArbitraryKeys: TestObject = {
|
|
156
|
+
name: "string",
|
|
157
|
+
count: 123,
|
|
158
|
+
flag: true,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const validationResult = objectSchema.validate(valueWithArbitraryKeys, {
|
|
162
|
+
path: ["test"],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(validationResult.isValid).toBe(true);
|
|
166
|
+
expect(validationResult.errors).toHaveLength(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("validates nested shape with multiple keys", () => {
|
|
170
|
+
const stringValidationSchema = new StringValidationSchema();
|
|
171
|
+
const numberValidationSchema = new NumberValidationSchema();
|
|
172
|
+
objectSchema.of({ name: stringValidationSchema, age: numberValidationSchema });
|
|
173
|
+
|
|
174
|
+
const validationResult = objectSchema.validate({ name: "Jane", age: 30 }, { path: ["user"] });
|
|
175
|
+
|
|
176
|
+
expect(validationResult.isValid).toBe(true);
|
|
177
|
+
expect(validationResult.errors).toHaveLength(0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("collects errors for multiple invalid shape properties", () => {
|
|
181
|
+
const stringValidationSchema = new StringValidationSchema();
|
|
182
|
+
const numberValidationSchema = new NumberValidationSchema();
|
|
183
|
+
objectSchema.of({ name: stringValidationSchema, age: numberValidationSchema });
|
|
184
|
+
|
|
185
|
+
const validationResult = objectSchema.validate({ name: 123, age: "not a number" }, { path: ["user"] });
|
|
186
|
+
|
|
187
|
+
expect(validationResult.isValid).toBe(false);
|
|
188
|
+
expect(validationResult.errors).toHaveLength(2);
|
|
189
|
+
expect(validationResult.errors.map((error) => error.path)).toContainEqual(["user", "name"]);
|
|
190
|
+
expect(validationResult.errors.map((error) => error.path)).toContainEqual(["user", "age"]);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("integration with validation rules", () => {
|
|
195
|
+
it("uses validation rule for count validation", () => {
|
|
196
|
+
const countValidationRuleMock = {
|
|
197
|
+
ruleType: RuleType.COUNT,
|
|
198
|
+
settings: { min: 2, max: 4 },
|
|
199
|
+
id: "count-rule",
|
|
200
|
+
prn: PRN.fromString("prn:plate:-1:cms:validation-rule:1"),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const objectSchemaWithCountRule = new ObjectValidationSchema<TestObject>(countValidationRuleMock);
|
|
204
|
+
const validationResult = objectSchemaWithCountRule.validate({ alpha: 1, beta: 2, gamma: 3 }, { path: ["test"] });
|
|
205
|
+
|
|
206
|
+
expect(validationResult.isValid).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("method chaining", () => {
|
|
211
|
+
it("supports method chaining", () => {
|
|
212
|
+
const stringValidationSchema = new StringValidationSchema();
|
|
213
|
+
|
|
214
|
+
const validationResult = objectSchema
|
|
215
|
+
.min(1)
|
|
216
|
+
.max(5)
|
|
217
|
+
.of({ key: stringValidationSchema })
|
|
218
|
+
.validate({ key: "value" }, { path: ["test"] });
|
|
219
|
+
|
|
220
|
+
expect(validationResult.isValid).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { RuleType } from "../enums/rule-types.enum";
|
|
2
|
+
import { validateWithFunction } from "../helpers/validate-with-function";
|
|
3
|
+
import { RuleInstance } from "../types/rule-instance.interface";
|
|
4
|
+
import { ValidationContext } from "../types/validation-context.interface";
|
|
5
|
+
import { ValidationErrorData } from "../types/validation-error-data.interface";
|
|
6
|
+
import { SchemaType, ValidationSchema } from "../types/validation-schema.interface";
|
|
7
|
+
import { CountValidationRuleSettings, validateKeysCount } from "../validation-rules/count.validation-rule";
|
|
8
|
+
import { BaseValidationSchema } from "./base.schema";
|
|
9
|
+
|
|
10
|
+
export type Shape = Record<string, ValidationSchema<SchemaType>>;
|
|
11
|
+
|
|
12
|
+
export class ObjectValidationSchema<T = unknown> extends BaseValidationSchema<T, ObjectValidationSchema<T>> {
|
|
13
|
+
public readonly type = "object";
|
|
14
|
+
private _shape?: Shape;
|
|
15
|
+
private _min?: number;
|
|
16
|
+
private _max?: number;
|
|
17
|
+
|
|
18
|
+
public of(shape: Shape): this {
|
|
19
|
+
this._shape = shape;
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public min(min: number): this {
|
|
24
|
+
this._min = min;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public max(max: number): this {
|
|
29
|
+
this._max = max;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected _typeCheck(value: unknown, path: string[]): ValidationErrorData[] {
|
|
34
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
path,
|
|
41
|
+
message: this._message ?? "Value must be an object",
|
|
42
|
+
provided: value,
|
|
43
|
+
validationRule: {
|
|
44
|
+
ruleType: RuleType.VALUE_TYPE,
|
|
45
|
+
settings: {},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
protected _validateAgainstSchema(value: T, context: ValidationContext = {}): ValidationErrorData[] {
|
|
52
|
+
const errors: ValidationErrorData[] = [];
|
|
53
|
+
|
|
54
|
+
const itemsCountError = this._validateCount(value, context.path ?? []);
|
|
55
|
+
if (itemsCountError) {
|
|
56
|
+
errors.push(itemsCountError);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
errors.push(...this._validateObject(value, context));
|
|
60
|
+
|
|
61
|
+
return errors;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private _validateCount(value: T, path: string[]): ValidationErrorData<CountValidationRuleSettings> | null {
|
|
65
|
+
if (this._min === undefined && this._max === undefined && this._validationRule === undefined) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const validationRule = this._validationRule ?? {
|
|
70
|
+
ruleType: RuleType.COUNT,
|
|
71
|
+
settings: {
|
|
72
|
+
min: this._min,
|
|
73
|
+
max: this._max,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return validateWithFunction<CountValidationRuleSettings, T>(
|
|
78
|
+
value,
|
|
79
|
+
validationRule as RuleInstance<CountValidationRuleSettings>,
|
|
80
|
+
path,
|
|
81
|
+
this._message ?? "Object keys count validation failed",
|
|
82
|
+
validateKeysCount,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private _validateObject(value: T, context: ValidationContext): ValidationErrorData[] {
|
|
87
|
+
if (this._shape === undefined) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const indexableValue = value as unknown as Record<string, unknown>;
|
|
92
|
+
|
|
93
|
+
const errors: ValidationErrorData[] = [];
|
|
94
|
+
for (const key of Object.keys(this._shape)) {
|
|
95
|
+
const result = this._shape[key].validate(indexableValue[key], {
|
|
96
|
+
...context,
|
|
97
|
+
path: [...(context.path ?? []), key],
|
|
98
|
+
});
|
|
99
|
+
if (!result.isValid) {
|
|
100
|
+
errors.push(...result.errors);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return errors;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ValidationContext } from "./validation-context.interface";
|
|
2
2
|
import { ValidationResult } from "./validation-result.interface";
|
|
3
3
|
|
|
4
|
-
export type SchemaType = "array" | "content-value" | "date" | "number" | "string" | "union";
|
|
4
|
+
export type SchemaType = "array" | "content-value" | "date" | "number" | "object" | "string" | "union";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* TType represents the value type this schema validates.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PRN } from "@platecms/delta-plate-resource-notation";
|
|
2
2
|
import { ContentValueType } from "@platecms/delta-types";
|
|
3
3
|
import { AllowedValuesValidationRule } from "./allowed-values.validation-rule";
|
|
4
|
+
import { ValidationRuleUpdateImpact } from "../enums/validation-rule-update-impact.enum";
|
|
4
5
|
|
|
5
6
|
describe("AllowedValuesValidationRule", () => {
|
|
6
7
|
let rulePrn: PRN;
|
|
@@ -54,4 +55,28 @@ describe("AllowedValuesValidationRule", () => {
|
|
|
54
55
|
});
|
|
55
56
|
});
|
|
56
57
|
});
|
|
58
|
+
|
|
59
|
+
describe("classifyUpdate", () => {
|
|
60
|
+
let rule: AllowedValuesValidationRule;
|
|
61
|
+
const allowedValues = [1, "2", true, new Date("2020-01-01"), PRN.fromString("prn:plate:-1:cms:content-item:1")];
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
rule = new AllowedValuesValidationRule(rulePrn, { allowedValues });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it.each([
|
|
68
|
+
[ValidationRuleUpdateImpact.NONE, { allowedValues: [...allowedValues, "a"] }], // extending
|
|
69
|
+
[ValidationRuleUpdateImpact.NONE, { allowedValues: [...allowedValues] }], // unchanged
|
|
70
|
+
[ValidationRuleUpdateImpact.INVALIDATES, { allowedValues: ["a"] }], // tightening
|
|
71
|
+
])("returns %s when the new allowed values are %s", (expected, newSettings) => {
|
|
72
|
+
expect(rule.classifyUpdate(newSettings)).toBe(expected);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("classifyCreate", () => {
|
|
77
|
+
it("returns INVALIDATES", () => {
|
|
78
|
+
const rule = new AllowedValuesValidationRule(rulePrn, { allowedValues: [] });
|
|
79
|
+
expect(rule.classifyCreate()).toBe(ValidationRuleUpdateImpact.INVALIDATES);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
57
82
|
});
|
|
@@ -5,6 +5,8 @@ import { RuleInstance } from "../types/rule-instance.interface";
|
|
|
5
5
|
import { RuleType } from "../enums/rule-types.enum";
|
|
6
6
|
import { BaseValidationRule, ContentValidationResultType } from "./base.validation-rule";
|
|
7
7
|
import { validationFunctionFromValidateValue } from "./validation-rule-function.factory";
|
|
8
|
+
import { ValidationRuleUpdateImpact } from "../enums/validation-rule-update-impact.enum";
|
|
9
|
+
import { isSuperset } from "../helpers/is-superset.helper";
|
|
8
10
|
|
|
9
11
|
export function validateValueAllowedValues(
|
|
10
12
|
value: ContentValueType,
|
|
@@ -46,6 +48,19 @@ export class AllowedValuesValidationRule extends BaseValidationRule<AllowedValue
|
|
|
46
48
|
public override validate(values: ContentValueType[]): ContentValidationResultType[] {
|
|
47
49
|
return validateAllowedValues(values, this);
|
|
48
50
|
}
|
|
51
|
+
|
|
52
|
+
public override classifyCreate(): ValidationRuleUpdateImpact {
|
|
53
|
+
return ValidationRuleUpdateImpact.INVALIDATES;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public override classifyUpdate(newSettings: AllowedValuesValidationRuleSettings): ValidationRuleUpdateImpact {
|
|
57
|
+
const oldAllowedValues = this.settings.allowedValues;
|
|
58
|
+
const newAllowedValues = newSettings.allowedValues;
|
|
59
|
+
|
|
60
|
+
return isSuperset(newAllowedValues, oldAllowedValues)
|
|
61
|
+
? ValidationRuleUpdateImpact.NONE
|
|
62
|
+
: ValidationRuleUpdateImpact.INVALIDATES;
|
|
63
|
+
}
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
export interface AllowedValuesValidationRuleSettings {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { PRN } from "@platecms/delta-plate-resource-notation";
|
|
2
2
|
import { ContentValueType } from "@platecms/delta-types";
|
|
3
|
+
import { RuleType } from "../enums/rule-types.enum";
|
|
4
|
+
import { ValidationRuleUpdateImpact } from "../enums/validation-rule-update-impact.enum";
|
|
3
5
|
import { ValidationErrorDetails } from "../errors/validation-error-details";
|
|
4
6
|
import { RuleInstance } from "../types/rule-instance.interface";
|
|
5
|
-
import { RuleType } from "../enums/rule-types.enum";
|
|
6
7
|
|
|
7
8
|
export type ContentValidationResultType<TRuleInstance extends RuleInstance<unknown> = RuleInstance<unknown>> =
|
|
8
9
|
| ValidationErrorDetails<TRuleInstance>
|
|
@@ -29,7 +30,23 @@ export abstract class BaseValidationRule<TSettings> implements RuleInstance<TSet
|
|
|
29
30
|
* and the result will be returned in the first element of the array. E.g. the CountValidationRule.
|
|
30
31
|
* @param values ContentValueType[] to validate
|
|
31
32
|
*/
|
|
32
|
-
public validate(_: ContentValueType[]): ContentValidationResultType[]
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
public abstract validate(_: ContentValueType[]): ContentValidationResultType[];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Classifies the impact of creating a validation rule.
|
|
37
|
+
* - {@link ValidationRuleUpdateImpact.INVALIDATES}: Some existing data may become invalid but can be fixed (e.g. adding constraints); set owners to draft.
|
|
38
|
+
* - {@link ValidationRuleUpdateImpact.BREAKS}: Only for VALUE_TYPE; all current values become invalid (e.g. adding type constraint); clear values and set owners to draft.
|
|
39
|
+
* @returns The impact of the create.
|
|
40
|
+
*/
|
|
41
|
+
public abstract classifyCreate(): ValidationRuleUpdateImpact;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Classifies the impact of updating a validation rule's settings.
|
|
45
|
+
* - {@link ValidationRuleUpdateImpact.NONE}: Update does not make existing data invalid (e.g. loosening constraints).
|
|
46
|
+
* - {@link ValidationRuleUpdateImpact.INVALIDATES}: Some existing data may become invalid but can be fixed (e.g. tightening constraints); set owners to draft.
|
|
47
|
+
* - {@link ValidationRuleUpdateImpact.BREAKS}: Only for VALUE_TYPE; all current values become invalid (e.g. changing type); clear values and set owners to draft.
|
|
48
|
+
* @param newSettings - The new settings to classify the update impact for.
|
|
49
|
+
* @returns The impact of the update.
|
|
50
|
+
*/
|
|
51
|
+
public abstract classifyUpdate(_: TSettings): ValidationRuleUpdateImpact;
|
|
35
52
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PRN } from "@platecms/delta-plate-resource-notation";
|
|
2
2
|
import { InvalidValidationRuleSettingsError } from "../errors/invalid-validation-rule-settings.error";
|
|
3
3
|
import { CountValidationRule } from "./count.validation-rule";
|
|
4
|
+
import { ValidationRuleUpdateImpact } from "../enums/validation-rule-update-impact.enum";
|
|
4
5
|
|
|
5
6
|
describe("CountValidationRule", () => {
|
|
6
7
|
const rulePrn = PRN.fromString("prn:plate:-1:cms:validation-rule:1");
|
|
@@ -64,4 +65,41 @@ describe("CountValidationRule", () => {
|
|
|
64
65
|
});
|
|
65
66
|
});
|
|
66
67
|
});
|
|
68
|
+
|
|
69
|
+
describe("classifyUpdate", () => {
|
|
70
|
+
it.each([
|
|
71
|
+
// OLD: { min: 1, max: 5 }
|
|
72
|
+
[ValidationRuleUpdateImpact.NONE, { min: 1, max: 5 }, { min: 1, max: 6 }], // widen upper bound
|
|
73
|
+
[ValidationRuleUpdateImpact.NONE, { min: 1, max: 5 }, { min: 0, max: 5 }], // widen lower bound
|
|
74
|
+
[ValidationRuleUpdateImpact.NONE, { min: 1, max: 5 }, { min: 0, max: 6 }], // widen both bounds
|
|
75
|
+
[ValidationRuleUpdateImpact.NONE, { min: 1, max: 5 }, { min: 1 }], // remove upper bound (→ Infinity)
|
|
76
|
+
[ValidationRuleUpdateImpact.NONE, { min: 1, max: 5 }, { max: 5 }], // remove lower bound (→ 0)
|
|
77
|
+
[ValidationRuleUpdateImpact.INVALIDATES, { min: 1, max: 5 }, { min: 0, max: 4 }], // narrow upper bound
|
|
78
|
+
[ValidationRuleUpdateImpact.INVALIDATES, { min: 1, max: 5 }, { min: 2, max: 5 }], // narrow lower bound
|
|
79
|
+
[ValidationRuleUpdateImpact.INVALIDATES, { min: 1, max: 5 }, { min: 2, max: 4 }], // narrow both bounds
|
|
80
|
+
[ValidationRuleUpdateImpact.NONE, { min: 1, max: 5 }, { min: 1, max: 5 }], // unchanged
|
|
81
|
+
|
|
82
|
+
// OLD: { min: 1 } (max = Infinity)
|
|
83
|
+
[ValidationRuleUpdateImpact.NONE, { min: 1 }, { min: 0 }], // widen lower bound
|
|
84
|
+
[ValidationRuleUpdateImpact.INVALIDATES, { min: 1 }, { min: 2 }], // narrow lower bound
|
|
85
|
+
[ValidationRuleUpdateImpact.INVALIDATES, { min: 1 }, { min: 1, max: 5 }], // introduce upper bound (tightening)
|
|
86
|
+
[ValidationRuleUpdateImpact.NONE, { min: 1 }, {}], // remove lower bound (→ 0)
|
|
87
|
+
|
|
88
|
+
// OLD: { max: 5 } (min = 0)
|
|
89
|
+
[ValidationRuleUpdateImpact.NONE, { max: 5 }, { max: 6 }], // widen upper bound
|
|
90
|
+
[ValidationRuleUpdateImpact.INVALIDATES, { max: 5 }, { max: 4 }], // narrow upper bound
|
|
91
|
+
[ValidationRuleUpdateImpact.INVALIDATES, { max: 5 }, { min: 1, max: 5 }], // introduce lower bound (tightening)
|
|
92
|
+
[ValidationRuleUpdateImpact.NONE, { max: 5 }, {}], // remove upper bound (→ Infinity)
|
|
93
|
+
])("returns %s when old settings are %s and new settings are %s", (expected, oldSettings, newSettings) => {
|
|
94
|
+
const rule = new CountValidationRule(rulePrn, oldSettings);
|
|
95
|
+
expect(rule.classifyUpdate(newSettings)).toBe(expected);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("classifyCreate", () => {
|
|
100
|
+
it("returns INVALIDATES", () => {
|
|
101
|
+
const rule = new CountValidationRule(rulePrn, { min: 1, max: 3 });
|
|
102
|
+
expect(rule.classifyCreate()).toBe(ValidationRuleUpdateImpact.INVALIDATES);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
67
105
|
});
|
|
@@ -5,6 +5,7 @@ import { ValidationErrorDetails } from "../errors/validation-error-details";
|
|
|
5
5
|
import { RuleInstance } from "../types/rule-instance.interface";
|
|
6
6
|
import { RuleType } from "../enums/rule-types.enum";
|
|
7
7
|
import { BaseValidationRule, ContentValidationResultType } from "./base.validation-rule";
|
|
8
|
+
import { ValidationRuleUpdateImpact } from "../enums/validation-rule-update-impact.enum";
|
|
8
9
|
|
|
9
10
|
export function validateValuesCount<T>(
|
|
10
11
|
values: T[],
|
|
@@ -25,6 +26,27 @@ export function validateValuesCount<T>(
|
|
|
25
26
|
return true;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
export function validateKeysCount<T>(
|
|
30
|
+
value: T,
|
|
31
|
+
ruleInstance: RuleInstance<CountValidationRuleSettings>,
|
|
32
|
+
): ContentValidationResultType {
|
|
33
|
+
const indexableValue = value as unknown as Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
if (ruleInstance.settings.min != null && Object.keys(indexableValue).length < ruleInstance.settings.min) {
|
|
36
|
+
return new ValidationErrorDetails(
|
|
37
|
+
`The amount of provided object keys must be greater than or equal ${ruleInstance.settings.min}.`,
|
|
38
|
+
ruleInstance,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (ruleInstance.settings.max != null && Object.keys(indexableValue).length > ruleInstance.settings.max) {
|
|
42
|
+
return new ValidationErrorDetails(
|
|
43
|
+
`The amount of provided object keys must be be less than or equal ${ruleInstance.settings.max}.`,
|
|
44
|
+
ruleInstance,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
/**
|
|
29
51
|
* Wrapper for validateValuesCount that wraps the result in an array to keep the same interface as the other validation functions.
|
|
30
52
|
*/
|
|
@@ -52,6 +74,19 @@ export class CountValidationRule extends BaseValidationRule<CountValidationRuleS
|
|
|
52
74
|
public override validate(value: ContentValueType[]): ContentValidationResultType[] {
|
|
53
75
|
return validateCount(value, this);
|
|
54
76
|
}
|
|
77
|
+
|
|
78
|
+
public override classifyCreate(): ValidationRuleUpdateImpact {
|
|
79
|
+
return ValidationRuleUpdateImpact.INVALIDATES;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public override classifyUpdate(newSettings: CountValidationRuleSettings): ValidationRuleUpdateImpact {
|
|
83
|
+
const oldSettings = this.settings;
|
|
84
|
+
const [oldMin, oldMax] = [oldSettings.min ?? 0, oldSettings.max ?? Infinity];
|
|
85
|
+
const [newMin, newMax] = [newSettings.min ?? 0, newSettings.max ?? Infinity];
|
|
86
|
+
const isLoosening = newMin <= oldMin && newMax >= oldMax;
|
|
87
|
+
|
|
88
|
+
return isLoosening ? ValidationRuleUpdateImpact.NONE : ValidationRuleUpdateImpact.INVALIDATES;
|
|
89
|
+
}
|
|
55
90
|
}
|
|
56
91
|
|
|
57
92
|
export interface CountValidationRuleSettings {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PRN } from "@platecms/delta-plate-resource-notation";
|
|
2
|
+
import { ValidationRuleUpdateImpact } from "../enums/validation-rule-update-impact.enum";
|
|
2
3
|
import { InvalidValidationRuleSettingsError } from "../errors/invalid-validation-rule-settings.error";
|
|
3
4
|
import { DateBetweenValidationRule } from "./date-between.validation-rule";
|
|
4
5
|
|
|
@@ -74,4 +75,86 @@ describe("DateBetweenValidationRule", () => {
|
|
|
74
75
|
});
|
|
75
76
|
});
|
|
76
77
|
});
|
|
78
|
+
|
|
79
|
+
describe("classifyUpdate", () => {
|
|
80
|
+
// eslint-disable-next-line func-style, id-length
|
|
81
|
+
const d = (iso: string): Date => new Date(iso);
|
|
82
|
+
|
|
83
|
+
it.each([
|
|
84
|
+
// OLD: bounded range
|
|
85
|
+
[
|
|
86
|
+
ValidationRuleUpdateImpact.NONE,
|
|
87
|
+
{ start: d("2025-01-10"), end: d("2025-01-20") },
|
|
88
|
+
{ start: d("2025-01-10"), end: d("2025-01-21") },
|
|
89
|
+
], // widen end
|
|
90
|
+
[
|
|
91
|
+
ValidationRuleUpdateImpact.NONE,
|
|
92
|
+
{ start: d("2025-01-10"), end: d("2025-01-20") },
|
|
93
|
+
{ start: d("2025-01-09"), end: d("2025-01-20") },
|
|
94
|
+
], // widen start (earlier)
|
|
95
|
+
[
|
|
96
|
+
ValidationRuleUpdateImpact.NONE,
|
|
97
|
+
{ start: d("2025-01-10"), end: d("2025-01-20") },
|
|
98
|
+
{ start: d("2025-01-09"), end: d("2025-01-21") },
|
|
99
|
+
], // widen both
|
|
100
|
+
|
|
101
|
+
[ValidationRuleUpdateImpact.NONE, { start: d("2025-01-10"), end: d("2025-01-20") }, { start: d("2025-01-10") }], // remove end (→ Infinity)
|
|
102
|
+
[ValidationRuleUpdateImpact.NONE, { start: d("2025-01-10"), end: d("2025-01-20") }, { end: d("2025-01-20") }], // remove start (→ -Infinity)
|
|
103
|
+
[ValidationRuleUpdateImpact.NONE, { start: d("2025-01-10"), end: d("2025-01-20") }, {}], // remove both
|
|
104
|
+
[
|
|
105
|
+
ValidationRuleUpdateImpact.INVALIDATES,
|
|
106
|
+
{ start: d("2025-01-10"), end: d("2025-01-20") },
|
|
107
|
+
{ start: d("2025-01-10"), end: d("2025-01-19") },
|
|
108
|
+
], // narrow end (earlier)
|
|
109
|
+
[
|
|
110
|
+
ValidationRuleUpdateImpact.INVALIDATES,
|
|
111
|
+
{ start: d("2025-01-10"), end: d("2025-01-20") },
|
|
112
|
+
{ start: d("2025-01-11"), end: d("2025-01-20") },
|
|
113
|
+
], // narrow start (later)
|
|
114
|
+
[
|
|
115
|
+
ValidationRuleUpdateImpact.INVALIDATES,
|
|
116
|
+
{ start: d("2025-01-10"), end: d("2025-01-20") },
|
|
117
|
+
{ start: d("2025-01-11"), end: d("2025-01-19") },
|
|
118
|
+
], // narrow both
|
|
119
|
+
// unchanged
|
|
120
|
+
[
|
|
121
|
+
ValidationRuleUpdateImpact.NONE,
|
|
122
|
+
{ start: d("2025-01-10"), end: d("2025-01-20") },
|
|
123
|
+
{ start: d("2025-01-10"), end: d("2025-01-20") },
|
|
124
|
+
], // unchanged
|
|
125
|
+
|
|
126
|
+
// OLD: start-only (end = Infinity)
|
|
127
|
+
[ValidationRuleUpdateImpact.NONE, { start: d("2025-01-10") }, { start: d("2025-01-09") }], // widen start (earlier)
|
|
128
|
+
[ValidationRuleUpdateImpact.INVALIDATES, { start: d("2025-01-10") }, { start: d("2025-01-11") }], // narrow start (later)
|
|
129
|
+
[
|
|
130
|
+
ValidationRuleUpdateImpact.INVALIDATES,
|
|
131
|
+
{ start: d("2025-01-10") },
|
|
132
|
+
{ start: d("2025-01-10"), end: d("2025-01-20") },
|
|
133
|
+
], // introduce end (tightening)
|
|
134
|
+
[ValidationRuleUpdateImpact.NONE, { start: d("2025-01-10") }, {}], // remove start (→ -Infinity)
|
|
135
|
+
|
|
136
|
+
// OLD: end-only (start = -Infinity)
|
|
137
|
+
[ValidationRuleUpdateImpact.NONE, { end: d("2025-01-20") }, { end: d("2025-01-21") }], // widen end (later)
|
|
138
|
+
[ValidationRuleUpdateImpact.INVALIDATES, { end: d("2025-01-20") }, { end: d("2025-01-19") }], // narrow end (earlier)
|
|
139
|
+
[
|
|
140
|
+
ValidationRuleUpdateImpact.INVALIDATES,
|
|
141
|
+
{ end: d("2025-01-20") },
|
|
142
|
+
{ start: d("2025-01-10"), end: d("2025-01-20") },
|
|
143
|
+
], // introduce start (tightening)
|
|
144
|
+
[ValidationRuleUpdateImpact.NONE, { end: d("2025-01-20") }, {}], // remove end (→ Infinity)
|
|
145
|
+
])("returns %s when old settings are %s and new settings are %s", (expected, oldSettings, newSettings) => {
|
|
146
|
+
const rule = new DateBetweenValidationRule(rulePrn, oldSettings);
|
|
147
|
+
expect(rule.classifyUpdate(newSettings)).toBe(expected);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("classifyCreate", () => {
|
|
152
|
+
it("returns INVALIDATES", () => {
|
|
153
|
+
const rule = new DateBetweenValidationRule(rulePrn, {
|
|
154
|
+
start: new Date(2020, 1, 2, 3, 4, 5),
|
|
155
|
+
end: new Date(2020, 1, 3, 3, 4, 6),
|
|
156
|
+
});
|
|
157
|
+
expect(rule.classifyCreate()).toBe(ValidationRuleUpdateImpact.INVALIDATES);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
77
160
|
});
|
|
@@ -6,6 +6,7 @@ import { RuleInstance } from "../types/rule-instance.interface";
|
|
|
6
6
|
import { RuleType } from "../enums/rule-types.enum";
|
|
7
7
|
import { BaseValidationRule, ContentValidationResultType } from "./base.validation-rule";
|
|
8
8
|
import { validationFunctionFromValidateValue } from "./validation-rule-function.factory";
|
|
9
|
+
import { ValidationRuleUpdateImpact } from "../enums/validation-rule-update-impact.enum";
|
|
9
10
|
|
|
10
11
|
export function validateValueDateBetween(
|
|
11
12
|
value: ContentValueType,
|
|
@@ -46,6 +47,20 @@ export class DateBetweenValidationRule extends BaseValidationRule<DateBetweenVal
|
|
|
46
47
|
public override validate(values: ContentValueType[]): ContentValidationResultType[] {
|
|
47
48
|
return validateDateBetween(values, this);
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
public override classifyCreate(): ValidationRuleUpdateImpact {
|
|
52
|
+
return ValidationRuleUpdateImpact.INVALIDATES;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public override classifyUpdate(newSettings: DateBetweenValidationRuleSettings): ValidationRuleUpdateImpact {
|
|
56
|
+
const oldStart = this.settings.start ? this.settings.start.getTime() : -Infinity;
|
|
57
|
+
const oldEnd = this.settings.end ? this.settings.end.getTime() : Infinity;
|
|
58
|
+
const newStart = newSettings.start ? newSettings.start.getTime() : -Infinity;
|
|
59
|
+
const newEnd = newSettings.end ? newSettings.end.getTime() : Infinity;
|
|
60
|
+
const extending = newStart <= oldStart && newEnd >= oldEnd;
|
|
61
|
+
|
|
62
|
+
return extending ? ValidationRuleUpdateImpact.NONE : ValidationRuleUpdateImpact.INVALIDATES;
|
|
63
|
+
}
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
export interface DateBetweenValidationRuleSettings {
|