@featurevisor/core 2.18.0 → 2.20.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 +1581 -1119
- 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 +686 -401
- package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/index.html +27 -27
- package/coverage/lcov-report/linter/lintProject.ts.html +25 -25
- package/coverage/lcov-report/linter/mutationNotation.ts.html +191 -122
- package/coverage/lcov-report/linter/printError.ts.html +1 -1
- package/coverage/lcov-report/linter/schema.ts.html +72 -72
- 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 +2955 -2095
- 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 +1 -1
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/linter/featureSchema.d.ts +228 -22
- package/lib/linter/featureSchema.js +137 -76
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +175 -0
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/mutationNotation.js +35 -10
- package/lib/linter/mutationNotation.js.map +1 -1
- package/lib/linter/mutationNotation.spec.js +31 -0
- package/lib/linter/mutationNotation.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 +1 -1
- package/src/linter/featureSchema.spec.ts +197 -0
- package/src/linter/featureSchema.ts +204 -109
- package/src/linter/mutationNotation.spec.ts +39 -0
- package/src/linter/mutationNotation.ts +30 -7
|
@@ -610,6 +610,34 @@ describe("featureSchema.ts :: getFeatureZodSchema (variablesSchema and variable
|
|
|
610
610
|
"maxLength",
|
|
611
611
|
);
|
|
612
612
|
});
|
|
613
|
+
|
|
614
|
+
it("accepts plain-key partial object overrides for structured object schemas", () => {
|
|
615
|
+
expectParseSuccess(
|
|
616
|
+
baseFeature({
|
|
617
|
+
variablesSchema: {
|
|
618
|
+
config: {
|
|
619
|
+
type: "object",
|
|
620
|
+
properties: {
|
|
621
|
+
theme: { type: "string" },
|
|
622
|
+
compact: { type: "boolean" },
|
|
623
|
+
},
|
|
624
|
+
required: ["theme", "compact"],
|
|
625
|
+
defaultValue: { theme: "light", compact: true },
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
variations: [
|
|
629
|
+
{ value: "control", weight: 50 },
|
|
630
|
+
{
|
|
631
|
+
value: "treatment",
|
|
632
|
+
weight: 50,
|
|
633
|
+
variableOverrides: {
|
|
634
|
+
config: [{ segments: "*", value: { theme: "dark" } }],
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
}),
|
|
639
|
+
);
|
|
640
|
+
});
|
|
613
641
|
});
|
|
614
642
|
|
|
615
643
|
describe("rules: variables", () => {
|
|
@@ -701,6 +729,175 @@ describe("featureSchema.ts :: getFeatureZodSchema (variablesSchema and variable
|
|
|
701
729
|
});
|
|
702
730
|
});
|
|
703
731
|
|
|
732
|
+
describe("rules: variableOverrides", () => {
|
|
733
|
+
it("accepts rule variableOverrides with values matching variablesSchema", () => {
|
|
734
|
+
expectParseSuccess(
|
|
735
|
+
baseFeature({
|
|
736
|
+
variablesSchema: {
|
|
737
|
+
config: {
|
|
738
|
+
type: "object",
|
|
739
|
+
properties: {
|
|
740
|
+
title: { type: "string" },
|
|
741
|
+
nested: {
|
|
742
|
+
type: "object",
|
|
743
|
+
properties: {
|
|
744
|
+
count: { type: "integer" },
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
defaultValue: {
|
|
749
|
+
title: "default",
|
|
750
|
+
nested: { count: 0 },
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
rules: {
|
|
755
|
+
staging: [
|
|
756
|
+
{
|
|
757
|
+
key: "r1",
|
|
758
|
+
segments: "*",
|
|
759
|
+
percentage: 100,
|
|
760
|
+
variableOverrides: {
|
|
761
|
+
config: [
|
|
762
|
+
{
|
|
763
|
+
segments: "countries/germany",
|
|
764
|
+
value: {
|
|
765
|
+
"nested.count": 5,
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
],
|
|
772
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
773
|
+
},
|
|
774
|
+
}),
|
|
775
|
+
);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it("rejects rule variableOverrides for unknown variable key", () => {
|
|
779
|
+
expectParseFailure(
|
|
780
|
+
baseFeature({
|
|
781
|
+
variablesSchema: {
|
|
782
|
+
config: { type: "string", defaultValue: "x" },
|
|
783
|
+
},
|
|
784
|
+
rules: {
|
|
785
|
+
staging: [
|
|
786
|
+
{
|
|
787
|
+
key: "r1",
|
|
788
|
+
segments: "*",
|
|
789
|
+
percentage: 100,
|
|
790
|
+
variableOverrides: {
|
|
791
|
+
unknown: [{ segments: "*", value: "x" }],
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
796
|
+
},
|
|
797
|
+
}),
|
|
798
|
+
"not defined in",
|
|
799
|
+
);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it("rejects mutation notation in rule variableOverrides key", () => {
|
|
803
|
+
expectParseFailure(
|
|
804
|
+
baseFeature({
|
|
805
|
+
variablesSchema: {
|
|
806
|
+
config: {
|
|
807
|
+
type: "object",
|
|
808
|
+
properties: { title: { type: "string" } },
|
|
809
|
+
defaultValue: { title: "x" },
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
rules: {
|
|
813
|
+
staging: [
|
|
814
|
+
{
|
|
815
|
+
key: "r1",
|
|
816
|
+
segments: "*",
|
|
817
|
+
percentage: 100,
|
|
818
|
+
variableOverrides: {
|
|
819
|
+
"config.title": [{ segments: "*", value: "y" }],
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
],
|
|
823
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
824
|
+
},
|
|
825
|
+
}),
|
|
826
|
+
"must be a declared variable key",
|
|
827
|
+
);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it("rejects rule variableOverride path not present in schema", () => {
|
|
831
|
+
expectParseFailure(
|
|
832
|
+
baseFeature({
|
|
833
|
+
variablesSchema: {
|
|
834
|
+
config: {
|
|
835
|
+
type: "object",
|
|
836
|
+
properties: { title: { type: "string" } },
|
|
837
|
+
defaultValue: { title: "x" },
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
rules: {
|
|
841
|
+
staging: [
|
|
842
|
+
{
|
|
843
|
+
key: "r1",
|
|
844
|
+
segments: "*",
|
|
845
|
+
percentage: 100,
|
|
846
|
+
variableOverrides: {
|
|
847
|
+
config: [
|
|
848
|
+
{
|
|
849
|
+
segments: "*",
|
|
850
|
+
value: {
|
|
851
|
+
"nested.missing": "x",
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
],
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
859
|
+
},
|
|
860
|
+
}),
|
|
861
|
+
"invalid for variable",
|
|
862
|
+
);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it("rejects non-null payload for :remove in rule variableOverrides", () => {
|
|
866
|
+
expectParseFailure(
|
|
867
|
+
baseFeature({
|
|
868
|
+
variablesSchema: {
|
|
869
|
+
config: {
|
|
870
|
+
type: "object",
|
|
871
|
+
properties: { title: { type: "string" }, optional: { type: "string" } },
|
|
872
|
+
defaultValue: { title: "x", optional: "y" },
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
rules: {
|
|
876
|
+
staging: [
|
|
877
|
+
{
|
|
878
|
+
key: "r1",
|
|
879
|
+
segments: "*",
|
|
880
|
+
percentage: 100,
|
|
881
|
+
variableOverrides: {
|
|
882
|
+
config: [
|
|
883
|
+
{
|
|
884
|
+
conditions: [{ attribute: "country", operator: "equals", value: "de" }],
|
|
885
|
+
value: {
|
|
886
|
+
"optional:remove": "not-null",
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
],
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
],
|
|
893
|
+
production: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
894
|
+
},
|
|
895
|
+
}),
|
|
896
|
+
"must use `null` as value",
|
|
897
|
+
);
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
|
|
704
901
|
describe("force: variables", () => {
|
|
705
902
|
it("accepts force variables that match variablesSchema", () => {
|
|
706
903
|
expectParseSuccess(
|
|
@@ -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,
|
|
@@ -1000,6 +995,162 @@ function refineVariableEntry(
|
|
|
1000
995
|
);
|
|
1001
996
|
}
|
|
1002
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
|
+
|
|
1003
1154
|
function refineForce({
|
|
1004
1155
|
ctx,
|
|
1005
1156
|
parsedFeature, // eslint-disable-line
|
|
@@ -1065,6 +1216,18 @@ function refineRules({
|
|
|
1065
1216
|
});
|
|
1066
1217
|
}
|
|
1067
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
|
+
|
|
1068
1231
|
// rules[n].variationWeights
|
|
1069
1232
|
if (rule.variationWeights) {
|
|
1070
1233
|
if (!parsedFeature.variations) {
|
|
@@ -1143,6 +1306,11 @@ export function getFeatureZodSchema(
|
|
|
1143
1306
|
const schemaZodSchema = getSchemaZodSchema(availableSchemaKeys);
|
|
1144
1307
|
const variableValueZodSchema = valueZodSchema;
|
|
1145
1308
|
const variableValueOrNullZodSchema = z.union([valueZodSchema, z.null()]);
|
|
1309
|
+
const overridePathMapValueZodSchema = z.record(variableValueOrNullZodSchema);
|
|
1310
|
+
const overrideValueZodSchema = z.union([
|
|
1311
|
+
variableValueOrNullZodSchema,
|
|
1312
|
+
overridePathMapValueZodSchema,
|
|
1313
|
+
]);
|
|
1146
1314
|
|
|
1147
1315
|
const variationValueZodSchema = z.string().min(1);
|
|
1148
1316
|
|
|
@@ -1191,6 +1359,26 @@ export function getFeatureZodSchema(
|
|
|
1191
1359
|
enabled: z.boolean().optional(),
|
|
1192
1360
|
variation: variationValueZodSchema.optional(),
|
|
1193
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(),
|
|
1194
1382
|
variationWeights: z.record(z.number().min(0).max(100)).optional(),
|
|
1195
1383
|
})
|
|
1196
1384
|
.strict(),
|
|
@@ -1459,13 +1647,13 @@ export function getFeatureZodSchema(
|
|
|
1459
1647
|
z
|
|
1460
1648
|
.object({
|
|
1461
1649
|
conditions: conditionsZodSchema,
|
|
1462
|
-
value:
|
|
1650
|
+
value: overrideValueZodSchema,
|
|
1463
1651
|
})
|
|
1464
1652
|
.strict(),
|
|
1465
1653
|
z
|
|
1466
1654
|
.object({
|
|
1467
1655
|
segments: groupSegmentsZodSchema,
|
|
1468
|
-
value:
|
|
1656
|
+
value: overrideValueZodSchema,
|
|
1469
1657
|
})
|
|
1470
1658
|
.strict(),
|
|
1471
1659
|
]),
|
|
@@ -1652,107 +1840,14 @@ export function getFeatureZodSchema(
|
|
|
1652
1840
|
|
|
1653
1841
|
// variations[n].variableOverrides[key][].value (path-map or full value)
|
|
1654
1842
|
if (variation.variableOverrides) {
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
continue;
|
|
1664
|
-
}
|
|
1665
|
-
// When variable references a schema by name, ensure it can be resolved
|
|
1666
|
-
const variableSchemaWithRef = variableSchema as { schema?: string };
|
|
1667
|
-
if (variableSchemaWithRef.schema) {
|
|
1668
|
-
const resolvedVarSchema = resolveVariableSchema(
|
|
1669
|
-
variableSchema as Parameters<typeof resolveVariableSchema>[0],
|
|
1670
|
-
schemasByKey,
|
|
1671
|
-
);
|
|
1672
|
-
if (!resolvedVarSchema) {
|
|
1673
|
-
ctx.addIssue({
|
|
1674
|
-
code: z.ZodIssueCode.custom,
|
|
1675
|
-
message: `Schema "${variableSchemaWithRef.schema}" could not be loaded for variable "${variableKey}".`,
|
|
1676
|
-
path: ["variations", variationN, "variableOverrides", variableKey],
|
|
1677
|
-
});
|
|
1678
|
-
continue;
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
const overrides = variation.variableOverrides[variableKey];
|
|
1682
|
-
if (!Array.isArray(overrides)) continue;
|
|
1683
|
-
overrides.forEach((override, overrideN) => {
|
|
1684
|
-
const overrideValue = override.value;
|
|
1685
|
-
const valuePath = [
|
|
1686
|
-
"variations",
|
|
1687
|
-
variationN,
|
|
1688
|
-
"variableOverrides",
|
|
1689
|
-
variableKey,
|
|
1690
|
-
overrideN,
|
|
1691
|
-
"value",
|
|
1692
|
-
];
|
|
1693
|
-
if (
|
|
1694
|
-
typeof overrideValue === "object" &&
|
|
1695
|
-
overrideValue !== null &&
|
|
1696
|
-
!Array.isArray(overrideValue)
|
|
1697
|
-
) {
|
|
1698
|
-
const pathMap = overrideValue as Record<string, unknown>;
|
|
1699
|
-
for (const pathKey of Object.keys(pathMap)) {
|
|
1700
|
-
const pathSegments = parsePathMapKey(pathKey);
|
|
1701
|
-
if (!pathSegments || pathSegments.length === 0) {
|
|
1702
|
-
ctx.addIssue({
|
|
1703
|
-
code: z.ZodIssueCode.custom,
|
|
1704
|
-
message: `Invalid mutation path "${pathKey}" in variableOverride for "${variableKey}".`,
|
|
1705
|
-
path: [...valuePath, pathKey],
|
|
1706
|
-
});
|
|
1707
|
-
continue;
|
|
1708
|
-
}
|
|
1709
|
-
const atPathSchema = resolveSchemaAtPath(
|
|
1710
|
-
variableSchema as Schema,
|
|
1711
|
-
pathSegments,
|
|
1712
|
-
schemasByKey,
|
|
1713
|
-
);
|
|
1714
|
-
if (!atPathSchema) {
|
|
1715
|
-
const effectiveSchema = resolveVariableSchema(
|
|
1716
|
-
variableSchema as Parameters<typeof resolveVariableSchema>[0],
|
|
1717
|
-
schemasByKey,
|
|
1718
|
-
);
|
|
1719
|
-
const isFlatObject =
|
|
1720
|
-
effectiveSchema?.type === "object" &&
|
|
1721
|
-
(!effectiveSchema.properties ||
|
|
1722
|
-
Object.keys(effectiveSchema.properties).length === 0);
|
|
1723
|
-
if (isFlatObject) {
|
|
1724
|
-
continue;
|
|
1725
|
-
}
|
|
1726
|
-
ctx.addIssue({
|
|
1727
|
-
code: z.ZodIssueCode.custom,
|
|
1728
|
-
message: `Path "${pathKey}" is invalid for variable "${variableKey}" (not in schema).`,
|
|
1729
|
-
path: [...valuePath, pathKey],
|
|
1730
|
-
});
|
|
1731
|
-
continue;
|
|
1732
|
-
}
|
|
1733
|
-
superRefineVariableValue(
|
|
1734
|
-
projectConfig,
|
|
1735
|
-
atPathSchema,
|
|
1736
|
-
pathMap[pathKey],
|
|
1737
|
-
[...valuePath, pathKey],
|
|
1738
|
-
ctx,
|
|
1739
|
-
pathKey,
|
|
1740
|
-
schemasByKey,
|
|
1741
|
-
);
|
|
1742
|
-
}
|
|
1743
|
-
} else {
|
|
1744
|
-
superRefineVariableValue(
|
|
1745
|
-
projectConfig,
|
|
1746
|
-
variableSchema as Parameters<typeof superRefineVariableValue>[1],
|
|
1747
|
-
overrideValue,
|
|
1748
|
-
valuePath,
|
|
1749
|
-
ctx,
|
|
1750
|
-
variableKey,
|
|
1751
|
-
schemasByKey,
|
|
1752
|
-
);
|
|
1753
|
-
}
|
|
1754
|
-
});
|
|
1755
|
-
}
|
|
1843
|
+
refineVariableOverrides({
|
|
1844
|
+
ctx,
|
|
1845
|
+
variableSchemaByKey,
|
|
1846
|
+
variableOverrides: variation.variableOverrides,
|
|
1847
|
+
pathPrefix: ["variations", variationN, "variableOverrides"],
|
|
1848
|
+
projectConfig,
|
|
1849
|
+
schemasByKey,
|
|
1850
|
+
});
|
|
1756
1851
|
}
|
|
1757
1852
|
});
|
|
1758
1853
|
}
|
|
@@ -395,6 +395,21 @@ describe("mutationNotation.ts", () => {
|
|
|
395
395
|
tags: arraySchema,
|
|
396
396
|
items: arrayOfObjectsSchema,
|
|
397
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
|
+
},
|
|
398
413
|
};
|
|
399
414
|
|
|
400
415
|
it("returns invalid when key is empty (parseMutationKey null)", () => {
|
|
@@ -558,6 +573,22 @@ describe("mutationNotation.ts", () => {
|
|
|
558
573
|
expect(r.operation).toBe("before");
|
|
559
574
|
});
|
|
560
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
|
+
|
|
561
592
|
it("invalid when path does not exist or does not point to array element", () => {
|
|
562
593
|
const r = validateMutationKey("config.fake[0]:after", variableSchemaByKey);
|
|
563
594
|
expect(r.valid).toBe(false);
|
|
@@ -612,6 +643,14 @@ describe("mutationNotation.ts", () => {
|
|
|
612
643
|
expect(r.valid).toBe(true);
|
|
613
644
|
});
|
|
614
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
|
+
|
|
615
654
|
it("invalid when path does not exist", () => {
|
|
616
655
|
const r = validateMutationKey("config.bad:remove", variableSchemaByKey);
|
|
617
656
|
expect(r.valid).toBe(false);
|
|
@@ -132,12 +132,7 @@ function getContainerSchemaAtPath(
|
|
|
132
132
|
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
133
133
|
for (const seg of pathWithoutLast) {
|
|
134
134
|
if (isOneOfSchema(current)) return null;
|
|
135
|
-
if (
|
|
136
|
-
if (current.type !== "array") return null;
|
|
137
|
-
const itemSchema = current.items;
|
|
138
|
-
if (!itemSchema || typeof itemSchema !== "object") return null;
|
|
139
|
-
current = resolveSchemaRef(itemSchema, schemasByKey);
|
|
140
|
-
} else {
|
|
135
|
+
if (seg.key) {
|
|
141
136
|
if (current.type !== "object") return null;
|
|
142
137
|
const props = current.properties;
|
|
143
138
|
const additional = current.additionalProperties;
|
|
@@ -150,12 +145,40 @@ function getContainerSchemaAtPath(
|
|
|
150
145
|
}
|
|
151
146
|
current = resolveSchemaRef(next, schemasByKey);
|
|
152
147
|
}
|
|
148
|
+
if ("index" in seg || "selector" in seg) {
|
|
149
|
+
if (current?.type !== "array") return null;
|
|
150
|
+
const itemSchema = current.items;
|
|
151
|
+
if (!itemSchema || typeof itemSchema !== "object") return null;
|
|
152
|
+
current = resolveSchemaRef(itemSchema, schemasByKey);
|
|
153
|
+
}
|
|
153
154
|
if (!current) return null;
|
|
154
155
|
}
|
|
155
156
|
if (!current) return null;
|
|
156
157
|
const parentSchema = current;
|
|
157
158
|
if ("index" in lastSegment || "selector" in lastSegment) {
|
|
158
|
-
|
|
159
|
+
let arraySchema: Schema | null = parentSchema;
|
|
160
|
+
if (lastSegment.key) {
|
|
161
|
+
if (arraySchema.type !== "object") return null;
|
|
162
|
+
const props = arraySchema.properties;
|
|
163
|
+
const additional = arraySchema.additionalProperties;
|
|
164
|
+
const next = props && typeof props === "object" ? props[lastSegment.key] : undefined;
|
|
165
|
+
if (next === undefined) {
|
|
166
|
+
if (!additional || typeof additional !== "object") return null;
|
|
167
|
+
arraySchema = resolveSchemaRef(additional, schemasByKey);
|
|
168
|
+
} else {
|
|
169
|
+
arraySchema = resolveSchemaRef(next, schemasByKey);
|
|
170
|
+
}
|
|
171
|
+
if (!arraySchema) return null;
|
|
172
|
+
}
|
|
173
|
+
const normalizedLastSegment =
|
|
174
|
+
"index" in lastSegment
|
|
175
|
+
? ({ key: "", index: lastSegment.index } as PathSegment)
|
|
176
|
+
: ({ key: "", selector: lastSegment.selector } as PathSegment);
|
|
177
|
+
return {
|
|
178
|
+
containerSchema: arraySchema,
|
|
179
|
+
lastSegment: normalizedLastSegment,
|
|
180
|
+
parentSchema: arraySchema,
|
|
181
|
+
};
|
|
159
182
|
}
|
|
160
183
|
const propSchema =
|
|
161
184
|
parentSchema.properties?.[lastSegment.key] ?? parentSchema.additionalProperties;
|