@featurevisor/core 2.20.0 → 2.22.1
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/CHANGELOG.md +19 -0
- package/coverage/clover.xml +1286 -515
- package/coverage/coverage-final.json +17 -16
- package/coverage/lcov-report/builder/allocator.ts.html +1 -1
- package/coverage/lcov-report/builder/buildDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
- package/coverage/lcov-report/builder/convertToV1.ts.html +1 -1
- package/coverage/lcov-report/builder/getFeatureRanges.ts.html +1 -1
- package/coverage/lcov-report/builder/hashes.ts.html +1 -1
- package/coverage/lcov-report/builder/index.html +1 -1
- package/coverage/lcov-report/builder/mutateVariables.ts.html +1 -1
- package/coverage/lcov-report/builder/mutator.ts.html +1 -1
- package/coverage/lcov-report/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/config/index.html +1 -1
- package/coverage/lcov-report/config/index.ts.html +1 -1
- package/coverage/lcov-report/config/projectConfig.ts.html +42 -42
- package/coverage/lcov-report/datasource/adapter.ts.html +2 -2
- package/coverage/lcov-report/datasource/datasource.ts.html +43 -43
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +48 -48
- package/coverage/lcov-report/datasource/index.html +19 -19
- package/coverage/lcov-report/datasource/index.ts.html +4 -4
- package/coverage/lcov-report/generate-code/index.html +116 -0
- package/coverage/lcov-report/generate-code/typescript.ts.html +3004 -0
- package/coverage/lcov-report/index.html +44 -29
- package/coverage/lcov-report/linter/attributeSchema.ts.html +1815 -30
- package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
- package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
- package/coverage/lcov-report/linter/conditionSchema.ts.html +1606 -157
- package/coverage/lcov-report/linter/featureSchema.ts.html +56 -56
- package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/index.html +39 -39
- package/coverage/lcov-report/linter/lintProject.ts.html +189 -78
- package/coverage/lcov-report/linter/mutationNotation.ts.html +1 -1
- package/coverage/lcov-report/linter/printError.ts.html +1 -1
- package/coverage/lcov-report/linter/schema.ts.html +59 -59
- package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/testSchema.ts.html +6 -6
- package/coverage/lcov-report/list/index.html +1 -1
- package/coverage/lcov-report/list/matrix.ts.html +1 -1
- package/coverage/lcov-report/parsers/index.html +1 -1
- package/coverage/lcov-report/parsers/index.ts.html +4 -4
- package/coverage/lcov-report/parsers/json.ts.html +2 -2
- package/coverage/lcov-report/parsers/yml.ts.html +6 -6
- package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
- package/coverage/lcov-report/tester/helpers.ts.html +1 -1
- package/coverage/lcov-report/tester/index.html +1 -1
- package/coverage/lcov-report/utils/extractKeys.ts.html +1 -1
- package/coverage/lcov-report/utils/git.ts.html +3 -3
- package/coverage/lcov-report/utils/index.html +1 -1
- package/coverage/lcov-report/utils/index.ts.html +1 -1
- package/coverage/lcov-report/utils/pretty.ts.html +1 -1
- package/coverage/lcov.info +2624 -926
- package/lib/generate-code/typescript.js +57 -29
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/generate-code/typescript.spec.d.ts +1 -0
- package/lib/generate-code/typescript.spec.js +78 -0
- package/lib/generate-code/typescript.spec.js.map +1 -0
- package/lib/linter/attributeSchema.d.ts +352 -20
- package/lib/linter/attributeSchema.js +426 -16
- package/lib/linter/attributeSchema.js.map +1 -1
- package/lib/linter/attributeSchema.spec.d.ts +1 -0
- package/lib/linter/attributeSchema.spec.js +266 -0
- package/lib/linter/attributeSchema.spec.js.map +1 -0
- package/lib/linter/conditionSchema.d.ts +2 -1
- package/lib/linter/conditionSchema.js +370 -100
- package/lib/linter/conditionSchema.js.map +1 -1
- package/lib/linter/conditionSchema.spec.js +200 -153
- package/lib/linter/conditionSchema.spec.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +114 -3
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/lintProject.js +45 -14
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/segmentSchema.spec.js +71 -1
- package/lib/linter/segmentSchema.spec.js.map +1 -1
- package/package.json +5 -5
- package/src/generate-code/typescript.spec.ts +108 -0
- package/src/generate-code/typescript.ts +80 -34
- package/src/linter/attributeSchema.spec.ts +342 -0
- package/src/linter/attributeSchema.ts +613 -18
- package/src/linter/conditionSchema.spec.ts +231 -189
- package/src/linter/conditionSchema.ts +582 -99
- package/src/linter/featureSchema.spec.ts +129 -4
- package/src/linter/lintProject.ts +55 -18
- package/src/linter/segmentSchema.spec.ts +82 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Attribute, AttributeProperty, Schema } from "@featurevisor/types";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
import { ProjectConfig } from "../config";
|
|
@@ -9,8 +10,8 @@ const stringOperators = [
|
|
|
9
10
|
"notContains",
|
|
10
11
|
"startsWith",
|
|
11
12
|
"endsWith",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
13
|
+
"matches",
|
|
14
|
+
"notMatches",
|
|
14
15
|
];
|
|
15
16
|
const semverOperators = [
|
|
16
17
|
"semverEquals",
|
|
@@ -21,24 +22,537 @@ const semverOperators = [
|
|
|
21
22
|
"semverLessThanOrEquals",
|
|
22
23
|
];
|
|
23
24
|
const dateOperators = ["before", "after"];
|
|
24
|
-
const
|
|
25
|
-
const
|
|
25
|
+
const arrayMembershipOperators = ["in", "notIn"];
|
|
26
|
+
const arrayOperators = ["includes", "notIncludes"];
|
|
26
27
|
const operatorsWithoutValue = ["exists", "notExists"];
|
|
27
28
|
|
|
28
29
|
const isoDateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|([+\-]\d{2}:\d{2})))?$/;
|
|
29
30
|
|
|
31
|
+
type SchemaNode = Attribute | AttributeProperty | Schema;
|
|
32
|
+
type ResolvedLeaf =
|
|
33
|
+
| { kind: "schema"; schema: SchemaNode }
|
|
34
|
+
| { kind: "flat-object-property" }
|
|
35
|
+
| { kind: "flat-array-item" };
|
|
36
|
+
|
|
30
37
|
function isIsoDateString(value: string): boolean {
|
|
31
38
|
return isoDateRegex.test(value);
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
function isPrimitiveAttributeValue(value: unknown): boolean {
|
|
42
|
+
return (
|
|
43
|
+
value === null ||
|
|
44
|
+
typeof value === "string" ||
|
|
45
|
+
typeof value === "number" ||
|
|
46
|
+
typeof value === "boolean"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isStringArray(value: unknown): value is string[] {
|
|
51
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function valueDeepEqual(a: unknown, b: unknown): boolean {
|
|
55
|
+
if (a === b) return true;
|
|
56
|
+
if (typeof a !== typeof b) return false;
|
|
57
|
+
if (a === null || b === null) return a === b;
|
|
58
|
+
|
|
59
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
60
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
61
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
62
|
+
if (a.length !== b.length) return false;
|
|
63
|
+
return a.every((value, index) => valueDeepEqual(value, b[index]));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const keysA = Object.keys(a as object).sort();
|
|
67
|
+
const keysB = Object.keys(b as object).sort();
|
|
68
|
+
if (keysA.length !== keysB.length || keysA.some((key, index) => key !== keysB[index])) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return keysA.every((key) =>
|
|
73
|
+
valueDeepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveSchemaRefs(schema: SchemaNode, schemasByKey: Record<string, Schema>): SchemaNode {
|
|
81
|
+
if ("schema" in schema && schema.schema && schemasByKey[schema.schema]) {
|
|
82
|
+
return resolveSchemaRefs(schemasByKey[schema.schema], schemasByKey);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return schema;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getLeafType(resolvedLeaf: ResolvedLeaf): string | undefined {
|
|
89
|
+
if (resolvedLeaf.kind === "flat-object-property" || resolvedLeaf.kind === "flat-array-item") {
|
|
90
|
+
return "primitive";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return "type" in resolvedLeaf.schema ? resolvedLeaf.schema.type : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getLeafTypes(resolvedLeaf: ResolvedLeaf, schemasByKey: Record<string, Schema>): string[] {
|
|
97
|
+
if (resolvedLeaf.kind === "flat-object-property" || resolvedLeaf.kind === "flat-array-item") {
|
|
98
|
+
return ["primitive"];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const resolved = resolveSchemaRefs(resolvedLeaf.schema, schemasByKey) as SchemaNode & {
|
|
102
|
+
type?: string;
|
|
103
|
+
oneOf?: unknown[];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (resolved.oneOf && Array.isArray(resolved.oneOf) && resolved.oneOf.length > 0) {
|
|
107
|
+
return Array.from(
|
|
108
|
+
new Set(
|
|
109
|
+
resolved.oneOf.flatMap((branch) =>
|
|
110
|
+
getLeafTypes({ kind: "schema", schema: branch as SchemaNode }, schemasByKey),
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return resolved.type ? [resolved.type] : [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveAttributePath(
|
|
120
|
+
attributePath: string,
|
|
121
|
+
attributesByKey: Record<string, Attribute>,
|
|
122
|
+
schemasByKey: Record<string, Schema>,
|
|
123
|
+
): ResolvedLeaf | null {
|
|
124
|
+
const [rootKey, ...rest] = attributePath.split(".");
|
|
125
|
+
const rootAttribute = attributesByKey[rootKey];
|
|
126
|
+
|
|
127
|
+
if (!rootAttribute) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let current: SchemaNode = rootAttribute;
|
|
132
|
+
|
|
133
|
+
if (rest.length === 0) {
|
|
134
|
+
return { kind: "schema", schema: resolveSchemaRefs(current, schemasByKey) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (let index = 0; index < rest.length; index++) {
|
|
138
|
+
const segment = rest[index];
|
|
139
|
+
current = resolveSchemaRefs(current, schemasByKey);
|
|
140
|
+
const type = "type" in current ? current.type : undefined;
|
|
141
|
+
|
|
142
|
+
if (type !== "object") {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if ("oneOf" in current && Array.isArray(current.oneOf) && current.oneOf.length > 0) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const properties = "properties" in current ? current.properties : undefined;
|
|
151
|
+
const additionalProperties =
|
|
152
|
+
"additionalProperties" in current ? current.additionalProperties : undefined;
|
|
153
|
+
|
|
154
|
+
if (properties && typeof properties === "object" && properties[segment]) {
|
|
155
|
+
current = properties[segment];
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (additionalProperties && typeof additionalProperties === "object") {
|
|
160
|
+
current = additionalProperties;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!properties && !additionalProperties) {
|
|
165
|
+
return index === rest.length - 1 ? { kind: "flat-object-property" } : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { kind: "schema", schema: resolveSchemaRefs(current, schemasByKey) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function matchesSchemaValue(
|
|
175
|
+
schema: SchemaNode,
|
|
176
|
+
value: unknown,
|
|
177
|
+
schemasByKey: Record<string, Schema>,
|
|
178
|
+
): boolean {
|
|
179
|
+
const resolved = resolveSchemaRefs(schema, schemasByKey) as SchemaNode & {
|
|
180
|
+
type?: string;
|
|
181
|
+
enum?: unknown[];
|
|
182
|
+
const?: unknown;
|
|
183
|
+
oneOf?: unknown[];
|
|
184
|
+
properties?: Record<string, unknown>;
|
|
185
|
+
additionalProperties?: unknown;
|
|
186
|
+
required?: string[];
|
|
187
|
+
items?: unknown;
|
|
188
|
+
minLength?: number;
|
|
189
|
+
maxLength?: number;
|
|
190
|
+
pattern?: string;
|
|
191
|
+
minimum?: number;
|
|
192
|
+
maximum?: number;
|
|
193
|
+
minItems?: number;
|
|
194
|
+
maxItems?: number;
|
|
195
|
+
uniqueItems?: boolean;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (resolved.oneOf && Array.isArray(resolved.oneOf) && resolved.oneOf.length > 0) {
|
|
199
|
+
const matches = resolved.oneOf.filter((branch) =>
|
|
200
|
+
matchesSchemaValue(branch as SchemaNode, value, schemasByKey),
|
|
201
|
+
).length;
|
|
202
|
+
return matches === 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (resolved.const !== undefined) {
|
|
206
|
+
return valueDeepEqual(value, resolved.const);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (Array.isArray(resolved.enum) && resolved.enum.length > 0) {
|
|
210
|
+
return resolved.enum.some((entry) => valueDeepEqual(entry, value));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const type = resolved.type;
|
|
214
|
+
if (!type) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (type === "boolean") {
|
|
219
|
+
return typeof value === "boolean";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (type === "string" || type === "semver" || type === "date") {
|
|
223
|
+
if (typeof value !== "string") {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (resolved.minLength !== undefined && value.length < resolved.minLength) return false;
|
|
228
|
+
if (resolved.maxLength !== undefined && value.length > resolved.maxLength) return false;
|
|
229
|
+
|
|
230
|
+
if (resolved.pattern !== undefined) {
|
|
231
|
+
try {
|
|
232
|
+
if (!new RegExp(resolved.pattern).test(value)) return false;
|
|
233
|
+
} catch {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (type === "date" && !isIsoDateString(value)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (type === "integer" || type === "double") {
|
|
246
|
+
if (typeof value !== "number") {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (type === "integer" && !Number.isInteger(value)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (resolved.minimum !== undefined && value < resolved.minimum) return false;
|
|
255
|
+
if (resolved.maximum !== undefined && value > resolved.maximum) return false;
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (type === "object") {
|
|
260
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const objectValue = value as Record<string, unknown>;
|
|
265
|
+
const properties = resolved.properties;
|
|
266
|
+
const additionalProperties = resolved.additionalProperties;
|
|
267
|
+
|
|
268
|
+
if ((!properties || typeof properties !== "object") && !additionalProperties) {
|
|
269
|
+
return Object.values(objectValue).every((entry) => isPrimitiveAttributeValue(entry));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (properties && typeof properties === "object") {
|
|
273
|
+
const required = new Set(resolved.required || []);
|
|
274
|
+
for (const key of required) {
|
|
275
|
+
if (!Object.prototype.hasOwnProperty.call(objectValue, key)) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const key of Object.keys(objectValue)) {
|
|
281
|
+
if (properties[key]) {
|
|
282
|
+
if (!matchesSchemaValue(properties[key] as SchemaNode, objectValue[key], schemasByKey)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (additionalProperties && typeof additionalProperties === "object") {
|
|
289
|
+
if (
|
|
290
|
+
!matchesSchemaValue(additionalProperties as SchemaNode, objectValue[key], schemasByKey)
|
|
291
|
+
) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (additionalProperties && typeof additionalProperties === "object") {
|
|
304
|
+
return Object.values(objectValue).every((entry) =>
|
|
305
|
+
matchesSchemaValue(additionalProperties as SchemaNode, entry, schemasByKey),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (type === "array") {
|
|
313
|
+
if (!Array.isArray(value)) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (resolved.minItems !== undefined && value.length < resolved.minItems) return false;
|
|
318
|
+
if (resolved.maxItems !== undefined && value.length > resolved.maxItems) return false;
|
|
319
|
+
|
|
320
|
+
if (resolved.uniqueItems) {
|
|
321
|
+
for (let i = 0; i < value.length; i++) {
|
|
322
|
+
for (let j = i + 1; j < value.length; j++) {
|
|
323
|
+
if (valueDeepEqual(value[i], value[j])) return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!resolved.items || typeof resolved.items !== "object") {
|
|
329
|
+
return value.every((entry) => typeof entry === "string");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return value.every((entry) =>
|
|
333
|
+
matchesSchemaValue(resolved.items as SchemaNode, entry, schemasByKey),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function matchesLeafValue(
|
|
341
|
+
leaf: ResolvedLeaf,
|
|
342
|
+
value: unknown,
|
|
343
|
+
schemasByKey: Record<string, Schema>,
|
|
344
|
+
): boolean {
|
|
345
|
+
if (leaf.kind === "flat-object-property" || leaf.kind === "flat-array-item") {
|
|
346
|
+
return leaf.kind === "flat-array-item"
|
|
347
|
+
? typeof value === "string"
|
|
348
|
+
: isPrimitiveAttributeValue(value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return matchesSchemaValue(leaf.schema, value, schemasByKey);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function getArrayItemLeaf(
|
|
355
|
+
leaf: ResolvedLeaf,
|
|
356
|
+
schemasByKey: Record<string, Schema>,
|
|
357
|
+
): ResolvedLeaf | null {
|
|
358
|
+
if (leaf.kind !== "schema") {
|
|
359
|
+
return leaf.kind === "flat-array-item" ? leaf : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const resolved = resolveSchemaRefs(leaf.schema, schemasByKey);
|
|
363
|
+
if ("oneOf" in resolved && Array.isArray(resolved.oneOf) && resolved.oneOf.length > 0) {
|
|
364
|
+
const itemLeaves = resolved.oneOf
|
|
365
|
+
.map((branch) =>
|
|
366
|
+
getArrayItemLeaf({ kind: "schema", schema: branch as SchemaNode }, schemasByKey),
|
|
367
|
+
)
|
|
368
|
+
.filter((item): item is ResolvedLeaf => item !== null);
|
|
369
|
+
|
|
370
|
+
if (itemLeaves.length === 0) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (itemLeaves.every((item) => item.kind === "flat-array-item")) {
|
|
375
|
+
return { kind: "flat-array-item" };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
kind: "schema",
|
|
380
|
+
schema: {
|
|
381
|
+
oneOf: itemLeaves.map((item) =>
|
|
382
|
+
item.kind === "schema" ? item.schema : ({ type: "string" } as AttributeProperty),
|
|
383
|
+
),
|
|
384
|
+
} as AttributeProperty,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!("type" in resolved) || resolved.type !== "array") {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if ("items" in resolved && resolved.items && typeof resolved.items === "object") {
|
|
393
|
+
return {
|
|
394
|
+
kind: "schema",
|
|
395
|
+
schema: resolveSchemaRefs(resolved.items as SchemaNode, schemasByKey),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return { kind: "flat-array-item" };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function addIssue(
|
|
403
|
+
context: z.RefinementCtx,
|
|
404
|
+
message: string,
|
|
405
|
+
path: (string | number)[] = ["value"],
|
|
406
|
+
): void {
|
|
407
|
+
context.addIssue({
|
|
408
|
+
code: z.ZodIssueCode.custom,
|
|
409
|
+
message,
|
|
410
|
+
path,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function validateAttributeAwareCondition(
|
|
415
|
+
data: {
|
|
416
|
+
attribute: string;
|
|
417
|
+
operator: string;
|
|
418
|
+
value?: unknown;
|
|
419
|
+
regexFlags?: string;
|
|
420
|
+
},
|
|
421
|
+
context: z.RefinementCtx,
|
|
422
|
+
attributesByKey: Record<string, Attribute>,
|
|
423
|
+
schemasByKey: Record<string, Schema>,
|
|
424
|
+
): void {
|
|
425
|
+
const resolvedLeaf = resolveAttributePath(data.attribute, attributesByKey, schemasByKey);
|
|
426
|
+
if (!resolvedLeaf) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const leafType = getLeafType(resolvedLeaf);
|
|
431
|
+
const leafTypes = getLeafTypes(resolvedLeaf, schemasByKey);
|
|
432
|
+
|
|
433
|
+
if (operatorsWithoutValue.includes(data.operator)) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (resolvedLeaf.kind === "schema" && leafType === "object") {
|
|
438
|
+
addIssue(
|
|
439
|
+
context,
|
|
440
|
+
`Attribute "${data.attribute}" resolves to an object. Use a nested attribute path or \`exists\`/\`notExists\` instead.`,
|
|
441
|
+
);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (
|
|
446
|
+
numericOperators.includes(data.operator) &&
|
|
447
|
+
!leafTypes.some((type) => ["integer", "double"].includes(type))
|
|
448
|
+
) {
|
|
449
|
+
addIssue(
|
|
450
|
+
context,
|
|
451
|
+
`Operator "${data.operator}" can only be used with integer or double attributes.`,
|
|
452
|
+
);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (
|
|
457
|
+
stringOperators.includes(data.operator) &&
|
|
458
|
+
!leafTypes.some((type) => ["string", "semver", "date"].includes(type))
|
|
459
|
+
) {
|
|
460
|
+
addIssue(context, `Operator "${data.operator}" can only be used with string-like attributes.`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (
|
|
465
|
+
semverOperators.includes(data.operator) &&
|
|
466
|
+
!leafTypes.some((type) => ["string", "semver"].includes(type))
|
|
467
|
+
) {
|
|
468
|
+
addIssue(
|
|
469
|
+
context,
|
|
470
|
+
`Operator "${data.operator}" can only be used with string or semver attributes.`,
|
|
471
|
+
);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (
|
|
476
|
+
dateOperators.includes(data.operator) &&
|
|
477
|
+
!leafTypes.some((type) => ["string", "date"].includes(type))
|
|
478
|
+
) {
|
|
479
|
+
addIssue(
|
|
480
|
+
context,
|
|
481
|
+
`Operator "${data.operator}" can only be used with string or date attributes.`,
|
|
482
|
+
);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (arrayMembershipOperators.includes(data.operator)) {
|
|
487
|
+
if (
|
|
488
|
+
!["primitive", "string", "semver", "date", "integer", "double", "boolean"].includes(
|
|
489
|
+
leafType || "",
|
|
490
|
+
)
|
|
491
|
+
) {
|
|
492
|
+
addIssue(
|
|
493
|
+
context,
|
|
494
|
+
`Operator "${data.operator}" can only be used with primitive attribute values.`,
|
|
495
|
+
);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!Array.isArray(data.value)) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
data.value.forEach((entry, index) => {
|
|
504
|
+
if (!matchesLeafValue(resolvedLeaf, entry, schemasByKey)) {
|
|
505
|
+
addIssue(
|
|
506
|
+
context,
|
|
507
|
+
`Value at index ${index} does not match the schema of attribute "${data.attribute}".`,
|
|
508
|
+
["value", index],
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (arrayOperators.includes(data.operator)) {
|
|
517
|
+
const itemLeaf = getArrayItemLeaf(resolvedLeaf, schemasByKey);
|
|
518
|
+
if (!itemLeaf) {
|
|
519
|
+
addIssue(context, `Operator "${data.operator}" can only be used with array attributes.`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const itemType = getLeafType(itemLeaf);
|
|
524
|
+
if (itemType === "object" || itemType === "array") {
|
|
525
|
+
addIssue(context, `Operator "${data.operator}" only supports arrays of primitive values.`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!matchesLeafValue(itemLeaf, data.value, schemasByKey)) {
|
|
530
|
+
addIssue(context, `Value does not match the item schema of attribute "${data.attribute}".`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (commonOperators.includes(data.operator)) {
|
|
537
|
+
if (data.value === null) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!matchesLeafValue(resolvedLeaf, data.value, schemasByKey)) {
|
|
542
|
+
addIssue(context, `Value does not match the schema of attribute "${data.attribute}".`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
34
547
|
export function getConditionsZodSchema(
|
|
35
548
|
projectConfig: ProjectConfig,
|
|
36
|
-
|
|
549
|
+
attributesByKey: Record<string, Attribute>,
|
|
550
|
+
schemasByKey: Record<string, Schema> = {},
|
|
37
551
|
) {
|
|
38
552
|
const plainConditionZodSchema = z
|
|
39
553
|
.object({
|
|
40
554
|
attribute: z.string().refine(
|
|
41
|
-
(value) =>
|
|
555
|
+
(value) => resolveAttributePath(value, attributesByKey, schemasByKey) !== null,
|
|
42
556
|
(value) => ({
|
|
43
557
|
message: `Unknown attribute "${value}"`,
|
|
44
558
|
}),
|
|
@@ -49,8 +563,8 @@ export function getConditionsZodSchema(
|
|
|
49
563
|
...stringOperators,
|
|
50
564
|
...semverOperators,
|
|
51
565
|
...dateOperators,
|
|
566
|
+
...arrayMembershipOperators,
|
|
52
567
|
...arrayOperators,
|
|
53
|
-
...regexOperators,
|
|
54
568
|
...operatorsWithoutValue,
|
|
55
569
|
]),
|
|
56
570
|
value: z
|
|
@@ -73,7 +587,6 @@ export function getConditionsZodSchema(
|
|
|
73
587
|
.optional(),
|
|
74
588
|
})
|
|
75
589
|
.superRefine((data, context) => {
|
|
76
|
-
// common
|
|
77
590
|
if (
|
|
78
591
|
commonOperators.includes(data.operator) &&
|
|
79
592
|
!(
|
|
@@ -85,117 +598,90 @@ export function getConditionsZodSchema(
|
|
|
85
598
|
data.value === null
|
|
86
599
|
)
|
|
87
600
|
) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
601
|
+
addIssue(
|
|
602
|
+
context,
|
|
603
|
+
`when operator is "${data.operator}", value has to be either a string, number, boolean, date or null`,
|
|
604
|
+
);
|
|
93
605
|
}
|
|
94
606
|
|
|
95
|
-
// numeric
|
|
96
607
|
if (numericOperators.includes(data.operator) && typeof data.value !== "number") {
|
|
97
|
-
|
|
98
|
-
code: z.ZodIssueCode.custom,
|
|
99
|
-
message: `when operator is "${data.operator}", value must be a number`,
|
|
100
|
-
path: ["value"],
|
|
101
|
-
});
|
|
608
|
+
addIssue(context, `when operator is "${data.operator}", value must be a number`);
|
|
102
609
|
}
|
|
103
610
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
path: ["value"],
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// semver
|
|
114
|
-
if (semverOperators.includes(data.operator) && typeof data.value !== "string") {
|
|
115
|
-
context.addIssue({
|
|
116
|
-
code: z.ZodIssueCode.custom,
|
|
117
|
-
message: `when operator is "${data.operator}", value must be a string`,
|
|
118
|
-
path: ["value"],
|
|
119
|
-
});
|
|
611
|
+
if (
|
|
612
|
+
[...stringOperators, ...semverOperators].includes(data.operator) &&
|
|
613
|
+
typeof data.value !== "string"
|
|
614
|
+
) {
|
|
615
|
+
addIssue(context, `when operator is "${data.operator}", value must be a string`);
|
|
120
616
|
}
|
|
121
617
|
|
|
122
|
-
// date
|
|
123
618
|
if (dateOperators.includes(data.operator)) {
|
|
124
|
-
// Value is required and must not be undefined or null
|
|
125
619
|
if (typeof data.value === "undefined" || data.value === null || data.value === "") {
|
|
126
|
-
|
|
127
|
-
code: z.ZodIssueCode.custom,
|
|
128
|
-
message: `when operator is "${data.operator}", value must be provided`,
|
|
129
|
-
path: ["value"],
|
|
130
|
-
});
|
|
620
|
+
addIssue(context, `when operator is "${data.operator}", value must be provided`);
|
|
131
621
|
} else if (typeof data.value === "string") {
|
|
132
|
-
// If it's a string, must be valid ISO 8601
|
|
133
622
|
if (!isIsoDateString(data.value)) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
});
|
|
139
|
-
} else {
|
|
140
|
-
// valid ISO 8601 string -- no issue
|
|
623
|
+
addIssue(
|
|
624
|
+
context,
|
|
625
|
+
`when operator is "${data.operator}", value must be a stringified date in ISO 8601 format`,
|
|
626
|
+
);
|
|
141
627
|
}
|
|
142
628
|
} else if (data.value instanceof Date) {
|
|
143
|
-
// If it's a Date, must be valid
|
|
144
629
|
if (isNaN(data.value.getTime())) {
|
|
145
|
-
|
|
146
|
-
code: z.ZodIssueCode.custom,
|
|
147
|
-
message: `when operator is "${data.operator}", value is a Date but invalid`,
|
|
148
|
-
path: ["value"],
|
|
149
|
-
});
|
|
630
|
+
addIssue(context, `when operator is "${data.operator}", value is a Date but invalid`);
|
|
150
631
|
}
|
|
151
|
-
// Valid JS Date -- no issue
|
|
152
632
|
} else {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
path: ["value"],
|
|
158
|
-
});
|
|
633
|
+
addIssue(
|
|
634
|
+
context,
|
|
635
|
+
`when operator is "${data.operator}", value must be a stringified date in ISO 8601 format or a valid Date object`,
|
|
636
|
+
);
|
|
159
637
|
}
|
|
160
638
|
}
|
|
161
639
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
context.addIssue({
|
|
165
|
-
code: z.ZodIssueCode.custom,
|
|
166
|
-
message: `when operator is "${data.operator}", value must be an array of strings`,
|
|
167
|
-
path: ["value"],
|
|
168
|
-
});
|
|
640
|
+
if (arrayMembershipOperators.includes(data.operator) && !Array.isArray(data.value)) {
|
|
641
|
+
addIssue(context, `when operator is "${data.operator}", value must be an array of strings`);
|
|
169
642
|
}
|
|
170
643
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
644
|
+
if (
|
|
645
|
+
arrayMembershipOperators.includes(data.operator) &&
|
|
646
|
+
Array.isArray(data.value) &&
|
|
647
|
+
!isStringArray(data.value)
|
|
648
|
+
) {
|
|
649
|
+
addIssue(context, `when operator is "${data.operator}", value must be an array of strings`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (!arrayMembershipOperators.includes(data.operator) && Array.isArray(data.value)) {
|
|
653
|
+
if (!["in", "notIn"].includes(data.operator)) {
|
|
654
|
+
addIssue(context, `when operator is "${data.operator}", value must not be an array`);
|
|
179
655
|
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
message: `when operator is nether "matches" nor "notMatches", regexFlags are not needed`,
|
|
186
|
-
path: ["regexFlags"],
|
|
187
|
-
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (regexFlagsRequired(data.operator)) {
|
|
659
|
+
if (typeof data.value !== "string") {
|
|
660
|
+
addIssue(context, `when operator is "${data.operator}", value must be a string`);
|
|
188
661
|
}
|
|
662
|
+
} else if (data.regexFlags) {
|
|
663
|
+
addIssue(
|
|
664
|
+
context,
|
|
665
|
+
`when operator is nether "matches" nor "notMatches", regexFlags are not needed`,
|
|
666
|
+
["regexFlags"],
|
|
667
|
+
);
|
|
189
668
|
}
|
|
190
669
|
|
|
191
|
-
// operators without value
|
|
192
670
|
if (operatorsWithoutValue.includes(data.operator) && data.value !== undefined) {
|
|
193
|
-
|
|
194
|
-
code: z.ZodIssueCode.custom,
|
|
195
|
-
message: `when operator is "${data.operator}", value is not needed`,
|
|
196
|
-
path: ["value"],
|
|
197
|
-
});
|
|
671
|
+
addIssue(context, `when operator is "${data.operator}", value is not needed`);
|
|
198
672
|
}
|
|
673
|
+
|
|
674
|
+
validateAttributeAwareCondition(
|
|
675
|
+
data as {
|
|
676
|
+
attribute: string;
|
|
677
|
+
operator: string;
|
|
678
|
+
value?: unknown;
|
|
679
|
+
regexFlags?: string;
|
|
680
|
+
},
|
|
681
|
+
context,
|
|
682
|
+
attributesByKey,
|
|
683
|
+
schemasByKey,
|
|
684
|
+
);
|
|
199
685
|
});
|
|
200
686
|
|
|
201
687
|
const andOrNotConditionZodSchema = z.union([
|
|
@@ -217,14 +703,11 @@ export function getConditionsZodSchema(
|
|
|
217
703
|
]);
|
|
218
704
|
|
|
219
705
|
const everyoneZodSchema = z.literal("*");
|
|
220
|
-
|
|
221
706
|
const conditionZodSchema = z.union([andOrNotConditionZodSchema, plainConditionZodSchema]);
|
|
222
707
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
z.array(conditionZodSchema),
|
|
226
|
-
everyoneZodSchema,
|
|
227
|
-
]);
|
|
708
|
+
return z.union([conditionZodSchema, z.array(conditionZodSchema), everyoneZodSchema]);
|
|
709
|
+
}
|
|
228
710
|
|
|
229
|
-
|
|
711
|
+
function regexFlagsRequired(operator: string): boolean {
|
|
712
|
+
return operator === "matches" || operator === "notMatches";
|
|
230
713
|
}
|