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
9 changes: 4 additions & 5 deletions src/client/snippets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
CreateSnippetRequest,
CreateSnippetParams,
CreateSnippetResponse,
CreateSnippetResponseSchema,
DeleteSnippetParams,
Expand All @@ -11,7 +11,6 @@ import {
GetSnippetsResponse,
GetSnippetsResponseSchema,
UpdateSnippetParams,
UpdateSnippetRequest,
UpdateSnippetResponse,
UpdateSnippetResponseSchema,
} from "../types/snippets.js";
Expand All @@ -33,7 +32,7 @@ export function Snippets<T extends Constructor<BaseIterableClient>>(Base: T) {
}

async createSnippet(
params: CreateSnippetRequest,
params: CreateSnippetParams,
opts?: { signal?: AbortSignal }
): Promise<CreateSnippetResponse> {
const response = await this.client.post("/api/snippets", params, opts);
Expand All @@ -53,11 +52,11 @@ export function Snippets<T extends Constructor<BaseIterableClient>>(Base: T) {

async updateSnippet(
params: UpdateSnippetParams,
body: UpdateSnippetRequest,
opts?: { signal?: AbortSignal }
): Promise<UpdateSnippetResponse> {
const { identifier, ...body } = params;
const response = await this.client.put(
`/api/snippets/${params.identifier}`,
`/api/snippets/${identifier}`,
body,
opts
);
Expand Down
12 changes: 6 additions & 6 deletions src/client/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
PreviewTemplateParams,
PushTemplate,
PushTemplateSchema,
SendTemplateProofParams,
SMSTemplate,
SMSTemplateSchema,
TemplateProofRequest,
UpdateEmailTemplateParams,
UpdateInAppTemplateParams,
UpdatePushTemplateParams,
Expand Down Expand Up @@ -91,7 +91,7 @@ export function Templates<T extends Constructor<BaseIterableClient>>(Base: T) {

async #sendTemplateProof(
pathSegment: string,
request: TemplateProofRequest
request: SendTemplateProofParams
): Promise<IterableSuccessResponse> {
const response = await this.client.post(
`/api/templates/${pathSegment}/proof`,
Expand Down Expand Up @@ -181,7 +181,7 @@ export function Templates<T extends Constructor<BaseIterableClient>>(Base: T) {
}

async sendEmailTemplateProof(
request: TemplateProofRequest
request: SendTemplateProofParams
): Promise<IterableSuccessResponse> {
return this.#sendTemplateProof("email", request);
}
Expand All @@ -208,7 +208,7 @@ export function Templates<T extends Constructor<BaseIterableClient>>(Base: T) {
}

async sendSMSTemplateProof(
request: TemplateProofRequest
request: SendTemplateProofParams
): Promise<IterableSuccessResponse> {
return this.#sendTemplateProof("sms", request);
}
Expand All @@ -231,7 +231,7 @@ export function Templates<T extends Constructor<BaseIterableClient>>(Base: T) {
}

async sendPushTemplateProof(
request: TemplateProofRequest
request: SendTemplateProofParams
): Promise<IterableSuccessResponse> {
return this.#sendTemplateProof("push", request);
}
Expand All @@ -254,7 +254,7 @@ export function Templates<T extends Constructor<BaseIterableClient>>(Base: T) {
}

async sendInAppTemplateProof(
request: TemplateProofRequest
request: SendTemplateProofParams
): Promise<IterableSuccessResponse> {
return this.#sendTemplateProof("inapp", request);
}
Expand Down
49 changes: 22 additions & 27 deletions src/types/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export const SnippetResponseSchema = z.object({

export type SnippetResponse = z.infer<typeof SnippetResponseSchema>;

// Request schemas
export const CreateSnippetRequestSchema = z.object({
// Parameter schemas for creating snippets
export const CreateSnippetParamsSchema = z.object({
content: z
.string()
.describe(
Expand All @@ -57,30 +57,7 @@ export const CreateSnippetRequestSchema = z.object({
),
});

export type CreateSnippetRequest = z.infer<typeof CreateSnippetRequestSchema>;

export const UpdateSnippetRequestSchema = z.object({
content: z
.string()
.describe(
'Content of the snippet. Handlebars must be valid. Disallowed content: script tags with JS sources or non-JSON content, inline JS event handlers (e.g., onload="..."), and javascript: in href or src attributes (anchors and iframes).'
),
description: z.string().optional().describe("Description of the snippet"),
createdByUserId: z
.string()
.optional()
.describe(
"User ID (email) of the updater. If not provided, defaults to the project creator."
),
variables: z
.array(z.string())
.optional()
.describe(
"List of variable names used in the content with a Handlebars expression such as {{myField}}. Variable names are case-sensitive and should be simple identifiers (letters, numbers, underscores). To learn more about using Handlebars in Snippets, see Customizing Snippets with Variables."
),
});

export type UpdateSnippetRequest = z.infer<typeof UpdateSnippetRequestSchema>;
export type CreateSnippetParams = z.infer<typeof CreateSnippetParamsSchema>;

// Response schemas
export const GetSnippetsResponseSchema = z.object({
Expand Down Expand Up @@ -113,7 +90,7 @@ export const DeleteSnippetResponseSchema = z.object({

export type DeleteSnippetResponse = z.infer<typeof DeleteSnippetResponseSchema>;

// Parameter schemas
// Identifier schema for get/update/delete operations
export const SnippetIdentifierSchema = z
.union([
z.string().describe("Snippet name"),
Expand All @@ -133,6 +110,24 @@ export type GetSnippetParams = z.infer<typeof GetSnippetParamsSchema>;

export const UpdateSnippetParamsSchema = z.object({
identifier: SnippetIdentifierSchema,
content: z
.string()
.describe(
'Content of the snippet. Handlebars must be valid. Disallowed content: script tags with JS sources or non-JSON content, inline JS event handlers (e.g., onload="..."), and javascript: in href or src attributes (anchors and iframes).'
),
description: z.string().optional().describe("Description of the snippet"),
createdByUserId: z
.string()
.optional()
.describe(
"User ID (email) of the updater. If not provided, defaults to the project creator."
),
variables: z
.array(z.string())
.optional()
.describe(
"List of variable names used in the content with a Handlebars expression such as {{myField}}. Variable names are case-sensitive and should be simple identifiers (letters, numbers, underscores). To learn more about using Handlebars in Snippets, see Customizing Snippets with Variables."
),
});

export type UpdateSnippetParams = z.infer<typeof UpdateSnippetParamsSchema>;
Expand Down
6 changes: 4 additions & 2 deletions src/types/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ export type BulkDeleteTemplatesResponse = z.infer<
>;

// Template proof schemas
export const TemplateProofRequestSchema = z
export const SendTemplateProofParamsSchema = z
.object({
templateId: z.number().describe("Template ID to send proof for"),
recipientEmail: z
Expand Down Expand Up @@ -490,7 +490,9 @@ export const TemplateProofRequestSchema = z
path: ["recipientEmail", "recipientUserId"],
});

export type TemplateProofRequest = z.infer<typeof TemplateProofRequestSchema>;
export type SendTemplateProofParams = z.infer<
typeof SendTemplateProofParamsSchema
>;

// Template preview schemas
export const TemplatePreviewRequestSchema = z.object({
Expand Down
26 changes: 11 additions & 15 deletions tests/integration/snippets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,12 @@ describe("Snippets Management Integration Tests", () => {
const updateResponse = await retryRateLimited(
() =>
withTimeout(
client.updateSnippet(
{ identifier: testSnippetName },
{
content: updatedContent,
description: "Updated CRUD test snippet",
variables: ["firstName", "company"],
}
)
client.updateSnippet({
identifier: testSnippetName,
content: updatedContent,
description: "Updated CRUD test snippet",
variables: ["firstName", "company"],
})
),
"Update snippet by name"
);
Expand All @@ -160,13 +158,11 @@ describe("Snippets Management Integration Tests", () => {
const updateByIdResponse = await retryRateLimited(
() =>
withTimeout(
client.updateSnippet(
{ identifier: snippetId },
{
content: updateByIdContent,
variables: ["user"],
}
)
client.updateSnippet({
identifier: snippetId,
content: updateByIdContent,
variables: ["user"],
})
),
"Update snippet by ID"
);
Expand Down
46 changes: 31 additions & 15 deletions tests/unit/snippets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {

import { IterableClient } from "../../src/client";
import {
CreateSnippetRequestSchema,
CreateSnippetParamsSchema,
SnippetResponseSchema,
UpdateSnippetRequestSchema,
UpdateSnippetParamsSchema,
} from "../../src/types/snippets.js";
import { createMockClient, createMockSnippet } from "../utils/test-helpers";

Expand Down Expand Up @@ -158,15 +158,19 @@ describe("Snippets Management", () => {
mockAxiosInstance.put.mockResolvedValue(mockResponse);

const params = {
identifier: "test-snippet",
content: "<p>Updated content {{name}}!</p>",
description: "Updated description",
};

await client.updateSnippet({ identifier: "test-snippet" }, params);
await client.updateSnippet(params);

expect(mockAxiosInstance.put).toHaveBeenCalledWith(
"/api/snippets/test-snippet",
params,
{
content: "<p>Updated content {{name}}!</p>",
description: "Updated description",
},
undefined
);
});
Expand All @@ -176,10 +180,11 @@ describe("Snippets Management", () => {
mockAxiosInstance.put.mockResolvedValue(mockResponse);

const params = {
identifier: 12345,
content: "<p>Updated content!</p>",
};

const result = await client.updateSnippet({ identifier: 12345 }, params);
const result = await client.updateSnippet(params);

expect(result).toHaveProperty("snippetId", 12345);
});
Expand Down Expand Up @@ -241,7 +246,7 @@ describe("Snippets Management", () => {
).toThrow();
});

it("should validate create snippet request schema", () => {
it("should validate create snippet params schema", () => {
const validRequest = {
name: "test-snippet",
content: "<p>Hello {{name}}!</p>",
Expand All @@ -251,53 +256,64 @@ describe("Snippets Management", () => {

// Valid request
expect(() =>
CreateSnippetRequestSchema.parse(validRequest)
CreateSnippetParamsSchema.parse(validRequest)
).not.toThrow();

// Missing required fields
expect(() =>
CreateSnippetRequestSchema.parse({ ...validRequest, name: undefined })
CreateSnippetParamsSchema.parse({ ...validRequest, name: undefined })
).toThrow();

expect(() =>
CreateSnippetRequestSchema.parse({
CreateSnippetParamsSchema.parse({
...validRequest,
content: undefined,
})
).toThrow();

// Optional fields should work
expect(() =>
CreateSnippetRequestSchema.parse({
CreateSnippetParamsSchema.parse({
name: "test",
content: "<p>Test</p>",
})
).not.toThrow();
});

it("should validate update snippet request schema", () => {
it("should validate update snippet params schema", () => {
const validRequest = {
identifier: "test-snippet",
content: "<p>Updated content {{name}}!</p>",
description: "Updated description",
variables: ["name"],
};

// Valid request
expect(() =>
UpdateSnippetRequestSchema.parse(validRequest)
UpdateSnippetParamsSchema.parse(validRequest)
).not.toThrow();

// Content is required for updates
// Identifier and content are required for updates
expect(() =>
UpdateSnippetParamsSchema.parse({
...validRequest,
identifier: undefined,
})
).toThrow();

expect(() =>
UpdateSnippetRequestSchema.parse({
UpdateSnippetParamsSchema.parse({
...validRequest,
content: undefined,
})
).toThrow();

// Other fields are optional
expect(() =>
UpdateSnippetRequestSchema.parse({ content: "<p>Test</p>" })
UpdateSnippetParamsSchema.parse({
identifier: 12345,
content: "<p>Test</p>",
})
).not.toThrow();
});
});
Expand Down
Loading