@featurevisor/core 2.11.0 → 2.12.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/CHANGELOG.md +11 -0
- package/coverage/clover.xml +684 -3
- package/coverage/coverage-final.json +4 -0
- package/coverage/lcov-report/builder/allocator.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/index.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/index.html +25 -10
- package/coverage/lcov-report/linter/conditionSchema.ts.html +775 -0
- package/coverage/lcov-report/linter/featureSchema.ts.html +4924 -0
- package/coverage/lcov-report/linter/index.html +161 -0
- package/coverage/lcov-report/linter/schema.ts.html +1471 -0
- package/coverage/lcov-report/linter/segmentSchema.ts.html +130 -0
- 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/json.ts.html +1 -1
- package/coverage/lcov-report/parsers/yml.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.info +1471 -0
- package/lib/builder/buildDatafile.js +15 -1
- package/lib/builder/buildDatafile.js.map +1 -1
- package/lib/config/projectConfig.d.ts +2 -0
- package/lib/config/projectConfig.js +3 -1
- package/lib/config/projectConfig.js.map +1 -1
- package/lib/datasource/datasource.d.ts +6 -1
- package/lib/datasource/datasource.js +16 -0
- package/lib/datasource/datasource.js.map +1 -1
- package/lib/datasource/filesystemAdapter.js +10 -0
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/generate-code/typescript.js +280 -46
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/linter/conditionSchema.spec.d.ts +1 -0
- package/lib/linter/conditionSchema.spec.js +331 -0
- package/lib/linter/conditionSchema.spec.js.map +1 -0
- package/lib/linter/featureSchema.d.ts +129 -20
- package/lib/linter/featureSchema.js +489 -48
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.d.ts +1 -0
- package/lib/linter/featureSchema.spec.js +978 -0
- package/lib/linter/featureSchema.spec.js.map +1 -0
- package/lib/linter/lintProject.js +67 -1
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/schema.d.ts +42 -0
- package/lib/linter/schema.js +417 -0
- package/lib/linter/schema.js.map +1 -0
- package/lib/linter/schema.spec.d.ts +1 -0
- package/lib/linter/schema.spec.js +483 -0
- package/lib/linter/schema.spec.js.map +1 -0
- package/lib/linter/segmentSchema.spec.d.ts +1 -0
- package/lib/linter/segmentSchema.spec.js +231 -0
- package/lib/linter/segmentSchema.spec.js.map +1 -0
- package/lib/tester/testFeature.js +5 -3
- package/lib/tester/testFeature.js.map +1 -1
- package/lib/utils/git.js +3 -0
- package/lib/utils/git.js.map +1 -1
- package/package.json +5 -5
- package/src/builder/buildDatafile.ts +17 -1
- package/src/config/projectConfig.ts +3 -0
- package/src/datasource/datasource.ts +23 -0
- package/src/datasource/filesystemAdapter.ts +7 -0
- package/src/generate-code/typescript.ts +330 -49
- package/src/linter/conditionSchema.spec.ts +446 -0
- package/src/linter/featureSchema.spec.ts +1218 -0
- package/src/linter/featureSchema.ts +671 -69
- package/src/linter/lintProject.ts +84 -0
- package/src/linter/schema.spec.ts +617 -0
- package/src/linter/schema.ts +462 -0
- package/src/linter/segmentSchema.spec.ts +273 -0
- package/src/tester/testFeature.ts +5 -3
- package/src/utils/git.ts +2 -0
- package/lib/linter/propertySchema.d.ts +0 -5
- package/lib/linter/propertySchema.js +0 -43
- package/lib/linter/propertySchema.js.map +0 -1
- package/src/linter/propertySchema.ts +0 -47
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import type { Schema } from "@featurevisor/types";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
import { ProjectConfig } from "../config";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
valueZodSchema,
|
|
7
|
+
propertyTypeEnum,
|
|
8
|
+
getSchemaZodSchema,
|
|
9
|
+
refineEnumMatchesType,
|
|
10
|
+
refineMinimumMaximum,
|
|
11
|
+
refineStringLengthPattern,
|
|
12
|
+
refineArrayItems,
|
|
13
|
+
} from "./schema";
|
|
5
14
|
|
|
6
15
|
const tagRegex = /^[a-z0-9-]+$/;
|
|
7
16
|
|
|
@@ -26,6 +35,221 @@ function getVariableLabel(variableSchema, variableKey, path) {
|
|
|
26
35
|
);
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Resolve variable schema to the Schema used for value validation.
|
|
40
|
+
* When variable has `schema` (reference), returns the parsed Schema from schemasByKey; otherwise returns the inline variable schema.
|
|
41
|
+
*/
|
|
42
|
+
function resolveVariableSchema(
|
|
43
|
+
variableSchema: {
|
|
44
|
+
schema?: string;
|
|
45
|
+
type?: string;
|
|
46
|
+
items?: unknown;
|
|
47
|
+
properties?: unknown;
|
|
48
|
+
required?: string[];
|
|
49
|
+
enum?: unknown[];
|
|
50
|
+
const?: unknown;
|
|
51
|
+
oneOf?: unknown[];
|
|
52
|
+
minimum?: number;
|
|
53
|
+
maximum?: number;
|
|
54
|
+
minLength?: number;
|
|
55
|
+
maxLength?: number;
|
|
56
|
+
pattern?: string;
|
|
57
|
+
minItems?: number;
|
|
58
|
+
maxItems?: number;
|
|
59
|
+
uniqueItems?: boolean;
|
|
60
|
+
},
|
|
61
|
+
schemasByKey?: Record<string, Schema>,
|
|
62
|
+
): {
|
|
63
|
+
type?: string;
|
|
64
|
+
items?: unknown;
|
|
65
|
+
properties?: unknown;
|
|
66
|
+
required?: string[];
|
|
67
|
+
enum?: unknown[];
|
|
68
|
+
const?: unknown;
|
|
69
|
+
oneOf?: unknown[];
|
|
70
|
+
minimum?: number;
|
|
71
|
+
maximum?: number;
|
|
72
|
+
minLength?: number;
|
|
73
|
+
maxLength?: number;
|
|
74
|
+
pattern?: string;
|
|
75
|
+
minItems?: number;
|
|
76
|
+
maxItems?: number;
|
|
77
|
+
uniqueItems?: boolean;
|
|
78
|
+
} | null {
|
|
79
|
+
if (variableSchema.schema) {
|
|
80
|
+
return schemasByKey?.[variableSchema.schema] ?? null;
|
|
81
|
+
}
|
|
82
|
+
return variableSchema as {
|
|
83
|
+
type?: string;
|
|
84
|
+
items?: unknown;
|
|
85
|
+
properties?: unknown;
|
|
86
|
+
required?: string[];
|
|
87
|
+
enum?: unknown[];
|
|
88
|
+
const?: unknown;
|
|
89
|
+
oneOf?: unknown[];
|
|
90
|
+
minimum?: number;
|
|
91
|
+
maximum?: number;
|
|
92
|
+
minLength?: number;
|
|
93
|
+
maxLength?: number;
|
|
94
|
+
pattern?: string;
|
|
95
|
+
minItems?: number;
|
|
96
|
+
maxItems?: number;
|
|
97
|
+
uniqueItems?: boolean;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Resolve a schema by following schema references (schema: key). Used for nested schemas that may have oneOf. */
|
|
102
|
+
function resolveSchemaRefs(
|
|
103
|
+
schema: { schema?: string; [k: string]: unknown },
|
|
104
|
+
schemasByKey?: Record<string, Schema>,
|
|
105
|
+
): { [k: string]: unknown } {
|
|
106
|
+
if (schema.schema && schemasByKey?.[schema.schema]) {
|
|
107
|
+
return resolveSchemaRefs(
|
|
108
|
+
schemasByKey[schema.schema] as { schema?: string; [k: string]: unknown },
|
|
109
|
+
schemasByKey,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return schema;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns true if the value matches the given schema (const, enum, type, object properties, array items, or exactly one of oneOf).
|
|
117
|
+
* Used for oneOf validation: value must match exactly one branch.
|
|
118
|
+
*/
|
|
119
|
+
function valueMatchesSchema(
|
|
120
|
+
schema: { [k: string]: unknown },
|
|
121
|
+
value: unknown,
|
|
122
|
+
schemasByKey?: Record<string, Schema>,
|
|
123
|
+
): boolean {
|
|
124
|
+
const resolved = resolveSchemaRefs(schema, schemasByKey) as {
|
|
125
|
+
type?: string;
|
|
126
|
+
const?: unknown;
|
|
127
|
+
enum?: unknown[];
|
|
128
|
+
oneOf?: unknown[];
|
|
129
|
+
properties?: Record<string, unknown>;
|
|
130
|
+
required?: string[];
|
|
131
|
+
items?: unknown;
|
|
132
|
+
minimum?: number;
|
|
133
|
+
maximum?: number;
|
|
134
|
+
minLength?: number;
|
|
135
|
+
maxLength?: number;
|
|
136
|
+
pattern?: string;
|
|
137
|
+
minItems?: number;
|
|
138
|
+
maxItems?: number;
|
|
139
|
+
uniqueItems?: boolean;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (resolved.oneOf && Array.isArray(resolved.oneOf) && resolved.oneOf.length > 0) {
|
|
143
|
+
const matchCount = resolved.oneOf.filter((branch) =>
|
|
144
|
+
valueMatchesSchema(branch as { [k: string]: unknown }, value, schemasByKey),
|
|
145
|
+
).length;
|
|
146
|
+
return matchCount === 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (resolved.const !== undefined) {
|
|
150
|
+
return valueDeepEqual(value, resolved.const);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (resolved.enum !== undefined && Array.isArray(resolved.enum)) {
|
|
154
|
+
return resolved.enum.some((e) => valueDeepEqual(value, e));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const type = resolved.type;
|
|
158
|
+
if (!type) return false;
|
|
159
|
+
|
|
160
|
+
if (type === "string") {
|
|
161
|
+
if (typeof value !== "string") return false;
|
|
162
|
+
const s = value as string;
|
|
163
|
+
if (resolved.minLength !== undefined && s.length < resolved.minLength) return false;
|
|
164
|
+
if (resolved.maxLength !== undefined && s.length > resolved.maxLength) return false;
|
|
165
|
+
if (resolved.pattern !== undefined) {
|
|
166
|
+
try {
|
|
167
|
+
if (!new RegExp(resolved.pattern).test(s)) return false;
|
|
168
|
+
} catch {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (type === "boolean") return typeof value === "boolean";
|
|
175
|
+
if (type === "integer") {
|
|
176
|
+
if (typeof value !== "number" || !Number.isInteger(value)) return false;
|
|
177
|
+
if (resolved.minimum !== undefined && (value as number) < resolved.minimum) return false;
|
|
178
|
+
if (resolved.maximum !== undefined && (value as number) > resolved.maximum) return false;
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
if (type === "double") {
|
|
182
|
+
if (typeof value !== "number") return false;
|
|
183
|
+
if (resolved.minimum !== undefined && (value as number) < resolved.minimum) return false;
|
|
184
|
+
if (resolved.maximum !== undefined && (value as number) > resolved.maximum) return false;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (type === "json") return typeof value === "string";
|
|
188
|
+
|
|
189
|
+
if (type === "object") {
|
|
190
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
191
|
+
const props = resolved.properties;
|
|
192
|
+
if (!props || typeof props !== "object") return true;
|
|
193
|
+
const obj = value as Record<string, unknown>;
|
|
194
|
+
const required = new Set(resolved.required || []);
|
|
195
|
+
for (const key of required) {
|
|
196
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) return false;
|
|
197
|
+
if (!valueMatchesSchema(props[key] as { [k: string]: unknown }, obj[key], schemasByKey))
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
for (const key of Object.keys(obj)) {
|
|
201
|
+
const propSchema = props[key];
|
|
202
|
+
if (!propSchema) return false;
|
|
203
|
+
if (!valueMatchesSchema(propSchema as { [k: string]: unknown }, obj[key], schemasByKey))
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (type === "array") {
|
|
210
|
+
if (!Array.isArray(value)) return false;
|
|
211
|
+
const arr = value as unknown[];
|
|
212
|
+
if (resolved.minItems !== undefined && arr.length < resolved.minItems) return false;
|
|
213
|
+
if (resolved.maxItems !== undefined && arr.length > resolved.maxItems) return false;
|
|
214
|
+
if (resolved.uniqueItems) {
|
|
215
|
+
for (let i = 0; i < arr.length; i++) {
|
|
216
|
+
for (let j = i + 1; j < arr.length; j++) {
|
|
217
|
+
if (valueDeepEqual(arr[i], arr[j])) return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const itemSchema = resolved.items;
|
|
222
|
+
if (!itemSchema || typeof itemSchema !== "object")
|
|
223
|
+
return arr.every((v) => typeof v === "string");
|
|
224
|
+
return arr.every((item) =>
|
|
225
|
+
valueMatchesSchema(itemSchema as { [k: string]: unknown }, item, schemasByKey),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Deep equality for variable values (primitives, plain objects, arrays). */
|
|
233
|
+
function valueDeepEqual(a: unknown, b: unknown): boolean {
|
|
234
|
+
if (a === b) return true;
|
|
235
|
+
if (typeof a !== typeof b) return false;
|
|
236
|
+
if (a === null || b === null) return a === b;
|
|
237
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
238
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
239
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
240
|
+
if (a.length !== b.length) return false;
|
|
241
|
+
return a.every((v, i) => valueDeepEqual(v, b[i]));
|
|
242
|
+
}
|
|
243
|
+
const keysA = Object.keys(a as object).sort();
|
|
244
|
+
const keysB = Object.keys(b as object).sort();
|
|
245
|
+
if (keysA.length !== keysB.length || keysA.some((k, i) => k !== keysB[i])) return false;
|
|
246
|
+
return keysA.every((k) =>
|
|
247
|
+
valueDeepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
29
253
|
/**
|
|
30
254
|
* Recursively validates that every `required` array (at this level and in nested
|
|
31
255
|
* object/array schemas) only contains keys that exist in the same level's `properties`.
|
|
@@ -87,6 +311,19 @@ function refineRequiredKeysInSchema(
|
|
|
87
311
|
ctx,
|
|
88
312
|
);
|
|
89
313
|
}
|
|
314
|
+
|
|
315
|
+
const oneOf = (schema as { oneOf?: unknown[] }).oneOf;
|
|
316
|
+
if (oneOf && Array.isArray(oneOf)) {
|
|
317
|
+
oneOf.forEach((branch, i) => {
|
|
318
|
+
if (branch && typeof branch === "object") {
|
|
319
|
+
refineRequiredKeysInSchema(
|
|
320
|
+
branch as Parameters<typeof refineRequiredKeysInSchema>[0],
|
|
321
|
+
[...pathPrefix, "oneOf", i],
|
|
322
|
+
ctx,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
90
327
|
}
|
|
91
328
|
|
|
92
329
|
function typeOfValue(value: unknown): string {
|
|
@@ -99,21 +336,68 @@ function typeOfValue(value: unknown): string {
|
|
|
99
336
|
/**
|
|
100
337
|
* Validates a variable value against an array schema. Recursively validates each item
|
|
101
338
|
* when the schema defines `items` (nested arrays/objects use the same refinement).
|
|
339
|
+
* Enforces minItems, maxItems, and uniqueItems when set.
|
|
102
340
|
*/
|
|
103
341
|
function refineVariableValueArray(
|
|
104
342
|
projectConfig: ProjectConfig,
|
|
105
|
-
variableSchema: {
|
|
343
|
+
variableSchema: {
|
|
344
|
+
items?: unknown;
|
|
345
|
+
type: string;
|
|
346
|
+
minItems?: number;
|
|
347
|
+
maxItems?: number;
|
|
348
|
+
uniqueItems?: boolean;
|
|
349
|
+
},
|
|
106
350
|
variableValue: unknown[],
|
|
107
351
|
path: (string | number)[],
|
|
108
352
|
ctx: z.RefinementCtx,
|
|
109
353
|
variableKey?: string,
|
|
354
|
+
schemasByKey?: Record<string, Schema>,
|
|
110
355
|
): void {
|
|
111
356
|
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
357
|
+
const minItems = variableSchema.minItems;
|
|
358
|
+
const maxItems = variableSchema.maxItems;
|
|
359
|
+
const uniqueItems = variableSchema.uniqueItems;
|
|
360
|
+
if (minItems !== undefined && variableValue.length < minItems) {
|
|
361
|
+
ctx.addIssue({
|
|
362
|
+
code: z.ZodIssueCode.custom,
|
|
363
|
+
message: `Variable "${label}" (type array) length (${variableValue.length}) is less than \`minItems\` (${minItems}).`,
|
|
364
|
+
path,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (maxItems !== undefined && variableValue.length > maxItems) {
|
|
368
|
+
ctx.addIssue({
|
|
369
|
+
code: z.ZodIssueCode.custom,
|
|
370
|
+
message: `Variable "${label}" (type array) length (${variableValue.length}) is greater than \`maxItems\` (${maxItems}).`,
|
|
371
|
+
path,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (uniqueItems) {
|
|
375
|
+
for (let i = 0; i < variableValue.length; i++) {
|
|
376
|
+
for (let j = i + 1; j < variableValue.length; j++) {
|
|
377
|
+
if (valueDeepEqual(variableValue[i], variableValue[j])) {
|
|
378
|
+
ctx.addIssue({
|
|
379
|
+
code: z.ZodIssueCode.custom,
|
|
380
|
+
message: `Variable "${label}" (type array) has duplicate items at indices ${i} and ${j} but \`uniqueItems\` is true.`,
|
|
381
|
+
path,
|
|
382
|
+
});
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
112
388
|
const itemSchema = variableSchema.items;
|
|
113
389
|
|
|
114
390
|
if (itemSchema) {
|
|
115
391
|
variableValue.forEach((item, index) => {
|
|
116
|
-
superRefineVariableValue(
|
|
392
|
+
superRefineVariableValue(
|
|
393
|
+
projectConfig,
|
|
394
|
+
itemSchema,
|
|
395
|
+
item,
|
|
396
|
+
[...path, index],
|
|
397
|
+
ctx,
|
|
398
|
+
variableKey,
|
|
399
|
+
schemasByKey,
|
|
400
|
+
);
|
|
117
401
|
});
|
|
118
402
|
} else {
|
|
119
403
|
if (!isArrayOfStrings(variableValue)) {
|
|
@@ -152,6 +436,7 @@ function refineVariableValueObject(
|
|
|
152
436
|
path: (string | number)[],
|
|
153
437
|
ctx: z.RefinementCtx,
|
|
154
438
|
variableKey?: string,
|
|
439
|
+
schemasByKey?: Record<string, Schema>,
|
|
155
440
|
): void {
|
|
156
441
|
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
157
442
|
const schemaProperties = variableSchema.properties;
|
|
@@ -190,6 +475,7 @@ function refineVariableValueObject(
|
|
|
190
475
|
[...path, key],
|
|
191
476
|
ctx,
|
|
192
477
|
key,
|
|
478
|
+
schemasByKey,
|
|
193
479
|
);
|
|
194
480
|
}
|
|
195
481
|
}
|
|
@@ -225,6 +511,7 @@ function superRefineVariableValue(
|
|
|
225
511
|
path,
|
|
226
512
|
ctx,
|
|
227
513
|
variableKey?: string,
|
|
514
|
+
schemasByKey?: Record<string, Schema>,
|
|
228
515
|
) {
|
|
229
516
|
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
230
517
|
|
|
@@ -244,6 +531,66 @@ function superRefineVariableValue(
|
|
|
244
531
|
return;
|
|
245
532
|
}
|
|
246
533
|
|
|
534
|
+
const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
|
|
535
|
+
if (variableSchema.schema && effectiveSchema === null) {
|
|
536
|
+
ctx.addIssue({
|
|
537
|
+
code: z.ZodIssueCode.custom,
|
|
538
|
+
message: `Schema "${variableSchema.schema}" could not be loaded for value validation.`,
|
|
539
|
+
path,
|
|
540
|
+
});
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (!effectiveSchema) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const effectiveOneOf = (effectiveSchema as { oneOf?: unknown[] }).oneOf;
|
|
549
|
+
if (effectiveOneOf !== undefined && Array.isArray(effectiveOneOf) && effectiveOneOf.length > 0) {
|
|
550
|
+
const matchCount = effectiveOneOf.filter((branch) =>
|
|
551
|
+
valueMatchesSchema(branch as { [k: string]: unknown }, variableValue, schemasByKey),
|
|
552
|
+
).length;
|
|
553
|
+
if (matchCount === 0) {
|
|
554
|
+
ctx.addIssue({
|
|
555
|
+
code: z.ZodIssueCode.custom,
|
|
556
|
+
message: `Variable "${label}" must match exactly one of the \`oneOf\` schemas (got ${JSON.stringify(variableValue)}; matched none).`,
|
|
557
|
+
path,
|
|
558
|
+
});
|
|
559
|
+
} else if (matchCount > 1) {
|
|
560
|
+
ctx.addIssue({
|
|
561
|
+
code: z.ZodIssueCode.custom,
|
|
562
|
+
message: `Variable "${label}" must match exactly one of the \`oneOf\` schemas (matched ${matchCount}).`,
|
|
563
|
+
path,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const effectiveConst = (effectiveSchema as { const?: unknown }).const;
|
|
570
|
+
if (effectiveConst !== undefined) {
|
|
571
|
+
if (!valueDeepEqual(variableValue, effectiveConst)) {
|
|
572
|
+
ctx.addIssue({
|
|
573
|
+
code: z.ZodIssueCode.custom,
|
|
574
|
+
message: `Variable "${label}" must equal the constant value defined in schema (got ${JSON.stringify(variableValue)}).`,
|
|
575
|
+
path,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const effectiveEnum = (effectiveSchema as { enum?: unknown[] }).enum;
|
|
582
|
+
if (effectiveEnum !== undefined && Array.isArray(effectiveEnum) && effectiveEnum.length > 0) {
|
|
583
|
+
const allowed = effectiveEnum.some((v) => valueDeepEqual(variableValue, v));
|
|
584
|
+
if (!allowed) {
|
|
585
|
+
ctx.addIssue({
|
|
586
|
+
code: z.ZodIssueCode.custom,
|
|
587
|
+
message: `Variable "${label}" must be one of the allowed enum values (got ${JSON.stringify(variableValue)}).`,
|
|
588
|
+
path,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
247
594
|
// Require a value (no undefined) for every variable usage
|
|
248
595
|
if (variableValue === undefined) {
|
|
249
596
|
ctx.addIssue({
|
|
@@ -254,10 +601,10 @@ function superRefineVariableValue(
|
|
|
254
601
|
return;
|
|
255
602
|
}
|
|
256
603
|
|
|
257
|
-
const expectedType =
|
|
604
|
+
const expectedType = effectiveSchema.type;
|
|
258
605
|
const gotType = typeOfValue(variableValue);
|
|
259
606
|
|
|
260
|
-
// string — only string allowed
|
|
607
|
+
// string — only string allowed; schema minLength/maxLength/pattern applied when set
|
|
261
608
|
if (expectedType === "string") {
|
|
262
609
|
if (typeof variableValue !== "string") {
|
|
263
610
|
ctx.addIssue({
|
|
@@ -268,6 +615,37 @@ function superRefineVariableValue(
|
|
|
268
615
|
return;
|
|
269
616
|
}
|
|
270
617
|
|
|
618
|
+
const strMinLen = (effectiveSchema as { minLength?: number }).minLength;
|
|
619
|
+
const strMaxLen = (effectiveSchema as { maxLength?: number }).maxLength;
|
|
620
|
+
const strPattern = (effectiveSchema as { pattern?: string }).pattern;
|
|
621
|
+
if (strMinLen !== undefined && variableValue.length < strMinLen) {
|
|
622
|
+
ctx.addIssue({
|
|
623
|
+
code: z.ZodIssueCode.custom,
|
|
624
|
+
message: `Variable "${label}" (type string) length (${variableValue.length}) is less than \`minLength\` (${strMinLen}).`,
|
|
625
|
+
path,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
if (strMaxLen !== undefined && variableValue.length > strMaxLen) {
|
|
629
|
+
ctx.addIssue({
|
|
630
|
+
code: z.ZodIssueCode.custom,
|
|
631
|
+
message: `Variable "${label}" (type string) length (${variableValue.length}) is greater than \`maxLength\` (${strMaxLen}).`,
|
|
632
|
+
path,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (strPattern !== undefined) {
|
|
636
|
+
try {
|
|
637
|
+
if (!new RegExp(strPattern).test(variableValue)) {
|
|
638
|
+
ctx.addIssue({
|
|
639
|
+
code: z.ZodIssueCode.custom,
|
|
640
|
+
message: `Variable "${label}" (type string) does not match \`pattern\`.`,
|
|
641
|
+
path,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
} catch {
|
|
645
|
+
// invalid regex already reported at schema parse time
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
271
649
|
if (
|
|
272
650
|
projectConfig.maxVariableStringLength &&
|
|
273
651
|
variableValue.length > projectConfig.maxVariableStringLength
|
|
@@ -306,6 +684,23 @@ function superRefineVariableValue(
|
|
|
306
684
|
message: `Variable "${label}" (type integer) must be an integer; got ${variableValue}.`,
|
|
307
685
|
path,
|
|
308
686
|
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const intMin = (effectiveSchema as { minimum?: number }).minimum;
|
|
690
|
+
const intMax = (effectiveSchema as { maximum?: number }).maximum;
|
|
691
|
+
if (intMin !== undefined && variableValue < intMin) {
|
|
692
|
+
ctx.addIssue({
|
|
693
|
+
code: z.ZodIssueCode.custom,
|
|
694
|
+
message: `Variable "${label}" (type integer) must be >= minimum (${intMin}); got ${variableValue}.`,
|
|
695
|
+
path,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
if (intMax !== undefined && variableValue > intMax) {
|
|
699
|
+
ctx.addIssue({
|
|
700
|
+
code: z.ZodIssueCode.custom,
|
|
701
|
+
message: `Variable "${label}" (type integer) must be <= maximum (${intMax}); got ${variableValue}.`,
|
|
702
|
+
path,
|
|
703
|
+
});
|
|
309
704
|
}
|
|
310
705
|
return;
|
|
311
706
|
}
|
|
@@ -326,6 +721,23 @@ function superRefineVariableValue(
|
|
|
326
721
|
message: `Variable "${label}" (type double) must be a finite number; got ${variableValue}.`,
|
|
327
722
|
path,
|
|
328
723
|
});
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const doubleMin = (effectiveSchema as { minimum?: number }).minimum;
|
|
727
|
+
const doubleMax = (effectiveSchema as { maximum?: number }).maximum;
|
|
728
|
+
if (doubleMin !== undefined && variableValue < doubleMin) {
|
|
729
|
+
ctx.addIssue({
|
|
730
|
+
code: z.ZodIssueCode.custom,
|
|
731
|
+
message: `Variable "${label}" (type double) must be >= minimum (${doubleMin}); got ${variableValue}.`,
|
|
732
|
+
path,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (doubleMax !== undefined && variableValue > doubleMax) {
|
|
736
|
+
ctx.addIssue({
|
|
737
|
+
code: z.ZodIssueCode.custom,
|
|
738
|
+
message: `Variable "${label}" (type double) must be <= maximum (${doubleMax}); got ${variableValue}.`,
|
|
739
|
+
path,
|
|
740
|
+
});
|
|
329
741
|
}
|
|
330
742
|
return;
|
|
331
743
|
}
|
|
@@ -352,7 +764,21 @@ function superRefineVariableValue(
|
|
|
352
764
|
});
|
|
353
765
|
return;
|
|
354
766
|
}
|
|
355
|
-
refineVariableValueArray(
|
|
767
|
+
refineVariableValueArray(
|
|
768
|
+
projectConfig,
|
|
769
|
+
effectiveSchema as {
|
|
770
|
+
items?: unknown;
|
|
771
|
+
type: string;
|
|
772
|
+
minItems?: number;
|
|
773
|
+
maxItems?: number;
|
|
774
|
+
uniqueItems?: boolean;
|
|
775
|
+
},
|
|
776
|
+
variableValue,
|
|
777
|
+
path,
|
|
778
|
+
ctx,
|
|
779
|
+
variableKey,
|
|
780
|
+
schemasByKey,
|
|
781
|
+
);
|
|
356
782
|
return;
|
|
357
783
|
}
|
|
358
784
|
|
|
@@ -372,11 +798,16 @@ function superRefineVariableValue(
|
|
|
372
798
|
}
|
|
373
799
|
refineVariableValueObject(
|
|
374
800
|
projectConfig,
|
|
375
|
-
|
|
801
|
+
effectiveSchema as {
|
|
802
|
+
properties?: Record<string, unknown>;
|
|
803
|
+
required?: string[];
|
|
804
|
+
type: string;
|
|
805
|
+
},
|
|
376
806
|
variableValue as Record<string, unknown>,
|
|
377
807
|
path,
|
|
378
808
|
ctx,
|
|
379
809
|
variableKey,
|
|
810
|
+
schemasByKey,
|
|
380
811
|
);
|
|
381
812
|
return;
|
|
382
813
|
}
|
|
@@ -431,6 +862,7 @@ function refineForce({
|
|
|
431
862
|
force,
|
|
432
863
|
pathPrefix,
|
|
433
864
|
projectConfig,
|
|
865
|
+
schemasByKey,
|
|
434
866
|
}) {
|
|
435
867
|
force.forEach((f, fN) => {
|
|
436
868
|
// force[n].variation
|
|
@@ -447,14 +879,24 @@ function refineForce({
|
|
|
447
879
|
// force[n].variables[key]
|
|
448
880
|
if (f.variables) {
|
|
449
881
|
Object.keys(f.variables).forEach((variableKey) => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
882
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
883
|
+
if (!variableSchema) {
|
|
884
|
+
ctx.addIssue({
|
|
885
|
+
code: z.ZodIssueCode.custom,
|
|
886
|
+
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
887
|
+
path: pathPrefix.concat([fN, "variables", variableKey]),
|
|
888
|
+
});
|
|
889
|
+
} else {
|
|
890
|
+
superRefineVariableValue(
|
|
891
|
+
projectConfig,
|
|
892
|
+
variableSchema,
|
|
893
|
+
f.variables[variableKey],
|
|
894
|
+
pathPrefix.concat([fN, "variables", variableKey]),
|
|
895
|
+
ctx,
|
|
896
|
+
variableKey,
|
|
897
|
+
schemasByKey,
|
|
898
|
+
);
|
|
899
|
+
}
|
|
458
900
|
});
|
|
459
901
|
}
|
|
460
902
|
});
|
|
@@ -468,19 +910,30 @@ function refineRules({
|
|
|
468
910
|
rules,
|
|
469
911
|
pathPrefix,
|
|
470
912
|
projectConfig,
|
|
913
|
+
schemasByKey,
|
|
471
914
|
}) {
|
|
472
915
|
rules.forEach((rule, ruleN) => {
|
|
473
916
|
// rules[n].variables[key]
|
|
474
917
|
if (rule.variables) {
|
|
475
918
|
Object.keys(rule.variables).forEach((variableKey) => {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
919
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
920
|
+
if (!variableSchema) {
|
|
921
|
+
ctx.addIssue({
|
|
922
|
+
code: z.ZodIssueCode.custom,
|
|
923
|
+
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
924
|
+
path: pathPrefix.concat([ruleN, "variables", variableKey]),
|
|
925
|
+
});
|
|
926
|
+
} else {
|
|
927
|
+
superRefineVariableValue(
|
|
928
|
+
projectConfig,
|
|
929
|
+
variableSchema,
|
|
930
|
+
rule.variables[variableKey],
|
|
931
|
+
pathPrefix.concat([ruleN, "variables", variableKey]),
|
|
932
|
+
ctx,
|
|
933
|
+
variableKey,
|
|
934
|
+
schemasByKey,
|
|
935
|
+
);
|
|
936
|
+
}
|
|
484
937
|
});
|
|
485
938
|
}
|
|
486
939
|
|
|
@@ -556,8 +1009,10 @@ export function getFeatureZodSchema(
|
|
|
556
1009
|
availableAttributeKeys: [string, ...string[]],
|
|
557
1010
|
availableSegmentKeys: [string, ...string[]],
|
|
558
1011
|
availableFeatureKeys: [string, ...string[]],
|
|
1012
|
+
availableSchemaKeys: string[] = [],
|
|
1013
|
+
schemasByKey: Record<string, Schema> = {},
|
|
559
1014
|
) {
|
|
560
|
-
const
|
|
1015
|
+
const schemaZodSchema = getSchemaZodSchema(availableSchemaKeys);
|
|
561
1016
|
const variableValueZodSchema = valueZodSchema;
|
|
562
1017
|
|
|
563
1018
|
const variationValueZodSchema = z.string().min(1);
|
|
@@ -739,13 +1194,31 @@ export function getFeatureZodSchema(
|
|
|
739
1194
|
.object({
|
|
740
1195
|
deprecated: z.boolean().optional(),
|
|
741
1196
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1197
|
+
// Reference to a reusable schema (mutually exclusive with type/properties/required/items)
|
|
1198
|
+
schema: z
|
|
1199
|
+
.string()
|
|
1200
|
+
.refine(
|
|
1201
|
+
(value) => availableSchemaKeys.includes(value),
|
|
1202
|
+
(value) => ({ message: `Unknown schema "${value}"` }),
|
|
1203
|
+
)
|
|
1204
|
+
.optional(),
|
|
1205
|
+
|
|
1206
|
+
// Inline schema (mutually exclusive with schema)
|
|
1207
|
+
type: z.union([z.literal("json"), propertyTypeEnum]).optional(),
|
|
1208
|
+
items: schemaZodSchema.optional(),
|
|
1209
|
+
properties: z.record(schemaZodSchema).optional(),
|
|
748
1210
|
required: z.array(z.string()).optional(),
|
|
1211
|
+
enum: z.array(variableValueZodSchema).optional(),
|
|
1212
|
+
const: variableValueZodSchema.optional(),
|
|
1213
|
+
oneOf: z.array(schemaZodSchema).min(1).optional(),
|
|
1214
|
+
minimum: z.number().optional(),
|
|
1215
|
+
maximum: z.number().optional(),
|
|
1216
|
+
minLength: z.number().optional(),
|
|
1217
|
+
maxLength: z.number().optional(),
|
|
1218
|
+
pattern: z.string().optional(),
|
|
1219
|
+
minItems: z.number().optional(),
|
|
1220
|
+
maxItems: z.number().optional(),
|
|
1221
|
+
uniqueItems: z.boolean().optional(),
|
|
749
1222
|
|
|
750
1223
|
description: z.string().optional(),
|
|
751
1224
|
|
|
@@ -756,6 +1229,76 @@ export function getFeatureZodSchema(
|
|
|
756
1229
|
})
|
|
757
1230
|
.strict()
|
|
758
1231
|
.superRefine((variableSchema, ctx) => {
|
|
1232
|
+
const hasRef = "schema" in variableSchema && variableSchema.schema != null;
|
|
1233
|
+
const hasInline =
|
|
1234
|
+
"type" in variableSchema &&
|
|
1235
|
+
variableSchema.type != null &&
|
|
1236
|
+
variableSchema.type !== undefined;
|
|
1237
|
+
const hasOneOf =
|
|
1238
|
+
"oneOf" in variableSchema &&
|
|
1239
|
+
Array.isArray(variableSchema.oneOf) &&
|
|
1240
|
+
variableSchema.oneOf.length > 0;
|
|
1241
|
+
if (hasRef && (hasInline || hasOneOf)) {
|
|
1242
|
+
ctx.addIssue({
|
|
1243
|
+
code: z.ZodIssueCode.custom,
|
|
1244
|
+
message:
|
|
1245
|
+
"Variable schema cannot have both `schema` (reference) and inline properties (`type`, `oneOf`, `properties`, `required`, `items`). Use one or the other.",
|
|
1246
|
+
path: [],
|
|
1247
|
+
});
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (hasRef) {
|
|
1251
|
+
const hasInlineStructure =
|
|
1252
|
+
("type" in variableSchema && variableSchema.type != null) ||
|
|
1253
|
+
("properties" in variableSchema && variableSchema.properties != null) ||
|
|
1254
|
+
("required" in variableSchema && variableSchema.required != null) ||
|
|
1255
|
+
("items" in variableSchema && variableSchema.items != null) ||
|
|
1256
|
+
("oneOf" in variableSchema && variableSchema.oneOf != null);
|
|
1257
|
+
const hasInlineValidation =
|
|
1258
|
+
("minimum" in variableSchema && variableSchema.minimum !== undefined) ||
|
|
1259
|
+
("maximum" in variableSchema && variableSchema.maximum !== undefined) ||
|
|
1260
|
+
("minLength" in variableSchema && variableSchema.minLength !== undefined) ||
|
|
1261
|
+
("maxLength" in variableSchema && variableSchema.maxLength !== undefined) ||
|
|
1262
|
+
("pattern" in variableSchema && variableSchema.pattern !== undefined) ||
|
|
1263
|
+
("minItems" in variableSchema && variableSchema.minItems !== undefined) ||
|
|
1264
|
+
("maxItems" in variableSchema && variableSchema.maxItems !== undefined) ||
|
|
1265
|
+
("uniqueItems" in variableSchema && variableSchema.uniqueItems !== undefined);
|
|
1266
|
+
if (hasInlineStructure) {
|
|
1267
|
+
ctx.addIssue({
|
|
1268
|
+
code: z.ZodIssueCode.custom,
|
|
1269
|
+
message:
|
|
1270
|
+
"When `schema` is set, do not set `type`, `oneOf`, `properties`, `required`, or `items`.",
|
|
1271
|
+
path: [],
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
if (hasInlineValidation) {
|
|
1275
|
+
ctx.addIssue({
|
|
1276
|
+
code: z.ZodIssueCode.custom,
|
|
1277
|
+
message:
|
|
1278
|
+
"When `schema` is set, do not set `minimum`, `maximum`, `minLength`, `maxLength`, `pattern`, `minItems`, `maxItems`, or `uniqueItems`; use the referenced schema to define these.",
|
|
1279
|
+
path: [],
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (!hasInline && !hasOneOf) {
|
|
1285
|
+
ctx.addIssue({
|
|
1286
|
+
code: z.ZodIssueCode.custom,
|
|
1287
|
+
message:
|
|
1288
|
+
"Variable schema must have either `schema` (reference to a schema key), `type` (inline schema), or `oneOf` (inline oneOf schemas).",
|
|
1289
|
+
path: [],
|
|
1290
|
+
});
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (hasInline && hasOneOf) {
|
|
1294
|
+
ctx.addIssue({
|
|
1295
|
+
code: z.ZodIssueCode.custom,
|
|
1296
|
+
message:
|
|
1297
|
+
"Variable schema cannot have both `type` and `oneOf` at the top level. Use one or the other.",
|
|
1298
|
+
path: [],
|
|
1299
|
+
});
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
759
1302
|
// Validate required ⊆ properties at this level and in all nested object schemas
|
|
760
1303
|
refineRequiredKeysInSchema(
|
|
761
1304
|
variableSchema as Parameters<typeof refineRequiredKeysInSchema>[0],
|
|
@@ -852,6 +1395,10 @@ export function getFeatureZodSchema(
|
|
|
852
1395
|
return;
|
|
853
1396
|
}
|
|
854
1397
|
|
|
1398
|
+
// Every variable value is validated against its schema from variablesSchema. Sources covered:
|
|
1399
|
+
// 1. variablesSchema[key].defaultValue 2. variablesSchema[key].disabledValue
|
|
1400
|
+
// 3. variations[n].variables[key] 4. variations[n].variableOverrides[key][].value
|
|
1401
|
+
// 5. rules[env][n].variables[key] 6. force[env][n].variables[key]
|
|
855
1402
|
const variableSchemaByKey = value.variablesSchema;
|
|
856
1403
|
const variationValues: string[] = [];
|
|
857
1404
|
|
|
@@ -866,6 +1413,41 @@ export function getFeatureZodSchema(
|
|
|
866
1413
|
variableKeys.forEach((variableKey) => {
|
|
867
1414
|
const variableSchema = variableSchemaByKey[variableKey];
|
|
868
1415
|
|
|
1416
|
+
// When type and enum are both present, all enum values must match the type
|
|
1417
|
+
const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
|
|
1418
|
+
if (
|
|
1419
|
+
effectiveSchema &&
|
|
1420
|
+
effectiveSchema.type &&
|
|
1421
|
+
Array.isArray(effectiveSchema.enum) &&
|
|
1422
|
+
effectiveSchema.enum.length > 0
|
|
1423
|
+
) {
|
|
1424
|
+
refineEnumMatchesType(
|
|
1425
|
+
effectiveSchema as Parameters<typeof refineEnumMatchesType>[0],
|
|
1426
|
+
["variablesSchema", variableKey],
|
|
1427
|
+
ctx,
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Inline variable schemas: validate minimum/maximum, minLength/maxLength/pattern, minItems/maxItems/uniqueItems
|
|
1432
|
+
if (!("schema" in variableSchema) || !variableSchema.schema) {
|
|
1433
|
+
const pathPrefix = ["variablesSchema", variableKey];
|
|
1434
|
+
refineMinimumMaximum(
|
|
1435
|
+
variableSchema as Parameters<typeof refineMinimumMaximum>[0],
|
|
1436
|
+
pathPrefix,
|
|
1437
|
+
ctx,
|
|
1438
|
+
);
|
|
1439
|
+
refineStringLengthPattern(
|
|
1440
|
+
variableSchema as Parameters<typeof refineStringLengthPattern>[0],
|
|
1441
|
+
pathPrefix,
|
|
1442
|
+
ctx,
|
|
1443
|
+
);
|
|
1444
|
+
refineArrayItems(
|
|
1445
|
+
variableSchema as Parameters<typeof refineArrayItems>[0],
|
|
1446
|
+
pathPrefix,
|
|
1447
|
+
ctx,
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
869
1451
|
if (variableKey === "variation") {
|
|
870
1452
|
ctx.addIssue({
|
|
871
1453
|
code: z.ZodIssueCode.custom,
|
|
@@ -882,6 +1464,7 @@ export function getFeatureZodSchema(
|
|
|
882
1464
|
["variablesSchema", variableKey, "defaultValue"],
|
|
883
1465
|
ctx,
|
|
884
1466
|
variableKey,
|
|
1467
|
+
schemasByKey,
|
|
885
1468
|
);
|
|
886
1469
|
|
|
887
1470
|
// disabledValue (only when present)
|
|
@@ -893,54 +1476,69 @@ export function getFeatureZodSchema(
|
|
|
893
1476
|
["variablesSchema", variableKey, "disabledValue"],
|
|
894
1477
|
ctx,
|
|
895
1478
|
variableKey,
|
|
1479
|
+
schemasByKey,
|
|
896
1480
|
);
|
|
897
1481
|
}
|
|
898
1482
|
});
|
|
899
1483
|
|
|
900
|
-
// variations
|
|
1484
|
+
// variations: validate variation.variables and variation.variableOverrides (each value against its variable schema)
|
|
901
1485
|
if (value.variations) {
|
|
902
1486
|
value.variations.forEach((variation, variationN) => {
|
|
903
|
-
if (!variation.variables) {
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
1487
|
// variations[n].variables[key]
|
|
908
|
-
|
|
909
|
-
const
|
|
1488
|
+
if (variation.variables) {
|
|
1489
|
+
for (const variableKey of Object.keys(variation.variables)) {
|
|
1490
|
+
const variableValue = variation.variables[variableKey];
|
|
1491
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
1492
|
+
if (!variableSchema) {
|
|
1493
|
+
ctx.addIssue({
|
|
1494
|
+
code: z.ZodIssueCode.custom,
|
|
1495
|
+
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
1496
|
+
path: ["variations", variationN, "variables", variableKey],
|
|
1497
|
+
});
|
|
1498
|
+
} else {
|
|
1499
|
+
superRefineVariableValue(
|
|
1500
|
+
projectConfig,
|
|
1501
|
+
variableSchema,
|
|
1502
|
+
variableValue,
|
|
1503
|
+
["variations", variationN, "variables", variableKey],
|
|
1504
|
+
ctx,
|
|
1505
|
+
variableKey,
|
|
1506
|
+
schemasByKey,
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
910
1511
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
"variations",
|
|
933
|
-
variationN,
|
|
934
|
-
"variableOverrides",
|
|
935
|
-
variableKey,
|
|
936
|
-
overrideN,
|
|
937
|
-
"value",
|
|
938
|
-
],
|
|
939
|
-
ctx,
|
|
1512
|
+
// variations[n].variableOverrides[key][].value (validated even when variation.variables is absent)
|
|
1513
|
+
if (variation.variableOverrides) {
|
|
1514
|
+
for (const variableKey of Object.keys(variation.variableOverrides)) {
|
|
1515
|
+
const overrides = variation.variableOverrides[variableKey];
|
|
1516
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
1517
|
+
if (!variableSchema) {
|
|
1518
|
+
ctx.addIssue({
|
|
1519
|
+
code: z.ZodIssueCode.custom,
|
|
1520
|
+
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
1521
|
+
path: ["variations", variationN, "variableOverrides", variableKey],
|
|
1522
|
+
});
|
|
1523
|
+
} else if (Array.isArray(overrides)) {
|
|
1524
|
+
overrides.forEach((override, overrideN) => {
|
|
1525
|
+
superRefineVariableValue(
|
|
1526
|
+
projectConfig,
|
|
1527
|
+
variableSchema,
|
|
1528
|
+
override.value,
|
|
1529
|
+
[
|
|
1530
|
+
"variations",
|
|
1531
|
+
variationN,
|
|
1532
|
+
"variableOverrides",
|
|
940
1533
|
variableKey,
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1534
|
+
overrideN,
|
|
1535
|
+
"value",
|
|
1536
|
+
],
|
|
1537
|
+
ctx,
|
|
1538
|
+
variableKey,
|
|
1539
|
+
schemasByKey,
|
|
1540
|
+
);
|
|
1541
|
+
});
|
|
944
1542
|
}
|
|
945
1543
|
}
|
|
946
1544
|
}
|
|
@@ -960,6 +1558,7 @@ export function getFeatureZodSchema(
|
|
|
960
1558
|
pathPrefix: ["rules", environmentKey],
|
|
961
1559
|
ctx,
|
|
962
1560
|
projectConfig,
|
|
1561
|
+
schemasByKey,
|
|
963
1562
|
});
|
|
964
1563
|
}
|
|
965
1564
|
|
|
@@ -973,6 +1572,7 @@ export function getFeatureZodSchema(
|
|
|
973
1572
|
pathPrefix: ["force", environmentKey],
|
|
974
1573
|
ctx,
|
|
975
1574
|
projectConfig,
|
|
1575
|
+
schemasByKey,
|
|
976
1576
|
});
|
|
977
1577
|
}
|
|
978
1578
|
}
|
|
@@ -989,6 +1589,7 @@ export function getFeatureZodSchema(
|
|
|
989
1589
|
pathPrefix: ["rules"],
|
|
990
1590
|
ctx,
|
|
991
1591
|
projectConfig,
|
|
1592
|
+
schemasByKey,
|
|
992
1593
|
});
|
|
993
1594
|
}
|
|
994
1595
|
|
|
@@ -1002,6 +1603,7 @@ export function getFeatureZodSchema(
|
|
|
1002
1603
|
pathPrefix: ["force"],
|
|
1003
1604
|
ctx,
|
|
1004
1605
|
projectConfig,
|
|
1606
|
+
schemasByKey,
|
|
1005
1607
|
});
|
|
1006
1608
|
}
|
|
1007
1609
|
}
|