@featurevisor/core 2.17.0 → 2.19.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 +1692 -1196
- package/coverage/coverage-final.json +29 -22
- package/coverage/lcov-report/builder/allocator.ts.html +26 -26
- package/coverage/lcov-report/builder/buildDatafile.ts.html +2017 -0
- package/coverage/lcov-report/builder/buildScopedConditions.ts.html +35 -35
- package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +213 -45
- package/coverage/lcov-report/builder/buildScopedSegments.ts.html +28 -28
- package/coverage/lcov-report/builder/convertToV1.ts.html +583 -0
- package/coverage/lcov-report/builder/getFeatureRanges.ts.html +268 -0
- package/coverage/lcov-report/builder/hashes.ts.html +412 -0
- package/coverage/lcov-report/builder/index.html +90 -30
- package/coverage/lcov-report/builder/mutateVariables.ts.html +66 -45
- package/coverage/lcov-report/builder/mutator.ts.html +95 -95
- package/coverage/lcov-report/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +60 -57
- package/coverage/lcov-report/config/index.html +1 -1
- package/coverage/lcov-report/config/index.ts.html +2 -2
- package/coverage/lcov-report/config/projectConfig.ts.html +28 -28
- package/coverage/lcov-report/datasource/adapter.ts.html +2 -2
- package/coverage/lcov-report/datasource/datasource.ts.html +4 -4
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +34 -34
- package/coverage/lcov-report/datasource/index.html +1 -1
- package/coverage/lcov-report/datasource/index.ts.html +1 -1
- package/coverage/lcov-report/index.html +39 -39
- package/coverage/lcov-report/linter/attributeSchema.ts.html +1 -1
- 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 +17 -17
- package/coverage/lcov-report/linter/featureSchema.ts.html +879 -417
- package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/index.html +34 -34
- package/coverage/lcov-report/linter/lintProject.ts.html +25 -25
- package/coverage/lcov-report/linter/mutationNotation.ts.html +224 -122
- package/coverage/lcov-report/linter/printError.ts.html +1 -1
- package/coverage/lcov-report/linter/schema.ts.html +175 -85
- package/coverage/lcov-report/linter/segmentSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/testSchema.ts.html +5 -5
- 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 +493 -0
- package/coverage/lcov-report/utils/git.ts.html +3 -3
- package/coverage/lcov-report/utils/index.html +54 -9
- package/coverage/lcov-report/utils/index.ts.html +94 -0
- package/coverage/lcov-report/utils/pretty.ts.html +121 -0
- package/coverage/lcov.info +3206 -2260
- package/json-schema/attribute.json +25 -9
- package/json-schema/feature.json +319 -238
- package/json-schema/segment.json +76 -45
- package/lib/builder/buildDatafile.js +50 -3
- package/lib/builder/buildDatafile.js.map +1 -1
- package/lib/builder/buildDatafile.spec.d.ts +1 -0
- package/lib/builder/buildDatafile.spec.js +233 -0
- package/lib/builder/buildDatafile.spec.js.map +1 -0
- package/lib/builder/buildScopedDatafile.js +37 -1
- package/lib/builder/buildScopedDatafile.js.map +1 -1
- package/lib/builder/buildScopedDatafile.spec.js +289 -0
- package/lib/builder/buildScopedDatafile.spec.js.map +1 -1
- package/lib/builder/mutateVariables.d.ts +1 -1
- package/lib/builder/mutateVariables.js +4 -1
- package/lib/builder/mutateVariables.js.map +1 -1
- package/lib/builder/mutateVariables.spec.js +29 -0
- package/lib/builder/mutateVariables.spec.js.map +1 -1
- package/lib/builder/traffic.js +1 -0
- package/lib/builder/traffic.js.map +1 -1
- package/lib/builder/traffic.spec.js +26 -0
- package/lib/builder/traffic.spec.js.map +1 -1
- package/lib/generate-code/typescript.js +63 -20
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/linter/featureSchema.d.ts +237 -22
- package/lib/linter/featureSchema.js +175 -84
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +268 -0
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/mutationNotation.js +54 -19
- package/lib/linter/mutationNotation.js.map +1 -1
- package/lib/linter/mutationNotation.spec.js +49 -0
- package/lib/linter/mutationNotation.spec.js.map +1 -1
- package/lib/linter/schema.d.ts +1 -0
- package/lib/linter/schema.js +13 -0
- package/lib/linter/schema.js.map +1 -1
- package/lib/linter/schema.spec.js +51 -0
- package/lib/linter/schema.spec.js.map +1 -1
- package/package.json +5 -5
- package/src/builder/buildDatafile.spec.ts +267 -0
- package/src/builder/buildDatafile.ts +85 -4
- package/src/builder/buildScopedDatafile.spec.ts +330 -0
- package/src/builder/buildScopedDatafile.ts +57 -1
- package/src/builder/mutateVariables.spec.ts +45 -0
- package/src/builder/mutateVariables.ts +7 -0
- package/src/builder/traffic.spec.ts +33 -0
- package/src/builder/traffic.ts +1 -0
- package/src/generate-code/typescript.ts +87 -20
- package/src/linter/featureSchema.spec.ts +315 -0
- package/src/linter/featureSchema.ts +269 -115
- package/src/linter/mutationNotation.spec.ts +62 -0
- package/src/linter/mutationNotation.ts +47 -13
- package/src/linter/schema.spec.ts +72 -0
- package/src/linter/schema.ts +30 -0
|
@@ -2,12 +2,7 @@ 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
|
+
import { isMutationKey, validateMutationKey } from "./mutationNotation";
|
|
11
6
|
import {
|
|
12
7
|
valueZodSchema,
|
|
13
8
|
propertyTypeEnum,
|
|
@@ -51,6 +46,7 @@ function resolveVariableSchema(
|
|
|
51
46
|
type?: string;
|
|
52
47
|
items?: unknown;
|
|
53
48
|
properties?: unknown;
|
|
49
|
+
additionalProperties?: unknown;
|
|
54
50
|
required?: string[];
|
|
55
51
|
enum?: unknown[];
|
|
56
52
|
const?: unknown;
|
|
@@ -69,6 +65,7 @@ function resolveVariableSchema(
|
|
|
69
65
|
type?: string;
|
|
70
66
|
items?: unknown;
|
|
71
67
|
properties?: unknown;
|
|
68
|
+
additionalProperties?: unknown;
|
|
72
69
|
required?: string[];
|
|
73
70
|
enum?: unknown[];
|
|
74
71
|
const?: unknown;
|
|
@@ -89,6 +86,7 @@ function resolveVariableSchema(
|
|
|
89
86
|
type?: string;
|
|
90
87
|
items?: unknown;
|
|
91
88
|
properties?: unknown;
|
|
89
|
+
additionalProperties?: unknown;
|
|
92
90
|
required?: string[];
|
|
93
91
|
enum?: unknown[];
|
|
94
92
|
const?: unknown;
|
|
@@ -133,6 +131,7 @@ function valueMatchesSchema(
|
|
|
133
131
|
enum?: unknown[];
|
|
134
132
|
oneOf?: unknown[];
|
|
135
133
|
properties?: Record<string, unknown>;
|
|
134
|
+
additionalProperties?: unknown;
|
|
136
135
|
required?: string[];
|
|
137
136
|
items?: unknown;
|
|
138
137
|
minimum?: number;
|
|
@@ -195,8 +194,16 @@ function valueMatchesSchema(
|
|
|
195
194
|
if (type === "object") {
|
|
196
195
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
197
196
|
const props = resolved.properties;
|
|
198
|
-
|
|
197
|
+
const additional = resolved.additionalProperties;
|
|
199
198
|
const obj = value as Record<string, unknown>;
|
|
199
|
+
if ((!props || typeof props !== "object") && (!additional || typeof additional !== "object")) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (!props || typeof props !== "object") {
|
|
203
|
+
return Object.keys(obj).every((key) =>
|
|
204
|
+
valueMatchesSchema(additional as { [k: string]: unknown }, obj[key], schemasByKey),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
200
207
|
const required = new Set(resolved.required || []);
|
|
201
208
|
for (const key of required) {
|
|
202
209
|
if (!Object.prototype.hasOwnProperty.call(obj, key)) return false;
|
|
@@ -205,9 +212,15 @@ function valueMatchesSchema(
|
|
|
205
212
|
}
|
|
206
213
|
for (const key of Object.keys(obj)) {
|
|
207
214
|
const propSchema = props[key];
|
|
208
|
-
if (
|
|
209
|
-
|
|
215
|
+
if (propSchema) {
|
|
216
|
+
if (!valueMatchesSchema(propSchema as { [k: string]: unknown }, obj[key], schemasByKey))
|
|
217
|
+
return false;
|
|
218
|
+
} else if (additional && typeof additional === "object") {
|
|
219
|
+
if (!valueMatchesSchema(additional as { [k: string]: unknown }, obj[key], schemasByKey))
|
|
220
|
+
return false;
|
|
221
|
+
} else {
|
|
210
222
|
return false;
|
|
223
|
+
}
|
|
211
224
|
}
|
|
212
225
|
return true;
|
|
213
226
|
}
|
|
@@ -259,6 +272,7 @@ function valueDeepEqual(a: unknown, b: unknown): boolean {
|
|
|
259
272
|
type SchemaLikeForRequired = {
|
|
260
273
|
type?: string;
|
|
261
274
|
properties?: Record<string, unknown>;
|
|
275
|
+
additionalProperties?: unknown;
|
|
262
276
|
required?: string[];
|
|
263
277
|
items?: unknown;
|
|
264
278
|
schema?: string;
|
|
@@ -325,6 +339,19 @@ function refineRequiredKeysInSchema(
|
|
|
325
339
|
}
|
|
326
340
|
}
|
|
327
341
|
}
|
|
342
|
+
const additionalProperties = (current as { additionalProperties?: unknown }).additionalProperties;
|
|
343
|
+
if (
|
|
344
|
+
additionalProperties &&
|
|
345
|
+
typeof additionalProperties === "object" &&
|
|
346
|
+
!Array.isArray(additionalProperties)
|
|
347
|
+
) {
|
|
348
|
+
refineRequiredKeysInSchema(
|
|
349
|
+
additionalProperties as SchemaLikeForRequired,
|
|
350
|
+
[...pathPrefix, "additionalProperties"],
|
|
351
|
+
ctx,
|
|
352
|
+
schemasByKey,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
328
355
|
|
|
329
356
|
if (items && typeof items === "object" && !Array.isArray(items)) {
|
|
330
357
|
refineRequiredKeysInSchema(
|
|
@@ -453,6 +480,7 @@ function refineVariableValueObject(
|
|
|
453
480
|
projectConfig: ProjectConfig,
|
|
454
481
|
variableSchema: {
|
|
455
482
|
properties?: Record<string, unknown>;
|
|
483
|
+
additionalProperties?: unknown;
|
|
456
484
|
required?: string[];
|
|
457
485
|
type: string;
|
|
458
486
|
},
|
|
@@ -464,6 +492,7 @@ function refineVariableValueObject(
|
|
|
464
492
|
): void {
|
|
465
493
|
const label = getVariableLabel(variableSchema, variableKey, path);
|
|
466
494
|
const schemaProperties = variableSchema.properties;
|
|
495
|
+
const additionalProperties = variableSchema.additionalProperties;
|
|
467
496
|
|
|
468
497
|
if (schemaProperties && typeof schemaProperties === "object") {
|
|
469
498
|
const requiredKeys =
|
|
@@ -485,7 +514,17 @@ function refineVariableValueObject(
|
|
|
485
514
|
|
|
486
515
|
for (const key of Object.keys(variableValue)) {
|
|
487
516
|
const propSchema = schemaProperties[key];
|
|
488
|
-
if (!propSchema) {
|
|
517
|
+
if (!propSchema && additionalProperties && typeof additionalProperties === "object") {
|
|
518
|
+
superRefineVariableValue(
|
|
519
|
+
projectConfig,
|
|
520
|
+
additionalProperties as Parameters<typeof superRefineVariableValue>[1],
|
|
521
|
+
variableValue[key],
|
|
522
|
+
[...path, key],
|
|
523
|
+
ctx,
|
|
524
|
+
key,
|
|
525
|
+
schemasByKey,
|
|
526
|
+
);
|
|
527
|
+
} else if (!propSchema) {
|
|
489
528
|
ctx.addIssue({
|
|
490
529
|
code: z.ZodIssueCode.custom,
|
|
491
530
|
message: `Unknown property "${key}" in variable "${label}" (not in schema)`,
|
|
@@ -503,6 +542,18 @@ function refineVariableValueObject(
|
|
|
503
542
|
);
|
|
504
543
|
}
|
|
505
544
|
}
|
|
545
|
+
} else if (additionalProperties && typeof additionalProperties === "object") {
|
|
546
|
+
for (const key of Object.keys(variableValue)) {
|
|
547
|
+
superRefineVariableValue(
|
|
548
|
+
projectConfig,
|
|
549
|
+
additionalProperties as Parameters<typeof superRefineVariableValue>[1],
|
|
550
|
+
variableValue[key],
|
|
551
|
+
[...path, key],
|
|
552
|
+
ctx,
|
|
553
|
+
key,
|
|
554
|
+
schemasByKey,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
506
557
|
} else {
|
|
507
558
|
for (const key of Object.keys(variableValue)) {
|
|
508
559
|
const propValue = variableValue[key];
|
|
@@ -944,6 +995,162 @@ function refineVariableEntry(
|
|
|
944
995
|
);
|
|
945
996
|
}
|
|
946
997
|
|
|
998
|
+
function refineVariableOverrides({
|
|
999
|
+
ctx,
|
|
1000
|
+
variableSchemaByKey,
|
|
1001
|
+
variableOverrides,
|
|
1002
|
+
pathPrefix,
|
|
1003
|
+
projectConfig,
|
|
1004
|
+
schemasByKey,
|
|
1005
|
+
}: {
|
|
1006
|
+
ctx: z.RefinementCtx;
|
|
1007
|
+
variableSchemaByKey: Record<string, unknown>;
|
|
1008
|
+
variableOverrides: Record<string, Array<{ value?: unknown }>>;
|
|
1009
|
+
pathPrefix: (string | number)[];
|
|
1010
|
+
projectConfig: ProjectConfig;
|
|
1011
|
+
schemasByKey?: Record<string, Schema>;
|
|
1012
|
+
}) {
|
|
1013
|
+
for (const variableKey of Object.keys(variableOverrides)) {
|
|
1014
|
+
if (isMutationKey(variableKey)) {
|
|
1015
|
+
ctx.addIssue({
|
|
1016
|
+
code: z.ZodIssueCode.custom,
|
|
1017
|
+
message:
|
|
1018
|
+
`Variable override key "${variableKey}" must be a declared variable key, not mutation notation. ` +
|
|
1019
|
+
"Use mutation notation inside each override's `value` object.",
|
|
1020
|
+
path: [...pathPrefix, variableKey],
|
|
1021
|
+
});
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const variableSchema = variableSchemaByKey[variableKey];
|
|
1026
|
+
if (!variableSchema) {
|
|
1027
|
+
ctx.addIssue({
|
|
1028
|
+
code: z.ZodIssueCode.custom,
|
|
1029
|
+
message: `Variable "${variableKey}" is not defined in \`variablesSchema\`.`,
|
|
1030
|
+
path: [...pathPrefix, variableKey],
|
|
1031
|
+
});
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const variableSchemaWithRef = variableSchema as { schema?: string };
|
|
1036
|
+
const resolvedVariableSchema = resolveVariableSchema(
|
|
1037
|
+
variableSchema as Parameters<typeof resolveVariableSchema>[0],
|
|
1038
|
+
schemasByKey,
|
|
1039
|
+
);
|
|
1040
|
+
if (variableSchemaWithRef.schema) {
|
|
1041
|
+
if (!resolvedVariableSchema) {
|
|
1042
|
+
ctx.addIssue({
|
|
1043
|
+
code: z.ZodIssueCode.custom,
|
|
1044
|
+
message: `Schema "${variableSchemaWithRef.schema}" could not be loaded for variable "${variableKey}".`,
|
|
1045
|
+
path: [...pathPrefix, variableKey],
|
|
1046
|
+
});
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const overrides = variableOverrides[variableKey];
|
|
1052
|
+
if (!Array.isArray(overrides)) {
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
overrides.forEach((override, overrideN) => {
|
|
1057
|
+
const overrideValue = override.value;
|
|
1058
|
+
const valuePath = [...pathPrefix, variableKey, overrideN, "value"];
|
|
1059
|
+
|
|
1060
|
+
if (
|
|
1061
|
+
typeof overrideValue === "object" &&
|
|
1062
|
+
overrideValue !== null &&
|
|
1063
|
+
!Array.isArray(overrideValue)
|
|
1064
|
+
) {
|
|
1065
|
+
const pathMap = overrideValue as Record<string, unknown>;
|
|
1066
|
+
const pathKeys = Object.keys(pathMap);
|
|
1067
|
+
const hasStructuredObjectSchema =
|
|
1068
|
+
resolvedVariableSchema?.type === "object" &&
|
|
1069
|
+
(Boolean(resolvedVariableSchema.properties) ||
|
|
1070
|
+
Boolean(resolvedVariableSchema.additionalProperties));
|
|
1071
|
+
const treatAsPathMap =
|
|
1072
|
+
pathKeys.some((pathKey) => isMutationKey(pathKey)) ||
|
|
1073
|
+
(hasStructuredObjectSchema && pathKeys.length > 0);
|
|
1074
|
+
|
|
1075
|
+
if (treatAsPathMap) {
|
|
1076
|
+
for (const pathKey of pathKeys) {
|
|
1077
|
+
const composedMutationKey = `${variableKey}.${pathKey}`;
|
|
1078
|
+
const validation = validateMutationKey(
|
|
1079
|
+
composedMutationKey,
|
|
1080
|
+
variableSchemaByKey as Record<string, Schema>,
|
|
1081
|
+
schemasByKey,
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
if (!validation.valid) {
|
|
1085
|
+
ctx.addIssue({
|
|
1086
|
+
code: z.ZodIssueCode.custom,
|
|
1087
|
+
message:
|
|
1088
|
+
validation.error ??
|
|
1089
|
+
`Invalid mutation path "${pathKey}" in variableOverride for "${variableKey}".`,
|
|
1090
|
+
path: [...valuePath, pathKey],
|
|
1091
|
+
});
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (validation.operation === "remove") {
|
|
1096
|
+
if (pathMap[pathKey] !== null) {
|
|
1097
|
+
ctx.addIssue({
|
|
1098
|
+
code: z.ZodIssueCode.custom,
|
|
1099
|
+
message: `Mutation "${pathKey}:remove" must use \`null\` as value.`,
|
|
1100
|
+
path: [...valuePath, pathKey],
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const fallbackValueSchema =
|
|
1107
|
+
validation.operation === "append" ||
|
|
1108
|
+
validation.operation === "prepend" ||
|
|
1109
|
+
validation.operation === "before" ||
|
|
1110
|
+
validation.operation === "after"
|
|
1111
|
+
? ({ type: "string" } as Schema)
|
|
1112
|
+
: null;
|
|
1113
|
+
const valueSchema = validation.valueSchema ?? fallbackValueSchema;
|
|
1114
|
+
if (!valueSchema) {
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
superRefineVariableValue(
|
|
1119
|
+
projectConfig,
|
|
1120
|
+
valueSchema,
|
|
1121
|
+
pathMap[pathKey],
|
|
1122
|
+
[...valuePath, pathKey],
|
|
1123
|
+
ctx,
|
|
1124
|
+
pathKey,
|
|
1125
|
+
schemasByKey,
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
} else {
|
|
1129
|
+
superRefineVariableValue(
|
|
1130
|
+
projectConfig,
|
|
1131
|
+
variableSchema as Parameters<typeof superRefineVariableValue>[1],
|
|
1132
|
+
overrideValue,
|
|
1133
|
+
valuePath,
|
|
1134
|
+
ctx,
|
|
1135
|
+
variableKey,
|
|
1136
|
+
schemasByKey,
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
} else {
|
|
1140
|
+
superRefineVariableValue(
|
|
1141
|
+
projectConfig,
|
|
1142
|
+
variableSchema as Parameters<typeof superRefineVariableValue>[1],
|
|
1143
|
+
overrideValue,
|
|
1144
|
+
valuePath,
|
|
1145
|
+
ctx,
|
|
1146
|
+
variableKey,
|
|
1147
|
+
schemasByKey,
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
947
1154
|
function refineForce({
|
|
948
1155
|
ctx,
|
|
949
1156
|
parsedFeature, // eslint-disable-line
|
|
@@ -1009,6 +1216,18 @@ function refineRules({
|
|
|
1009
1216
|
});
|
|
1010
1217
|
}
|
|
1011
1218
|
|
|
1219
|
+
// rules[n].variableOverrides[key][].value (path-map or full value)
|
|
1220
|
+
if (rule.variableOverrides) {
|
|
1221
|
+
refineVariableOverrides({
|
|
1222
|
+
ctx,
|
|
1223
|
+
variableSchemaByKey,
|
|
1224
|
+
variableOverrides: rule.variableOverrides,
|
|
1225
|
+
pathPrefix: [...pathPrefix, ruleN, "variableOverrides"],
|
|
1226
|
+
projectConfig,
|
|
1227
|
+
schemasByKey,
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1012
1231
|
// rules[n].variationWeights
|
|
1013
1232
|
if (rule.variationWeights) {
|
|
1014
1233
|
if (!parsedFeature.variations) {
|
|
@@ -1087,6 +1306,11 @@ export function getFeatureZodSchema(
|
|
|
1087
1306
|
const schemaZodSchema = getSchemaZodSchema(availableSchemaKeys);
|
|
1088
1307
|
const variableValueZodSchema = valueZodSchema;
|
|
1089
1308
|
const variableValueOrNullZodSchema = z.union([valueZodSchema, z.null()]);
|
|
1309
|
+
const overridePathMapValueZodSchema = z.record(variableValueOrNullZodSchema);
|
|
1310
|
+
const overrideValueZodSchema = z.union([
|
|
1311
|
+
variableValueOrNullZodSchema,
|
|
1312
|
+
overridePathMapValueZodSchema,
|
|
1313
|
+
]);
|
|
1090
1314
|
|
|
1091
1315
|
const variationValueZodSchema = z.string().min(1);
|
|
1092
1316
|
|
|
@@ -1135,6 +1359,26 @@ export function getFeatureZodSchema(
|
|
|
1135
1359
|
enabled: z.boolean().optional(),
|
|
1136
1360
|
variation: variationValueZodSchema.optional(),
|
|
1137
1361
|
variables: z.record(variableValueOrNullZodSchema).optional(),
|
|
1362
|
+
variableOverrides: z
|
|
1363
|
+
.record(
|
|
1364
|
+
z.array(
|
|
1365
|
+
z.union([
|
|
1366
|
+
z
|
|
1367
|
+
.object({
|
|
1368
|
+
conditions: conditionsZodSchema,
|
|
1369
|
+
value: overrideValueZodSchema,
|
|
1370
|
+
})
|
|
1371
|
+
.strict(),
|
|
1372
|
+
z
|
|
1373
|
+
.object({
|
|
1374
|
+
segments: groupSegmentsZodSchema,
|
|
1375
|
+
value: overrideValueZodSchema,
|
|
1376
|
+
})
|
|
1377
|
+
.strict(),
|
|
1378
|
+
]),
|
|
1379
|
+
),
|
|
1380
|
+
)
|
|
1381
|
+
.optional(),
|
|
1138
1382
|
variationWeights: z.record(z.number().min(0).max(100)).optional(),
|
|
1139
1383
|
})
|
|
1140
1384
|
.strict(),
|
|
@@ -1280,6 +1524,7 @@ export function getFeatureZodSchema(
|
|
|
1280
1524
|
type: z.union([z.literal("json"), propertyTypeEnum]).optional(),
|
|
1281
1525
|
items: schemaZodSchema.optional(),
|
|
1282
1526
|
properties: z.record(schemaZodSchema).optional(),
|
|
1527
|
+
additionalProperties: schemaZodSchema.optional(),
|
|
1283
1528
|
required: z.array(z.string()).optional(),
|
|
1284
1529
|
enum: z.array(variableValueZodSchema).optional(),
|
|
1285
1530
|
const: variableValueZodSchema.optional(),
|
|
@@ -1315,7 +1560,7 @@ export function getFeatureZodSchema(
|
|
|
1315
1560
|
ctx.addIssue({
|
|
1316
1561
|
code: z.ZodIssueCode.custom,
|
|
1317
1562
|
message:
|
|
1318
|
-
"Variable schema cannot have both `schema` (reference) and inline properties (`type`, `oneOf`, `properties`, `required`, `items`). Use one or the other.",
|
|
1563
|
+
"Variable schema cannot have both `schema` (reference) and inline properties (`type`, `oneOf`, `properties`, `additionalProperties`, `required`, `items`). Use one or the other.",
|
|
1319
1564
|
path: [],
|
|
1320
1565
|
});
|
|
1321
1566
|
return;
|
|
@@ -1324,6 +1569,8 @@ export function getFeatureZodSchema(
|
|
|
1324
1569
|
const hasInlineStructure =
|
|
1325
1570
|
("type" in variableSchema && variableSchema.type != null) ||
|
|
1326
1571
|
("properties" in variableSchema && variableSchema.properties != null) ||
|
|
1572
|
+
("additionalProperties" in variableSchema &&
|
|
1573
|
+
variableSchema.additionalProperties != null) ||
|
|
1327
1574
|
("required" in variableSchema && variableSchema.required != null) ||
|
|
1328
1575
|
("items" in variableSchema && variableSchema.items != null) ||
|
|
1329
1576
|
("oneOf" in variableSchema && variableSchema.oneOf != null);
|
|
@@ -1340,7 +1587,7 @@ export function getFeatureZodSchema(
|
|
|
1340
1587
|
ctx.addIssue({
|
|
1341
1588
|
code: z.ZodIssueCode.custom,
|
|
1342
1589
|
message:
|
|
1343
|
-
"When `schema` is set, do not set `type`, `oneOf`, `properties`, `required`, or `items`.",
|
|
1590
|
+
"When `schema` is set, do not set `type`, `oneOf`, `properties`, `additionalProperties`, `required`, or `items`.",
|
|
1344
1591
|
path: [],
|
|
1345
1592
|
});
|
|
1346
1593
|
}
|
|
@@ -1400,13 +1647,13 @@ export function getFeatureZodSchema(
|
|
|
1400
1647
|
z
|
|
1401
1648
|
.object({
|
|
1402
1649
|
conditions: conditionsZodSchema,
|
|
1403
|
-
value:
|
|
1650
|
+
value: overrideValueZodSchema,
|
|
1404
1651
|
})
|
|
1405
1652
|
.strict(),
|
|
1406
1653
|
z
|
|
1407
1654
|
.object({
|
|
1408
1655
|
segments: groupSegmentsZodSchema,
|
|
1409
|
-
value:
|
|
1656
|
+
value: overrideValueZodSchema,
|
|
1410
1657
|
})
|
|
1411
1658
|
.strict(),
|
|
1412
1659
|
]),
|
|
@@ -1593,107 +1840,14 @@ export function getFeatureZodSchema(
|
|
|
1593
1840
|
|
|
1594
1841
|
// variations[n].variableOverrides[key][].value (path-map or full value)
|
|
1595
1842
|
if (variation.variableOverrides) {
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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 {
|
|
1685
|
-
superRefineVariableValue(
|
|
1686
|
-
projectConfig,
|
|
1687
|
-
variableSchema as Parameters<typeof superRefineVariableValue>[1],
|
|
1688
|
-
overrideValue,
|
|
1689
|
-
valuePath,
|
|
1690
|
-
ctx,
|
|
1691
|
-
variableKey,
|
|
1692
|
-
schemasByKey,
|
|
1693
|
-
);
|
|
1694
|
-
}
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1843
|
+
refineVariableOverrides({
|
|
1844
|
+
ctx,
|
|
1845
|
+
variableSchemaByKey,
|
|
1846
|
+
variableOverrides: variation.variableOverrides,
|
|
1847
|
+
pathPrefix: ["variations", variationN, "variableOverrides"],
|
|
1848
|
+
projectConfig,
|
|
1849
|
+
schemasByKey,
|
|
1850
|
+
});
|
|
1697
1851
|
}
|
|
1698
1852
|
});
|
|
1699
1853
|
}
|
|
@@ -257,6 +257,17 @@ describe("mutationNotation.ts", () => {
|
|
|
257
257
|
expect(result).toBeNull();
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
+
it("resolves unknown object key via additionalProperties", () => {
|
|
261
|
+
const result = resolveSchemaAtPath(
|
|
262
|
+
{
|
|
263
|
+
type: "object",
|
|
264
|
+
additionalProperties: { type: "string" },
|
|
265
|
+
},
|
|
266
|
+
[{ key: "dynamicKey" }],
|
|
267
|
+
);
|
|
268
|
+
expect(result).toEqual({ type: "string" });
|
|
269
|
+
});
|
|
270
|
+
|
|
260
271
|
it("returns null when stepping into non-object", () => {
|
|
261
272
|
const result = resolveSchemaAtPath(objectSchema, [{ key: "width" }, { key: "foo" }]);
|
|
262
273
|
expect(result).toBeNull();
|
|
@@ -384,6 +395,21 @@ describe("mutationNotation.ts", () => {
|
|
|
384
395
|
tags: arraySchema,
|
|
385
396
|
items: arrayOfObjectsSchema,
|
|
386
397
|
settings: nestedObjectSchema,
|
|
398
|
+
partialMutationShowcase: {
|
|
399
|
+
type: "object",
|
|
400
|
+
properties: {
|
|
401
|
+
rows: {
|
|
402
|
+
type: "array",
|
|
403
|
+
items: {
|
|
404
|
+
type: "object",
|
|
405
|
+
properties: {
|
|
406
|
+
id: { type: "integer" },
|
|
407
|
+
label: { type: "string" },
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
},
|
|
387
413
|
};
|
|
388
414
|
|
|
389
415
|
it("returns invalid when key is empty (parseMutationKey null)", () => {
|
|
@@ -446,6 +472,18 @@ describe("mutationNotation.ts", () => {
|
|
|
446
472
|
expect(r.error).toContain("path does not exist");
|
|
447
473
|
});
|
|
448
474
|
|
|
475
|
+
it("valid when path uses additionalProperties", () => {
|
|
476
|
+
const schemas: Record<string, Schema> = {
|
|
477
|
+
labels: {
|
|
478
|
+
type: "object",
|
|
479
|
+
additionalProperties: { type: "string" },
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
const r = validateMutationKey("labels.headline", schemas);
|
|
483
|
+
expect(r.valid).toBe(true);
|
|
484
|
+
expect(r.valueSchema).toEqual({ type: "string" });
|
|
485
|
+
});
|
|
486
|
+
|
|
449
487
|
it("invalid when target schema is oneOf", () => {
|
|
450
488
|
const schemas: Record<string, Schema> = {
|
|
451
489
|
x: {
|
|
@@ -535,6 +573,22 @@ describe("mutationNotation.ts", () => {
|
|
|
535
573
|
expect(r.operation).toBe("before");
|
|
536
574
|
});
|
|
537
575
|
|
|
576
|
+
it("valid on nested array selector segment in object variable", () => {
|
|
577
|
+
const r = validateMutationKey(
|
|
578
|
+
"partialMutationShowcase.rows[id=1]:after",
|
|
579
|
+
variableSchemaByKey,
|
|
580
|
+
);
|
|
581
|
+
expect(r.valid).toBe(true);
|
|
582
|
+
expect(r.operation).toBe("after");
|
|
583
|
+
expect(r.valueSchema).toEqual({
|
|
584
|
+
type: "object",
|
|
585
|
+
properties: {
|
|
586
|
+
id: { type: "integer" },
|
|
587
|
+
label: { type: "string" },
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
538
592
|
it("invalid when path does not exist or does not point to array element", () => {
|
|
539
593
|
const r = validateMutationKey("config.fake[0]:after", variableSchemaByKey);
|
|
540
594
|
expect(r.valid).toBe(false);
|
|
@@ -589,6 +643,14 @@ describe("mutationNotation.ts", () => {
|
|
|
589
643
|
expect(r.valid).toBe(true);
|
|
590
644
|
});
|
|
591
645
|
|
|
646
|
+
it("valid when removing nested array element by selector", () => {
|
|
647
|
+
const r = validateMutationKey(
|
|
648
|
+
"partialMutationShowcase.rows[id=2]:remove",
|
|
649
|
+
variableSchemaByKey,
|
|
650
|
+
);
|
|
651
|
+
expect(r.valid).toBe(true);
|
|
652
|
+
});
|
|
653
|
+
|
|
592
654
|
it("invalid when path does not exist", () => {
|
|
593
655
|
const r = validateMutationKey("config.bad:remove", variableSchemaByKey);
|
|
594
656
|
expect(r.valid).toBe(false);
|