@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
|
@@ -956,6 +956,336 @@ describe("core: buildScopedDatafile", function () {
|
|
|
956
956
|
});
|
|
957
957
|
});
|
|
958
958
|
|
|
959
|
+
describe("features with traffic and variableOverrides", function () {
|
|
960
|
+
test("traffic variableOverride with matching segments becomes *", function () {
|
|
961
|
+
const datafile: DatafileContent = {
|
|
962
|
+
schemaVersion: "2",
|
|
963
|
+
revision: "unknown",
|
|
964
|
+
segments: {
|
|
965
|
+
mobile: {
|
|
966
|
+
conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
features: {
|
|
970
|
+
feature1: {
|
|
971
|
+
bucketBy: ["userId"],
|
|
972
|
+
traffic: [
|
|
973
|
+
{
|
|
974
|
+
key: "rule1",
|
|
975
|
+
segments: "*",
|
|
976
|
+
percentage: 100000,
|
|
977
|
+
variableOverrides: {
|
|
978
|
+
config: [{ segments: "mobile", value: { source: "mobile" } }],
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
],
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
const result = buildScopedDatafile(datafile, { device: "mobile" });
|
|
987
|
+
|
|
988
|
+
expect(result.features.feature1.traffic[0].variableOverrides?.config[0].segments).toEqual(
|
|
989
|
+
"*",
|
|
990
|
+
);
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
test("traffic variableOverride with non-matching segments remains", function () {
|
|
994
|
+
const datafile: DatafileContent = {
|
|
995
|
+
schemaVersion: "2",
|
|
996
|
+
revision: "unknown",
|
|
997
|
+
segments: {
|
|
998
|
+
mobile: {
|
|
999
|
+
conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
|
|
1000
|
+
},
|
|
1001
|
+
},
|
|
1002
|
+
features: {
|
|
1003
|
+
feature1: {
|
|
1004
|
+
bucketBy: ["userId"],
|
|
1005
|
+
traffic: [
|
|
1006
|
+
{
|
|
1007
|
+
key: "rule1",
|
|
1008
|
+
segments: "*",
|
|
1009
|
+
percentage: 100000,
|
|
1010
|
+
variableOverrides: {
|
|
1011
|
+
config: [{ segments: "mobile", value: { source: "mobile" } }],
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
],
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
const result = buildScopedDatafile(datafile, { device: "desktop" });
|
|
1020
|
+
|
|
1021
|
+
expect(result.features.feature1.traffic[0].variableOverrides?.config[0].segments).toEqual(
|
|
1022
|
+
"mobile",
|
|
1023
|
+
);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test("traffic variableOverride with matching conditions becomes *", function () {
|
|
1027
|
+
const datafile: DatafileContent = {
|
|
1028
|
+
schemaVersion: "2",
|
|
1029
|
+
revision: "unknown",
|
|
1030
|
+
segments: {},
|
|
1031
|
+
features: {
|
|
1032
|
+
feature1: {
|
|
1033
|
+
bucketBy: ["userId"],
|
|
1034
|
+
traffic: [
|
|
1035
|
+
{
|
|
1036
|
+
key: "rule1",
|
|
1037
|
+
segments: "*",
|
|
1038
|
+
percentage: 100000,
|
|
1039
|
+
variableOverrides: {
|
|
1040
|
+
config: [
|
|
1041
|
+
{
|
|
1042
|
+
conditions: [{ attribute: "country", operator: "equals", value: "nl" }],
|
|
1043
|
+
value: { source: "nl" },
|
|
1044
|
+
},
|
|
1045
|
+
],
|
|
1046
|
+
},
|
|
1047
|
+
},
|
|
1048
|
+
],
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
const result = buildScopedDatafile(datafile, { country: "nl" });
|
|
1054
|
+
|
|
1055
|
+
expect(result.features.feature1.traffic[0].variableOverrides?.config[0].conditions).toEqual(
|
|
1056
|
+
"*",
|
|
1057
|
+
);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
test("traffic variableOverride with non-matching conditions remains", function () {
|
|
1061
|
+
const datafile: DatafileContent = {
|
|
1062
|
+
schemaVersion: "2",
|
|
1063
|
+
revision: "unknown",
|
|
1064
|
+
segments: {},
|
|
1065
|
+
features: {
|
|
1066
|
+
feature1: {
|
|
1067
|
+
bucketBy: ["userId"],
|
|
1068
|
+
traffic: [
|
|
1069
|
+
{
|
|
1070
|
+
key: "rule1",
|
|
1071
|
+
segments: "*",
|
|
1072
|
+
percentage: 100000,
|
|
1073
|
+
variableOverrides: {
|
|
1074
|
+
config: [
|
|
1075
|
+
{
|
|
1076
|
+
conditions: [{ attribute: "country", operator: "equals", value: "nl" }],
|
|
1077
|
+
value: { source: "nl" },
|
|
1078
|
+
},
|
|
1079
|
+
],
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
],
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
const result = buildScopedDatafile(datafile, { country: "de" });
|
|
1088
|
+
|
|
1089
|
+
expect(result.features.feature1.traffic[0].variableOverrides?.config[0].conditions).toEqual([
|
|
1090
|
+
{ attribute: "country", operator: "equals", value: "nl" },
|
|
1091
|
+
]);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
test("traffic variableOverride supports array/group segments and conditions", function () {
|
|
1095
|
+
const datafile: DatafileContent = {
|
|
1096
|
+
schemaVersion: "2",
|
|
1097
|
+
revision: "unknown",
|
|
1098
|
+
segments: {
|
|
1099
|
+
mobile: {
|
|
1100
|
+
conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
|
|
1101
|
+
},
|
|
1102
|
+
touch: {
|
|
1103
|
+
conditions: [{ attribute: "touch", operator: "equals", value: true }],
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
features: {
|
|
1107
|
+
feature1: {
|
|
1108
|
+
bucketBy: ["userId"],
|
|
1109
|
+
traffic: [
|
|
1110
|
+
{
|
|
1111
|
+
key: "rule1",
|
|
1112
|
+
segments: "*",
|
|
1113
|
+
percentage: 100000,
|
|
1114
|
+
variableOverrides: {
|
|
1115
|
+
config: [
|
|
1116
|
+
{
|
|
1117
|
+
segments: { and: ["mobile", "touch"] },
|
|
1118
|
+
value: { source: "group-segments" },
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
conditions: {
|
|
1122
|
+
and: [
|
|
1123
|
+
{ attribute: "country", operator: "equals", value: "nl" },
|
|
1124
|
+
{ attribute: "tier", operator: "equals", value: "premium" },
|
|
1125
|
+
],
|
|
1126
|
+
},
|
|
1127
|
+
value: { source: "group-conditions" },
|
|
1128
|
+
},
|
|
1129
|
+
],
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
1132
|
+
],
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const result = buildScopedDatafile(datafile, {
|
|
1138
|
+
device: "mobile",
|
|
1139
|
+
touch: true,
|
|
1140
|
+
country: "nl",
|
|
1141
|
+
tier: "premium",
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
expect(result.features.feature1.traffic[0].variableOverrides?.config[0].segments).toEqual(
|
|
1145
|
+
"*",
|
|
1146
|
+
);
|
|
1147
|
+
expect(result.features.feature1.traffic[0].variableOverrides?.config[1].conditions).toEqual(
|
|
1148
|
+
"*",
|
|
1149
|
+
);
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
test("traffic variableOverride supports stringified segments and conditions", function () {
|
|
1153
|
+
const datafile: DatafileContent = {
|
|
1154
|
+
schemaVersion: "2",
|
|
1155
|
+
revision: "unknown",
|
|
1156
|
+
segments: {
|
|
1157
|
+
mobile: {
|
|
1158
|
+
conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
|
|
1159
|
+
},
|
|
1160
|
+
touch: {
|
|
1161
|
+
conditions: [{ attribute: "touch", operator: "equals", value: true }],
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
features: {
|
|
1165
|
+
feature1: {
|
|
1166
|
+
bucketBy: ["userId"],
|
|
1167
|
+
traffic: [
|
|
1168
|
+
{
|
|
1169
|
+
key: "rule1",
|
|
1170
|
+
segments: "*",
|
|
1171
|
+
percentage: 100000,
|
|
1172
|
+
variableOverrides: {
|
|
1173
|
+
config: [
|
|
1174
|
+
{
|
|
1175
|
+
segments: JSON.stringify({ and: ["mobile", "touch"] }),
|
|
1176
|
+
value: { source: "stringified-segments" },
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
conditions: JSON.stringify([
|
|
1180
|
+
{ attribute: "country", operator: "equals", value: "nl" },
|
|
1181
|
+
]),
|
|
1182
|
+
value: { source: "stringified-conditions" },
|
|
1183
|
+
},
|
|
1184
|
+
],
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
],
|
|
1188
|
+
},
|
|
1189
|
+
},
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
const result = buildScopedDatafile(datafile, {
|
|
1193
|
+
device: "mobile",
|
|
1194
|
+
touch: true,
|
|
1195
|
+
country: "nl",
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
expect(result.features.feature1.traffic[0].variableOverrides?.config[0].segments).toEqual(
|
|
1199
|
+
"*",
|
|
1200
|
+
);
|
|
1201
|
+
expect(result.features.feature1.traffic[0].variableOverrides?.config[1].conditions).toEqual(
|
|
1202
|
+
"*",
|
|
1203
|
+
);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
test("multiple traffic entries still apply consecutive * dedupe", function () {
|
|
1207
|
+
const datafile: DatafileContent = {
|
|
1208
|
+
schemaVersion: "2",
|
|
1209
|
+
revision: "unknown",
|
|
1210
|
+
segments: {
|
|
1211
|
+
web: {
|
|
1212
|
+
conditions: [{ attribute: "platform", operator: "equals", value: "web" }],
|
|
1213
|
+
},
|
|
1214
|
+
mobile: {
|
|
1215
|
+
conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
|
|
1216
|
+
},
|
|
1217
|
+
},
|
|
1218
|
+
features: {
|
|
1219
|
+
feature1: {
|
|
1220
|
+
bucketBy: ["userId"],
|
|
1221
|
+
traffic: [
|
|
1222
|
+
{
|
|
1223
|
+
key: "rule1",
|
|
1224
|
+
segments: "web",
|
|
1225
|
+
percentage: 50000,
|
|
1226
|
+
variableOverrides: {
|
|
1227
|
+
config: [{ segments: "mobile", value: { source: "rule1" } }],
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
key: "rule2",
|
|
1232
|
+
segments: "web",
|
|
1233
|
+
percentage: 50000,
|
|
1234
|
+
variableOverrides: {
|
|
1235
|
+
config: [{ segments: "mobile", value: { source: "rule2" } }],
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1238
|
+
],
|
|
1239
|
+
},
|
|
1240
|
+
},
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
const result = buildScopedDatafile(datafile, {
|
|
1244
|
+
platform: "web",
|
|
1245
|
+
device: "mobile",
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
expect(result.features.feature1.traffic).toHaveLength(1);
|
|
1249
|
+
expect(result.features.feature1.traffic[0].key).toEqual("rule1");
|
|
1250
|
+
expect(result.features.feature1.traffic[0].variableOverrides?.config[0].segments).toEqual(
|
|
1251
|
+
"*",
|
|
1252
|
+
);
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
test("does not mutate original datafile traffic variableOverrides", function () {
|
|
1256
|
+
const datafile: DatafileContent = {
|
|
1257
|
+
schemaVersion: "2",
|
|
1258
|
+
revision: "unknown",
|
|
1259
|
+
segments: {
|
|
1260
|
+
mobile: {
|
|
1261
|
+
conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1264
|
+
features: {
|
|
1265
|
+
feature1: {
|
|
1266
|
+
bucketBy: ["userId"],
|
|
1267
|
+
traffic: [
|
|
1268
|
+
{
|
|
1269
|
+
key: "rule1",
|
|
1270
|
+
segments: "*",
|
|
1271
|
+
percentage: 100000,
|
|
1272
|
+
variableOverrides: {
|
|
1273
|
+
config: [{ segments: "mobile", value: { source: "mobile" } }],
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
],
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
const original = JSON.stringify(datafile);
|
|
1282
|
+
|
|
1283
|
+
buildScopedDatafile(datafile, { device: "mobile" });
|
|
1284
|
+
|
|
1285
|
+
expect(JSON.stringify(datafile)).toEqual(original);
|
|
1286
|
+
});
|
|
1287
|
+
});
|
|
1288
|
+
|
|
959
1289
|
describe("features with variations and variableOverrides", function () {
|
|
960
1290
|
test("variableOverride with matching segments becomes *", function () {
|
|
961
1291
|
const datafile: DatafileContent = {
|
|
@@ -4,6 +4,23 @@ import { DatafileReader, createLogger } from "@featurevisor/sdk";
|
|
|
4
4
|
import { buildScopedConditions } from "./buildScopedConditions";
|
|
5
5
|
import { buildScopedSegments } from "./buildScopedSegments";
|
|
6
6
|
|
|
7
|
+
function parseIfStringified<T>(value: T): T {
|
|
8
|
+
if (typeof value !== "string" || value === "*") {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const firstChar = value[0];
|
|
13
|
+
if (firstChar !== "{" && firstChar !== "[") {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(value) as T;
|
|
19
|
+
} catch {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
export function buildScopedDatafile(
|
|
8
25
|
originalDatafileContent: DatafileContent,
|
|
9
26
|
context: Context,
|
|
@@ -76,13 +93,52 @@ export function buildScopedDatafile(
|
|
|
76
93
|
|
|
77
94
|
// segments
|
|
78
95
|
if (traffic.segments) {
|
|
96
|
+
const segments = parseIfStringified(traffic.segments);
|
|
79
97
|
feature.traffic[trafficI].segments = buildScopedSegments(
|
|
80
98
|
originalDatafileReader,
|
|
81
|
-
|
|
99
|
+
segments,
|
|
82
100
|
context,
|
|
83
101
|
removeSegments,
|
|
84
102
|
);
|
|
85
103
|
}
|
|
104
|
+
|
|
105
|
+
// variable overrides
|
|
106
|
+
if (traffic.variableOverrides) {
|
|
107
|
+
for (const variableKey in traffic.variableOverrides) {
|
|
108
|
+
const variableOverrides = traffic.variableOverrides[variableKey];
|
|
109
|
+
|
|
110
|
+
for (
|
|
111
|
+
let variableOverrideI = 0;
|
|
112
|
+
variableOverrideI < variableOverrides.length;
|
|
113
|
+
variableOverrideI++
|
|
114
|
+
) {
|
|
115
|
+
const variableOverride = variableOverrides[variableOverrideI];
|
|
116
|
+
|
|
117
|
+
// segments
|
|
118
|
+
if (variableOverride.segments) {
|
|
119
|
+
const segments = parseIfStringified(variableOverride.segments);
|
|
120
|
+
variableOverride.segments = buildScopedSegments(
|
|
121
|
+
originalDatafileReader,
|
|
122
|
+
segments,
|
|
123
|
+
context,
|
|
124
|
+
removeSegments,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// conditions
|
|
129
|
+
if (variableOverride.conditions) {
|
|
130
|
+
const conditions = parseIfStringified(variableOverride.conditions);
|
|
131
|
+
variableOverride.conditions = buildScopedConditions(
|
|
132
|
+
originalDatafileReader,
|
|
133
|
+
conditions,
|
|
134
|
+
context,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
variableOverrides[variableOverrideI] = variableOverride;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
86
142
|
}
|
|
87
143
|
}
|
|
88
144
|
|
|
@@ -987,6 +987,51 @@ describe("mutateVariables", function () {
|
|
|
987
987
|
const result = resolveMutationsForSingleVariable(schema, "foo", {});
|
|
988
988
|
expect(result).toEqual({});
|
|
989
989
|
});
|
|
990
|
+
|
|
991
|
+
test("uses provided baseValue instead of defaultValue for path mutations", function () {
|
|
992
|
+
const schema: Record<string, VariableSchema> = {
|
|
993
|
+
cfg: {
|
|
994
|
+
type: "object",
|
|
995
|
+
defaultValue: { source: "default", nested: { value: 1 } },
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const result = resolveMutationsForSingleVariable(
|
|
1000
|
+
schema,
|
|
1001
|
+
"cfg",
|
|
1002
|
+
{
|
|
1003
|
+
"nested.value": 2,
|
|
1004
|
+
},
|
|
1005
|
+
{ source: "rule", nested: { value: 10 }, extra: true },
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
expect(result).toEqual({
|
|
1009
|
+
source: "rule",
|
|
1010
|
+
nested: { value: 2 },
|
|
1011
|
+
extra: true,
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
test("baseValue with full replacement key still applies replacement first", function () {
|
|
1016
|
+
const schema: Record<string, VariableSchema> = {
|
|
1017
|
+
cfg: {
|
|
1018
|
+
type: "object",
|
|
1019
|
+
defaultValue: { a: 1, b: 2 },
|
|
1020
|
+
},
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
const result = resolveMutationsForSingleVariable(
|
|
1024
|
+
schema,
|
|
1025
|
+
"cfg",
|
|
1026
|
+
{
|
|
1027
|
+
cfg: { a: 100, c: 300 },
|
|
1028
|
+
c: 999,
|
|
1029
|
+
},
|
|
1030
|
+
{ a: 10, b: 20 },
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
expect(result).toEqual({ a: 100, c: 999 });
|
|
1034
|
+
});
|
|
990
1035
|
});
|
|
991
1036
|
|
|
992
1037
|
describe("does not mutate input", function () {
|
|
@@ -89,14 +89,21 @@ export function resolveMutationsForSingleVariable(
|
|
|
89
89
|
variablesSchema: Record<string, VariableSchema> | undefined,
|
|
90
90
|
variableKey: string,
|
|
91
91
|
overrideValue: VariableValue,
|
|
92
|
+
baseValue?: VariableValue,
|
|
92
93
|
): VariableValue {
|
|
93
94
|
if (!variablesSchema || !variablesSchema[variableKey]) return overrideValue;
|
|
94
95
|
if (overrideValue === null || overrideValue === undefined) return overrideValue;
|
|
95
96
|
if (typeof overrideValue !== "object" || Array.isArray(overrideValue)) {
|
|
96
97
|
return overrideValue;
|
|
97
98
|
}
|
|
99
|
+
|
|
98
100
|
const pathMap = overrideValue as Record<string, VariableValue>;
|
|
99
101
|
const flat: Record<string, VariableValue> = {};
|
|
102
|
+
|
|
103
|
+
if (typeof baseValue !== "undefined") {
|
|
104
|
+
flat[variableKey] = JSON.parse(JSON.stringify(baseValue)) as VariableValue;
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
for (const [k, v] of Object.entries(pathMap)) {
|
|
101
108
|
flat[k === variableKey ? variableKey : variableKey + "." + k] = v;
|
|
102
109
|
}
|
|
@@ -910,4 +910,37 @@ describe("core: Traffic", function () {
|
|
|
910
910
|
},
|
|
911
911
|
]);
|
|
912
912
|
});
|
|
913
|
+
|
|
914
|
+
it("should include variableOverrides from parsed rule in traffic", function () {
|
|
915
|
+
const variableOverrides = {
|
|
916
|
+
config: [
|
|
917
|
+
{
|
|
918
|
+
segments: "mobile",
|
|
919
|
+
value: {
|
|
920
|
+
"layout.width": 900,
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
],
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
const result = getTraffic(
|
|
927
|
+
[
|
|
928
|
+
{
|
|
929
|
+
value: "control",
|
|
930
|
+
weight: 100,
|
|
931
|
+
},
|
|
932
|
+
],
|
|
933
|
+
[
|
|
934
|
+
{
|
|
935
|
+
key: "1",
|
|
936
|
+
segments: "*",
|
|
937
|
+
percentage: 100,
|
|
938
|
+
variableOverrides,
|
|
939
|
+
},
|
|
940
|
+
],
|
|
941
|
+
undefined,
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
expect(result[0].variableOverrides).toEqual(variableOverrides);
|
|
945
|
+
});
|
|
913
946
|
});
|
package/src/builder/traffic.ts
CHANGED
|
@@ -48,6 +48,63 @@ function formatObjectKey(key: string): string {
|
|
|
48
48
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function buildObjectTypeFromSchema(
|
|
52
|
+
schema: Schema,
|
|
53
|
+
schemasByKey: Record<string, Schema>,
|
|
54
|
+
schemaTypeNames?: Record<string, string>,
|
|
55
|
+
): string {
|
|
56
|
+
const props = schema.properties;
|
|
57
|
+
const additional = schema.additionalProperties;
|
|
58
|
+
const hasProps = props && typeof props === "object" && Object.keys(props).length > 0;
|
|
59
|
+
const hasAdditional = additional && typeof additional === "object";
|
|
60
|
+
|
|
61
|
+
if (hasProps && hasAdditional) {
|
|
62
|
+
const requiredSet = new Set(schema.required || []);
|
|
63
|
+
const propEntries = Object.entries(props as Record<string, Schema>).map(([k, v]) => {
|
|
64
|
+
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
65
|
+
const optional = !requiredSet.has(k);
|
|
66
|
+
return optional
|
|
67
|
+
? `${formatObjectKey(k)}?: ${propType}`
|
|
68
|
+
: `${formatObjectKey(k)}: ${propType}`;
|
|
69
|
+
});
|
|
70
|
+
const propUnion = Object.entries(props as Record<string, Schema>)
|
|
71
|
+
.map(([, v]) => schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames))
|
|
72
|
+
.join(" | ");
|
|
73
|
+
const additionalType = schemaToTypeScriptType(
|
|
74
|
+
additional as Schema,
|
|
75
|
+
schemasByKey,
|
|
76
|
+
schemaTypeNames,
|
|
77
|
+
);
|
|
78
|
+
const indexType = [additionalType, propUnion].filter(Boolean).join(" | ");
|
|
79
|
+
return `{ ${propEntries.join("; ")}; [key: string]: ${indexType} }`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (hasProps) {
|
|
83
|
+
const requiredSet = new Set(schema.required || []);
|
|
84
|
+
const entries = Object.entries(props as Record<string, Schema>)
|
|
85
|
+
.map(([k, v]) => {
|
|
86
|
+
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
87
|
+
const optional = !requiredSet.has(k);
|
|
88
|
+
return optional
|
|
89
|
+
? `${formatObjectKey(k)}?: ${propType}`
|
|
90
|
+
: `${formatObjectKey(k)}: ${propType}`;
|
|
91
|
+
})
|
|
92
|
+
.join("; ");
|
|
93
|
+
return `{ ${entries} }`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (hasAdditional) {
|
|
97
|
+
const additionalType = schemaToTypeScriptType(
|
|
98
|
+
additional as Schema,
|
|
99
|
+
schemasByKey,
|
|
100
|
+
schemaTypeNames,
|
|
101
|
+
);
|
|
102
|
+
return `Record<string, ${additionalType}>`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return "Record<string, unknown>";
|
|
106
|
+
}
|
|
107
|
+
|
|
51
108
|
function formatTypeImport(typeNames: string[], fromPath: string): string {
|
|
52
109
|
if (typeNames.length === 0) {
|
|
53
110
|
return "";
|
|
@@ -132,19 +189,7 @@ function schemaToTypeScriptType(
|
|
|
132
189
|
}
|
|
133
190
|
return "string[]";
|
|
134
191
|
case "object": {
|
|
135
|
-
|
|
136
|
-
if (props && typeof props === "object" && Object.keys(props).length > 0) {
|
|
137
|
-
const requiredSet = new Set(resolved.required || []);
|
|
138
|
-
const entries = Object.entries(props)
|
|
139
|
-
.map(([k, v]) => {
|
|
140
|
-
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
141
|
-
const optional = !requiredSet.has(k);
|
|
142
|
-
return optional ? `${k}?: ${propType}` : `${k}: ${propType}`;
|
|
143
|
-
})
|
|
144
|
-
.join("; ");
|
|
145
|
-
return `{ ${entries} }`;
|
|
146
|
-
}
|
|
147
|
-
return "Record<string, unknown>";
|
|
192
|
+
return buildObjectTypeFromSchema(resolved, schemasByKey, schemaTypeNames);
|
|
148
193
|
}
|
|
149
194
|
default:
|
|
150
195
|
return "unknown";
|
|
@@ -265,13 +310,17 @@ function generateVariableTypeDeclarations(
|
|
|
265
310
|
|
|
266
311
|
if (type === "object") {
|
|
267
312
|
const resolvedEffective =
|
|
268
|
-
effective && "properties" in effective
|
|
313
|
+
effective && ("properties" in effective || "additionalProperties" in effective)
|
|
269
314
|
? (resolveSchema(effective as Schema, schemasByKey) as Schema)
|
|
270
315
|
: undefined;
|
|
271
316
|
const props = resolvedEffective?.properties;
|
|
272
|
-
|
|
317
|
+
const additional = resolvedEffective?.additionalProperties;
|
|
318
|
+
if (
|
|
319
|
+
(props && typeof props === "object" && Object.keys(props).length > 0) ||
|
|
320
|
+
(additional && typeof additional === "object")
|
|
321
|
+
) {
|
|
273
322
|
const requiredSet = new Set(resolvedEffective?.required || []);
|
|
274
|
-
const entries = Object.entries(props)
|
|
323
|
+
const entries = Object.entries(props ?? {})
|
|
275
324
|
.map(([k, v]) => {
|
|
276
325
|
const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
|
|
277
326
|
if (schemaTypeNames)
|
|
@@ -280,11 +329,29 @@ function generateVariableTypeDeclarations(
|
|
|
280
329
|
});
|
|
281
330
|
const optional = !requiredSet.has(k);
|
|
282
331
|
return optional
|
|
283
|
-
? `${INDENT_NS_BODY}${k}?: ${propType};`
|
|
284
|
-
: `${INDENT_NS_BODY}${k}: ${propType};`;
|
|
332
|
+
? `${INDENT_NS_BODY}${formatObjectKey(k)}?: ${propType};`
|
|
333
|
+
: `${INDENT_NS_BODY}${formatObjectKey(k)}: ${propType};`;
|
|
285
334
|
})
|
|
286
|
-
.
|
|
287
|
-
|
|
335
|
+
.filter(Boolean);
|
|
336
|
+
if (additional && typeof additional === "object") {
|
|
337
|
+
const additionalType = schemaToTypeScriptType(
|
|
338
|
+
additional as Schema,
|
|
339
|
+
schemasByKey,
|
|
340
|
+
schemaTypeNames,
|
|
341
|
+
);
|
|
342
|
+
if (schemaTypeNames)
|
|
343
|
+
Object.values(schemaTypeNames).forEach((n) => {
|
|
344
|
+
if (additionalType.includes(n)) addSchemaUsed(n);
|
|
345
|
+
});
|
|
346
|
+
const propUnion = Object.entries(props ?? {})
|
|
347
|
+
.map(([, v]) => schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames))
|
|
348
|
+
.join(" | ");
|
|
349
|
+
const indexType = [additionalType, propUnion].filter(Boolean).join(" | ");
|
|
350
|
+
entries.push(`${INDENT_NS_BODY}[key: string]: ${indexType};`);
|
|
351
|
+
}
|
|
352
|
+
declarations.push(
|
|
353
|
+
`${INDENT_NS}export interface ${typeName} {\n${entries.join("\n")}\n${INDENT_NS}}`,
|
|
354
|
+
);
|
|
288
355
|
return { declarations, returnTypeName: typeName, genericArg: typeName, schemaTypesUsed };
|
|
289
356
|
}
|
|
290
357
|
declarations.push(`${INDENT_NS}export type ${typeName} = Record<string, unknown>;`);
|