@featurevisor/core 2.13.0 → 2.15.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 +22 -0
- package/coverage/clover.xml +1499 -1028
- package/coverage/coverage-final.json +21 -17
- 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 +38 -8
- package/coverage/lcov-report/builder/mutateVariables.ts.html +400 -0
- package/coverage/lcov-report/builder/mutator.ts.html +796 -0
- 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 +30 -15
- package/coverage/lcov-report/config/index.ts.html +88 -0
- package/coverage/lcov-report/config/projectConfig.ts.html +79 -46
- package/coverage/lcov-report/datasource/adapter.ts.html +74 -11
- package/coverage/lcov-report/datasource/datasource.ts.html +77 -26
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +693 -69
- package/coverage/lcov-report/datasource/index.html +35 -35
- package/coverage/lcov-report/datasource/index.ts.html +1 -1
- package/coverage/lcov-report/index.html +53 -53
- package/coverage/lcov-report/linter/attributeSchema.ts.html +4 -4
- 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 +7 -7
- package/coverage/lcov-report/linter/featureSchema.ts.html +848 -389
- package/coverage/lcov-report/linter/groupSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/index.html +50 -35
- package/coverage/lcov-report/linter/lintProject.ts.html +437 -92
- package/coverage/lcov-report/linter/mutationNotation.ts.html +1309 -0
- package/coverage/lcov-report/linter/printError.ts.html +9 -9
- package/coverage/lcov-report/linter/schema.ts.html +69 -69
- package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/testSchema.ts.html +10 -10
- 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/git.ts.html +118 -16
- package/coverage/lcov-report/utils/index.html +14 -14
- package/coverage/lcov.info +2918 -1932
- package/lib/builder/buildDatafile.js +8 -3
- package/lib/builder/buildDatafile.js.map +1 -1
- package/lib/builder/mutateVariables.d.ts +14 -0
- package/lib/builder/mutateVariables.js +90 -0
- package/lib/builder/mutateVariables.js.map +1 -0
- package/lib/builder/mutateVariables.spec.d.ts +1 -0
- package/lib/builder/mutateVariables.spec.js +1045 -0
- package/lib/builder/mutateVariables.spec.js.map +1 -0
- package/lib/builder/mutator.d.ts +20 -0
- package/lib/builder/mutator.js +223 -0
- package/lib/builder/mutator.js.map +1 -0
- package/lib/builder/mutator.spec.d.ts +1 -0
- package/lib/builder/mutator.spec.js +368 -0
- package/lib/builder/mutator.spec.js.map +1 -0
- package/lib/config/projectConfig.d.ts +3 -0
- package/lib/config/projectConfig.js +7 -1
- package/lib/config/projectConfig.js.map +1 -1
- package/lib/config/projectConfig.spec.d.ts +1 -0
- package/lib/config/projectConfig.spec.js +24 -0
- package/lib/config/projectConfig.spec.js.map +1 -0
- package/lib/datasource/adapter.d.ts +7 -0
- package/lib/datasource/adapter.js +7 -0
- package/lib/datasource/adapter.js.map +1 -1
- package/lib/datasource/datasource.d.ts +3 -1
- package/lib/datasource/datasource.js +6 -0
- package/lib/datasource/datasource.js.map +1 -1
- package/lib/datasource/filesystemAdapter.d.ts +8 -0
- package/lib/datasource/filesystemAdapter.js +134 -7
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/datasource/filesystemAdapter.spec.d.ts +1 -0
- package/lib/datasource/filesystemAdapter.spec.js +88 -0
- package/lib/datasource/filesystemAdapter.spec.js.map +1 -0
- package/lib/linter/conditionSchema.spec.js +2 -0
- package/lib/linter/conditionSchema.spec.js.map +1 -1
- package/lib/linter/featureSchema.d.ts +7 -7
- package/lib/linter/featureSchema.js +176 -103
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +167 -0
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/lintProject.d.ts +2 -0
- package/lib/linter/lintProject.js +85 -8
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/lintProject.spec.js +37 -0
- package/lib/linter/lintProject.spec.js.map +1 -1
- package/lib/linter/mutationNotation.d.ts +47 -0
- package/lib/linter/mutationNotation.js +381 -0
- package/lib/linter/mutationNotation.js.map +1 -0
- package/lib/linter/mutationNotation.spec.d.ts +1 -0
- package/lib/linter/mutationNotation.spec.js +549 -0
- package/lib/linter/mutationNotation.spec.js.map +1 -0
- package/lib/linter/segmentSchema.spec.js +2 -0
- package/lib/linter/segmentSchema.spec.js.map +1 -1
- package/lib/utils/git.js +32 -7
- package/lib/utils/git.js.map +1 -1
- package/package.json +2 -2
- package/src/builder/buildDatafile.ts +29 -3
- package/src/builder/mutateVariables.spec.ts +1134 -0
- package/src/builder/mutateVariables.ts +105 -0
- package/src/builder/mutator.spec.ts +413 -0
- package/src/builder/mutator.ts +237 -0
- package/src/config/projectConfig.spec.ts +31 -0
- package/src/config/projectConfig.ts +11 -0
- package/src/datasource/adapter.ts +21 -0
- package/src/datasource/datasource.ts +18 -1
- package/src/datasource/filesystemAdapter.spec.ts +153 -0
- package/src/datasource/filesystemAdapter.ts +216 -8
- package/src/linter/conditionSchema.spec.ts +2 -0
- package/src/linter/featureSchema.spec.ts +203 -0
- package/src/linter/featureSchema.ts +320 -167
- package/src/linter/lintProject.spec.ts +74 -0
- package/src/linter/lintProject.ts +123 -8
- package/src/linter/mutationNotation.spec.ts +642 -0
- package/src/linter/mutationNotation.ts +408 -0
- package/src/linter/segmentSchema.spec.ts +2 -0
- package/src/utils/git.ts +41 -7
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import type { Schema } from "@featurevisor/types";
|
|
1
|
+
import type { Schema, SchemaType } from "@featurevisor/types";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
import { ProjectConfig } from "../config";
|
|
5
|
+
import {
|
|
6
|
+
isMutationKey,
|
|
7
|
+
validateMutationKey,
|
|
8
|
+
parsePathMapKey,
|
|
9
|
+
resolveSchemaAtPath,
|
|
10
|
+
} from "./mutationNotation";
|
|
5
11
|
import {
|
|
6
12
|
valueZodSchema,
|
|
7
13
|
propertyTypeEnum,
|
|
@@ -250,27 +256,42 @@ function valueDeepEqual(a: unknown, b: unknown): boolean {
|
|
|
250
256
|
return false;
|
|
251
257
|
}
|
|
252
258
|
|
|
259
|
+
type SchemaLikeForRequired = {
|
|
260
|
+
type?: string;
|
|
261
|
+
properties?: Record<string, unknown>;
|
|
262
|
+
required?: string[];
|
|
263
|
+
items?: unknown;
|
|
264
|
+
schema?: string;
|
|
265
|
+
oneOf?: unknown[];
|
|
266
|
+
};
|
|
267
|
+
|
|
253
268
|
/**
|
|
254
269
|
* Recursively validates that every `required` array (at this level and in nested
|
|
255
270
|
* object/array schemas) only contains keys that exist in the same level's `properties`.
|
|
256
271
|
* Adds Zod issues with the correct path for invalid required field names.
|
|
272
|
+
* When schemasByKey is provided, resolves schema references (schema: key) so that
|
|
273
|
+
* referenced and nested schemas are validated too.
|
|
257
274
|
*/
|
|
258
275
|
function refineRequiredKeysInSchema(
|
|
259
|
-
schema:
|
|
260
|
-
type?: string;
|
|
261
|
-
properties?: Record<string, unknown>;
|
|
262
|
-
required?: string[];
|
|
263
|
-
items?: unknown;
|
|
264
|
-
},
|
|
276
|
+
schema: SchemaLikeForRequired,
|
|
265
277
|
pathPrefix: (string | number)[],
|
|
266
278
|
ctx: z.RefinementCtx,
|
|
279
|
+
schemasByKey?: Record<string, Schema>,
|
|
267
280
|
): void {
|
|
268
281
|
if (!schema || typeof schema !== "object") return;
|
|
269
282
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
283
|
+
// Resolve schema reference so we validate the referenced schema (and can recurse into it)
|
|
284
|
+
let current: SchemaLikeForRequired = schema;
|
|
285
|
+
if (schema.schema && schemasByKey?.[schema.schema]) {
|
|
286
|
+
current = schemasByKey[schema.schema] as SchemaLikeForRequired;
|
|
287
|
+
refineRequiredKeysInSchema(current, pathPrefix, ctx, schemasByKey);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const effectiveType = current.type;
|
|
292
|
+
const properties = current.properties;
|
|
293
|
+
const required = current.required;
|
|
294
|
+
const items = current.items;
|
|
274
295
|
|
|
275
296
|
if (
|
|
276
297
|
effectiveType === "object" &&
|
|
@@ -296,9 +317,10 @@ function refineRequiredKeysInSchema(
|
|
|
296
317
|
const nested = properties[key];
|
|
297
318
|
if (nested && typeof nested === "object") {
|
|
298
319
|
refineRequiredKeysInSchema(
|
|
299
|
-
nested as
|
|
320
|
+
nested as SchemaLikeForRequired,
|
|
300
321
|
[...pathPrefix, "properties", key],
|
|
301
322
|
ctx,
|
|
323
|
+
schemasByKey,
|
|
302
324
|
);
|
|
303
325
|
}
|
|
304
326
|
}
|
|
@@ -306,20 +328,22 @@ function refineRequiredKeysInSchema(
|
|
|
306
328
|
|
|
307
329
|
if (items && typeof items === "object" && !Array.isArray(items)) {
|
|
308
330
|
refineRequiredKeysInSchema(
|
|
309
|
-
items as
|
|
331
|
+
items as SchemaLikeForRequired,
|
|
310
332
|
[...pathPrefix, "items"],
|
|
311
333
|
ctx,
|
|
334
|
+
schemasByKey,
|
|
312
335
|
);
|
|
313
336
|
}
|
|
314
337
|
|
|
315
|
-
const oneOf = (
|
|
338
|
+
const oneOf = (current as { oneOf?: unknown[] }).oneOf;
|
|
316
339
|
if (oneOf && Array.isArray(oneOf)) {
|
|
317
340
|
oneOf.forEach((branch, i) => {
|
|
318
341
|
if (branch && typeof branch === "object") {
|
|
319
342
|
refineRequiredKeysInSchema(
|
|
320
|
-
branch as
|
|
343
|
+
branch as SchemaLikeForRequired,
|
|
321
344
|
[...pathPrefix, "oneOf", i],
|
|
322
345
|
ctx,
|
|
346
|
+
schemasByKey,
|
|
323
347
|
);
|
|
324
348
|
}
|
|
325
349
|
});
|
|
@@ -504,15 +528,21 @@ function refineVariableValueObject(
|
|
|
504
528
|
}
|
|
505
529
|
}
|
|
506
530
|
|
|
531
|
+
/** Schema or variable schema (e.g. type "json", defaultValue); used for value validation. */
|
|
532
|
+
type VariableSchemaLike =
|
|
533
|
+
| (Omit<Schema, "type"> & { type?: SchemaType | "json" })
|
|
534
|
+
| null
|
|
535
|
+
| undefined;
|
|
536
|
+
|
|
507
537
|
function superRefineVariableValue(
|
|
508
538
|
projectConfig: ProjectConfig,
|
|
509
|
-
variableSchema,
|
|
510
|
-
variableValue,
|
|
511
|
-
path,
|
|
512
|
-
ctx,
|
|
539
|
+
variableSchema: VariableSchemaLike,
|
|
540
|
+
variableValue: unknown,
|
|
541
|
+
path: (string | number)[],
|
|
542
|
+
ctx: z.RefinementCtx,
|
|
513
543
|
variableKey?: string,
|
|
514
544
|
schemasByKey?: Record<string, Schema>,
|
|
515
|
-
) {
|
|
545
|
+
): void {
|
|
516
546
|
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
517
547
|
|
|
518
548
|
if (!variableSchema) {
|
|
@@ -854,6 +884,66 @@ function superRefineVariableValue(
|
|
|
854
884
|
});
|
|
855
885
|
}
|
|
856
886
|
|
|
887
|
+
/**
|
|
888
|
+
* Validate a single variable entry (key + value) in variables map.
|
|
889
|
+
* Key may be a root variable name or mutation notation (e.g. "config.width", "items[0].name").
|
|
890
|
+
*/
|
|
891
|
+
function refineVariableEntry(
|
|
892
|
+
projectConfig: ProjectConfig,
|
|
893
|
+
variableSchemaByKey: Record<string, unknown>,
|
|
894
|
+
variableKey: string,
|
|
895
|
+
variableValue: unknown,
|
|
896
|
+
path: (string | number)[],
|
|
897
|
+
ctx: z.RefinementCtx,
|
|
898
|
+
schemasByKey?: Record<string, Schema>,
|
|
899
|
+
): void {
|
|
900
|
+
if (isMutationKey(variableKey)) {
|
|
901
|
+
const validation = validateMutationKey(
|
|
902
|
+
variableKey,
|
|
903
|
+
variableSchemaByKey as Record<string, Schema>,
|
|
904
|
+
schemasByKey,
|
|
905
|
+
);
|
|
906
|
+
if (!validation.valid && validation.error) {
|
|
907
|
+
ctx.addIssue({
|
|
908
|
+
code: z.ZodIssueCode.custom,
|
|
909
|
+
message: validation.error,
|
|
910
|
+
path,
|
|
911
|
+
});
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (validation.valueSchema) {
|
|
915
|
+
superRefineVariableValue(
|
|
916
|
+
projectConfig,
|
|
917
|
+
validation.valueSchema,
|
|
918
|
+
variableValue,
|
|
919
|
+
path,
|
|
920
|
+
ctx,
|
|
921
|
+
variableKey,
|
|
922
|
+
schemasByKey,
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
928
|
+
if (!variableSchema) {
|
|
929
|
+
ctx.addIssue({
|
|
930
|
+
code: z.ZodIssueCode.custom,
|
|
931
|
+
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
932
|
+
path,
|
|
933
|
+
});
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
superRefineVariableValue(
|
|
937
|
+
projectConfig,
|
|
938
|
+
variableSchema as Parameters<typeof superRefineVariableValue>[1],
|
|
939
|
+
variableValue,
|
|
940
|
+
path,
|
|
941
|
+
ctx,
|
|
942
|
+
variableKey,
|
|
943
|
+
schemasByKey,
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
857
947
|
function refineForce({
|
|
858
948
|
ctx,
|
|
859
949
|
parsedFeature, // eslint-disable-line
|
|
@@ -879,24 +969,15 @@ function refineForce({
|
|
|
879
969
|
// force[n].variables[key]
|
|
880
970
|
if (f.variables) {
|
|
881
971
|
Object.keys(f.variables).forEach((variableKey) => {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
projectConfig,
|
|
892
|
-
variableSchema,
|
|
893
|
-
f.variables[variableKey],
|
|
894
|
-
pathPrefix.concat([fN, "variables", variableKey]),
|
|
895
|
-
ctx,
|
|
896
|
-
variableKey,
|
|
897
|
-
schemasByKey,
|
|
898
|
-
);
|
|
899
|
-
}
|
|
972
|
+
refineVariableEntry(
|
|
973
|
+
projectConfig,
|
|
974
|
+
variableSchemaByKey,
|
|
975
|
+
variableKey,
|
|
976
|
+
f.variables[variableKey],
|
|
977
|
+
pathPrefix.concat([fN, "variables", variableKey]),
|
|
978
|
+
ctx,
|
|
979
|
+
schemasByKey,
|
|
980
|
+
);
|
|
900
981
|
});
|
|
901
982
|
}
|
|
902
983
|
});
|
|
@@ -916,24 +997,15 @@ function refineRules({
|
|
|
916
997
|
// rules[n].variables[key]
|
|
917
998
|
if (rule.variables) {
|
|
918
999
|
Object.keys(rule.variables).forEach((variableKey) => {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
projectConfig,
|
|
929
|
-
variableSchema,
|
|
930
|
-
rule.variables[variableKey],
|
|
931
|
-
pathPrefix.concat([ruleN, "variables", variableKey]),
|
|
932
|
-
ctx,
|
|
933
|
-
variableKey,
|
|
934
|
-
schemasByKey,
|
|
935
|
-
);
|
|
936
|
-
}
|
|
1000
|
+
refineVariableEntry(
|
|
1001
|
+
projectConfig,
|
|
1002
|
+
variableSchemaByKey,
|
|
1003
|
+
variableKey,
|
|
1004
|
+
rule.variables[variableKey],
|
|
1005
|
+
pathPrefix.concat([ruleN, "variables", variableKey]),
|
|
1006
|
+
ctx,
|
|
1007
|
+
schemasByKey,
|
|
1008
|
+
);
|
|
937
1009
|
});
|
|
938
1010
|
}
|
|
939
1011
|
|
|
@@ -1014,6 +1086,7 @@ export function getFeatureZodSchema(
|
|
|
1014
1086
|
) {
|
|
1015
1087
|
const schemaZodSchema = getSchemaZodSchema(availableSchemaKeys);
|
|
1016
1088
|
const variableValueZodSchema = valueZodSchema;
|
|
1089
|
+
const variableValueOrNullZodSchema = z.union([valueZodSchema, z.null()]);
|
|
1017
1090
|
|
|
1018
1091
|
const variationValueZodSchema = z.string().min(1);
|
|
1019
1092
|
|
|
@@ -1061,7 +1134,7 @@ export function getFeatureZodSchema(
|
|
|
1061
1134
|
|
|
1062
1135
|
enabled: z.boolean().optional(),
|
|
1063
1136
|
variation: variationValueZodSchema.optional(),
|
|
1064
|
-
variables: z.record(
|
|
1137
|
+
variables: z.record(variableValueOrNullZodSchema).optional(),
|
|
1065
1138
|
variationWeights: z.record(z.number().min(0).max(100)).optional(),
|
|
1066
1139
|
})
|
|
1067
1140
|
.strict(),
|
|
@@ -1109,7 +1182,7 @@ export function getFeatureZodSchema(
|
|
|
1109
1182
|
segments: groupSegmentsZodSchema,
|
|
1110
1183
|
enabled: z.boolean().optional(),
|
|
1111
1184
|
variation: variationValueZodSchema.optional(),
|
|
1112
|
-
variables: z.record(
|
|
1185
|
+
variables: z.record(variableValueOrNullZodSchema).optional(),
|
|
1113
1186
|
})
|
|
1114
1187
|
.strict(),
|
|
1115
1188
|
z
|
|
@@ -1117,7 +1190,7 @@ export function getFeatureZodSchema(
|
|
|
1117
1190
|
conditions: conditionsZodSchema,
|
|
1118
1191
|
enabled: z.boolean().optional(),
|
|
1119
1192
|
variation: variationValueZodSchema.optional(),
|
|
1120
|
-
variables: z.record(
|
|
1193
|
+
variables: z.record(variableValueOrNullZodSchema).optional(),
|
|
1121
1194
|
})
|
|
1122
1195
|
.strict(),
|
|
1123
1196
|
]),
|
|
@@ -1299,11 +1372,12 @@ export function getFeatureZodSchema(
|
|
|
1299
1372
|
});
|
|
1300
1373
|
return;
|
|
1301
1374
|
}
|
|
1302
|
-
// Validate required ⊆ properties at this level and in all nested object schemas
|
|
1375
|
+
// Validate required ⊆ properties at this level and in all nested object schemas (resolve refs when schemasByKey provided)
|
|
1303
1376
|
refineRequiredKeysInSchema(
|
|
1304
|
-
variableSchema as
|
|
1377
|
+
variableSchema as SchemaLikeForRequired,
|
|
1305
1378
|
[],
|
|
1306
1379
|
ctx,
|
|
1380
|
+
schemasByKey,
|
|
1307
1381
|
);
|
|
1308
1382
|
}),
|
|
1309
1383
|
)
|
|
@@ -1318,7 +1392,7 @@ export function getFeatureZodSchema(
|
|
|
1318
1392
|
description: z.string().optional(),
|
|
1319
1393
|
value: variationValueZodSchema,
|
|
1320
1394
|
weight: z.number().min(0).max(100),
|
|
1321
|
-
variables: z.record(
|
|
1395
|
+
variables: z.record(variableValueOrNullZodSchema).optional(),
|
|
1322
1396
|
variableOverrides: z
|
|
1323
1397
|
.record(
|
|
1324
1398
|
z.array(
|
|
@@ -1391,15 +1465,9 @@ export function getFeatureZodSchema(
|
|
|
1391
1465
|
}
|
|
1392
1466
|
}
|
|
1393
1467
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
}
|
|
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]
|
|
1402
|
-
const variableSchemaByKey = value.variablesSchema;
|
|
1468
|
+
// When variablesSchema is absent, variableSchemaByKey is {} so any variable key used in
|
|
1469
|
+
// rules/force/variations will be reported as "not defined in variablesSchema".
|
|
1470
|
+
const variableSchemaByKey = value.variablesSchema ?? {};
|
|
1403
1471
|
const variationValues: string[] = [];
|
|
1404
1472
|
|
|
1405
1473
|
if (value.variations) {
|
|
@@ -1408,78 +1476,102 @@ export function getFeatureZodSchema(
|
|
|
1408
1476
|
});
|
|
1409
1477
|
}
|
|
1410
1478
|
|
|
1411
|
-
// variablesSchema[key]
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
}
|
|
1479
|
+
// variablesSchema[key] — only when feature defines variablesSchema
|
|
1480
|
+
if (value.variablesSchema) {
|
|
1481
|
+
const variableKeys = Object.keys(variableSchemaByKey);
|
|
1482
|
+
variableKeys.forEach((variableKey) => {
|
|
1483
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
1430
1484
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1485
|
+
// When variable references a schema by name, ensure it resolves and validate the referenced schema
|
|
1486
|
+
if ("schema" in variableSchema && variableSchema.schema) {
|
|
1487
|
+
const resolvedSchema = resolveVariableSchema(
|
|
1488
|
+
variableSchema as Parameters<typeof resolveVariableSchema>[0],
|
|
1489
|
+
schemasByKey,
|
|
1490
|
+
);
|
|
1491
|
+
if (!resolvedSchema) {
|
|
1492
|
+
ctx.addIssue({
|
|
1493
|
+
code: z.ZodIssueCode.custom,
|
|
1494
|
+
message: `Schema "${variableSchema.schema}" could not be loaded for variable "${variableKey}".`,
|
|
1495
|
+
path: ["variablesSchema", variableKey],
|
|
1496
|
+
});
|
|
1497
|
+
} else {
|
|
1498
|
+
refineRequiredKeysInSchema(
|
|
1499
|
+
resolvedSchema as SchemaLikeForRequired,
|
|
1500
|
+
["variablesSchema", variableKey],
|
|
1501
|
+
ctx,
|
|
1502
|
+
schemasByKey,
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1450
1506
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1507
|
+
// When type and enum are both present, all enum values must match the type
|
|
1508
|
+
const effectiveSchema = resolveVariableSchema(variableSchema, schemasByKey);
|
|
1509
|
+
if (
|
|
1510
|
+
effectiveSchema &&
|
|
1511
|
+
effectiveSchema.type &&
|
|
1512
|
+
Array.isArray(effectiveSchema.enum) &&
|
|
1513
|
+
effectiveSchema.enum.length > 0
|
|
1514
|
+
) {
|
|
1515
|
+
refineEnumMatchesType(
|
|
1516
|
+
effectiveSchema as Parameters<typeof refineEnumMatchesType>[0],
|
|
1517
|
+
["variablesSchema", variableKey],
|
|
1518
|
+
ctx,
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1458
1521
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1522
|
+
// Inline variable schemas: validate minimum/maximum, minLength/maxLength/pattern, minItems/maxItems/uniqueItems
|
|
1523
|
+
if (!("schema" in variableSchema) || !variableSchema.schema) {
|
|
1524
|
+
const pathPrefix = ["variablesSchema", variableKey];
|
|
1525
|
+
refineMinimumMaximum(
|
|
1526
|
+
variableSchema as Parameters<typeof refineMinimumMaximum>[0],
|
|
1527
|
+
pathPrefix,
|
|
1528
|
+
ctx,
|
|
1529
|
+
);
|
|
1530
|
+
refineStringLengthPattern(
|
|
1531
|
+
variableSchema as Parameters<typeof refineStringLengthPattern>[0],
|
|
1532
|
+
pathPrefix,
|
|
1533
|
+
ctx,
|
|
1534
|
+
);
|
|
1535
|
+
refineArrayItems(
|
|
1536
|
+
variableSchema as Parameters<typeof refineArrayItems>[0],
|
|
1537
|
+
pathPrefix,
|
|
1538
|
+
ctx,
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1469
1541
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1542
|
+
if (variableKey === "variation") {
|
|
1543
|
+
ctx.addIssue({
|
|
1544
|
+
code: z.ZodIssueCode.custom,
|
|
1545
|
+
message: `Variable key "${variableKey}" is reserved and cannot be used.`,
|
|
1546
|
+
path: ["variablesSchema", variableKey],
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// defaultValue
|
|
1472
1551
|
superRefineVariableValue(
|
|
1473
1552
|
projectConfig,
|
|
1474
1553
|
variableSchema,
|
|
1475
|
-
variableSchema.
|
|
1476
|
-
["variablesSchema", variableKey, "
|
|
1554
|
+
variableSchema.defaultValue,
|
|
1555
|
+
["variablesSchema", variableKey, "defaultValue"],
|
|
1477
1556
|
ctx,
|
|
1478
1557
|
variableKey,
|
|
1479
1558
|
schemasByKey,
|
|
1480
1559
|
);
|
|
1481
|
-
|
|
1482
|
-
|
|
1560
|
+
|
|
1561
|
+
// disabledValue (only when present)
|
|
1562
|
+
if (variableSchema.disabledValue !== undefined) {
|
|
1563
|
+
superRefineVariableValue(
|
|
1564
|
+
projectConfig,
|
|
1565
|
+
variableSchema,
|
|
1566
|
+
variableSchema.disabledValue,
|
|
1567
|
+
["variablesSchema", variableKey, "disabledValue"],
|
|
1568
|
+
ctx,
|
|
1569
|
+
variableKey,
|
|
1570
|
+
schemasByKey,
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1483
1575
|
|
|
1484
1576
|
// variations: validate variation.variables and variation.variableOverrides (each value against its variable schema)
|
|
1485
1577
|
if (value.variations) {
|
|
@@ -1487,32 +1579,21 @@ export function getFeatureZodSchema(
|
|
|
1487
1579
|
// variations[n].variables[key]
|
|
1488
1580
|
if (variation.variables) {
|
|
1489
1581
|
for (const variableKey of Object.keys(variation.variables)) {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
superRefineVariableValue(
|
|
1500
|
-
projectConfig,
|
|
1501
|
-
variableSchema,
|
|
1502
|
-
variableValue,
|
|
1503
|
-
["variations", variationN, "variables", variableKey],
|
|
1504
|
-
ctx,
|
|
1505
|
-
variableKey,
|
|
1506
|
-
schemasByKey,
|
|
1507
|
-
);
|
|
1508
|
-
}
|
|
1582
|
+
refineVariableEntry(
|
|
1583
|
+
projectConfig,
|
|
1584
|
+
variableSchemaByKey,
|
|
1585
|
+
variableKey,
|
|
1586
|
+
variation.variables[variableKey],
|
|
1587
|
+
["variations", variationN, "variables", variableKey],
|
|
1588
|
+
ctx,
|
|
1589
|
+
schemasByKey,
|
|
1590
|
+
);
|
|
1509
1591
|
}
|
|
1510
1592
|
}
|
|
1511
1593
|
|
|
1512
|
-
// variations[n].variableOverrides[key][].value (
|
|
1594
|
+
// variations[n].variableOverrides[key][].value (path-map or full value)
|
|
1513
1595
|
if (variation.variableOverrides) {
|
|
1514
1596
|
for (const variableKey of Object.keys(variation.variableOverrides)) {
|
|
1515
|
-
const overrides = variation.variableOverrides[variableKey];
|
|
1516
1597
|
const variableSchema = variableSchemaByKey[variableKey];
|
|
1517
1598
|
if (!variableSchema) {
|
|
1518
1599
|
ctx.addIssue({
|
|
@@ -1520,26 +1601,98 @@ export function getFeatureZodSchema(
|
|
|
1520
1601
|
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
1521
1602
|
path: ["variations", variationN, "variableOverrides", variableKey],
|
|
1522
1603
|
});
|
|
1523
|
-
|
|
1524
|
-
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
// When variable references a schema by name, ensure it can be resolved
|
|
1607
|
+
const variableSchemaWithRef = variableSchema as { schema?: string };
|
|
1608
|
+
if (variableSchemaWithRef.schema) {
|
|
1609
|
+
const resolvedVarSchema = resolveVariableSchema(
|
|
1610
|
+
variableSchema as Parameters<typeof resolveVariableSchema>[0],
|
|
1611
|
+
schemasByKey,
|
|
1612
|
+
);
|
|
1613
|
+
if (!resolvedVarSchema) {
|
|
1614
|
+
ctx.addIssue({
|
|
1615
|
+
code: z.ZodIssueCode.custom,
|
|
1616
|
+
message: `Schema "${variableSchemaWithRef.schema}" could not be loaded for variable "${variableKey}".`,
|
|
1617
|
+
path: ["variations", variationN, "variableOverrides", variableKey],
|
|
1618
|
+
});
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
const overrides = variation.variableOverrides[variableKey];
|
|
1623
|
+
if (!Array.isArray(overrides)) continue;
|
|
1624
|
+
overrides.forEach((override, overrideN) => {
|
|
1625
|
+
const overrideValue = override.value;
|
|
1626
|
+
const valuePath = [
|
|
1627
|
+
"variations",
|
|
1628
|
+
variationN,
|
|
1629
|
+
"variableOverrides",
|
|
1630
|
+
variableKey,
|
|
1631
|
+
overrideN,
|
|
1632
|
+
"value",
|
|
1633
|
+
];
|
|
1634
|
+
if (
|
|
1635
|
+
typeof overrideValue === "object" &&
|
|
1636
|
+
overrideValue !== null &&
|
|
1637
|
+
!Array.isArray(overrideValue)
|
|
1638
|
+
) {
|
|
1639
|
+
const pathMap = overrideValue as Record<string, unknown>;
|
|
1640
|
+
for (const pathKey of Object.keys(pathMap)) {
|
|
1641
|
+
const pathSegments = parsePathMapKey(pathKey);
|
|
1642
|
+
if (!pathSegments || pathSegments.length === 0) {
|
|
1643
|
+
ctx.addIssue({
|
|
1644
|
+
code: z.ZodIssueCode.custom,
|
|
1645
|
+
message: `Invalid mutation path "${pathKey}" in variableOverride for "${variableKey}".`,
|
|
1646
|
+
path: [...valuePath, pathKey],
|
|
1647
|
+
});
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const atPathSchema = resolveSchemaAtPath(
|
|
1651
|
+
variableSchema as Schema,
|
|
1652
|
+
pathSegments,
|
|
1653
|
+
schemasByKey,
|
|
1654
|
+
);
|
|
1655
|
+
if (!atPathSchema) {
|
|
1656
|
+
const effectiveSchema = resolveVariableSchema(
|
|
1657
|
+
variableSchema as Parameters<typeof resolveVariableSchema>[0],
|
|
1658
|
+
schemasByKey,
|
|
1659
|
+
);
|
|
1660
|
+
const isFlatObject =
|
|
1661
|
+
effectiveSchema?.type === "object" &&
|
|
1662
|
+
(!effectiveSchema.properties ||
|
|
1663
|
+
Object.keys(effectiveSchema.properties).length === 0);
|
|
1664
|
+
if (isFlatObject) {
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
ctx.addIssue({
|
|
1668
|
+
code: z.ZodIssueCode.custom,
|
|
1669
|
+
message: `Path "${pathKey}" is invalid for variable "${variableKey}" (not in schema).`,
|
|
1670
|
+
path: [...valuePath, pathKey],
|
|
1671
|
+
});
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
superRefineVariableValue(
|
|
1675
|
+
projectConfig,
|
|
1676
|
+
atPathSchema,
|
|
1677
|
+
pathMap[pathKey],
|
|
1678
|
+
[...valuePath, pathKey],
|
|
1679
|
+
ctx,
|
|
1680
|
+
pathKey,
|
|
1681
|
+
schemasByKey,
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
} else {
|
|
1525
1685
|
superRefineVariableValue(
|
|
1526
1686
|
projectConfig,
|
|
1527
|
-
variableSchema,
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
"variations",
|
|
1531
|
-
variationN,
|
|
1532
|
-
"variableOverrides",
|
|
1533
|
-
variableKey,
|
|
1534
|
-
overrideN,
|
|
1535
|
-
"value",
|
|
1536
|
-
],
|
|
1687
|
+
variableSchema as Parameters<typeof superRefineVariableValue>[1],
|
|
1688
|
+
overrideValue,
|
|
1689
|
+
valuePath,
|
|
1537
1690
|
ctx,
|
|
1538
1691
|
variableKey,
|
|
1539
1692
|
schemasByKey,
|
|
1540
1693
|
);
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1543
1696
|
}
|
|
1544
1697
|
}
|
|
1545
1698
|
});
|