UNPKG

@featurevisor/core

Version:

Core package of Featurevisor for Node.js usage

550 lines 23.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getFeatureZodSchema = getFeatureZodSchema; const zod_1 = require("zod"); const tagRegex = /^[a-z0-9-]+$/; function isFlatObject(value) { let isFlat = true; Object.keys(value).forEach((key) => { if (typeof value[key] === "object") { isFlat = false; } }); return isFlat; } function isArrayOfStrings(value) { return Array.isArray(value) && value.every((v) => typeof v === "string"); } function superRefineVariableValue(projectConfig, variableSchema, variableValue, path, ctx) { if (!variableSchema) { let message = `Unknown variable with value: ${variableValue}`; if (path.length > 0) { const lastPath = path[path.length - 1]; if (typeof lastPath === "string") { message = `Unknown variable "${lastPath}" with value: ${variableValue}`; } } ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message, path, }); return; } // string if (variableSchema.type === "string") { if (typeof variableValue !== "string") { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): ${variableValue}`, path, }); } if (projectConfig.maxVariableStringLength && variableValue.length > projectConfig.maxVariableStringLength) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Variable "${variableSchema.key}" value is too long (${variableValue.length} characters), max length is ${projectConfig.maxVariableStringLength}`, path, }); } return; } // integer, double if (["integer", "double"].indexOf(variableSchema.type) > -1) { if (typeof variableValue !== "number") { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): ${variableValue}`, path, }); } return; } // boolean if (variableSchema.type === "boolean") { if (typeof variableValue !== "boolean") { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): ${variableValue}`, path, }); } return; } // array if (variableSchema.type === "array") { if (!Array.isArray(variableValue) || !isArrayOfStrings(variableValue)) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): \n\n${variableValue}\n\n`, path, }); } if (projectConfig.maxVariableArrayStringifiedLength) { const stringified = JSON.stringify(variableValue); if (stringified.length > projectConfig.maxVariableArrayStringifiedLength) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Variable "${variableSchema.key}" array is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableArrayStringifiedLength}`, path, }); } } return; } // object if (variableSchema.type === "object") { if (typeof variableValue !== "object" || !isFlatObject(variableValue)) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): \n\n${variableValue}\n\n`, path, }); } if (projectConfig.maxVariableObjectStringifiedLength) { const stringified = JSON.stringify(variableValue); if (stringified.length > projectConfig.maxVariableObjectStringifiedLength) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Variable "${variableSchema.key}" object is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableObjectStringifiedLength}`, path, }); } } return; } // json if (variableSchema.type === "json") { try { JSON.parse(variableValue); if (projectConfig.maxVariableJSONStringifiedLength) { const stringified = variableValue; if (stringified.length > projectConfig.maxVariableJSONStringifiedLength) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Variable "${variableSchema.key}" JSON is too long (${stringified.length} characters), max length is ${projectConfig.maxVariableJSONStringifiedLength}`, path, }); } } // eslint-disable-next-line } catch (e) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Invalid value for variable "${variableSchema.key}" (${variableSchema.type}): \n\n${variableValue}\n\n`, path, }); } return; } } function refineForce({ ctx, parsedFeature, // eslint-disable-line variableSchemaByKey, variationValues, force, pathPrefix, projectConfig, }) { force.forEach((f, fN) => { // force[n].variation if (f.variation) { if (variationValues.indexOf(f.variation) === -1) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Unknown variation "${f.variation}" in force`, path: [...pathPrefix, fN, "variation"], }); } } // force[n].variables[key] if (f.variables) { Object.keys(f.variables).forEach((variableKey) => { superRefineVariableValue(projectConfig, variableSchemaByKey[variableKey], f.variables[variableKey], pathPrefix.concat([fN, "variables", variableKey]), ctx); }); } }); } function refineRules({ ctx, parsedFeature, variableSchemaByKey, variationValues, rules, pathPrefix, projectConfig, }) { rules.forEach((rule, ruleN) => { // rules[n].variables[key] if (rule.variables) { Object.keys(rule.variables).forEach((variableKey) => { superRefineVariableValue(projectConfig, variableSchemaByKey[variableKey], rule.variables[variableKey], pathPrefix.concat([ruleN, "variables", variableKey]), ctx); }); } // rules[n].variationWeights if (rule.variationWeights) { if (!parsedFeature.variations) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: "Variation weights are overridden from rule, but no variations are present in feature.", path: pathPrefix.concat([ruleN, "variationWeights"]), }); } else { const overriddenVariationValues = Object.keys(rule.variationWeights); const overriddenVariationWeights = Object.values(rule.variationWeights); // unique keys if (overriddenVariationValues.length !== new Set(overriddenVariationValues).size) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: "Duplicate variation values found in variationWeights: " + overriddenVariationValues.join(", "), path: pathPrefix.concat([ruleN, "variationWeights"]), }); } // all original variations must be used const missingVariations = variationValues.filter((v) => !overriddenVariationValues.includes(v)); if (missingVariations.length > 0) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: "Missing variations: " + missingVariations.join(", "), path: pathPrefix.concat([ruleN, "variationWeights"]), }); } // unknown variations const unknownVariations = overriddenVariationValues.filter((v) => !variationValues.includes(v)); if (unknownVariations.length > 0) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: "Variation weights contain unknown variations: " + unknownVariations.join(", "), path: pathPrefix.concat([ruleN, "variationWeights"]), }); } // weights sum must be 100 const weightsSum = overriddenVariationWeights.reduce((sum, weight) => sum + weight, 0); if (weightsSum !== 100) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: "Variation weights must sum to 100", path: pathPrefix.concat([ruleN, "variationWeights"]), }); } } } }); } function getFeatureZodSchema(projectConfig, conditionsZodSchema, availableAttributeKeys, availableSegmentKeys, availableFeatureKeys) { const variationValueZodSchema = zod_1.z.string().min(1); const variableValueZodSchema = zod_1.z.union([ zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean(), zod_1.z.array(zod_1.z.string()), zod_1.z.record(zod_1.z.unknown()).refine((value) => { return isFlatObject(value); }, { message: "object is not flat", }), ]); const plainGroupSegment = zod_1.z.string().refine((value) => value === "*" || availableSegmentKeys.includes(value), (value) => ({ message: `Unknown segment key "${value}"`, })); const andOrNotGroupSegment = zod_1.z.union([ zod_1.z .object({ and: zod_1.z.array(zod_1.z.lazy(() => groupSegmentZodSchema)), }) .strict(), zod_1.z .object({ or: zod_1.z.array(zod_1.z.lazy(() => groupSegmentZodSchema)), }) .strict(), zod_1.z .object({ not: zod_1.z.array(zod_1.z.lazy(() => groupSegmentZodSchema)), }) .strict(), ]); const groupSegmentZodSchema = zod_1.z.union([andOrNotGroupSegment, plainGroupSegment]); const groupSegmentsZodSchema = zod_1.z.union([zod_1.z.array(groupSegmentZodSchema), groupSegmentZodSchema]); const exposeSchema = zod_1.z .union([zod_1.z.boolean(), zod_1.z.array(zod_1.z.string().refine((value) => projectConfig.tags.includes(value)))]) .optional(); const rulesSchema = zod_1.z .array(zod_1.z .object({ key: zod_1.z.string(), description: zod_1.z.string().optional(), segments: groupSegmentsZodSchema, percentage: zod_1.z.number().min(0).max(100), enabled: zod_1.z.boolean().optional(), variation: variationValueZodSchema.optional(), variables: zod_1.z.record(variableValueZodSchema).optional(), variationWeights: zod_1.z.record(zod_1.z.number().min(0).max(100)).optional(), }) .strict()) // must have at least one rule .refine((value) => value.length > 0, () => ({ message: "Must have at least one rule", })) // duplicate rules .refine((value) => { const keys = value.map((v) => v.key); return keys.length === new Set(keys).size; }, (value) => ({ message: "Duplicate rule keys found: " + value.map((v) => v.key).join(", "), })) // enforce catch-all rule .refine((value) => { if (!projectConfig.enforceCatchAllRule) { return true; } const hasCatchAllAsLastRule = value[value.length - 1].segments === "*"; return hasCatchAllAsLastRule; }, () => ({ message: `Missing catch-all rule with \`segments: "*"\` at the end`, })); const forceSchema = zod_1.z .array(zod_1.z.union([ zod_1.z .object({ segments: groupSegmentsZodSchema, enabled: zod_1.z.boolean().optional(), variation: variationValueZodSchema.optional(), variables: zod_1.z.record(variableValueZodSchema).optional(), }) .strict(), zod_1.z .object({ conditions: conditionsZodSchema, enabled: zod_1.z.boolean().optional(), variation: variationValueZodSchema.optional(), variables: zod_1.z.record(variableValueZodSchema).optional(), }) .strict(), ])) .optional(); const attributeKeyZodSchema = zod_1.z.string().refine((value) => value === "*" || availableAttributeKeys.includes(value), (value) => ({ message: `Unknown attribute "${value}"`, })); const featureKeyZodSchema = zod_1.z.string().refine((value) => availableFeatureKeys.includes(value), (value) => ({ message: `Unknown feature "${value}"`, })); const environmentKeys = projectConfig.environments || []; const featureZodSchema = zod_1.z .object({ archived: zod_1.z.boolean().optional(), deprecated: zod_1.z.boolean().optional(), description: zod_1.z.string(), tags: zod_1.z .array(zod_1.z.string().refine((value) => tagRegex.test(value), (value) => ({ message: `Tag "${value}" must be lower cased and alphanumeric, and may contain hyphens.`, }))) .refine((value) => { return value.length === new Set(value).size; }, (value) => ({ message: "Duplicate tags found: " + value.join(", "), })), required: zod_1.z .array(zod_1.z.union([ featureKeyZodSchema, zod_1.z .object({ key: featureKeyZodSchema, variation: zod_1.z.string().optional(), }) .strict(), ])) .optional(), bucketBy: zod_1.z.union([ attributeKeyZodSchema, zod_1.z.array(attributeKeyZodSchema), zod_1.z .object({ or: zod_1.z.array(attributeKeyZodSchema), }) .strict(), ]), variablesSchema: zod_1.z .record(zod_1.z .object({ deprecated: zod_1.z.boolean().optional(), type: zod_1.z.enum(["string", "integer", "boolean", "double", "array", "object", "json"]), description: zod_1.z.string().optional(), defaultValue: variableValueZodSchema, useDefaultWhenDisabled: zod_1.z.boolean().optional(), disabledValue: variableValueZodSchema.optional(), }) .strict()) .optional(), disabledVariationValue: variationValueZodSchema.optional(), variations: zod_1.z .array(zod_1.z .object({ description: zod_1.z.string().optional(), value: variationValueZodSchema, weight: zod_1.z.number().min(0).max(100), variables: zod_1.z.record(variableValueZodSchema).optional(), variableOverrides: zod_1.z .record(zod_1.z.array(zod_1.z.union([ zod_1.z .object({ conditions: conditionsZodSchema, value: variableValueZodSchema, }) .strict(), zod_1.z .object({ segments: groupSegmentsZodSchema, value: variableValueZodSchema, }) .strict(), ]))) .optional(), }) .strict()) .refine((value) => { const variationValues = value.map((v) => v.value); return variationValues.length === new Set(variationValues).size; }, (value) => ({ message: "Duplicate variation values found: " + value.map((v) => v.value).join(", "), })) .optional(), expose: projectConfig.environments === false ? exposeSchema.optional() : zod_1.z.record(zod_1.z.enum(environmentKeys), exposeSchema).optional(), force: projectConfig.environments === false ? forceSchema : zod_1.z.record(zod_1.z.enum(environmentKeys), forceSchema).optional(), rules: projectConfig.environments === false ? rulesSchema : zod_1.z.record(zod_1.z.enum(environmentKeys), rulesSchema), }) .strict() .superRefine((value, ctx) => { // disabledVariationValue if (value.disabledVariationValue) { if (!value.variations) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: "Disabled variation value is set, but no variations are present in feature.", path: ["disabledVariationValue"], }); } else { const variationValues = value.variations.map((v) => v.value); if (variationValues.indexOf(value.disabledVariationValue) === -1) { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Disabled variation value "${value.disabledVariationValue}" is not one of the defined variations: ${variationValues.join(", ")}`, path: ["disabledVariationValue"], }); } } } if (!value.variablesSchema) { return; } const variableSchemaByKey = value.variablesSchema; const variationValues = []; if (value.variations) { value.variations.forEach((variation) => { variationValues.push(variation.value); }); } // variablesSchema[key] const variableKeys = Object.keys(variableSchemaByKey); variableKeys.forEach((variableKey) => { const variableSchema = variableSchemaByKey[variableKey]; if (variableKey === "variation") { ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Variable key "${variableKey}" is reserved and cannot be used.`, path: ["variablesSchema", variableKey], }); } // defaultValue superRefineVariableValue(projectConfig, variableSchema, variableSchema.defaultValue, ["variablesSchema", variableKey, "defaultValue"], ctx); // disabledValue superRefineVariableValue(projectConfig, variableSchema, variableSchema.defaultValue, ["variablesSchema", variableKey, "disabledValue"], ctx); }); // variations if (value.variations) { value.variations.forEach((variation, variationN) => { if (!variation.variables) { return; } // variations[n].variables[key] for (const variableKey of Object.keys(variation.variables)) { const variableValue = variation.variables[variableKey]; superRefineVariableValue(projectConfig, variableSchemaByKey[variableKey], variableValue, ["variations", variationN, "variables", variableKey], ctx); // variations[n].variableOverrides[n].value if (variation.variableOverrides) { for (const variableKey of Object.keys(variation.variableOverrides)) { const overrides = variation.variableOverrides[variableKey]; if (Array.isArray(overrides)) { overrides.forEach((override, overrideN) => { superRefineVariableValue(projectConfig, variableSchemaByKey[variableKey], override.value, [ "variations", variationN, "variableOverrides", variableKey, overrideN, "value", ], ctx); }); } } } } }); } if (environmentKeys.length > 0) { // with environments for (const environmentKey of environmentKeys) { // rules if (value.rules && value.rules[environmentKey]) { refineRules({ parsedFeature: value, variableSchemaByKey, variationValues, rules: value.rules[environmentKey], pathPrefix: ["rules", environmentKey], ctx, projectConfig, }); } // force if (value.force && value.force[environmentKey]) { refineForce({ parsedFeature: value, variableSchemaByKey, variationValues, force: value.force[environmentKey], pathPrefix: ["force", environmentKey], ctx, projectConfig, }); } } } else { // no environments // rules if (value.rules) { refineRules({ parsedFeature: value, variableSchemaByKey, variationValues, rules: value.rules, pathPrefix: ["rules"], ctx, projectConfig, }); } // force if (value.force) { refineForce({ parsedFeature: value, variableSchemaByKey, variationValues, force: value.force, pathPrefix: ["force"], ctx, projectConfig, }); } } }); return featureZodSchema; } //# sourceMappingURL=featureSchema.js.map