Skip to content

Commit

Permalink
Got Experiences Working and added some validation
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanyoung-dev committed Sep 3, 2024
1 parent acc84a4 commit 4bc7ca6
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 108 deletions.
83 changes: 46 additions & 37 deletions src/models/flow.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -48,7 +57,7 @@ export enum TrafficDefinitionType {
}

export interface ISplitDefinition {
ref: string;
ref?: string;
split?: number;
lowSplit?: number;
highSplit?: number;
Expand Down Expand Up @@ -91,7 +100,7 @@ export enum FlowChannel {
}

export interface ISampleSizeDefinition {
baseValue: number;
minimumDetectableDifference: number;
confidenceLevel: number;
baseValue?: number;
minimumDetectableDifference?: number;
confidenceLevel?: number;
}
46 changes: 46 additions & 0 deletions src/schema/flowSchema.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
96 changes: 31 additions & 65 deletions src/schema/flowSchema.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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(),
Expand Down
46 changes: 40 additions & 6 deletions src/services/flows.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -82,17 +89,44 @@ export class FlowService extends BaseService {
*/
public CreateExperience = async (flow: IFlowDefinition): Promise<IFlowDefinition | undefined> => {
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;

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);
}
}
};

Expand Down

0 comments on commit 4bc7ca6

Please sign in to comment.