import * as z from "zod" import { defineAction } from "../../automation/actions" import { hasEmailBody, normalizeEmailBody, OPTIONAL_EMAIL_BODY_SCHEMA, } from "../../lib/email" import { getResendApi, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS } from "./lib" const RESEND_ACCOUNT = "resend" const RESERVED_TEMPLATE_VARIABLE_KEYS = new Set([ "EMAIL", "FIRST_NAME", "LAST_NAME", "RESEND_UNSUBSCRIBE_URL", "contact", "this", ]) const TEMPLATE_VARIABLE_KEY_SCHEMA = z .string() .min(1) .refine((key) => !RESERVED_TEMPLATE_VARIABLE_KEYS.has(key), { message: "This template variable key is reserved by Resend.", }) const TEMPLATE_VARIABLE_INPUT_SCHEMA = z.discriminatedUnion("type", [ z.object({ /** Optional value used when the sender omits this variable. */ fallbackValue: z.string().nullable().optional(), /** Template variable name. */ key: TEMPLATE_VARIABLE_KEY_SCHEMA, /** Variable value type. */ type: z.literal("string"), }), z.object({ /** Optional value used when the sender omits this variable. */ fallbackValue: z.number().nullable().optional(), /** Template variable name. */ key: TEMPLATE_VARIABLE_KEY_SCHEMA, /** Variable value type. */ type: z.literal("number"), }), ]) const TEMPLATE_VARIABLE_WIRE_SCHEMA = z.discriminatedUnion("type", [ z.object({ created_at: z.string(), fallback_value: z.string().nullable(), key: z.string(), type: z.literal("string"), updated_at: z.string(), }), z.object({ created_at: z.string(), fallback_value: z.number().nullable(), key: z.string(), type: z.literal("number"), updated_at: z.string(), }), ]) const TEMPLATE_VARIABLE_SCHEMA = TEMPLATE_VARIABLE_WIRE_SCHEMA.transform( ({ created_at: createdAt, fallback_value: fallbackValue, updated_at: updatedAt, ...variable }) => ({ ...variable, createdAt, fallbackValue, updatedAt }), ) const TEMPLATE_WIRE_SCHEMA = z.object({ alias: z.string().nullable(), created_at: z.string(), current_version_id: z.string(), from: z.string().nullable(), has_unpublished_versions: z.boolean(), html: z.string(), id: z.string(), name: z.string(), object: z.literal("template"), published_at: z.string().nullable(), reply_to: z.string().array().nullable(), status: z.enum(["draft", "published"]), subject: z.string().nullable(), text: z.string().nullable(), updated_at: z.string(), variables: TEMPLATE_VARIABLE_WIRE_SCHEMA.array().nullable(), }) const TEMPLATE_SCHEMA = z .object({ alias: z.string().nullable(), created_at: z.string(), current_version_id: z.string(), from: z.string().nullable(), has_unpublished_versions: z.boolean(), html: z.string(), id: z.string(), name: z.string(), object: z.literal("template"), published_at: z.string().nullable(), reply_to: z.string().array().nullable(), status: z.enum(["draft", "published"]), subject: z.string().nullable(), text: z.string().nullable(), updated_at: z.string(), variables: TEMPLATE_VARIABLE_SCHEMA.array().nullable(), }) .transform( ({ created_at: createdAt, current_version_id: currentVersionId, has_unpublished_versions: hasUnpublishedVersions, published_at: publishedAt, reply_to: replyTo, updated_at: updatedAt, ...template }) => ({ ...template, createdAt, currentVersionId, hasUnpublishedVersions, publishedAt, replyTo, updatedAt, }), ) const TEMPLATE_LIST_ITEM_WIRE_SCHEMA = TEMPLATE_WIRE_SCHEMA.pick({ alias: true, created_at: true, id: true, name: true, published_at: true, status: true, updated_at: true, }) const TEMPLATE_LIST_ITEM_SCHEMA = TEMPLATE_LIST_ITEM_WIRE_SCHEMA.transform( ({ created_at: createdAt, published_at: publishedAt, updated_at: updatedAt, ...template }) => ({ ...template, createdAt, publishedAt, updatedAt }), ) const TEMPLATE_LIST_WIRE_SCHEMA = z.object({ data: TEMPLATE_LIST_ITEM_WIRE_SCHEMA.array(), has_more: z.boolean(), object: z.literal("list"), }) const TEMPLATE_LIST_SCHEMA = z .object({ data: TEMPLATE_LIST_ITEM_SCHEMA.array(), has_more: z.boolean(), object: z.literal("list"), }) .transform(({ has_more: hasMore, ...result }) => ({ ...result, hasMore })) const TEMPLATE_MUTATION_SCHEMA = z.object({ id: z.string(), object: z.literal("template"), }) const TEMPLATE_DELETE_SCHEMA = TEMPLATE_MUTATION_SCHEMA.extend({ deleted: z.boolean(), }) const TEMPLATE_IDENTIFIER_SCHEMA = z.string().trim().min(1) const TEMPLATE_CONTENT_FIELDS = { ...OPTIONAL_EMAIL_BODY_SCHEMA.shape, alias: z.string().min(1).nullable().optional(), from: z.string().min(1).nullable().optional(), name: z.string().min(1), replyTo: z .union([z.string().min(1), z.string().min(1).array().min(1)]) .transform((value) => (Array.isArray(value) ? value : [value])) .optional(), subject: z.string().min(1).nullable().optional(), variables: TEMPLATE_VARIABLE_INPUT_SCHEMA.array().max(50).optional(), } const CREATE_TEMPLATE_SCHEMA = z .object(TEMPLATE_CONTENT_FIELDS) .refine(hasEmailBody, { message: "html, markdown, or text is required." }) const UPDATE_TEMPLATE_PATCH_SCHEMA = z .object({ ...TEMPLATE_CONTENT_FIELDS, name: TEMPLATE_CONTENT_FIELDS.name.optional(), }) .refine((patch) => Object.keys(patch).length > 0, { message: "Provide at least one template field to update.", }) const PAGINATION_SCHEMA = z .object({ after: z.string().min(1).optional(), before: z.string().min(1).optional(), limit: z.number().int().min(1).max(100).prefault(20), }) .refine(({ after, before }) => !(after && before), { message: "after and before cannot be used together.", }) /** Creates a draft Resend template. */ export const createResendTemplate = defineAction("Create Resend template") .describe("Creates a draft email template with typed variables.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(CREATE_TEMPLATE_SCHEMA) .output(TEMPLATE_MUTATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { if (!hasEmailBody(input)) throw new Error("Template content is required.") return await getResendApi(account).call("templates", { body: toTemplateWire(input), httpMethod: "POST", responseSchema: TEMPLATE_MUTATION_SCHEMA, }) }) /** Lists Resend templates with cursor pagination. */ export const listResendTemplates = defineAction("List Resend templates") .describe("Lists draft and published Resend templates.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(PAGINATION_SCHEMA) .output(TEMPLATE_LIST_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await getResendApi(account).call("templates", { query: input, responseSchema: TEMPLATE_LIST_WIRE_SCHEMA, }), ) /** Retrieves a Resend template by ID or alias. */ export const getResendTemplate = defineAction("Get Resend template") .describe("Retrieves a Resend template by ID or alias.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(z.object({ template: TEMPLATE_IDENTIFIER_SCHEMA })) .output(TEMPLATE_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await getResendApi(account).call( `templates/${encodeURIComponent(input.template)}`, { responseSchema: TEMPLATE_WIRE_SCHEMA }, ), ) /** Updates a draft or the unpublished version of a Resend template. */ export const updateResendTemplate = defineAction("Update Resend template") .describe("Updates content, metadata, or variables for a Resend template.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input( z.object({ patch: UPDATE_TEMPLATE_PATCH_SCHEMA, template: TEMPLATE_IDENTIFIER_SCHEMA, }), ) .output(TEMPLATE_MUTATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { patch, template } = input return await getResendApi(account).call( `templates/${encodeURIComponent(template)}`, { body: toTemplatePatchWire(patch), httpMethod: "PATCH", responseSchema: TEMPLATE_MUTATION_SCHEMA, }, ) }) /** Deletes a Resend template by ID or alias. */ export const deleteResendTemplate = defineAction("Delete Resend template") .describe("Permanently deletes a Resend template by ID or alias.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(z.object({ template: TEMPLATE_IDENTIFIER_SCHEMA })) .output(TEMPLATE_DELETE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler( async ({ account, input }) => await getResendApi(account).call( `templates/${encodeURIComponent(input.template)}`, { httpMethod: "DELETE", responseSchema: TEMPLATE_DELETE_SCHEMA }, ), ) /** Publishes the current Resend template version. */ export const publishResendTemplate = defineAction("Publish Resend template") .describe("Publishes the current version of a Resend template.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(z.object({ template: TEMPLATE_IDENTIFIER_SCHEMA })) .output(TEMPLATE_MUTATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler( async ({ account, input }) => await getResendApi(account).call( `templates/${encodeURIComponent(input.template)}/publish`, { httpMethod: "POST", responseSchema: TEMPLATE_MUTATION_SCHEMA }, ), ) /** Duplicates a Resend template into a new draft. */ export const duplicateResendTemplate = defineAction("Duplicate Resend template") .describe("Duplicates a Resend template into a new draft.") .account(RESEND_ACCOUNT, RESEND_FULL_ACCESS_ACCOUNT_OPTIONS) .input(z.object({ template: TEMPLATE_IDENTIFIER_SCHEMA })) .output(TEMPLATE_MUTATION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler( async ({ account, input }) => await getResendApi(account).call( `templates/${encodeURIComponent(input.template)}/duplicate`, { httpMethod: "POST", responseSchema: TEMPLATE_MUTATION_SCHEMA }, ), ) /** * Converts typed template variables to Resend's wire names. * * @param variables - Validated template variables. */ function toTemplateVariablesWire( variables: z.output[] | undefined, ) { return variables?.map(({ fallbackValue, ...variable }) => ({ ...variable, ...(fallbackValue === undefined ? {} : { fallback_value: fallbackValue }), })) } /** * Converts a template create input to Resend's wire format. * * @param input - Validated template create input. * @throws {Error} When template content is absent. */ function toTemplateWire(input: z.output) { if (!hasEmailBody(input)) throw new Error("Template content is required.") const { alias, from, name, replyTo, subject, variables } = input return { ...normalizeEmailBody(input), name, ...(alias === undefined ? {} : { alias }), ...(from === undefined ? {} : { from }), ...(replyTo === undefined ? {} : { reply_to: replyTo }), ...(subject === undefined ? {} : { subject }), ...(variables === undefined ? {} : { variables: toTemplateVariablesWire(variables) }), } } /** * Converts a template patch to Resend's wire format. * * @param patch - Validated template patch. */ function toTemplatePatchWire( patch: z.output, ) { const { alias, from, name, replyTo, subject, variables } = patch return { ...(hasEmailBody(patch) ? normalizeEmailBody(patch) : {}), ...(alias === undefined ? {} : { alias }), ...(from === undefined ? {} : { from }), ...(name === undefined ? {} : { name }), ...(replyTo === undefined ? {} : { reply_to: replyTo }), ...(subject === undefined ? {} : { subject }), ...(variables === undefined ? {} : { variables: toTemplateVariablesWire(variables) }), } }