import {z} from 'zod' export type BucketingKeyCustomDataDto = {bucketingKeyType?: ('VISITOR_CODE' | 'CUSTOM_DATA') | undefined, customDataId?: number | undefined}; export type BucketingKeyVisitorCodeDto = {bucketingKeyType?: ('VISITOR_CODE' | 'CUSTOM_DATA') | undefined}; export type FeatureFlagIntegrations = {deliveryRules?: Array | undefined, experimentRules?: Array | undefined}; export type ReleasePeriodRequest = {releaseFrom?: string | undefined, releaseTo?: string | undefined, timeZone: string} | null; export type RollbackConditionRequest = {comparisonOperator: 'GREATER_THAN' | 'LESS_THAN' | 'GREATER_THAN' | 'LESS_THAN', criteria: 'UPLIFT' | 'DOWNLIFT' | 'CONVERSION_RATE' | 'UPLIFT' | 'DOWNLIFT' | 'CONVERSION_RATE', goalId: number, id?: (number | null) | undefined, matchValue: number, recipients?: (Array | null) | undefined, target: 'RULE' | 'ENVIRONMENT' | 'RULE' | 'ENVIRONMENT', visitors: number} | null; export type FeatureTrafficAllocation = {exposition?: number | undefined, variationId?: number | undefined, variationKey?: string | undefined}; export type ExperimentationRuleRequest = {id?: (number | null) | undefined, name?: (string | null) | undefined, orderIndex?: (number | null) | undefined, reallocation?: (boolean | null) | undefined, release?: ReleasePeriodRequest | undefined, rollbackConditions?: (Array | null) | undefined, segmentId?: (number | null) | undefined, state?: ('ACTIVE' | 'PAUSED' | 'ACTIVE' | 'PAUSED' | null) | undefined, type: 'TARGETED_DELIVERY' | 'PROGRESSIVE_DELIVERY' | 'EXPERIMENTATION' | 'PROGRESSIVE_DELIVERY' | 'TARGETED_DELIVERY' | 'EXPERIMENTATION', controlVariationKey?: (string | null) | undefined, exposition: number | null, multiArmedBandit?: boolean | undefined, trafficAllocationMethod?: ('CONTEXTUAL_BANDIT' | 'MANUAL' | 'MULTI_ARMED_BANDIT' | 'CONTEXTUAL_BANDIT, MANUAL, MULTI_ARMED_BANDIT') | undefined, trafficAllocations?: Array | undefined}; export type RolloutStep = {dateTime?: string | undefined, exposure?: number | undefined}; export type CustomRampUpRollout = {type: 'PROGRESSIVE_EXPOSURE' | 'CUSTOM_RAMP_UP', steps: Array}; export type RolloutFrequency = {unit: 'Nanos' | 'Micros' | 'Millis' | 'Seconds' | 'Minutes' | 'Hours' | 'HalfDays' | 'Days' | 'Weeks' | 'Months' | 'Years' | 'Decades' | 'Centuries' | 'Millennia' | 'Eras' | 'Forever', value: number}; export type RolloutPercentageRange = {start: number, stop: number}; export type ProgressiveExposure = {type: 'PROGRESSIVE_EXPOSURE' | 'CUSTOM_RAMP_UP', frequency?: RolloutFrequency | undefined, increaseBy: number, percentageRange: RolloutPercentageRange, releaseDateTime?: string | undefined}; export type ProgressiveRuleRequest = {id?: (number | null) | undefined, name?: (string | null) | undefined, orderIndex?: (number | null) | undefined, reallocation?: (boolean | null) | undefined, release?: ReleasePeriodRequest | undefined, rollbackConditions?: (Array | null) | undefined, segmentId?: (number | null) | undefined, state?: ('ACTIVE' | 'PAUSED' | 'ACTIVE' | 'PAUSED' | null) | undefined, type: 'TARGETED_DELIVERY' | 'PROGRESSIVE_DELIVERY' | 'EXPERIMENTATION' | 'PROGRESSIVE_DELIVERY' | 'TARGETED_DELIVERY' | 'EXPERIMENTATION', rolloutConfiguration: CustomRampUpRollout | ProgressiveExposure, variationKey: string}; export type TargetedRuleRequest = {id?: (number | null) | undefined, name?: (string | null) | undefined, orderIndex?: (number | null) | undefined, reallocation?: (boolean | null) | undefined, release?: ReleasePeriodRequest | undefined, rollbackConditions?: (Array | null) | undefined, segmentId?: (number | null) | undefined, state?: ('ACTIVE' | 'PAUSED' | 'ACTIVE' | 'PAUSED' | null) | undefined, type: 'TARGETED_DELIVERY' | 'PROGRESSIVE_DELIVERY' | 'EXPERIMENTATION' | 'PROGRESSIVE_DELIVERY' | 'TARGETED_DELIVERY' | 'EXPERIMENTATION', exposition: number | null, variationKey: string}; export type EnvironmentRequest = {defaultVariationKey: string, environmentKey: string, featureEnabled: boolean, integrations?: FeatureFlagIntegrations | undefined, rolloutRules?: (Array) | undefined}; export type VariableRequest = {allowedValues?: (Array | null) | undefined, key: string, type: 'BOOLEAN' | 'NUMBER' | 'STRING' | 'JSON' | 'JS' | 'CSS' | 'ENUM', value: string}; export type VariationVariableRequest = {key: string, value: string}; export type VariationRequest = {key: string, name: string, variables?: Array | undefined}; export type CreateFeatureFlagRequest = {attributionWindow?: number | undefined, bucketingKey?: (BucketingKeyCustomDataDto | BucketingKeyVisitorCodeDto) | undefined, description?: string | undefined, environmentConfigurations?: Array | undefined, featureKey: string, name: string, primaryGoalId?: number | undefined, secondaryGoalIds?: Array | undefined, siteCode: string, tags?: Array | undefined, variables?: Array | undefined, variations?: Array | undefined}; export type CreateApiFeatureFlagsArgs = {body: CreateFeatureFlagRequest}; export type CreateApiFeatureFlagsOptions = {baseUrl?: string | undefined, fetch?: typeof fetch | undefined}; export const bucketingKeyCustomDataDto = z.object({bucketingKeyType: z.enum(["VISITOR_CODE", "CUSTOM_DATA"]).optional(), customDataId: z.number().int().optional()}); export const bucketingKeyVisitorCodeDto = z.object({bucketingKeyType: z.enum(["VISITOR_CODE", "CUSTOM_DATA"]).optional()}); export const featureFlagIntegrations: z.ZodType = z.object({deliveryRules: z.array(z.lazy(() => featureFlagIntegrations)).optional(), experimentRules: z.array(z.lazy(() => featureFlagIntegrations)).optional()}); export const featureFlagRelease = z.object({releaseFrom: z.string().optional(), releaseTo: z.string().optional(), timeZone: z.string().optional()}); export const featureRollbackCondition = z.object({comparisonOperator: z.enum(["GREATER_THAN", "LESS_THAN"]).optional(), criteria: z.enum(["UPLIFT", "DOWNLIFT", "CONVERSION_RATE"]).optional(), goalId: z.number().int().optional(), id: z.number().int().optional(), matchValue: z.number().gte(0.0001).lte(100).optional(), recipients: z.array(z.string()).optional(), target: z.enum(["RULE", "ENVIRONMENT"]).optional(), visitors: z.number().int().gte(1).optional()}); export const featureTrafficAllocation = z.object({exposition: z.number().optional(), variationId: z.number().int().optional(), variationKey: z.string().optional()}); export const experimentationRuleResponse = z.object({experimentId: z.number().int().optional(), id: z.number().int().optional(), name: z.string().optional(), release: featureFlagRelease.optional(), rollbackConditions: z.array(featureRollbackCondition).optional(), segmentId: z.number().int().optional(), status: z.enum(["ACTIVE", "PAUSED", "PLANNED"]).optional(), trafficAllocations: z.array(featureTrafficAllocation).optional(), type: z.enum(["TARGETED_DELIVERY", "PROGRESSIVE_DELIVERY", "EXPERIMENTATION"]), controlVariationKey: z.string().optional(), exposition: z.number().int().gte(0).lte(100).optional(), multiArmedBandit: z.boolean().optional(), trafficAllocationMethod: z.enum(["CONTEXTUAL_BANDIT", "MANUAL", "MULTI_ARMED_BANDIT"]).optional()}); export const featureRolloutTargetedRule = z.object({experimentId: z.number().int().optional(), id: z.number().int().optional(), name: z.string().optional(), release: featureFlagRelease.optional(), rollbackConditions: z.array(featureRollbackCondition).optional(), segmentId: z.number().int().optional(), status: z.enum(["ACTIVE", "PAUSED", "PLANNED"]).optional(), trafficAllocations: z.array(featureTrafficAllocation).optional(), type: z.enum(["TARGETED_DELIVERY", "PROGRESSIVE_DELIVERY", "EXPERIMENTATION"]), exposition: z.number().int().gte(0).lte(100).optional(), variationKey: z.string().optional()}); export const rolloutStep = z.object({dateTime: z.string().optional(), exposure: z.number().optional()}); export const customRampUpRollout = z.object({type: z.enum(["PROGRESSIVE_EXPOSURE", "CUSTOM_RAMP_UP"]), steps: z.array(rolloutStep)}); export const rolloutFrequency = z.object({unit: z.enum(["Nanos", "Micros", "Millis", "Seconds", "Minutes", "Hours", "HalfDays", "Days", "Weeks", "Months", "Years", "Decades", "Centuries", "Millennia", "Eras", "Forever"]), value: z.number().int().gte(1)}); export const rolloutPercentageRange = z.object({start: z.number().int().gte(0).lte(100), stop: z.number().int().gte(0).lte(100)}); export const progressiveExposure = z.object({type: z.enum(["PROGRESSIVE_EXPOSURE", "CUSTOM_RAMP_UP"]), frequency: rolloutFrequency.optional(), increaseBy: z.number().int().gte(1).lte(100), percentageRange: rolloutPercentageRange, releaseDateTime: z.string().optional()}); export const progressiveRuleResponse = z.object({experimentId: z.number().int().optional(), id: z.number().int().optional(), name: z.string().optional(), release: featureFlagRelease.optional(), rollbackConditions: z.array(featureRollbackCondition).optional(), segmentId: z.number().int().optional(), status: z.enum(["ACTIVE", "PAUSED", "PLANNED"]).optional(), trafficAllocations: z.array(featureTrafficAllocation).optional(), type: z.enum(["TARGETED_DELIVERY", "PROGRESSIVE_DELIVERY", "EXPERIMENTATION"]), currentExposition: z.number().int().optional(), rolloutConfiguration: z.union([customRampUpRollout, progressiveExposure]).optional(), variationKey: z.string().optional()}); export const environmentResponse = z.object({dateModified: z.string().optional(), defaultVariationKey: z.string().optional(), environmentKey: z.string().optional(), featureEnabled: z.boolean().optional(), integrations: featureFlagIntegrations.optional(), rolloutRules: z.array(z.union([experimentationRuleResponse, featureRolloutTargetedRule, progressiveRuleResponse])).optional()}); export const variableResponse = z.object({allowedValues: z.array(z.string().nullable()).nullable().optional(), key: z.string(), type: z.enum(["BOOLEAN", "NUMBER", "STRING", "JSON", "JS", "CSS", "ENUM"]), value: z.string()}); export const variationVariableResponse = z.object({key: z.string(), type: z.enum(["BOOLEAN", "NUMBER", "STRING", "JSON", "JS", "CSS", "ENUM"]), value: z.string()}); export const variationResponse = z.object({key: z.string(), name: z.string(), variables: z.array(variationVariableResponse).optional()}); export const featureFlagConfiguration = z.object({archived: z.boolean().optional(), attributionWindow: z.number().int().optional(), bucketingKey: z.union([bucketingKeyCustomDataDto, bucketingKeyVisitorCodeDto]).optional(), createdById: z.number().int().optional(), dateContentModified: z.string().optional(), dateCreated: z.string().optional(), dateModified: z.string().optional(), description: z.string().max(5000).optional(), environmentConfigurations: z.array(environmentResponse).optional(), featureKey: z.string().min(1).max(255), health: z.enum(["PERMANENT", "HEALTHY", "STALE", "SMELLY"]).optional(), id: z.number().int(), isArchived: z.boolean().optional(), name: z.string().min(1).max(100), primaryGoalId: z.number().int().optional(), secondaryGoalIds: z.array(z.number().int()).optional(), siteCode: z.string().min(1).max(10), tags: z.array(z.string()).optional(), teamId: z.number().int().optional(), variables: z.array(variableResponse).optional(), variations: z.array(variationResponse).optional()}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * Create a new feature flag configuration * * POST /feature-flags */ export const createApiFeatureFlags = async (args: CreateApiFeatureFlagsArgs, options?: CreateApiFeatureFlagsOptions) => { const {baseUrl = "https://api.kameleoon.com", fetch: fetchFn = fetch} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('content-type', "application/json") headers.set('accept', "application/json") const url = `${baseUrl.replace(/\/+$/, '')}/feature-flags${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "POST", headers, body: JSON.stringify(args.body)}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(featureFlagConfiguration, await res.json()) };