@featurevisor/core 1.5.1 → 1.6.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.
Files changed (60) hide show
  1. package/.eslintcache +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/coverage/clover.xml +2 -2
  4. package/coverage/lcov-report/index.html +1 -1
  5. package/coverage/lcov-report/lib/builder/allocator.js.html +1 -1
  6. package/coverage/lcov-report/lib/builder/index.html +1 -1
  7. package/coverage/lcov-report/lib/builder/traffic.js.html +1 -1
  8. package/coverage/lcov-report/lib/tester/checkIfObjectsAreEqual.js.html +1 -1
  9. package/coverage/lcov-report/lib/tester/index.html +1 -1
  10. package/coverage/lcov-report/lib/tester/matrix.js.html +1 -1
  11. package/coverage/lcov-report/src/builder/allocator.ts.html +1 -1
  12. package/coverage/lcov-report/src/builder/index.html +1 -1
  13. package/coverage/lcov-report/src/builder/traffic.ts.html +1 -1
  14. package/coverage/lcov-report/src/tester/checkIfObjectsAreEqual.ts.html +1 -1
  15. package/coverage/lcov-report/src/tester/index.html +1 -1
  16. package/coverage/lcov-report/src/tester/matrix.ts.html +1 -1
  17. package/lib/linter/attributeSchema.d.ts +17 -2
  18. package/lib/linter/attributeSchema.js +13 -11
  19. package/lib/linter/attributeSchema.js.map +1 -1
  20. package/lib/linter/checkPercentageExceedingSlot.d.ts +3 -0
  21. package/lib/linter/checkPercentageExceedingSlot.js +86 -0
  22. package/lib/linter/checkPercentageExceedingSlot.js.map +1 -0
  23. package/lib/linter/conditionSchema.d.ts +2 -2
  24. package/lib/linter/conditionSchema.js +112 -57
  25. package/lib/linter/conditionSchema.js.map +1 -1
  26. package/lib/linter/featureSchema.d.ts +229 -2
  27. package/lib/linter/featureSchema.js +195 -139
  28. package/lib/linter/featureSchema.js.map +1 -1
  29. package/lib/linter/groupSchema.d.ts +32 -2
  30. package/lib/linter/groupSchema.js +28 -97
  31. package/lib/linter/groupSchema.js.map +1 -1
  32. package/lib/linter/lintProject.js +169 -118
  33. package/lib/linter/lintProject.js.map +1 -1
  34. package/lib/linter/printError.d.ts +2 -0
  35. package/lib/linter/printError.js +20 -0
  36. package/lib/linter/printError.js.map +1 -0
  37. package/lib/linter/segmentSchema.d.ts +14 -2
  38. package/lib/linter/segmentSchema.js +12 -10
  39. package/lib/linter/segmentSchema.js.map +1 -1
  40. package/lib/linter/testSchema.d.ts +90 -2
  41. package/lib/linter/testSchema.js +49 -38
  42. package/lib/linter/testSchema.js.map +1 -1
  43. package/lib/tester/cliFormat.d.ts +1 -0
  44. package/lib/tester/cliFormat.js +2 -1
  45. package/lib/tester/cliFormat.js.map +1 -1
  46. package/package.json +4 -4
  47. package/src/linter/attributeSchema.ts +11 -9
  48. package/src/linter/checkPercentageExceedingSlot.ts +41 -0
  49. package/src/linter/conditionSchema.ts +120 -97
  50. package/src/linter/featureSchema.ts +241 -177
  51. package/src/linter/groupSchema.ts +38 -54
  52. package/src/linter/lintProject.ts +144 -62
  53. package/src/linter/printError.ts +21 -0
  54. package/src/linter/segmentSchema.ts +10 -8
  55. package/src/linter/testSchema.ts +67 -50
  56. package/src/tester/cliFormat.ts +1 -0
  57. package/lib/linter/printJoiError.d.ts +0 -2
  58. package/lib/linter/printJoiError.js +0 -14
  59. package/lib/linter/printJoiError.js.map +0 -1
  60. package/src/linter/printJoiError.ts +0 -11
@@ -1,24 +1,24 @@
1
- import * as Joi from "joi";
1
+ import { z } from "zod";
2
2
 
3
3
  import { ProjectConfig } from "../config";
4
4
 
5
5
  const tagRegex = /^[a-z0-9-]+$/;
6
6
 
7
- export function getFeatureJoiSchema(
7
+ export function getFeatureZodSchema(
8
8
  projectConfig: ProjectConfig,
9
- conditionsJoiSchema,
10
- availableSegmentKeys: string[],
11
- availableFeatureKeys: string[],
9
+ conditionsZodSchema,
10
+ availableAttributeKeys: [string, ...string[]],
11
+ availableSegmentKeys: [string, ...string[]],
12
+ availableFeatureKeys: [string, ...string[]],
12
13
  ) {
13
- const variationValueJoiSchema = Joi.string().required();
14
- const variableValueJoiSchema = Joi.alternatives()
15
- .try(
16
- // @TODO: make it stricter based on variableSchema.type
17
- Joi.string(),
18
- Joi.number(),
19
- Joi.boolean(),
20
- Joi.array().items(Joi.string()),
21
- Joi.object().custom(function (value) {
14
+ const variationValueZodSchema = z.string().min(1);
15
+ const variableValueZodSchema = z.union([
16
+ z.string(),
17
+ z.number(),
18
+ z.boolean(),
19
+ z.array(z.string()),
20
+ z.record(z.unknown()).refine(
21
+ (value) => {
22
22
  let isFlat = true;
23
23
 
24
24
  Object.keys(value).forEach((key) => {
@@ -27,177 +27,241 @@ export function getFeatureJoiSchema(
27
27
  }
28
28
  });
29
29
 
30
- if (!isFlat) {
31
- throw new Error("object is not flat");
32
- }
33
-
34
- return value;
35
- }),
36
- )
37
- .allow("");
38
-
39
- const plainGroupSegment = Joi.string().valid("*", ...availableSegmentKeys);
40
-
41
- const andOrNotGroupSegment = Joi.alternatives()
42
- .try(
43
- Joi.object({
44
- and: Joi.array().items(Joi.link("#andOrNotGroupSegment"), plainGroupSegment),
45
- }),
46
- Joi.object({
47
- or: Joi.array().items(Joi.link("#andOrNotGroupSegment"), plainGroupSegment),
48
- }),
49
- Joi.object({
50
- // @TODO: allow plainGroupSegment as well?
51
- not: Joi.array().items(Joi.link("#andOrNotGroupSegment"), plainGroupSegment),
52
- }),
53
- )
54
- .id("andOrNotGroupSegment");
55
-
56
- const groupSegment = Joi.alternatives().try(andOrNotGroupSegment, plainGroupSegment);
57
-
58
- const groupSegmentsJoiSchema = Joi.alternatives().try(
59
- Joi.array().items(groupSegment),
60
- groupSegment,
30
+ return isFlat;
31
+ },
32
+ {
33
+ message: "object is not flat",
34
+ },
35
+ ),
36
+ ]);
37
+
38
+ const plainGroupSegment = z.string().refine(
39
+ (value) => value === "*" || availableSegmentKeys.includes(value),
40
+ (value) => ({
41
+ message: `Unknown segment key "${value}"`,
42
+ }),
61
43
  );
62
44
 
63
- const environmentJoiSchema = Joi.object({
64
- expose: Joi.alternatives().try(
65
- Joi.boolean(),
66
- Joi.array().items(Joi.string().valid(...projectConfig.tags)),
67
- ),
68
- rules: Joi.array()
69
- .items(
70
- Joi.object({
71
- key: Joi.string().required(),
72
- description: Joi.string().optional(),
73
- segments: groupSegmentsJoiSchema.required(),
74
- percentage: Joi.number().precision(3).min(0).max(100).required(),
75
-
76
- enabled: Joi.boolean().optional(),
77
- variation: variationValueJoiSchema.optional(), // @TODO: only allowed if feature.variations is present
78
- variables: Joi.object().optional(), // @TODO: make it stricter
79
- }),
80
- )
81
- .unique("key")
82
- .required(),
83
- force: Joi.array().items(
84
- Joi.object({
85
- // @TODO: either of the two below should be required
86
- segments: groupSegmentsJoiSchema.optional(),
87
- conditions: conditionsJoiSchema.optional(),
88
-
89
- enabled: Joi.boolean().optional(),
90
- variation: variationValueJoiSchema.optional(),
91
- variables: Joi.object().optional(), // @TODO: make it stricter
92
- }),
93
- ),
94
- });
45
+ const andOrNotGroupSegment = z.union([
46
+ z
47
+ .object({
48
+ and: z.array(z.lazy(() => groupSegmentZodSchema)),
49
+ })
50
+ .strict(),
51
+ z
52
+ .object({
53
+ or: z.array(z.lazy(() => groupSegmentZodSchema)),
54
+ })
55
+ .strict(),
56
+ z
57
+ .object({
58
+ not: z.array(z.lazy(() => groupSegmentZodSchema)),
59
+ })
60
+ .strict(),
61
+ ]);
62
+
63
+ const groupSegmentZodSchema = z.union([andOrNotGroupSegment, plainGroupSegment]);
64
+
65
+ const groupSegmentsZodSchema = z.union([z.array(groupSegmentZodSchema), groupSegmentZodSchema]);
66
+
67
+ const environmentZodSchema = z
68
+ .object({
69
+ expose: z
70
+ .union([
71
+ z.boolean(),
72
+ z.array(z.string().refine((value) => projectConfig.tags.includes(value))),
73
+ ])
74
+ .optional(),
75
+ rules: z
76
+ .array(
77
+ z
78
+ .object({
79
+ key: z.string(),
80
+ description: z.string().optional(),
81
+ segments: groupSegmentsZodSchema,
82
+ percentage: z.number().min(0).max(100),
83
+
84
+ enabled: z.boolean().optional(),
85
+ variation: variationValueZodSchema.optional(),
86
+ variables: z.record(variableValueZodSchema).optional(),
87
+ })
88
+ .strict(),
89
+ )
90
+ .refine(
91
+ (value) => {
92
+ const keys = value.map((v) => v.key);
93
+ return keys.length === new Set(keys).size;
94
+ },
95
+ (value) => ({
96
+ message: "Duplicate rule keys found: " + value.map((v) => v.key).join(", "),
97
+ }),
98
+ ),
99
+ force: z
100
+ .array(
101
+ z.union([
102
+ z
103
+ .object({
104
+ segments: groupSegmentsZodSchema,
105
+ enabled: z.boolean().optional(),
106
+ variation: variationValueZodSchema.optional(),
107
+ variables: z.record(variableValueZodSchema).optional(),
108
+ })
109
+ .strict(),
110
+ z
111
+ .object({
112
+ conditions: conditionsZodSchema,
113
+ enabled: z.boolean().optional(),
114
+ variation: variationValueZodSchema.optional(),
115
+ variables: z.record(variableValueZodSchema).optional(),
116
+ })
117
+ .strict(),
118
+ ]),
119
+ )
120
+ .optional(),
121
+ })
122
+ .strict();
95
123
 
96
124
  const allEnvironmentsSchema = {};
97
125
  projectConfig.environments.forEach((environmentKey) => {
98
- allEnvironmentsSchema[environmentKey] = environmentJoiSchema.required();
126
+ allEnvironmentsSchema[environmentKey] = environmentZodSchema;
99
127
  });
100
- const allEnvironmentsJoiSchema = Joi.object(allEnvironmentsSchema);
101
-
102
- const featureJoiSchema = Joi.object({
103
- archived: Joi.boolean().optional(),
104
- deprecated: Joi.boolean().optional(),
105
- description: Joi.string().required(),
106
- tags: Joi.array()
107
- .items(
108
- Joi.string().custom((value) => {
109
- if (!tagRegex.test(value)) {
110
- throw new Error("tag must be lower cased and alphanumeric, and may contain hyphens.");
111
- }
128
+ const allEnvironmentsZodSchema = z.object(allEnvironmentsSchema).strict();
129
+
130
+ const attributeKeyZodSchema = z.string().refine(
131
+ (value) => value === "*" || availableAttributeKeys.includes(value),
132
+ (value) => ({
133
+ message: `Unknown attribute "${value}"`,
134
+ }),
135
+ );
136
+
137
+ const featureKeyZodSchema = z.string().refine(
138
+ (value) => availableFeatureKeys.includes(value),
139
+ (value) => ({
140
+ message: `Unknown feature "${value}"`,
141
+ }),
142
+ );
112
143
 
113
- return value;
114
- }),
115
- )
116
- .required(),
117
-
118
- required: Joi.array()
119
- .items(
120
- Joi.alternatives().try(
121
- Joi.string()
122
- .required()
123
- .valid(...availableFeatureKeys),
124
- Joi.object({
125
- key: Joi.string()
126
- .required()
127
- .valid(...availableFeatureKeys),
128
- variation: Joi.string().optional(), // @TODO: can be made stricter
144
+ const featureZodSchema = z
145
+ .object({
146
+ archived: z.boolean().optional(),
147
+ deprecated: z.boolean().optional(),
148
+ description: z.string(),
149
+ tags: z
150
+ .array(
151
+ z.string().refine(
152
+ (value) => tagRegex.test(value),
153
+ (value) => ({
154
+ message: `Tag "${value}" must be lower cased and alphanumeric, and may contain hyphens.`,
155
+ }),
156
+ ),
157
+ )
158
+ .refine(
159
+ (value) => {
160
+ return value.length === new Set(value).size;
161
+ },
162
+ (value) => ({
163
+ message: "Duplicate tags found: " + value.join(", "),
129
164
  }),
130
165
  ),
131
- )
132
- .optional(),
133
-
134
- bucketBy: Joi.alternatives()
135
- .try(
136
- // plain
137
- Joi.string(),
138
-
139
- // and
140
- Joi.array().items(Joi.string()),
141
-
142
- // or
143
- Joi.object({
144
- or: Joi.array().items(Joi.string()),
145
- }),
146
- )
147
- .required(),
148
-
149
- variablesSchema: Joi.array()
150
- .items(
151
- Joi.object({
152
- key: Joi.string().disallow("variation").required(),
153
- type: Joi.string()
154
- .valid("string", "integer", "boolean", "double", "array", "object", "json")
155
- .required(),
156
- description: Joi.string().optional(),
157
- defaultValue: variableValueJoiSchema, // @TODO: make it stricter based on `type`
158
- }),
159
- )
160
- .unique("key"),
161
-
162
- variations: Joi.array()
163
- .items(
164
- Joi.object({
165
- description: Joi.string(),
166
- value: variationValueJoiSchema.required(),
167
- weight: Joi.number().precision(3).min(0).max(100).required(),
168
- variables: Joi.array()
169
- .items(
170
- Joi.object({
171
- key: Joi.string(),
172
- value: variableValueJoiSchema,
173
- overrides: Joi.array().items(
174
- Joi.object({
175
- // @TODO: either segments or conditions prsent at a time
176
- segments: groupSegmentsJoiSchema,
177
- conditions: conditionsJoiSchema,
178
-
179
- // @TODO: make it stricter based on `type`
180
- value: variableValueJoiSchema,
181
- }),
182
- ),
183
- }),
184
- )
185
- .unique("key"),
186
- }),
187
- )
188
- .custom((value) => {
189
- const total = value.reduce((acc, v) => acc + v.weight, 0);
190
-
191
- if (total !== 100) {
192
- throw new Error(`Sum of all variation weights must be 100, got ${total}`);
193
- }
194
-
195
- return value;
196
- })
197
- .optional(),
198
-
199
- environments: allEnvironmentsJoiSchema.required(),
200
- });
166
+ required: z
167
+ .array(
168
+ z.union([
169
+ featureKeyZodSchema,
170
+ z
171
+ .object({
172
+ key: featureKeyZodSchema,
173
+ variation: z.string().optional(),
174
+ })
175
+ .strict(),
176
+ ]),
177
+ )
178
+ .optional(),
179
+ bucketBy: z.union([
180
+ attributeKeyZodSchema,
181
+ z.array(attributeKeyZodSchema),
182
+ z
183
+ .object({
184
+ or: z.array(attributeKeyZodSchema),
185
+ })
186
+ .strict(),
187
+ ]),
188
+ variablesSchema: z
189
+ .array(
190
+ z
191
+ .object({
192
+ key: z
193
+ .string()
194
+ .min(1)
195
+ .refine((value) => value !== "variation", {
196
+ message: `variable key cannot be "variation"`,
197
+ }),
198
+ type: z.enum(["string", "integer", "boolean", "double", "array", "object", "json"]),
199
+ description: z.string().optional(),
200
+ defaultValue: variableValueZodSchema,
201
+ })
202
+ .strict(),
203
+ )
204
+ .refine(
205
+ (value) => {
206
+ const keys = value.map((v) => v.key);
207
+ return keys.length === new Set(keys).size;
208
+ },
209
+ (value) => ({
210
+ message: "Duplicate variable keys found: " + value.map((v) => v.key).join(", "),
211
+ }),
212
+ )
213
+ .optional(),
214
+ variations: z
215
+ .array(
216
+ z
217
+ .object({
218
+ description: z.string().optional(),
219
+ value: variationValueZodSchema,
220
+ weight: z.number().min(0).max(100),
221
+ variables: z
222
+ .array(
223
+ z
224
+ .object({
225
+ key: z.string().min(1),
226
+ value: variableValueZodSchema,
227
+ overrides: z
228
+ .array(
229
+ z.union([
230
+ z
231
+ .object({
232
+ conditions: conditionsZodSchema,
233
+ value: variableValueZodSchema,
234
+ })
235
+ .strict(),
236
+ z
237
+ .object({
238
+ segments: groupSegmentsZodSchema,
239
+ value: variableValueZodSchema,
240
+ })
241
+ .strict(),
242
+ ]),
243
+ )
244
+ .optional(),
245
+ })
246
+ .strict(),
247
+ )
248
+ .optional(),
249
+ })
250
+ .strict(),
251
+ )
252
+ .refine(
253
+ (value) => {
254
+ const variationValues = value.map((v) => v.value);
255
+ return variationValues.length === new Set(variationValues).size;
256
+ },
257
+ (value) => ({
258
+ message: "Duplicate variation values found: " + value.map((v) => v.value).join(", "),
259
+ }),
260
+ )
261
+ .optional(),
262
+ environments: allEnvironmentsZodSchema,
263
+ })
264
+ .strict();
201
265
 
202
- return featureJoiSchema;
266
+ return featureZodSchema;
203
267
  }
@@ -1,63 +1,47 @@
1
- import * as Joi from "joi";
1
+ import { z } from "zod";
2
2
 
3
3
  import { ProjectConfig } from "../config";
4
4
  import { Datasource } from "../datasource";
5
5
 
6
- export function getGroupJoiSchema(
6
+ export function getGroupZodSchema(
7
7
  projectConfig: ProjectConfig,
8
8
  datasource: Datasource,
9
9
  availableFeatureKeys: string[],
10
10
  ) {
11
- const groupJoiSchema = Joi.object({
12
- description: Joi.string().required(),
13
- slots: Joi.array()
14
- .items(
15
- Joi.object({
16
- feature: Joi.string().valid(...availableFeatureKeys),
17
- percentage: Joi.number().precision(3).min(0).max(100).required(),
18
- }),
19
- )
20
- .custom(async function (value) {
21
- const totalPercentage = value.reduce((acc, slot) => acc + slot.percentage, 0);
22
-
23
- if (totalPercentage !== 100) {
24
- throw new Error("total percentage is not 100");
25
- }
26
-
27
- for (const slot of value) {
28
- const maxPercentageForRule = slot.percentage;
29
-
30
- if (slot.feature) {
31
- const featureKey = slot.feature;
32
- const featureExists = availableFeatureKeys.indexOf(featureKey) > -1;
33
-
34
- if (!featureExists) {
35
- throw new Error(`feature ${featureKey} not found`);
36
- }
37
-
38
- const parsedFeature = await datasource.readFeature(featureKey);
39
-
40
- const environmentKeys = Object.keys(parsedFeature.environments);
41
- for (const environmentKey of environmentKeys) {
42
- const environment = parsedFeature.environments[environmentKey];
43
- const rules = environment.rules;
44
-
45
- for (const rule of rules) {
46
- if (rule.percentage > maxPercentageForRule) {
47
- // @TODO: this does not help with same feature belonging to multiple slots. fix that.
48
- throw new Error(
49
- `Feature ${featureKey}'s rule ${rule.key} in ${environmentKey} has a percentage of ${rule.percentage} which is greater than the maximum percentage of ${maxPercentageForRule} for the slot`,
50
- );
51
- }
52
- }
53
- }
54
- }
55
- }
56
-
57
- return value;
58
- })
59
- .required(),
60
- });
61
-
62
- return groupJoiSchema;
11
+ const groupZodSchema = z
12
+ .object({
13
+ description: z.string(),
14
+ slots: z
15
+ .array(
16
+ z
17
+ .object({
18
+ feature: z
19
+ .string()
20
+ .optional()
21
+ .refine(
22
+ (value) => {
23
+ if (value && availableFeatureKeys.indexOf(value) === -1) {
24
+ return false;
25
+ }
26
+
27
+ return true;
28
+ },
29
+ (value) => ({ message: `Unknown feature "${value}"` }),
30
+ ),
31
+ percentage: z.number().min(0).max(100),
32
+ })
33
+ .strict(),
34
+ )
35
+ .refine(
36
+ (value) => {
37
+ const totalPercentage = value.reduce((acc, slot) => acc + slot.percentage, 0);
38
+
39
+ return totalPercentage === 100;
40
+ },
41
+ { message: "Total percentage of all slots is not 100" },
42
+ ),
43
+ })
44
+ .strict();
45
+
46
+ return groupZodSchema;
63
47
  }