@platecms/delta-validation 0.13.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/rule-to-schema.spec.ts +4 -4
- package/src/lib/helpers/schema-builder.ts +5 -0
- package/src/lib/helpers/validate-with-function.spec.ts +3 -3
- package/src/lib/schemas/array.schema.spec.ts +1 -1
- package/src/lib/schemas/content-value.schema.spec.ts +5 -5
- package/src/lib/schemas/content-value.schema.ts +27 -2
- package/src/lib/schemas/date.schema.spec.ts +1 -1
- package/src/lib/schemas/object.schema.spec.ts +223 -0
- package/src/lib/schemas/object.schema.ts +106 -0
- package/src/lib/schemas/string.schema.spec.ts +1 -1
- package/src/lib/types/validation-schema.interface.ts +1 -1
- package/src/lib/validation-rules/allowed-values.validation-rule.spec.ts +30 -5
- 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 +39 -1
- package/src/lib/validation-rules/count.validation-rule.ts +35 -0
- package/src/lib/validation-rules/date-between.validation-rule.spec.ts +84 -1
- package/src/lib/validation-rules/date-between.validation-rule.ts +19 -4
- package/src/lib/validation-rules/decimal-count.validation-rule.spec.ts +27 -1
- package/src/lib/validation-rules/decimal-count.validation-rule.ts +12 -0
- package/src/lib/validation-rules/number-between.validation-rule.spec.ts +45 -1
- package/src/lib/validation-rules/number-between.validation-rule.ts +14 -0
- package/src/lib/validation-rules/relatable-content-types.validation-rule.spec.ts +38 -13
- package/src/lib/validation-rules/relatable-content-types.validation-rule.ts +17 -0
- package/src/lib/validation-rules/string-format.validation-rule.spec.ts +48 -22
- package/src/lib/validation-rules/string-format.validation-rule.ts +14 -0
- package/src/lib/validation-rules/value-type.validation-rule.spec.ts +105 -75
- package/src/lib/validation-rules/value-type.validation-rule.ts +17 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platecms/delta-validation",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "This package contains the validation system for Plate.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"src/**/*"
|
|
18
18
|
],
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@platecms/delta-cast": "
|
|
21
|
-
"@platecms/delta-plate-resource-notation": "
|
|
22
|
-
"@platecms/delta-types": "
|
|
20
|
+
"@platecms/delta-cast": "1.3.0",
|
|
21
|
+
"@platecms/delta-plate-resource-notation": "1.3.0",
|
|
22
|
+
"@platecms/delta-types": "1.3.0",
|
|
23
23
|
"class-transformer": "0.5.1",
|
|
24
24
|
"reflect-metadata": "0.2.2",
|
|
25
25
|
"tslib": "2.8.1"
|
package/src/index.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
export * from "./lib/enums/rule-types.enum";
|
|
2
|
+
export * from "./lib/enums/validation-rule-update-impact.enum";
|
|
1
3
|
export * from "./lib/errors/invalid-validation-arguments.error";
|
|
2
4
|
export * from "./lib/errors/invalid-validation-rule-settings.error";
|
|
3
|
-
export * from "./lib/errors/validation.error";
|
|
4
5
|
export * from "./lib/errors/validation-error-details";
|
|
6
|
+
export * from "./lib/errors/validation.error";
|
|
5
7
|
export * from "./lib/helpers/deserialize-validation-rule";
|
|
6
|
-
export * from "./lib/
|
|
7
|
-
export type * from "./lib/types/existing-content-validation-rules-settings.type";
|
|
8
|
+
export * from "./lib/helpers/fields-to-schema";
|
|
8
9
|
export * from "./lib/helpers/rule-to-schema";
|
|
9
10
|
export * from "./lib/helpers/schema-builder";
|
|
11
|
+
export type * from "./lib/types/existing-content-validation-rules-settings.type";
|
|
12
|
+
export type * from "./lib/types/existing-content-validation-rules.type";
|
|
10
13
|
export type * from "./lib/types/existing-validation-schemas.type";
|
|
14
|
+
export type * from "./lib/types/rule-instance.interface";
|
|
15
|
+
export type * from "./lib/types/validation-context.interface";
|
|
11
16
|
export type * from "./lib/types/validation-error-data.interface";
|
|
12
17
|
export type * from "./lib/types/validation-result.interface";
|
|
13
|
-
export type * from "./lib/types/validation-context.interface";
|
|
14
|
-
export type * from "./lib/types/rule-instance.interface";
|
|
15
|
-
export * from "./lib/enums/rule-types.enum";
|
|
16
|
-
export type * from "./lib/types/existing-content-validation-rules.type";
|
|
17
18
|
export * from "./lib/validation-rules/allowed-values.validation-rule";
|
|
18
19
|
export * from "./lib/validation-rules/base.validation-rule";
|
|
19
20
|
export * from "./lib/validation-rules/count.validation-rule";
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { PRN } from "@platecms/delta-plate-resource-notation";
|
|
2
|
+
import { ContentValueTypeNames } from "@platecms/delta-types";
|
|
3
|
+
import { RuleType } from "../enums/rule-types.enum";
|
|
4
|
+
import { ObjectValidationSchema } from "../schemas/object.schema";
|
|
5
|
+
import { FieldToValidate, fieldsToSchema } from "./fields-to-schema";
|
|
6
|
+
import { RuleInstance } from "../types/rule-instance.interface";
|
|
7
|
+
import { ValidationResult } from "../types/validation-result.interface";
|
|
8
|
+
|
|
9
|
+
describe("fieldsToSchema", () => {
|
|
10
|
+
const mockPrn = PRN.fromString("prn:plate:-1:cms:validation-rule:1");
|
|
11
|
+
|
|
12
|
+
function mkField(slug: string, rules: RuleInstance<unknown>[] = []): FieldToValidate {
|
|
13
|
+
return {
|
|
14
|
+
slug,
|
|
15
|
+
contentValidationRules: rules.map((rule) => ({ ...rule, prn: rule.prn ?? mockPrn })),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validateOk(schema: ObjectValidationSchema, value: unknown): ValidationResult {
|
|
20
|
+
const res = schema.validate(value, { path: [] });
|
|
21
|
+
expect(res.isValid).toBe(true);
|
|
22
|
+
expect(res.errors?.length ?? 0).toBe(0);
|
|
23
|
+
return res;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateFail(schema: ObjectValidationSchema, value: unknown): ValidationResult {
|
|
27
|
+
const res = schema.validate(value, { path: [] });
|
|
28
|
+
expect(res.isValid).toBe(false);
|
|
29
|
+
expect(res.errors?.length).toBeGreaterThan(0);
|
|
30
|
+
return res;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function expectError(res: ValidationResult, opts: { pathEndsWith?: (number | string)[]; ruleType?: RuleType }): void {
|
|
34
|
+
const err = res.errors[0];
|
|
35
|
+
if (opts.pathEndsWith) {
|
|
36
|
+
expect(err.path.slice(-opts.pathEndsWith.length)).toEqual(opts.pathEndsWith);
|
|
37
|
+
}
|
|
38
|
+
if (opts.ruleType !== undefined) {
|
|
39
|
+
expect(err.validationRule?.ruleType).toBe(opts.ruleType);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it("returns ObjectValidationSchema with type object", () => {
|
|
44
|
+
const schema = fieldsToSchema([]);
|
|
45
|
+
expect(schema).toBeInstanceOf(ObjectValidationSchema);
|
|
46
|
+
expect(schema.type).toBe("object");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("accepts empty object when contentFields is empty", () => {
|
|
50
|
+
validateOk(fieldsToSchema([]), {});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("treats missing field as empty array (if this is your intended contract)", () => {
|
|
54
|
+
// If your contract is different, flip this test accordingly.
|
|
55
|
+
const schema = fieldsToSchema([mkField("items", [])]);
|
|
56
|
+
validateOk(schema, {}); // missing key
|
|
57
|
+
validateOk(schema, { items: [] }); // explicit
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rejects non-array field values", () => {
|
|
61
|
+
const schema = fieldsToSchema([mkField("items", [])]);
|
|
62
|
+
const res = validateFail(schema, { items: "nope" });
|
|
63
|
+
expectError(res, { pathEndsWith: ["items"] });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("COUNT", () => {
|
|
67
|
+
const schema = fieldsToSchema([mkField("items", [{ ruleType: RuleType.COUNT, settings: { min: 1, max: 2 } }])]);
|
|
68
|
+
|
|
69
|
+
test.each([
|
|
70
|
+
[{ items: [] }, false],
|
|
71
|
+
[{ items: [1] }, true],
|
|
72
|
+
[{ items: [1, 2] }, true],
|
|
73
|
+
[{ items: [1, 2, 3] }, false],
|
|
74
|
+
])("COUNT %o => %s", (input, ok) => {
|
|
75
|
+
const res = schema.validate(input, { path: [] });
|
|
76
|
+
expect(res.isValid).toBe(ok);
|
|
77
|
+
if (!ok) {
|
|
78
|
+
expectError(res, { pathEndsWith: ["items"], ruleType: RuleType.COUNT });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("ALLOWED_VALUES", () => {
|
|
84
|
+
const schema = fieldsToSchema([
|
|
85
|
+
mkField("choice", [{ ruleType: RuleType.ALLOWED_VALUES, settings: { allowedValues: ["a", "b"] } }]),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
it("accepts allowed and rejects not allowed", () => {
|
|
89
|
+
validateOk(schema, { choice: ["a"] });
|
|
90
|
+
|
|
91
|
+
const res = validateFail(schema, { choice: ["x"] });
|
|
92
|
+
expectError(res, { pathEndsWith: ["choice", "0"], ruleType: RuleType.ALLOWED_VALUES });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("VALUE_TYPE", () => {
|
|
97
|
+
const schema = fieldsToSchema([
|
|
98
|
+
mkField("vals", [
|
|
99
|
+
{
|
|
100
|
+
ruleType: RuleType.VALUE_TYPE,
|
|
101
|
+
settings: { allowedTypes: [ContentValueTypeNames.STRING, ContentValueTypeNames.NUMBER] },
|
|
102
|
+
},
|
|
103
|
+
]),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
it("accepts allowed and rejects disallowed", () => {
|
|
107
|
+
validateOk(schema, { vals: ["ok", 1] });
|
|
108
|
+
|
|
109
|
+
const res = validateFail(schema, { vals: [true] });
|
|
110
|
+
// Depending on your validator, the error could be on vals[0] or vals.
|
|
111
|
+
expect(res.errors[0].validationRule?.ruleType).toBe(RuleType.VALUE_TYPE);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("STRING_FORMAT", () => {
|
|
116
|
+
const schema = fieldsToSchema([
|
|
117
|
+
mkField("emails", [{ ruleType: RuleType.STRING_FORMAT, settings: { allowedFormat: ".*@.*\\..*" } }]),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
it("accepts matching and rejects non-matching", () => {
|
|
121
|
+
validateOk(schema, { emails: ["a@b.co"] });
|
|
122
|
+
|
|
123
|
+
const res = validateFail(schema, { emails: ["invalid"] });
|
|
124
|
+
expectError(res, { pathEndsWith: ["emails", "0"], ruleType: RuleType.STRING_FORMAT });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("NUMBER_BETWEEN", () => {
|
|
129
|
+
const schema = fieldsToSchema([
|
|
130
|
+
mkField("scores", [{ ruleType: RuleType.NUMBER_BETWEEN, settings: { min: 0, max: 100 } }]),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
test.each([
|
|
134
|
+
[{ scores: [50] }, true],
|
|
135
|
+
[{ scores: [-1] }, false],
|
|
136
|
+
[{ scores: [101] }, false],
|
|
137
|
+
])("NUMBER_BETWEEN %o => %s", (input, ok) => {
|
|
138
|
+
const res = schema.validate(input, { path: [] });
|
|
139
|
+
expect(res.isValid).toBe(ok);
|
|
140
|
+
if (!ok) {
|
|
141
|
+
expectError(res, { ruleType: RuleType.NUMBER_BETWEEN });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("DECIMAL_COUNT", () => {
|
|
147
|
+
const schema = fieldsToSchema([mkField("amounts", [{ ruleType: RuleType.DECIMAL_COUNT, settings: { max: 2 } }])]);
|
|
148
|
+
|
|
149
|
+
it("accepts within max decimals and rejects above", () => {
|
|
150
|
+
validateOk(schema, { amounts: [3.14] });
|
|
151
|
+
|
|
152
|
+
const res = validateFail(schema, { amounts: [3.141] });
|
|
153
|
+
expectError(res, { ruleType: RuleType.DECIMAL_COUNT });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("DATE_BETWEEN", () => {
|
|
158
|
+
const start = new Date("2023-01-01");
|
|
159
|
+
const end = new Date("2023-12-31");
|
|
160
|
+
|
|
161
|
+
const schema = fieldsToSchema([mkField("dates", [{ ruleType: RuleType.DATE_BETWEEN, settings: { start, end } }])]);
|
|
162
|
+
|
|
163
|
+
it("accepts in range and rejects outside", () => {
|
|
164
|
+
validateOk(schema, { dates: [new Date("2023-06-01")] });
|
|
165
|
+
|
|
166
|
+
const res = validateFail(schema, { dates: [new Date("2024-01-01")] });
|
|
167
|
+
expectError(res, { ruleType: RuleType.DATE_BETWEEN });
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("multiple fields", () => {
|
|
172
|
+
const schema = fieldsToSchema([
|
|
173
|
+
mkField("tags", []),
|
|
174
|
+
mkField("count", [{ ruleType: RuleType.COUNT, settings: { min: 0, max: 1 } }]),
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
it("accepts valid and rejects invalid count with correct path", () => {
|
|
178
|
+
validateOk(schema, { tags: [], count: [1] });
|
|
179
|
+
|
|
180
|
+
const res = validateFail(schema, { tags: [], count: [1, 2] });
|
|
181
|
+
expectError(res, { pathEndsWith: ["count"], ruleType: RuleType.COUNT });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { PRN } from "@platecms/delta-plate-resource-notation";
|
|
2
|
+
import { ContentValueType, ContentValueTypeNames } from "@platecms/delta-types";
|
|
3
|
+
import { RuleType } from "../enums/rule-types.enum";
|
|
4
|
+
import { ArrayValidationSchema } from "../schemas/array.schema";
|
|
5
|
+
import { ContentValueValidationSchema } from "../schemas/content-value.schema";
|
|
6
|
+
import { DateValidationSchema } from "../schemas/date.schema";
|
|
7
|
+
import { NumberValidationSchema } from "../schemas/number.schema";
|
|
8
|
+
import { ObjectValidationSchema, Shape } from "../schemas/object.schema";
|
|
9
|
+
import { StringValidationSchema } from "../schemas/string.schema";
|
|
10
|
+
import { RuleInstance } from "../types/rule-instance.interface";
|
|
11
|
+
import { AllowedValuesValidationRuleSettings } from "../validation-rules/allowed-values.validation-rule";
|
|
12
|
+
import { CountValidationRuleSettings } from "../validation-rules/count.validation-rule";
|
|
13
|
+
import { DateBetweenValidationRuleSettings } from "../validation-rules/date-between.validation-rule";
|
|
14
|
+
import { DecimalCountValidationRuleSettings } from "../validation-rules/decimal-count.validation-rule";
|
|
15
|
+
import { NumberBetweenValidationRuleSettings } from "../validation-rules/number-between.validation-rule";
|
|
16
|
+
import { RelatableContentTypesValidationRuleSettings } from "../validation-rules/relatable-content-types.validation-rule";
|
|
17
|
+
import { StringFormatValidationRuleSettings } from "../validation-rules/string-format.validation-rule";
|
|
18
|
+
import { ValueTypeValidationRuleSettings } from "../validation-rules/value-type.validation-rule";
|
|
19
|
+
|
|
20
|
+
interface FieldToValidate {
|
|
21
|
+
slug: string;
|
|
22
|
+
contentValidationRules: RuleInstance<unknown>[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface FieldSchemaBuilder {
|
|
26
|
+
array: ArrayValidationSchema<ContentValueType>;
|
|
27
|
+
item: ContentValueValidationSchema;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createFieldBuilder(): FieldSchemaBuilder {
|
|
31
|
+
const item = new ContentValueValidationSchema();
|
|
32
|
+
const array = new ArrayValidationSchema<ContentValueType>().of(item);
|
|
33
|
+
return { array, item };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function applyRule(builder: FieldSchemaBuilder, rule: RuleInstance<unknown>): void {
|
|
37
|
+
switch (rule.ruleType) {
|
|
38
|
+
case RuleType.COUNT: {
|
|
39
|
+
const settings = rule.settings as CountValidationRuleSettings;
|
|
40
|
+
builder.array.min(settings.min ?? 0).max(settings.max ?? 0);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case RuleType.ALLOWED_VALUES: {
|
|
45
|
+
const settings = rule.settings as AllowedValuesValidationRuleSettings;
|
|
46
|
+
builder.item.allowedValues(settings.allowedValues);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case RuleType.VALUE_TYPE: {
|
|
51
|
+
const settings = rule.settings as ValueTypeValidationRuleSettings;
|
|
52
|
+
builder.item.allowedValueTypes(settings.allowedTypes);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case RuleType.RELATABLE_CONTENT_TYPES: {
|
|
57
|
+
const settings = rule.settings as RelatableContentTypesValidationRuleSettings<PRN>;
|
|
58
|
+
builder.item.allowedContentTypes(settings.allowedContentTypes);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case RuleType.STRING_FORMAT: {
|
|
63
|
+
const settings = rule.settings as StringFormatValidationRuleSettings;
|
|
64
|
+
builder.item.subSchema(
|
|
65
|
+
ContentValueTypeNames.STRING,
|
|
66
|
+
new StringValidationSchema(rule).format(settings.allowedFormat),
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case RuleType.NUMBER_BETWEEN: {
|
|
72
|
+
const settings = rule.settings as NumberBetweenValidationRuleSettings;
|
|
73
|
+
const num =
|
|
74
|
+
(builder.item.subSchemas[ContentValueTypeNames.NUMBER] as NumberValidationSchema | undefined) ??
|
|
75
|
+
new NumberValidationSchema();
|
|
76
|
+
|
|
77
|
+
if (settings.min !== undefined) {
|
|
78
|
+
num.min(settings.min);
|
|
79
|
+
}
|
|
80
|
+
if (settings.max !== undefined) {
|
|
81
|
+
num.max(settings.max);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
builder.item.subSchema(ContentValueTypeNames.NUMBER, num);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case RuleType.DECIMAL_COUNT: {
|
|
89
|
+
const settings = rule.settings as DecimalCountValidationRuleSettings;
|
|
90
|
+
const num =
|
|
91
|
+
(builder.item.subSchemas[ContentValueTypeNames.NUMBER] as NumberValidationSchema | undefined) ??
|
|
92
|
+
new NumberValidationSchema();
|
|
93
|
+
num.maxDecimals(settings.max);
|
|
94
|
+
|
|
95
|
+
builder.item.subSchema(ContentValueTypeNames.NUMBER, num);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case RuleType.DATE_BETWEEN: {
|
|
100
|
+
const settings = rule.settings as DateBetweenValidationRuleSettings;
|
|
101
|
+
const date = new DateValidationSchema(rule);
|
|
102
|
+
|
|
103
|
+
if (settings.start) {
|
|
104
|
+
date.start(settings.start);
|
|
105
|
+
}
|
|
106
|
+
if (settings.end) {
|
|
107
|
+
date.end(settings.end);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
builder.item.subSchema(ContentValueTypeNames.DATE, date);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generates an object validation schema based on the provided content fields and their associated validation rules.
|
|
117
|
+
*
|
|
118
|
+
* Iterates over each field, applies its content validation rules to a builder, and constructs a validation schema shape
|
|
119
|
+
* with field slugs as keys. The resulting shape is set as the structure for a new ObjectValidationSchema.
|
|
120
|
+
*
|
|
121
|
+
* @param contentFields Array of validated field definitions, each containing validation rules to apply.
|
|
122
|
+
* @returns An ObjectValidationSchema instance representing the entire set of fields with their validation logic.
|
|
123
|
+
*/
|
|
124
|
+
function fieldsToSchema(contentFields: FieldToValidate[]): ObjectValidationSchema {
|
|
125
|
+
const shape: Shape = {};
|
|
126
|
+
|
|
127
|
+
for (const field of contentFields) {
|
|
128
|
+
const builder = createFieldBuilder();
|
|
129
|
+
|
|
130
|
+
for (const rule of field.contentValidationRules) {
|
|
131
|
+
applyRule(builder, rule);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
shape[field.slug] = builder.array;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return new ObjectValidationSchema().of(shape);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { fieldsToSchema };
|
|
141
|
+
export type { FieldToValidate };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContentValueType,
|
|
3
|
+
ContentValueTypeName,
|
|
4
|
+
ContentValueTypeNames,
|
|
5
|
+
PRN_RESOURCE_TYPE_TO_CONTENT_VALUE_TYPE_NAME_MAP,
|
|
6
|
+
} from "@platecms/delta-types";
|
|
7
|
+
import { PRN } from "@platecms/delta-plate-resource-notation";
|
|
8
|
+
|
|
9
|
+
export function getContentValueTypeName(value: ContentValueType): ContentValueTypeName<ContentValueType> {
|
|
10
|
+
if (typeof value === "string") {
|
|
11
|
+
return ContentValueTypeNames.STRING;
|
|
12
|
+
}
|
|
13
|
+
if (typeof value === "number") {
|
|
14
|
+
return ContentValueTypeNames.NUMBER;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === "boolean") {
|
|
17
|
+
return ContentValueTypeNames.BOOLEAN;
|
|
18
|
+
}
|
|
19
|
+
if (value instanceof Date) {
|
|
20
|
+
return ContentValueTypeNames.DATE;
|
|
21
|
+
}
|
|
22
|
+
if (value instanceof PRN) {
|
|
23
|
+
return PRN_RESOURCE_TYPE_TO_CONTENT_VALUE_TYPE_NAME_MAP[value.resourceType];
|
|
24
|
+
}
|
|
25
|
+
return ContentValueTypeNames.SMART_TEXT;
|
|
26
|
+
}
|
|
@@ -5,14 +5,14 @@ import { ArrayValidationSchema } from "../schemas/array.schema";
|
|
|
5
5
|
import { ruleToSchema } from "./rule-to-schema";
|
|
6
6
|
|
|
7
7
|
describe("ruleToSchema", () => {
|
|
8
|
-
const mockPrn = PRN.fromString("prn:plate:-1:
|
|
8
|
+
const mockPrn = PRN.fromString("prn:plate:-1:cms:validation-rule:1");
|
|
9
9
|
|
|
10
10
|
describe("ALLOWED_VALUES rule", () => {
|
|
11
11
|
it("converts ALLOWED_VALUES rule to array schema with inner content-value schema", () => {
|
|
12
12
|
const rule = {
|
|
13
13
|
ruleType: RuleType.ALLOWED_VALUES,
|
|
14
14
|
settings: { allowedValues: ["option1", "option2", "option3"] },
|
|
15
|
-
prn: PRN.fromString("prn:plate:-1:
|
|
15
|
+
prn: PRN.fromString("prn:plate:-1:cms:validation-rule:1"),
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const schema = ruleToSchema(rule);
|
|
@@ -68,7 +68,7 @@ describe("ruleToSchema", () => {
|
|
|
68
68
|
const rule = {
|
|
69
69
|
ruleType: RuleType.STRING_FORMAT,
|
|
70
70
|
settings: { allowedFormat: ".*@.*\\..*" },
|
|
71
|
-
prn: PRN.fromString("prn:plate:-1:
|
|
71
|
+
prn: PRN.fromString("prn:plate:-1:cms:validation-rule:1"),
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
const schema = ruleToSchema(rule);
|
|
@@ -241,7 +241,7 @@ describe("ruleToSchema", () => {
|
|
|
241
241
|
const rule = {
|
|
242
242
|
ruleType: RuleType.RELATABLE_CONTENT_TYPES,
|
|
243
243
|
settings: { allowedTypes: [ContentValueTypeNames.STRING, ContentValueTypeNames.NUMBER] },
|
|
244
|
-
prn: PRN.fromString("prn:plate:-1:
|
|
244
|
+
prn: PRN.fromString("prn:plate:-1:cms:validation-rule:1"),
|
|
245
245
|
};
|
|
246
246
|
|
|
247
247
|
const schema = ruleToSchema(rule);
|
|
@@ -2,6 +2,7 @@ import { ArrayValidationSchema } from "../schemas/array.schema";
|
|
|
2
2
|
import { ContentValueValidationSchema } from "../schemas/content-value.schema";
|
|
3
3
|
import { DateValidationSchema } from "../schemas/date.schema";
|
|
4
4
|
import { NumberValidationSchema } from "../schemas/number.schema";
|
|
5
|
+
import { ObjectValidationSchema } from "../schemas/object.schema";
|
|
5
6
|
import { StringValidationSchema } from "../schemas/string.schema";
|
|
6
7
|
|
|
7
8
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
@@ -22,6 +23,10 @@ export class SchemaBuilder {
|
|
|
22
23
|
return new ArrayValidationSchema<T>();
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
public static object<T>(): ObjectValidationSchema<T> {
|
|
27
|
+
return new ObjectValidationSchema<T>();
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
public static contentValue(): ContentValueValidationSchema {
|
|
26
31
|
return new ContentValueValidationSchema();
|
|
27
32
|
}
|
|
@@ -6,7 +6,7 @@ import { RuleType } from "../enums/rule-types.enum";
|
|
|
6
6
|
import { validateWithFunction } from "./validate-with-function";
|
|
7
7
|
|
|
8
8
|
describe("validate-with-function", () => {
|
|
9
|
-
const mockRulePrn = PRN.fromString("prn:plate:-1:
|
|
9
|
+
const mockRulePrn = PRN.fromString("prn:plate:-1:cms:validation-rule:1");
|
|
10
10
|
const mockPath = ["content-type-prn", "content-field-prn"];
|
|
11
11
|
const fallbackErrorMessage = "Validation failed";
|
|
12
12
|
|
|
@@ -182,7 +182,7 @@ describe("validate-with-function", () => {
|
|
|
182
182
|
|
|
183
183
|
// Test with PRN
|
|
184
184
|
const prnResult = validateWithFunction(
|
|
185
|
-
PRN.fromString("prn:plate:-1:
|
|
185
|
+
PRN.fromString("prn:plate:-1:cms:content-item:1"),
|
|
186
186
|
mockRule,
|
|
187
187
|
mockPath,
|
|
188
188
|
fallbackErrorMessage,
|
|
@@ -229,7 +229,7 @@ describe("validate-with-function", () => {
|
|
|
229
229
|
42,
|
|
230
230
|
true,
|
|
231
231
|
new Date("2023-01-01"),
|
|
232
|
-
PRN.fromString("prn:plate:-1:
|
|
232
|
+
PRN.fromString("prn:plate:-1:cms:content-item:1"),
|
|
233
233
|
{ type: "root", children: [] },
|
|
234
234
|
];
|
|
235
235
|
|
|
@@ -105,7 +105,7 @@ describe("ArrayValidationSchema", () => {
|
|
|
105
105
|
ruleType: RuleType.COUNT,
|
|
106
106
|
settings: { min: 2, max: 4 },
|
|
107
107
|
id: "count-rule",
|
|
108
|
-
prn: PRN.fromString("prn:plate:-1:
|
|
108
|
+
prn: PRN.fromString("prn:plate:-1:cms:validation-rule:1"),
|
|
109
109
|
};
|
|
110
110
|
|
|
111
111
|
const schemaWithRule = new ArrayValidationSchema<ContentValueType>(mockRule);
|
|
@@ -37,7 +37,7 @@ describe("ContentValueValidationSchema", () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it("passes validation when value is a PRN", () => {
|
|
40
|
-
const prn = PRN.fromString("prn:plate:-1:
|
|
40
|
+
const prn = PRN.fromString("prn:plate:-1:cms:content-item:1");
|
|
41
41
|
const result = schema.validate(prn, { path: ["test"] });
|
|
42
42
|
expect(result.isValid).toBe(true);
|
|
43
43
|
expect(result.errors).toHaveLength(0);
|
|
@@ -92,10 +92,10 @@ describe("ContentValueValidationSchema", () => {
|
|
|
92
92
|
|
|
93
93
|
describe("allowed content types validation", () => {
|
|
94
94
|
it("passes validation when PRN content type is in allowed types", () => {
|
|
95
|
-
const allowedTypes = [PRN.fromString("prn:plate:-1:
|
|
95
|
+
const allowedTypes = [PRN.fromString("prn:plate:-1:cms:content-item:1")];
|
|
96
96
|
schema.allowedContentTypes(allowedTypes);
|
|
97
97
|
|
|
98
|
-
const prn = PRN.fromString("prn:plate:-1:
|
|
98
|
+
const prn = PRN.fromString("prn:plate:-1:cms:content-item:1");
|
|
99
99
|
const result = schema.validate(prn, { path: ["test"] });
|
|
100
100
|
expect(result.isValid).toBe(true);
|
|
101
101
|
expect(result.errors).toHaveLength(0);
|
|
@@ -106,7 +106,7 @@ describe("ContentValueValidationSchema", () => {
|
|
|
106
106
|
const mockRule = {
|
|
107
107
|
ruleType: RuleType.VALUE_TYPE,
|
|
108
108
|
settings: { allowedTypes: allowedValueTypes },
|
|
109
|
-
prn: PRN.fromString("prn:plate:-1:
|
|
109
|
+
prn: PRN.fromString("prn:plate:-1:cms:validation-rule:1"),
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
const schemaWithRule = new ContentValueValidationSchema(mockRule).allowedValueTypes(allowedValueTypes);
|
|
@@ -119,7 +119,7 @@ describe("ContentValueValidationSchema", () => {
|
|
|
119
119
|
describe("method chaining", () => {
|
|
120
120
|
it("supports method chaining", () => {
|
|
121
121
|
const allowedValueTypes = [ContentValueTypeNames.STRING, ContentValueTypeNames.NUMBER];
|
|
122
|
-
const allowedContentTypes = [PRN.fromString("prn:plate:-1:
|
|
122
|
+
const allowedContentTypes = [PRN.fromString("prn:plate:-1:cms:content-item:1")];
|
|
123
123
|
|
|
124
124
|
const result = schema
|
|
125
125
|
.allowedValueTypes(allowedValueTypes)
|
|
@@ -10,11 +10,20 @@ import {
|
|
|
10
10
|
validateValueValueType,
|
|
11
11
|
} from "../validation-rules/value-type.validation-rule";
|
|
12
12
|
import { BaseValidationSchema } from "./base.schema";
|
|
13
|
+
import { ValidationSchema } from "../types/validation-schema.interface";
|
|
14
|
+
import { getContentValueTypeName } from "../helpers/get-content-value-type-name";
|
|
15
|
+
|
|
16
|
+
type SubSchemas = Partial<Record<ContentValueTypeName<ContentValueType>, ValidationSchema<ContentValueType>>>;
|
|
13
17
|
|
|
14
18
|
export class ContentValueValidationSchema extends BaseValidationSchema<ContentValueType, ContentValueValidationSchema> {
|
|
15
19
|
public readonly type = "content-value";
|
|
16
20
|
private _allowedValueTypes?: ContentValueTypeName<ContentValueType>[];
|
|
17
21
|
private _allowedContentTypes?: PRN[];
|
|
22
|
+
private _subSchemas: SubSchemas = {};
|
|
23
|
+
|
|
24
|
+
public get subSchemas(): SubSchemas {
|
|
25
|
+
return this._subSchemas;
|
|
26
|
+
}
|
|
18
27
|
|
|
19
28
|
public allowedContentTypes(types: PRN[]): this {
|
|
20
29
|
this._allowedContentTypes = types;
|
|
@@ -26,13 +35,29 @@ export class ContentValueValidationSchema extends BaseValidationSchema<ContentVa
|
|
|
26
35
|
return this;
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
public subSchema(type: ContentValueTypeName<ContentValueType>, schema: ValidationSchema<ContentValueType>): this {
|
|
39
|
+
this._subSchemas[type] = schema;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
protected _typeCheck(value: unknown, path: string[]): ValidationErrorData[] {
|
|
30
44
|
const allowedValueTypesError = this._validateAllowedValueTypes(value as ContentValueType, path);
|
|
31
45
|
return allowedValueTypesError ? [allowedValueTypesError] : [];
|
|
32
46
|
}
|
|
33
47
|
|
|
34
|
-
protected _validateAgainstSchema(
|
|
35
|
-
|
|
48
|
+
protected _validateAgainstSchema(value: ContentValueType, context: ValidationContext = {}): ValidationErrorData[] {
|
|
49
|
+
const errors: ValidationErrorData[] = [];
|
|
50
|
+
const valueType = getContentValueTypeName(value);
|
|
51
|
+
|
|
52
|
+
const subSchema = this._subSchemas[valueType];
|
|
53
|
+
if (subSchema !== undefined) {
|
|
54
|
+
const result = subSchema.validate(value, context);
|
|
55
|
+
if (!result.isValid) {
|
|
56
|
+
errors.push(...result.errors);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return errors;
|
|
36
61
|
}
|
|
37
62
|
|
|
38
63
|
private _validateAllowedValueTypes(
|
|
@@ -88,7 +88,7 @@ describe("DateValidationSchema", () => {
|
|
|
88
88
|
const mockRule = {
|
|
89
89
|
ruleType: RuleType.DATE_BETWEEN,
|
|
90
90
|
settings: { start: startDate, end: endDate },
|
|
91
|
-
prn: PRN.fromString("prn:plate:-1:
|
|
91
|
+
prn: PRN.fromString("prn:plate:-1:cms:validation-rule:1"),
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
schema = new DateValidationSchema(mockRule).start(startDate).end(endDate);
|