diff --git a/.changeset/mighty-dodos-study.md b/.changeset/mighty-dodos-study.md new file mode 100644 index 00000000000..aeae5b5a5a4 --- /dev/null +++ b/.changeset/mighty-dodos-study.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/resend": patch +--- + +Upgrade resend integration to use resend-node 2.0.0 diff --git a/docs/integrations/apis/resend.mdx b/docs/integrations/apis/resend.mdx index d47bc8fef31..e04b03cfbeb 100644 --- a/docs/integrations/apis/resend.mdx +++ b/docs/integrations/apis/resend.mdx @@ -67,7 +67,7 @@ client.defineJob({ resend, }, run: async (payload, io, ctx) => { - await io.resend.sendEmail("send-email", { + await io.resend.emails.send("send-email", { to: payload.to, subject: payload.subject, text: payload.text, @@ -79,6 +79,10 @@ client.defineJob({ ## Tasks -| Function Name | Description | -| ------------- | ------------- | -| `sendEmail` | Send an email | +| Function Name | Description | +| --------------- | -------------------------------- | +| `emails.send` | Send an email | +| `emails.create` | Create an email | +| `emails.get` | Get an email | +| `batch.send` | Send a batch of emails at once | +| `batch.create` | Create a batch of emails at once | diff --git a/integrations/resend/package.json b/integrations/resend/package.json index b6723069ccc..36ebfc716d7 100644 --- a/integrations/resend/package.json +++ b/integrations/resend/package.json @@ -26,9 +26,9 @@ "dependencies": { "@trigger.dev/integration-kit": "workspace:^2.2.4", "@trigger.dev/sdk": "workspace:^2.2.4", - "resend": "^1.0.0" + "resend": "^2.0.0" }, "engines": { - "node": ">=16.8.0" + "node": ">=18.0.0" } -} +} \ No newline at end of file diff --git a/integrations/resend/src/batch.ts b/integrations/resend/src/batch.ts new file mode 100644 index 00000000000..a5575f7598f --- /dev/null +++ b/integrations/resend/src/batch.ts @@ -0,0 +1,81 @@ +import { IntegrationTaskKey, retry } from "@trigger.dev/sdk"; +import { Resend } from "resend"; +import type { ResendRunTask } from "./index"; +import { handleResendError } from "./utils"; + +type SendEmailResult = NonNullable>["data"]>; +type CreateEmailResult = NonNullable>["data"]>; + +export class Batch { + constructor(private runTask: ResendRunTask) {} + + send( + key: IntegrationTaskKey, + payload: Parameters[0], + options?: Parameters[1] + ): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.batch.send(payload, options); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Send Batch Email", + params: payload, + properties: [ + { + label: "Count", + text: String(payload.length), + }, + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } + + create( + key: IntegrationTaskKey, + payload: Parameters[0], + options?: Parameters[1] + ): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.batch.create(payload, options); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Create Batch Email", + params: payload, + properties: [ + { + label: "Count", + text: String(payload.length), + }, + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } +} diff --git a/integrations/resend/src/emails.ts b/integrations/resend/src/emails.ts new file mode 100644 index 00000000000..e0e28a290e8 --- /dev/null +++ b/integrations/resend/src/emails.ts @@ -0,0 +1,123 @@ +import { IntegrationTaskKey, Prettify, retry } from "@trigger.dev/sdk"; +import type { ResendRunTask } from "./index"; +import { Resend } from "resend"; +import { handleResendError } from "./utils"; + +type SendEmailResult = NonNullable>["data"]>; +type CreateEmailResult = NonNullable>["data"]>; +type GetEmailResult = NonNullable>["data"]>; + +export class Emails { + constructor(private runTask: ResendRunTask) {} + + send( + key: IntegrationTaskKey, + payload: Parameters[0], + options?: Parameters[1] + ): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.emails.send(payload, options); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Send Email", + params: payload, + properties: [ + { + label: "From", + text: payload.from, + }, + { + label: "To", + text: Array.isArray(payload.to) ? payload.to.join(", ") : payload.to, + }, + ...(payload.subject ? [{ label: "Subject", text: payload.subject }] : []), + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } + + create( + key: IntegrationTaskKey, + payload: Parameters[0], + options?: Parameters[1] + ): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.emails.create(payload, options); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Create Email", + params: payload, + properties: [ + { + label: "From", + text: payload.from, + }, + { + label: "To", + text: Array.isArray(payload.to) ? payload.to.join(", ") : payload.to, + }, + ...(payload.subject ? [{ label: "Subject", text: payload.subject }] : []), + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } + + get(key: IntegrationTaskKey, payload: string): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.emails.get(payload); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Get Email", + params: payload, + properties: [ + { + label: "ID", + text: payload, + }, + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } +} diff --git a/integrations/resend/src/index.ts b/integrations/resend/src/index.ts index bd2a663d6e4..cc3a83f4247 100644 --- a/integrations/resend/src/index.ts +++ b/integrations/resend/src/index.ts @@ -1,5 +1,4 @@ import { - retry, type ConnectionAuth, type IO, type IOTask, @@ -10,10 +9,8 @@ import { type TriggerIntegration, } from "@trigger.dev/sdk"; import { Resend as ResendClient } from "resend"; - -type ResendType = InstanceType; -type SendEmailData = Parameters[0]; -type SendEmailResponse = Awaited>; +import { Emails } from "./emails"; +import { Batch } from "./batch"; type ErrorResponse = { statusCode: number; @@ -51,6 +48,8 @@ export type ResendIntegrationOptions = { apiKey?: string; }; +export type ResendRunTask = InstanceType["runTask"]; + export class Resend implements TriggerIntegration { /** * @internal @@ -120,33 +119,42 @@ export class Resend implements TriggerIntegration { ); } - sendEmail(key: IntegrationTaskKey, params: SendEmailData): Promise { - return this.runTask( - key, - async (client) => { - const response = await client.sendEmail(params); - if ("statusCode" in response) { - throw response; - } - return response; - }, - { - name: "Send Email", - params, - properties: [ - { - label: "From", - text: params.from, - }, - { - label: "To", - text: Array.isArray(params.to) ? params.to.join(", ") : params.to, - }, - ...(params.subject ? [{ label: "Subject", text: params.subject }] : []), - ], - retry: retry.standardBackoff, - }, - onError - ); + /** + * Access the Resend Emails API + * @example + * ```ts + * const response = await io.resend.emails.send("📧", { + * to: payload.to, + * subject: payload.subject, + * text: payload.text, + * from: "hello@trigger.dev" + * }); + * ``` + */ + get emails() { + return new Emails(this.runTask.bind(this)); + } + + /** + * Access the Resend Batch Emails API + * @example + * ```ts + * const response = await io.resend.batch.send("📧", [{ + * to: payload.to, + * subject: payload.subject, + * text: payload.text, + * from: "hello@trigger.dev" + * }]); + * ``` + */ + get batch() { + return new Batch(this.runTask.bind(this)); + } + + /** + * @deprecated Please use resend.emails.send instead + */ + async sendEmail(...args: Parameters) { + return this.emails.send(...args); } } diff --git a/integrations/resend/src/utils.ts b/integrations/resend/src/utils.ts new file mode 100644 index 00000000000..12f184ea4f8 --- /dev/null +++ b/integrations/resend/src/utils.ts @@ -0,0 +1,52 @@ +import { ErrorResponse } from "resend"; + +interface ResendErrorResponse extends ErrorResponse { + statusCode: number; +} + +function isRequestError(error: unknown): error is ResendErrorResponse { + return ( + typeof error === "object" && + error !== null && + "message" in error && + "name" in error && + typeof error.name === "string" && + "statusCode" in error && + typeof error.statusCode === "number" + ); +} + +// See https://resend.com/docs/api-reference/errors +const retriableErrorCode = ["rate_limit_exceeded", "application_error", "internal_server_error"]; + +export function handleResendError(error: unknown) { + if (isRequestError(error)) { + if (retriableErrorCode.includes(error.name)) { + return error; + } + + return { + skipRetrying: true, + error, + }; + } + + if (error instanceof Error) { + return error; + } + + if (typeof error === "string") { + return new Error(error); + } + + if ( + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" + ) { + return new Error(error.message); + } + + return new Error("Unknown error"); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ddc8470110e..9224c3531a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -530,13 +530,13 @@ importers: '@trigger.dev/sdk': workspace:^2.2.4 '@trigger.dev/tsconfig': workspace:* '@types/node': '18' - resend: ^1.0.0 + resend: ^2.0.0 rimraf: ^3.0.2 tsup: ^6.5.0 dependencies: '@trigger.dev/integration-kit': link:../../packages/integration-kit '@trigger.dev/sdk': link:../../packages/trigger-sdk - resend: 1.0.0 + resend: 2.0.0 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@types/node': 18.15.13 @@ -10601,6 +10601,16 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false + /@react-email/render/0.0.9: + resolution: {integrity: sha512-nrim7wiACnaXsGtL7GF6jp3Qmml8J6vAjAH88jkC8lIbfNZaCyuPQHANjyYIXlvQeAbsWADQJFZgOHUqFqjh/A==} + engines: {node: '>=18.0.0'} + dependencies: + html-to-text: 9.0.5 + pretty: 2.0.0 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + /@react-email/section/0.0.1: resolution: {integrity: sha512-Pb2HcGNhTOtFRJVB+Kq2vTUnd8M+LuZBuHfpUD16ua/m2/mbW5WeXAfSuJ2Tl6qspzvDKLHqVX+o6HBc1qgl2Q==} dependencies: @@ -11154,6 +11164,13 @@ packages: selderee: 0.10.0 dev: false + /@selderee/plugin-htmlparser2/0.11.0: + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + dev: false + /@sendgrid/client/7.7.0: resolution: {integrity: sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==} engines: {node: 6.* || 8.* || >=10.*} @@ -17627,7 +17644,6 @@ packages: /deepmerge/4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - dev: true /default-browser-id/3.0.0: resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} @@ -20552,7 +20568,7 @@ packages: chalk: 4.1.2 chokidar: 3.5.3 cosmiconfig: 7.1.0 - deepmerge: 4.2.2 + deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 minimatch: 3.1.2 @@ -21581,6 +21597,17 @@ packages: selderee: 0.10.0 dev: false + /html-to-text/9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + dev: false + /html-void-elements/2.0.1: resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} @@ -21616,6 +21643,15 @@ packages: entities: 4.4.0 dev: false + /htmlparser2/8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.0.1 + entities: 4.4.0 + dev: false + /http-cache-semantics/4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -26130,6 +26166,13 @@ packages: peberminta: 0.8.0 dev: false + /parseley/0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + dev: false + /parseurl/1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -26222,6 +26265,10 @@ packages: resolution: {integrity: sha512-YYEs+eauIjDH5nUEGi18EohWE0nV2QbGTqmxQcqgZ/0g+laPCQmuIqq7EBLVi9uim9zMgfJv0QBZEnQ3uHw/Tw==} dev: false + /peberminta/0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + dev: false + /peek-stream/1.1.3: resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} dependencies: @@ -28218,15 +28265,11 @@ packages: - debug dev: false - /resend/1.0.0: - resolution: {integrity: sha512-8LEE4gncmcm8bsvxvahZFpFk5hxUrKdagqWoX/MRXVU2YZ9coYxqZDeDYXG9pexz1A694bjE1hiQbBAA+bHAow==} - engines: {node: '>=16'} + /resend/2.0.0: + resolution: {integrity: sha512-jAh0DN84ZjjmzGM2vMjJ1hphPBg1mG98dzopF7kJzmin62v8ESg4og2iCKWdkAboGOT2SeO5exbr/8Xh8gLddw==} + engines: {node: '>=18'} dependencies: - '@react-email/render': 0.0.7 - node-fetch: 2.6.12 - type-fest: 3.13.0 - transitivePeerDependencies: - - encoding + '@react-email/render': 0.0.9 dev: false /resize-observer-polyfill/1.5.1: @@ -28620,6 +28663,12 @@ packages: parseley: 0.11.0 dev: false + /selderee/0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + dependencies: + parseley: 0.12.1 + dev: false + /sembear/0.5.2: resolution: {integrity: sha512-Ij1vCAdFgWABd7zTg50Xw1/p0JgESNxuLlneEAsmBrKishA06ulTTL/SHGmNy2Zud7+rKrHTKNI6moJsn1ppAQ==} dependencies: diff --git a/references/job-catalog/src/resend.ts b/references/job-catalog/src/resend.ts index 93452914082..f480b833703 100644 --- a/references/job-catalog/src/resend.ts +++ b/references/job-catalog/src/resend.ts @@ -1,6 +1,6 @@ import { createExpressServer } from "@trigger.dev/express"; import { Resend } from "@trigger.dev/resend"; -import { TriggerClient, eventTrigger } from "@trigger.dev/sdk"; +import { TriggerClient, eventTrigger, invokeTrigger } from "@trigger.dev/sdk"; import { z } from "zod"; export const client = new TriggerClient({ @@ -20,12 +20,73 @@ client.defineJob({ id: "send-resend-email", name: "Send Resend Email", version: "0.1.0", - trigger: eventTrigger({ - name: "send.email", + trigger: invokeTrigger({ schema: z.object({ - to: z.union([z.string(), z.array(z.string())]), - subject: z.string(), - text: z.string(), + to: z.union([z.string(), z.array(z.string())]).default("eric@trigger.dev"), + subject: z.string().default("This is a test email"), + text: z.string().default("This is a test email"), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.emails.send("📧", { + to: payload.to, + subject: payload.subject, + text: payload.text, + from: "Trigger.dev ", + }); + + await io.logger.info("Sent email", { response }); + + const emailDetails = await io.resend.emails.get("get-email", response.id); + }, +}); + +client.defineJob({ + id: "batch-send-resend-email", + name: "Batch Send Resend Email", + version: "0.1.0", + trigger: invokeTrigger({ + schema: z.object({ + to: z.union([z.string(), z.array(z.string())]).default("eric@trigger.dev"), + subject: z.string().default("This is a test email"), + text: z.string().default("This is a test email"), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.batch.send("📧", [ + { + to: payload.to, + subject: payload.subject, + text: payload.text, + from: "Trigger.dev ", + }, + { + to: payload.to, + subject: payload.subject, + text: payload.text, + from: "Trigger.dev ", + }, + ]); + + await io.logger.info("Sent batched email", { response }); + }, +}); + +client.defineJob({ + id: "send-resend-email-deprecated", + name: "Send Resend Email Deprecated", + version: "0.1.0", + trigger: invokeTrigger({ + schema: z.object({ + to: z.union([z.string(), z.array(z.string())]).default("eric@trigger.dev"), + subject: z.string().default("This is a test email"), + text: z.string().default("This is a test email"), }), }), integrations: {