Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/client/campaigns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {
ArchiveCampaignsResponseSchema,
CampaignMetricsResponse,
CancelCampaignParams,
CreateCampaignParams,
CreateAndScheduleCampaignParams,
CreateCampaignResponse,
CreateCampaignResponseSchema,
CreateTriggeredCampaignParams,
DeactivateTriggeredCampaignParams,
GetCampaignMetricsParams,
GetCampaignParams,
Expand Down Expand Up @@ -75,8 +76,15 @@ export function Campaigns<T extends Constructor<BaseIterableClient>>(Base: T) {
return this.validateResponse(response, GetCampaignResponseSchema);
}

async createCampaign(
params: CreateCampaignParams
async createAndScheduleCampaign(
params: CreateAndScheduleCampaignParams
): Promise<CreateCampaignResponse> {
const response = await this.client.post("/api/campaigns/create", params);
return this.validateResponse(response, CreateCampaignResponseSchema);
}

async createTriggeredCampaign(
params: CreateTriggeredCampaignParams
): Promise<CreateCampaignResponse> {
const response = await this.client.post("/api/campaigns/create", params);
return this.validateResponse(response, CreateCampaignResponseSchema);
Expand Down
111 changes: 61 additions & 50 deletions src/types/campaigns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ export type GetCampaignMetricsParams = z.infer<
export type CampaignMetricsResponse = z.infer<
typeof CampaignMetricsResponseSchema
>;
export type CreateCampaignParams = z.infer<typeof CreateCampaignParamsSchema>;
export type CreateCampaignResponse = z.infer<
typeof CreateCampaignResponseSchema
>;
Expand Down Expand Up @@ -145,56 +144,68 @@ export const CampaignMetricsResponseSchema = z
.array(z.record(z.string(), z.string()))
.describe("Parsed campaign metrics data");

// Campaign creation schemas
export const CreateCampaignParamsSchema = z
.object({
name: z
.string()
.describe("The name to use in Iterable for the new campaign"),
templateId: z
.number()
.describe("The ID of a template to associate with the new campaign"),
listIds: z
.array(z.number())
.describe(
"Array of list IDs to which the campaign should be sent (for blast campaigns)"
)
.optional(),
campaignDataFields: z
.record(z.string(), z.any())
.optional()
.describe(
"A JSON object containing campaign-level data fields that are available as merge parameters (for example, {{field}}) during message rendering. These fields are available in templates, data feed URLs, and all other contexts where merge parameters are supported. Campaign-level fields are overridden by user and event data fields of the same name."
),
sendAt: IterableDateTimeSchema.optional().describe(
"Scheduled send time for blast campaign (YYYY-MM-DD HH:MM:SS UTC). Required when listIds is provided."
// Shared campaign data fields used by both blast and triggered campaign creation
const campaignDataFieldsSchema = z
.record(z.string(), z.any())
.optional()
.describe(
"A JSON object containing campaign-level data fields that are available as merge parameters (for example, {{field}}) during message rendering. These fields are available in templates, data feed URLs, and all other contexts where merge parameters are supported. Campaign-level fields are overridden by user and event data fields of the same name."
);

// Create and schedule a blast campaign
export const CreateAndScheduleCampaignParamsSchema = z.object({
name: z
.string()
.describe("The name to use in Iterable for the new campaign"),
templateId: z
.number()
.describe("The ID of a template to associate with the new campaign"),
listIds: z
.array(z.number())
.min(1)
.describe("Array of list IDs to which the campaign should be sent"),
sendAt: IterableDateTimeSchema.describe(
"Scheduled send time (YYYY-MM-DD HH:MM:SS UTC)"
),
campaignDataFields: campaignDataFieldsSchema,
sendMode: z
.enum(["ProjectTimeZone", "RecipientTimeZone"])
.optional()
.describe("Send mode for blast campaigns"),
startTimeZone: z
.string()
.optional()
.describe("Starting timezone for recipient timezone sends (IANA format)"),
defaultTimeZone: z
.string()
.optional()
.describe(
"Default timezone for recipients without known timezone (IANA format)"
),
sendMode: z
.enum(["ProjectTimeZone", "RecipientTimeZone"])
.optional()
.describe("Send mode for blast campaigns"),
startTimeZone: z
.string()
.optional()
.describe(
"Starting timezone for recipient timezone sends (IANA format)"
),
defaultTimeZone: z
.string()
.optional()
.describe(
"Default timezone for recipients without known timezone (IANA format)"
),
suppressionListIds: z
.array(z.number())
.optional()
.describe("Array of suppression list IDs"),
})
.refine((data) => !data.listIds?.length || data.sendAt, {
message:
"sendAt is required for blast campaigns (when listIds is provided).",
path: ["sendAt"],
});
suppressionListIds: z
.array(z.number())
.optional()
.describe("Array of suppression list IDs"),
});

export type CreateAndScheduleCampaignParams = z.infer<
typeof CreateAndScheduleCampaignParamsSchema
>;

// Create a triggered campaign
export const CreateTriggeredCampaignParamsSchema = z.object({
name: z
.string()
.describe("The name to use in Iterable for the new campaign"),
templateId: z
.number()
.describe("The ID of a template to associate with the new campaign"),
campaignDataFields: campaignDataFieldsSchema,
});

export type CreateTriggeredCampaignParams = z.infer<
typeof CreateTriggeredCampaignParamsSchema
>;

export const CreateCampaignResponseSchema = z.object({
campaignId: z.number(),
Expand Down
38 changes: 24 additions & 14 deletions tests/integration/campaigns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,26 @@ describe("Campaign Management Integration Tests", () => {
return parseInt(templateIdMatch[1]);
};

const createTestCampaign = async (params: {
const createTestBlastCampaign = async (params: {
name: string;
templateId: number;
listIds?: number[];
sendAt?: string;
listIds: number[];
sendAt: string;
}) => {
const createResponse = await retryRateLimited(
() => withTimeout(client.createCampaign(params)),
`Create campaign: ${params.name}`
() => withTimeout(client.createAndScheduleCampaign(params)),
`Create blast campaign: ${params.name}`
);
return createResponse.campaignId;
};

const createTestTriggeredCampaign = async (params: {
name: string;
templateId: number;
}) => {
const createResponse = await retryRateLimited(
() => withTimeout(client.createTriggeredCampaign(params)),
`Create triggered campaign: ${params.name}`
);
return createResponse.campaignId;
};
Expand Down Expand Up @@ -464,7 +475,7 @@ describe("Campaign Management Integration Tests", () => {

it("should create and archive a test campaign", async () => {
const campaignName = uniqueId("MCP-Test-Campaign");
const campaignId = await createTestCampaign({
const campaignId = await createTestTriggeredCampaign({
name: campaignName,
templateId: testTemplateId,
});
Expand Down Expand Up @@ -507,7 +518,7 @@ describe("Campaign Management Integration Tests", () => {
const sendAtDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
const sendAt = sendAtDate.toISOString().replace('T', ' ').substring(0, 19);

const campaignId = await createTestCampaign({
const campaignId = await createTestBlastCampaign({
name: campaignName,
templateId: testTemplateId,
listIds: [testListId],
Expand Down Expand Up @@ -546,12 +557,12 @@ describe("Campaign Management Integration Tests", () => {
}, 60000);

it("should archive multiple campaigns at once", async () => {
const campaignId1 = await createTestCampaign({
const campaignId1 = await createTestTriggeredCampaign({
name: uniqueId("MCP-Test-Bulk-1"),
templateId: testTemplateId,
});

const campaignId2 = await createTestCampaign({
const campaignId2 = await createTestTriggeredCampaign({
name: uniqueId("MCP-Test-Bulk-2"),
templateId: testTemplateId,
});
Expand Down Expand Up @@ -588,7 +599,7 @@ describe("Campaign Management Integration Tests", () => {
}, 60000);

it("should abort a campaign", async () => {
const campaignId = await createTestCampaign({
const campaignId = await createTestTriggeredCampaign({
name: uniqueId("MCP-Test-Abort"),
templateId: testTemplateId,
});
Expand All @@ -611,8 +622,7 @@ describe("Campaign Management Integration Tests", () => {
}, 60000);

it("should activate and deactivate a triggered campaign", async () => {
// No listIds = Triggered campaign
const campaignId = await createTestCampaign({
const campaignId = await createTestTriggeredCampaign({
name: uniqueId("MCP-Test-Triggered"),
templateId: testTemplateId,
});
Expand Down Expand Up @@ -659,7 +669,7 @@ describe("Campaign Management Integration Tests", () => {
}, 60000);

it("should trigger a campaign", async () => {
const campaignId = await createTestCampaign({
const campaignId = await createTestTriggeredCampaign({
name: uniqueId("MCP-Test-Trigger"),
templateId: testTemplateId,
});
Expand Down Expand Up @@ -696,7 +706,7 @@ describe("Campaign Management Integration Tests", () => {
const sendAtDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
const sendAt = sendAtDate.toISOString().replace('T', ' ').substring(0, 19);

const campaignId = await createTestCampaign({
const campaignId = await createTestBlastCampaign({
name: uniqueId("MCP-Test-Send"),
templateId: testTemplateId,
listIds: [testListId],
Expand Down
Loading