@featurevisor/core 2.9.0 → 2.11.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 (29) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +2 -2
  3. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  4. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  7. package/coverage/lcov-report/builder/index.html +1 -1
  8. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  9. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  10. package/coverage/lcov-report/index.html +1 -1
  11. package/coverage/lcov-report/list/index.html +1 -1
  12. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  13. package/coverage/lcov-report/parsers/index.html +1 -1
  14. package/coverage/lcov-report/parsers/json.ts.html +1 -1
  15. package/coverage/lcov-report/parsers/yml.ts.html +1 -1
  16. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  17. package/coverage/lcov-report/tester/index.html +1 -1
  18. package/lib/generate-code/typescript.js +150 -16
  19. package/lib/generate-code/typescript.js.map +1 -1
  20. package/lib/linter/featureSchema.d.ts +142 -101
  21. package/lib/linter/featureSchema.js +269 -81
  22. package/lib/linter/featureSchema.js.map +1 -1
  23. package/lib/linter/propertySchema.d.ts +5 -0
  24. package/lib/linter/propertySchema.js +43 -0
  25. package/lib/linter/propertySchema.js.map +1 -0
  26. package/package.json +5 -5
  27. package/src/generate-code/typescript.ts +168 -18
  28. package/src/linter/featureSchema.ts +358 -96
  29. package/src/linter/propertySchema.ts +47 -0
@@ -1,23 +1,221 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import { ProjectConfig } from "../config";
4
+ import { valueZodSchema, propertyTypeEnum, getPropertyZodSchema } from "./propertySchema";
4
5
 
5
6
  const tagRegex = /^[a-z0-9-]+$/;
6
7
 
7
- function isFlatObject(value) {
8
- let isFlat = true;
8
+ function isArrayOfStrings(value: unknown): value is string[] {
9
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
10
+ }
11
+
12
+ function isFlatObjectValue(value: unknown): boolean {
13
+ return (
14
+ value === null ||
15
+ typeof value === "string" ||
16
+ typeof value === "number" ||
17
+ typeof value === "boolean"
18
+ );
19
+ }
20
+
21
+ function getVariableLabel(variableSchema, variableKey, path) {
22
+ return (
23
+ variableKey ??
24
+ variableSchema?.key ??
25
+ (path.length > 0 ? String(path[path.length - 1]) : "variable")
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Recursively validates that every `required` array (at this level and in nested
31
+ * object/array schemas) only contains keys that exist in the same level's `properties`.
32
+ * Adds Zod issues with the correct path for invalid required field names.
33
+ */
34
+ function refineRequiredKeysInSchema(
35
+ schema: {
36
+ type?: string;
37
+ properties?: Record<string, unknown>;
38
+ required?: string[];
39
+ items?: unknown;
40
+ },
41
+ pathPrefix: (string | number)[],
42
+ ctx: z.RefinementCtx,
43
+ ): void {
44
+ if (!schema || typeof schema !== "object") return;
45
+
46
+ const effectiveType = schema.type;
47
+ const properties = schema.properties;
48
+ const required = schema.required;
49
+ const items = schema.items;
50
+
51
+ if (
52
+ effectiveType === "object" &&
53
+ Array.isArray(required) &&
54
+ required.length > 0 &&
55
+ properties &&
56
+ typeof properties === "object"
57
+ ) {
58
+ const allowedKeys = Object.keys(properties);
59
+ required.forEach((key, index) => {
60
+ if (!allowedKeys.includes(key)) {
61
+ ctx.addIssue({
62
+ code: z.ZodIssueCode.custom,
63
+ message: `Unknown required field "${key}". \`required\` must only contain property names defined in \`properties\`. Allowed: ${allowedKeys.length ? allowedKeys.join(", ") : "(none)"}.`,
64
+ path: [...pathPrefix, "required", index],
65
+ });
66
+ }
67
+ });
68
+ }
9
69
 
10
- Object.keys(value).forEach((key) => {
11
- if (typeof value[key] === "object") {
12
- isFlat = false;
70
+ if (properties && typeof properties === "object") {
71
+ for (const key of Object.keys(properties)) {
72
+ const nested = properties[key];
73
+ if (nested && typeof nested === "object") {
74
+ refineRequiredKeysInSchema(
75
+ nested as Parameters<typeof refineRequiredKeysInSchema>[0],
76
+ [...pathPrefix, "properties", key],
77
+ ctx,
78
+ );
79
+ }
13
80
  }
14
- });
81
+ }
15
82
 
16
- return isFlat;
83
+ if (items && typeof items === "object" && !Array.isArray(items)) {
84
+ refineRequiredKeysInSchema(
85
+ items as Parameters<typeof refineRequiredKeysInSchema>[0],
86
+ [...pathPrefix, "items"],
87
+ ctx,
88
+ );
89
+ }
17
90
  }
18
91
 
19
- function isArrayOfStrings(value) {
20
- return Array.isArray(value) && value.every((v) => typeof v === "string");
92
+ function typeOfValue(value: unknown): string {
93
+ if (value === null) return "null";
94
+ if (value === undefined) return "undefined";
95
+ if (Array.isArray(value)) return "array";
96
+ return typeof value;
97
+ }
98
+
99
+ /**
100
+ * Validates a variable value against an array schema. Recursively validates each item
101
+ * when the schema defines `items` (nested arrays/objects use the same refinement).
102
+ */
103
+ function refineVariableValueArray(
104
+ projectConfig: ProjectConfig,
105
+ variableSchema: { items?: unknown; type: string },
106
+ variableValue: unknown[],
107
+ path: (string | number)[],
108
+ ctx: z.RefinementCtx,
109
+ variableKey?: string,
110
+ ): void {
111
+ const label = getVariableLabel(variableSchema, variableKey, path);
112
+ const itemSchema = variableSchema.items;
113
+
114
+ if (itemSchema) {
115
+ variableValue.forEach((item, index) => {
116
+ superRefineVariableValue(projectConfig, itemSchema, item, [...path, index], ctx, variableKey);
117
+ });
118
+ } else {
119
+ if (!isArrayOfStrings(variableValue)) {
120
+ ctx.addIssue({
121
+ code: z.ZodIssueCode.custom,
122
+ message: `Variable "${label}" (type array): when \`items\` is not set, array must contain only strings; found non-string element.`,
123
+ path,
124
+ });
125
+ }
126
+ }
127
+
128
+ if (projectConfig.maxVariableArrayStringifiedLength) {
129
+ const stringified = JSON.stringify(variableValue);
130
+ if (stringified.length > projectConfig.maxVariableArrayStringifiedLength) {
131
+ ctx.addIssue({
132
+ code: z.ZodIssueCode.custom,
133
+ message: `Variable "${label}" array is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableArrayStringifiedLength}`,
134
+ path,
135
+ });
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Validates a variable value against an object schema. Recursively validates each property
142
+ * when the schema defines `properties` (nested objects/arrays use the same refinement).
143
+ */
144
+ function refineVariableValueObject(
145
+ projectConfig: ProjectConfig,
146
+ variableSchema: {
147
+ properties?: Record<string, unknown>;
148
+ required?: string[];
149
+ type: string;
150
+ },
151
+ variableValue: Record<string, unknown>,
152
+ path: (string | number)[],
153
+ ctx: z.RefinementCtx,
154
+ variableKey?: string,
155
+ ): void {
156
+ const label = getVariableLabel(variableSchema, variableKey, path);
157
+ const schemaProperties = variableSchema.properties;
158
+
159
+ if (schemaProperties && typeof schemaProperties === "object") {
160
+ const requiredKeys =
161
+ variableSchema.required && variableSchema.required.length > 0
162
+ ? variableSchema.required.filter((k) =>
163
+ Object.prototype.hasOwnProperty.call(schemaProperties, k),
164
+ )
165
+ : Object.keys(schemaProperties);
166
+
167
+ for (const key of requiredKeys) {
168
+ if (!Object.prototype.hasOwnProperty.call(variableValue, key)) {
169
+ ctx.addIssue({
170
+ code: z.ZodIssueCode.custom,
171
+ message: `Missing required property "${key}" in variable "${label}"`,
172
+ path: [...path, key],
173
+ });
174
+ }
175
+ }
176
+
177
+ for (const key of Object.keys(variableValue)) {
178
+ const propSchema = schemaProperties[key];
179
+ if (!propSchema) {
180
+ ctx.addIssue({
181
+ code: z.ZodIssueCode.custom,
182
+ message: `Unknown property "${key}" in variable "${label}" (not in schema)`,
183
+ path: [...path, key],
184
+ });
185
+ } else {
186
+ superRefineVariableValue(
187
+ projectConfig,
188
+ propSchema,
189
+ variableValue[key],
190
+ [...path, key],
191
+ ctx,
192
+ key,
193
+ );
194
+ }
195
+ }
196
+ } else {
197
+ for (const key of Object.keys(variableValue)) {
198
+ const propValue = variableValue[key];
199
+ if (!isFlatObjectValue(propValue)) {
200
+ ctx.addIssue({
201
+ code: z.ZodIssueCode.custom,
202
+ message: `Variable "${label}" is a flat object (no \`properties\` in schema); property "${key}" must be a primitive (string, number, boolean, or null), got: ${typeof propValue}`,
203
+ path: [...path, key],
204
+ });
205
+ }
206
+ }
207
+ }
208
+
209
+ if (projectConfig.maxVariableObjectStringifiedLength) {
210
+ const stringified = JSON.stringify(variableValue);
211
+ if (stringified.length > projectConfig.maxVariableObjectStringifiedLength) {
212
+ ctx.addIssue({
213
+ code: z.ZodIssueCode.custom,
214
+ message: `Variable "${label}" object is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableObjectStringifiedLength}`,
215
+ path,
216
+ });
217
+ }
218
+ }
21
219
  }
22
220
 
23
221
  function superRefineVariableValue(
@@ -26,17 +224,16 @@ function superRefineVariableValue(
26
224
  variableValue,
27
225
  path,
28
226
  ctx,
227
+ variableKey?: string,
29
228
  ) {
30
- if (!variableSchema) {
31
- let message = `Unknown variable with value: ${variableValue}`;
229
+ const label = getVariableLabel(variableSchema, variableKey, path);
32
230
 
33
- if (path.length > 0) {
34
- const lastPath = path[path.length - 1];
35
-
36
- if (typeof lastPath === "string") {
37
- message = `Unknown variable "${lastPath}" with value: ${variableValue}`;
38
- }
39
- }
231
+ if (!variableSchema) {
232
+ const variableName =
233
+ path.length > 0 && typeof path[path.length - 1] === "string"
234
+ ? String(path[path.length - 1])
235
+ : "variable";
236
+ const message = `Variable "${variableName}" is used but not defined in variablesSchema. Define it under variablesSchema first, then use it here.`;
40
237
 
41
238
  ctx.addIssue({
42
239
  code: z.ZodIssueCode.custom,
@@ -47,14 +244,28 @@ function superRefineVariableValue(
47
244
  return;
48
245
  }
49
246
 
50
- // string
51
- if (variableSchema.type === "string") {
247
+ // Require a value (no undefined) for every variable usage
248
+ if (variableValue === undefined) {
249
+ ctx.addIssue({
250
+ code: z.ZodIssueCode.custom,
251
+ message: `Variable "${label}" value is required (got undefined).`,
252
+ path,
253
+ });
254
+ return;
255
+ }
256
+
257
+ const expectedType = variableSchema.type;
258
+ const gotType = typeOfValue(variableValue);
259
+
260
+ // string — only string allowed
261
+ if (expectedType === "string") {
52
262
  if (typeof variableValue !== "string") {
53
263
  ctx.addIssue({
54
264
  code: z.ZodIssueCode.custom,
55
- message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): ${variableValue}`,
265
+ message: `Variable "${label}" (type string) must be a string; got ${gotType}.`,
56
266
  path,
57
267
  });
268
+ return;
58
269
  }
59
270
 
60
271
  if (
@@ -63,7 +274,7 @@ function superRefineVariableValue(
63
274
  ) {
64
275
  ctx.addIssue({
65
276
  code: z.ZodIssueCode.custom,
66
- message: `Variable "${variableSchema.key}" value is too long (${variableValue.length} characters), max length is ${projectConfig.maxVariableStringLength}`,
277
+ message: `Variable "${label}" value is too long (${variableValue.length} characters), max length is ${projectConfig.maxVariableStringLength}`,
67
278
  path,
68
279
  });
69
280
  }
@@ -71,94 +282,123 @@ function superRefineVariableValue(
71
282
  return;
72
283
  }
73
284
 
74
- // integer, double
75
- if (["integer", "double"].indexOf(variableSchema.type) > -1) {
285
+ // integer — only integer number allowed (no NaN, no Infinity, no float)
286
+ if (expectedType === "integer") {
76
287
  if (typeof variableValue !== "number") {
77
288
  ctx.addIssue({
78
289
  code: z.ZodIssueCode.custom,
79
- message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): ${variableValue}`,
290
+ message: `Variable "${label}" (type integer) must be a number; got ${gotType}.`,
291
+ path,
292
+ });
293
+ return;
294
+ }
295
+ if (!Number.isFinite(variableValue)) {
296
+ ctx.addIssue({
297
+ code: z.ZodIssueCode.custom,
298
+ message: `Variable "${label}" (type integer) must be a finite number; got ${variableValue}.`,
299
+ path,
300
+ });
301
+ return;
302
+ }
303
+ if (!Number.isInteger(variableValue)) {
304
+ ctx.addIssue({
305
+ code: z.ZodIssueCode.custom,
306
+ message: `Variable "${label}" (type integer) must be an integer; got ${variableValue}.`,
80
307
  path,
81
308
  });
82
309
  }
83
-
84
310
  return;
85
311
  }
86
312
 
87
- // boolean
88
- if (variableSchema.type === "boolean") {
89
- if (typeof variableValue !== "boolean") {
313
+ // double — only finite number allowed
314
+ if (expectedType === "double") {
315
+ if (typeof variableValue !== "number") {
90
316
  ctx.addIssue({
91
317
  code: z.ZodIssueCode.custom,
92
- message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): ${variableValue}`,
318
+ message: `Variable "${label}" (type double) must be a number; got ${gotType}.`,
319
+ path,
320
+ });
321
+ return;
322
+ }
323
+ if (!Number.isFinite(variableValue)) {
324
+ ctx.addIssue({
325
+ code: z.ZodIssueCode.custom,
326
+ message: `Variable "${label}" (type double) must be a finite number; got ${variableValue}.`,
93
327
  path,
94
328
  });
95
329
  }
96
-
97
330
  return;
98
331
  }
99
332
 
100
- // array
101
- if (variableSchema.type === "array") {
102
- if (!Array.isArray(variableValue) || !isArrayOfStrings(variableValue)) {
333
+ // boolean — only boolean allowed
334
+ if (expectedType === "boolean") {
335
+ if (typeof variableValue !== "boolean") {
103
336
  ctx.addIssue({
104
337
  code: z.ZodIssueCode.custom,
105
- message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): \n\n${variableValue}\n\n`,
338
+ message: `Variable "${label}" (type boolean) must be a boolean; got ${gotType}.`,
106
339
  path,
107
340
  });
108
341
  }
109
-
110
- if (projectConfig.maxVariableArrayStringifiedLength) {
111
- const stringified = JSON.stringify(variableValue);
112
-
113
- if (stringified.length > projectConfig.maxVariableArrayStringifiedLength) {
114
- ctx.addIssue({
115
- code: z.ZodIssueCode.custom,
116
- message: `Variable "${variableSchema.key}" array is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableArrayStringifiedLength}`,
117
- path,
118
- });
119
- }
120
- }
121
-
122
342
  return;
123
343
  }
124
344
 
125
- // object
126
- if (variableSchema.type === "object") {
127
- if (typeof variableValue !== "object" || !isFlatObject(variableValue)) {
345
+ // array — only array allowed; without items schema = array of strings
346
+ if (expectedType === "array") {
347
+ if (!Array.isArray(variableValue)) {
128
348
  ctx.addIssue({
129
349
  code: z.ZodIssueCode.custom,
130
- message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): \n\n${variableValue}\n\n`,
350
+ message: `Variable "${label}" (type array) must be an array; got ${gotType}.`,
131
351
  path,
132
352
  });
353
+ return;
133
354
  }
355
+ refineVariableValueArray(projectConfig, variableSchema, variableValue, path, ctx, variableKey);
356
+ return;
357
+ }
134
358
 
135
- if (projectConfig.maxVariableObjectStringifiedLength) {
136
- const stringified = JSON.stringify(variableValue);
137
-
138
- if (stringified.length > projectConfig.maxVariableObjectStringifiedLength) {
139
- ctx.addIssue({
140
- code: z.ZodIssueCode.custom,
141
- message: `Variable "${variableSchema.key}" object is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableObjectStringifiedLength}`,
142
- path,
143
- });
144
- }
359
+ // object — only plain object allowed (no null, no array)
360
+ if (expectedType === "object") {
361
+ if (
362
+ typeof variableValue !== "object" ||
363
+ variableValue === null ||
364
+ Array.isArray(variableValue)
365
+ ) {
366
+ ctx.addIssue({
367
+ code: z.ZodIssueCode.custom,
368
+ message: `Variable "${label}" (type object) must be a plain object; got ${gotType}.`,
369
+ path,
370
+ });
371
+ return;
145
372
  }
146
-
373
+ refineVariableValueObject(
374
+ projectConfig,
375
+ variableSchema,
376
+ variableValue as Record<string, unknown>,
377
+ path,
378
+ ctx,
379
+ variableKey,
380
+ );
147
381
  return;
148
382
  }
149
383
 
150
- // json
151
- if (variableSchema.type === "json") {
384
+ // json — only string containing valid JSON allowed
385
+ if (expectedType === "json") {
386
+ if (typeof variableValue !== "string") {
387
+ ctx.addIssue({
388
+ code: z.ZodIssueCode.custom,
389
+ message: `Variable "${label}" (type json) must be a string (JSON string); got ${gotType}.`,
390
+ path,
391
+ });
392
+ return;
393
+ }
152
394
  try {
153
- JSON.parse(variableValue as string);
395
+ JSON.parse(variableValue);
154
396
 
155
397
  if (projectConfig.maxVariableJSONStringifiedLength) {
156
- const stringified = variableValue;
157
-
158
- if (stringified.length > projectConfig.maxVariableJSONStringifiedLength) {
398
+ if (variableValue.length > projectConfig.maxVariableJSONStringifiedLength) {
159
399
  ctx.addIssue({
160
400
  code: z.ZodIssueCode.custom,
161
- message: `Variable "${variableSchema.key}" JSON is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableJSONStringifiedLength}`,
401
+ message: `Variable "${label}" JSON is too long (${variableValue.length} characters), max length is ${projectConfig.maxVariableJSONStringifiedLength}`,
162
402
  path,
163
403
  });
164
404
  }
@@ -167,13 +407,20 @@ function superRefineVariableValue(
167
407
  } catch (e) {
168
408
  ctx.addIssue({
169
409
  code: z.ZodIssueCode.custom,
170
- message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): \n\n${variableValue}\n\n`,
410
+ message: `Variable "${label}" (type json) must be a valid JSON string; parse failed.`,
171
411
  path,
172
412
  });
173
413
  }
174
414
 
175
415
  return;
176
416
  }
417
+
418
+ // Unknown variable type — schema is invalid or unsupported
419
+ ctx.addIssue({
420
+ code: z.ZodIssueCode.custom,
421
+ message: `Variable "${label}" has unknown or unsupported type "${String(expectedType)}" in variablesSchema.`,
422
+ path,
423
+ });
177
424
  }
178
425
 
179
426
  function refineForce({
@@ -206,6 +453,7 @@ function refineForce({
206
453
  f.variables[variableKey],
207
454
  pathPrefix.concat([fN, "variables", variableKey]),
208
455
  ctx,
456
+ variableKey,
209
457
  );
210
458
  });
211
459
  }
@@ -231,6 +479,7 @@ function refineRules({
231
479
  rule.variables[variableKey],
232
480
  pathPrefix.concat([ruleN, "variables", variableKey]),
233
481
  ctx,
482
+ variableKey,
234
483
  );
235
484
  });
236
485
  }
@@ -308,21 +557,10 @@ export function getFeatureZodSchema(
308
557
  availableSegmentKeys: [string, ...string[]],
309
558
  availableFeatureKeys: [string, ...string[]],
310
559
  ) {
560
+ const propertyZodSchema = getPropertyZodSchema();
561
+ const variableValueZodSchema = valueZodSchema;
562
+
311
563
  const variationValueZodSchema = z.string().min(1);
312
- const variableValueZodSchema = z.union([
313
- z.string(),
314
- z.number(),
315
- z.boolean(),
316
- z.array(z.string()),
317
- z.record(z.unknown()).refine(
318
- (value) => {
319
- return isFlatObject(value);
320
- },
321
- {
322
- message: "object is not flat",
323
- },
324
- ),
325
- ]);
326
564
 
327
565
  const plainGroupSegment = z.string().refine(
328
566
  (value) => value === "*" || availableSegmentKeys.includes(value),
@@ -500,13 +738,31 @@ export function getFeatureZodSchema(
500
738
  z
501
739
  .object({
502
740
  deprecated: z.boolean().optional(),
503
- type: z.enum(["string", "integer", "boolean", "double", "array", "object", "json"]),
741
+
742
+ type: z.union([z.literal("json"), propertyTypeEnum]),
743
+ // array: when omitted, treated as array of strings
744
+ items: propertyZodSchema.optional(),
745
+ // object: when omitted, treated as flat object (primitive values only)
746
+ properties: z.record(propertyZodSchema).optional(),
747
+ // object: optional list of required property names
748
+ required: z.array(z.string()).optional(),
749
+
504
750
  description: z.string().optional(),
751
+
505
752
  defaultValue: variableValueZodSchema,
506
- useDefaultWhenDisabled: z.boolean().optional(),
507
753
  disabledValue: variableValueZodSchema.optional(),
754
+
755
+ useDefaultWhenDisabled: z.boolean().optional(),
508
756
  })
509
- .strict(),
757
+ .strict()
758
+ .superRefine((variableSchema, ctx) => {
759
+ // Validate required ⊆ properties at this level and in all nested object schemas
760
+ refineRequiredKeysInSchema(
761
+ variableSchema as Parameters<typeof refineRequiredKeysInSchema>[0],
762
+ [],
763
+ ctx,
764
+ );
765
+ }),
510
766
  )
511
767
  .optional(),
512
768
 
@@ -625,16 +881,20 @@ export function getFeatureZodSchema(
625
881
  variableSchema.defaultValue,
626
882
  ["variablesSchema", variableKey, "defaultValue"],
627
883
  ctx,
884
+ variableKey,
628
885
  );
629
886
 
630
- // disabledValue
631
- superRefineVariableValue(
632
- projectConfig,
633
- variableSchema,
634
- variableSchema.defaultValue,
635
- ["variablesSchema", variableKey, "disabledValue"],
636
- ctx,
637
- );
887
+ // disabledValue (only when present)
888
+ if (variableSchema.disabledValue !== undefined) {
889
+ superRefineVariableValue(
890
+ projectConfig,
891
+ variableSchema,
892
+ variableSchema.disabledValue,
893
+ ["variablesSchema", variableKey, "disabledValue"],
894
+ ctx,
895
+ variableKey,
896
+ );
897
+ }
638
898
  });
639
899
 
640
900
  // variations
@@ -654,6 +914,7 @@ export function getFeatureZodSchema(
654
914
  variableValue,
655
915
  ["variations", variationN, "variables", variableKey],
656
916
  ctx,
917
+ variableKey,
657
918
  );
658
919
 
659
920
  // variations[n].variableOverrides[n].value
@@ -676,6 +937,7 @@ export function getFeatureZodSchema(
676
937
  "value",
677
938
  ],
678
939
  ctx,
940
+ variableKey,
679
941
  );
680
942
  });
681
943
  }
@@ -0,0 +1,47 @@
1
+ import type { PropertySchema, Value } from "@featurevisor/types";
2
+ import { z } from "zod";
3
+
4
+ // Recursive schema for Value: boolean | string | number | ObjectValue | Value[]
5
+ export const valueZodSchema: z.ZodType<Value> = z.lazy(() =>
6
+ z.union([
7
+ z.boolean(),
8
+ z.string(),
9
+ z.number(),
10
+ // | Date // @TODO: support in future
11
+ z.record(z.string(), valueZodSchema),
12
+ z.array(valueZodSchema),
13
+ ]),
14
+ );
15
+
16
+ // @TODO: support "date" in future
17
+ // @TODO: consider "semver" in future
18
+ // @TODO: consider "url" in future
19
+ export const propertyTypeEnum = z.enum([
20
+ "boolean",
21
+ "string",
22
+ "integer",
23
+ "double",
24
+ "object",
25
+ "array",
26
+ ]);
27
+
28
+ export function getPropertyZodSchema() {
29
+ const propertyZodSchema: z.ZodType<PropertySchema> = z.lazy(() =>
30
+ z
31
+ .object({
32
+ description: z.string().optional(),
33
+ type: propertyTypeEnum.optional(),
34
+ // enum?: Value[]; const?: Value;
35
+ // Numeric: maximum?, minimum?
36
+ // String: maxLength?, minLength?, pattern?
37
+ items: propertyZodSchema.optional(),
38
+ // maxItems?, minItems?, uniqueItems?
39
+ required: z.array(z.string()).optional(),
40
+ properties: z.record(z.string(), propertyZodSchema).optional(),
41
+ // Annotations: default?: Value; examples?: Value[];
42
+ })
43
+ .strict(),
44
+ );
45
+
46
+ return propertyZodSchema;
47
+ }