diff --git a/src/models/flow.ts b/src/models/flow.ts index 7801b39..06989d9 100644 --- a/src/models/flow.ts +++ b/src/models/flow.ts @@ -1,45 +1,54 @@ import { IHrefProp } from '.'; export interface IFlowDefinition { - name: string; - clientKey: string; - href: string; - ref: string; - modifiedByRef: string; - modifiedAt: string; - revision: number; - archived: boolean; - friendlyId: string; - type: FlowType; - subtype: FlowSubType; - channels: FlowChannel; - triggers: string; // TODO: Come back to this - tags: string[]; - businessProcess: string; // possibly an enum - traffic: ITrafficDefinition; - variants: any; // TODO: Come back to this - transpiledVariants: string; - status: FlowStatus; - schedule: IScheduleDefinition; - revisions: IHrefProp; - sampleSizeConfig: ISampleSizeDefinition; + name?: string; + clientKey?: string; + href?: string; + ref?: string; + modifiedByRef?: string; + modifiedAt?: string; + revision?: number; + archived?: boolean; + friendlyId?: string; + type?: FlowType; + subtype?: FlowSubType; + channels?: FlowChannel[]; + triggers?: string; // TODO: Come back to this + tags?: string[]; + businessProcess?: string; // possibly an enum + traffic?: ITrafficDefinition; + variants?: any; // TODO: Come back to this + transpiledVariants?: string; + status?: FlowStatus; + schedule?: IScheduleDefinition; + revisions?: IHrefProp; + sampleSizeConfig?: ISampleSizeDefinition; } export interface IScheduleDefinition { - type: string; - startDate: string; - endDate: string; + type?: string; + startDate?: string; + endDate?: string; +} + +export enum FlowBusinessProcessType { + Interactive = 'interactive_v1', + Triggered = 'triggered_v1', +} + +export enum FlowScheduleType { + Simple = 'simpleSchedule', } export interface ITrafficDefinition { - type: TrafficDefinitionType; - weightingAlgorithm: WeightingAlgorithm; - allocation: number; - allocationHigh: number; - allocationLow: number; - splits: ISplitDefinition[]; - coupled: boolean; - modifiedAt: string; + type?: TrafficDefinitionType; + weightingAlgorithm?: WeightingAlgorithm; + allocation?: number; + allocationHigh?: number; + allocationLow?: number; + splits?: ISplitDefinition[]; + coupled?: boolean; + modifiedAt?: string; } export enum TrafficDefinitionType { @@ -48,7 +57,7 @@ export enum TrafficDefinitionType { } export interface ISplitDefinition { - ref: string; + ref?: string; split?: number; lowSplit?: number; highSplit?: number; @@ -91,7 +100,7 @@ export enum FlowChannel { } export interface ISampleSizeDefinition { - baseValue: number; - minimumDetectableDifference: number; - confidenceLevel: number; + baseValue?: number; + minimumDetectableDifference?: number; + confidenceLevel?: number; } diff --git a/src/schema/flowSchema.spec.ts b/src/schema/flowSchema.spec.ts new file mode 100644 index 0000000..85a7e22 --- /dev/null +++ b/src/schema/flowSchema.spec.ts @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import { FlowChannel, FlowScheduleType, FlowStatus, FlowType, IFlowDefinition } from '../models'; +import { CreateExperienceSchema } from './flowSchema'; + +describe('Flow Experience Input Schema', () => { + let experienceDefinition: IFlowDefinition; + + beforeEach(() => { + // Initialize the base experience definition before each test + experienceDefinition = { + name: 'Test API Experience', + friendlyId: 'test_api_experience', + type: FlowType.WebFlow, + channels: [FlowChannel.Web], + status: FlowStatus.Draft, + schedule: { + type: FlowScheduleType.Simple, + startDate: new Date().toISOString(), + }, + }; + }); + + it('should return false for bad friendly id', () => { + experienceDefinition.friendlyId = 'test-api-experience'; // Invalid friendlyId + + const validationResult = CreateExperienceSchema.safeParse(experienceDefinition); + + expect(validationResult.success).to.be.equal(false); + }); + + it('should return true for valid friendly id', () => { + experienceDefinition.friendlyId = 'test_api_experience'; // Valid friendlyId + + const validationResult = CreateExperienceSchema.safeParse(experienceDefinition); + + expect(validationResult.success).to.be.equal(true); + }); + + it('should return false for missing name', () => { + delete experienceDefinition.name; // Missing name + + const validationResult = CreateExperienceSchema.safeParse(experienceDefinition); + + expect(validationResult.success).to.be.equal(false); + }); +}); diff --git a/src/schema/flowSchema.ts b/src/schema/flowSchema.ts index 440e13d..ec01a53 100644 --- a/src/schema/flowSchema.ts +++ b/src/schema/flowSchema.ts @@ -1,79 +1,50 @@ import { z } from 'zod'; -import { FlowType } from '../models'; +import { + FlowChannel, + FlowScheduleType, + FlowStatus, + FlowType, + TrafficDefinitionType, +} from '../models'; const FlowTypeValues = Object.values(FlowType) as [string, ...string[]]; +const FlowTrafficTypeValues = Object.values(TrafficDefinitionType) as [string, ...string[]]; +const FlowStatusValues = Object.values(FlowStatus) as [string, ...string[]]; +const FlowScheduleTypeValues = Object.values(FlowScheduleType) as [string, ...string[]]; +const FlowChannelValues = Object.values(FlowChannel) as [string, ...string[]]; const CreateFlowSchema = z.object({ name: z.string().min(1, { message: 'name is a required field' }), - friendlyId: z.string().min(1, { message: 'friendlyId is a required field' }), - type: z.enum(FlowTypeValues), - channels: z.string().min(1, { message: 'channels is a required field' }), - //businessProcess: z.string(), (this should get calculated automatically by type) - traffic: z.object({ - type: z.string(), - weightingAlgorithm: z.string(), - allocation: z.number(), - allocationHigh: z.number(), - allocationLow: z.number(), - splits: z.array( - z.object({ - ref: z.string(), - split: z.number(), - lowSplit: z.number(), - highSplit: z.number(), - }) - ), - coupled: z.boolean(), - modifiedAt: z.string(), - }), - transpiledVariants: z.string().optional(), - status: z.string().optional(), + friendlyId: z + .string() + .min(1, { message: 'friendlyId is a required field' }) + .regex(/^[a-z0-9_]*$/, { message: 'friendlyId must match "^[a-z0-9_]*$"' }), + type: z.enum(FlowTypeValues, { errorMap: () => ({ message: 'type is a required field' }) }), + channels: z + .array(z.enum(FlowChannelValues)) + .min(1, { message: 'channels should contain atleast one channel' }), + status: z.enum(FlowStatusValues, { errorMap: () => ({ message: 'status is a required field' }) }), schedule: z.object({ - type: z.string(), - startDate: z.string(), - endDate: z.string(), + type: z.enum(FlowScheduleTypeValues, { + errorMap: () => ({ message: 'schedule.type is a required field' }), + }), + startDate: z + .string() + .min(1, { message: 'schedule.startDate is a required field - Should use ISO 8601 Format' }), }), }); export const CreateExperienceSchema = CreateFlowSchema.extend({ - //subtype: z.string().optional(), // Shouldn't be provided - Gets automatically set to 'experience' - //businessProcess: z.string(), // This will be calculated automatically based on other input - traffic: z.object({ - type: z.string(), - weightingAlgorithm: z.string(), - allocation: z.number(), - allocationHigh: z.number(), - allocationLow: z.number(), - splits: z.array( - z.object({ - ref: z.string(), - split: z.number(), - lowSplit: z.number(), - highSplit: z.number(), - }) - ), - coupled: z.boolean(), - modifiedAt: z.string(), - }), - schedule: z.object({ - type: z.string(), - startDate: z.string(), - endDate: z.string(), - }), - sampleSizeConfig: z.object({ - baseValue: z.number(), - minimumDetectableDifference: z.number(), - confidenceLevel: z.number(), - }), + // No differences for Experiences for Now }); export const CreateExperimentSchema = CreateFlowSchema.extend({ - //subtype: z.string(), // Gets automatically set to 'experiment' for this schema - //businessProcess: z.string(), This will be calculated automatically based on other input traffic: z.object({ - type: z.string(), + type: z.enum(FlowTrafficTypeValues, { + errorMap: () => ({ message: 'traffic.type is a required field' }), + }), weightingAlgorithm: z.string(), - allocation: z.number(), + // not required for experiments allocation: z.number(), allocationHigh: z.number(), allocationLow: z.number(), splits: z.array( @@ -88,11 +59,6 @@ export const CreateExperimentSchema = CreateFlowSchema.extend({ modifiedAt: z.string(), }), variants: z.any(), - schedule: z.object({ - type: z.string(), - startDate: z.string(), - endDate: z.string(), - }), sampleSizeConfig: z.object({ baseValue: z.number(), minimumDetectableDifference: z.number(), diff --git a/src/services/flows.ts b/src/services/flows.ts index 98a3daf..c3bc8be 100644 --- a/src/services/flows.ts +++ b/src/services/flows.ts @@ -1,7 +1,14 @@ import { z } from 'zod'; import { Client } from '../client'; -import { IFlowDefinition } from '../models'; -import { CreateExperimentSchema } from '../schema'; +import { + FlowBusinessProcessType, + FlowSubType, + FlowType, + IFlowDefinition, + TrafficDefinitionType, + WeightingAlgorithm, +} from '../models'; +import { CreateExperienceSchema, CreateExperimentSchema } from '../schema'; import { BaseService } from './base'; /** @@ -82,9 +89,32 @@ export class FlowService extends BaseService { */ public CreateExperience = async (flow: IFlowDefinition): Promise => { try { - //const validatedData = CreateExperienceSchema.parse(flow); - - const response = await this.Post(`v3/flowDefinitions`, flow); + // Validate the Input Data using Zod + const validatedData = CreateExperienceSchema.parse(flow); + + // Add default values for Experience before validation + const mergedData = { + ...validatedData, + subtype: FlowSubType.Experience, + businessProcess: + validatedData.type === FlowType.Triggered + ? FlowBusinessProcessType.Triggered + : FlowBusinessProcessType.Interactive, + traffic: { + type: TrafficDefinitionType.Simple, + allocation: 100, + weightingAlgorithm: WeightingAlgorithm.UserDefined, + splits: [], + }, + variants: [], + sampleSizeConfig: { + baseValue: 0.02, // Default Value + minimumDetectableDifference: 0.2, // Default Value + confidenceLevel: 0.95, // Default Value + }, + }; + + const response = await this.Post(`v3/flowDefinitions`, mergedData); if (response.ok) { let flow: IFlowDefinition = (await response.json()) as IFlowDefinition; @@ -92,7 +122,11 @@ export class FlowService extends BaseService { return flow; } } catch (ex) { - throw new Error(ex as string); + if (ex instanceof z.ZodError) { + console.error('Flow Definition failed validation: ', ex.errors); + } else { + throw new Error(ex as string); + } } };