From 4dd6cf18dcc7ededb8c87cb5810c0e6a79754a6e Mon Sep 17 00:00:00 2001 From: Kritik Jiyaviya Date: Wed, 17 Jan 2024 19:36:45 +0530 Subject: [PATCH] feat: Add support for the new Resend audience API endpoints (#850) * feat: Add support for the new Resend audience API endpoints * update pnpm-lock.yaml * update pnpm-lock.yaml * Merge branch 'main' into feat/support-for-resend-audience-api * latest pnpm-lock.yaml * Create lovely-items-end.md * Update pnpm-lock.yaml --------- Co-authored-by: Matt Aitken --- .changeset/lovely-items-end.md | 5 + docs/integrations/apis/resend-tasks.mdx | 23 ++- integrations/resend/package.json | 2 +- integrations/resend/src/audiences.ts | 134 +++++++++++++++ integrations/resend/src/contacts.ts | 188 +++++++++++++++++++++ integrations/resend/src/index.ts | 34 ++++ pnpm-lock.yaml | 19 +-- references/job-catalog/src/resend.ts | 210 ++++++++++++++++++++++++ 8 files changed, 593 insertions(+), 22 deletions(-) create mode 100644 .changeset/lovely-items-end.md create mode 100644 integrations/resend/src/audiences.ts create mode 100644 integrations/resend/src/contacts.ts diff --git a/.changeset/lovely-items-end.md b/.changeset/lovely-items-end.md new file mode 100644 index 0000000000..25a1c7903a --- /dev/null +++ b/.changeset/lovely-items-end.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/resend": patch +--- + +Add support for the new Resend audience API endpoints diff --git a/docs/integrations/apis/resend-tasks.mdx b/docs/integrations/apis/resend-tasks.mdx index a59d2ffd21..82e33327a0 100644 --- a/docs/integrations/apis/resend-tasks.mdx +++ b/docs/integrations/apis/resend-tasks.mdx @@ -26,13 +26,22 @@ Send an email to a recipient with a text payload. [Official Resend Docs](https:/ ## Tasks -| 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 | +| 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 | +| `audiences.create` | Create an audience | +| `audiences.get` | Get an audience | +| `audiences.remove` | Remove an audience | +| `audiences.list` | List audiences | +| `contacts.create` | Create a contact | +| `contacts.get` | Get a contact | +| `contacts.update` | Update a contact | +| `contacts.remove` | Remove a contact | +| `contacts.list` | List contacts | ## Example diff --git a/integrations/resend/package.json b/integrations/resend/package.json index 2394fefc85..497ac78b1c 100644 --- a/integrations/resend/package.json +++ b/integrations/resend/package.json @@ -26,7 +26,7 @@ "dependencies": { "@trigger.dev/integration-kit": "workspace:^2.3.13", "@trigger.dev/sdk": "workspace:^2.3.13", - "resend": "^2.0.0" + "resend": "^2.1.0" }, "engines": { "node": ">=18.0.0" diff --git a/integrations/resend/src/audiences.ts b/integrations/resend/src/audiences.ts new file mode 100644 index 0000000000..ba9cbae59a --- /dev/null +++ b/integrations/resend/src/audiences.ts @@ -0,0 +1,134 @@ +import { IntegrationTaskKey, retry } from "@trigger.dev/sdk"; +import type { ResendRunTask } from "./index"; +import { Resend } from "resend"; +import { handleResendError } from "./utils"; + +type CreateAudienceResult = NonNullable>["data"]>; +type GetAudienceResult = NonNullable>["data"]>; +type DeleteAudienceResult = NonNullable>["data"]>; +type ListAudiencesResult = NonNullable>["data"]>; + +export class Audiences { + constructor(private runTask: ResendRunTask) {} + + create( + key: IntegrationTaskKey, + payload: Parameters[0], + options?: Parameters[1] + ): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.audiences.create(payload, options); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Create Audience", + params: payload, + properties: [ + { + label: "Name", + text: payload.name, + }, + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } + + get(key: IntegrationTaskKey, payload: string): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.audiences.get(payload); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Get Audience", + params: payload, + properties: [ + { + label: "ID", + text: payload, + }, + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } + + remove(key: IntegrationTaskKey, payload: string): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.audiences.remove(payload); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Remove Audience", + params: payload, + properties: [ + { + label: "ID", + text: payload, + }, + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } + + list(key: IntegrationTaskKey): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.audiences.list(); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "List Audiences", + retry: retry.standardBackoff, + }, + handleResendError + ); + } +} diff --git a/integrations/resend/src/contacts.ts b/integrations/resend/src/contacts.ts new file mode 100644 index 0000000000..e94aa5805b --- /dev/null +++ b/integrations/resend/src/contacts.ts @@ -0,0 +1,188 @@ +import { IntegrationTaskKey, retry } from "@trigger.dev/sdk"; +import type { ResendRunTask } from "./index"; +import { Resend } from "resend"; +import { handleResendError } from "./utils"; + +type CreateContactResult = NonNullable>["data"]>; +type GetContactResult = NonNullable>["data"]>; +type UpdateContactResult = NonNullable>["data"]>; +type DeleteContactResult = NonNullable>["data"]>; +type ListContactsResult = NonNullable>["data"]>; + +export class Contacts { + constructor(private runTask: ResendRunTask) {} + + create( + key: IntegrationTaskKey, + payload: Parameters[0], + options?: Parameters[1] + ): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.contacts.create(payload, options); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Create Contact", + params: payload, + properties: [ + { + label: "Email", + text: payload.email, + }, + ...(payload.first_name && payload.last_name + ? [{ label: "Name", text: payload.first_name + " " + payload.last_name }] + : []), + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } + + get( + key: IntegrationTaskKey, + payload: Parameters[0] + ): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.contacts.get(payload); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Get Contact", + params: payload, + properties: [ + { + label: "Id", + text: payload.id, + }, + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } + + update( + key: IntegrationTaskKey, + payload: Parameters[0] + ): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.contacts.update(payload); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Update Contact", + params: payload, + properties: [ + { + label: "Id", + text: payload.id, + }, + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } + + remove( + key: IntegrationTaskKey, + payload: Parameters[0] + ): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.contacts.remove(payload); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "Remove Contact", + params: payload, + properties: [ + { + label: "Id", + text: payload.id, + }, + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } + + list( + key: IntegrationTaskKey, + payload: Parameters[0] + ): Promise { + return this.runTask( + key, + async (client, task) => { + const { error, data } = await client.contacts.list(payload); + + if (error) { + throw error; + } + + if (!data) { + throw new Error("No data returned from Resend"); + } + + return data; + }, + { + name: "List Contacts", + params: payload, + properties: [ + { + label: "Audience Id", + text: payload.audience_id, + }, + ], + retry: retry.standardBackoff, + }, + handleResendError + ); + } +} diff --git a/integrations/resend/src/index.ts b/integrations/resend/src/index.ts index cc3a83f424..2bab5f5db0 100644 --- a/integrations/resend/src/index.ts +++ b/integrations/resend/src/index.ts @@ -11,6 +11,8 @@ import { import { Resend as ResendClient } from "resend"; import { Emails } from "./emails"; import { Batch } from "./batch"; +import { Contacts } from "./contacts"; +import { Audiences } from "./audiences"; type ErrorResponse = { statusCode: number; @@ -157,4 +159,36 @@ export class Resend implements TriggerIntegration { async sendEmail(...args: Parameters) { return this.emails.send(...args); } + + /** + * Access the Resend Audiences API + * @example + * ```ts + * const response = await io.resend.audiences.create("📧", { + * name: payload.name, + * }); + * ``` + */ + + get audiences() { + return new Audiences(this.runTask.bind(this)); + } + + /** + * Access the Resend Contacts API + * @example + * ```ts + * const response = await io.resend.contacts.create("📧", { + * email: payload.email, + * first_name: payload.first_name, + * last_name: payload.last_name, + * unsubscribed: false + * audienceId: payload.audienceId + * }); + * ``` + */ + + get contacts() { + return new Contacts(this.runTask.bind(this)); + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ac25b1116..811769159d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -625,14 +625,14 @@ importers: '@trigger.dev/tsconfig': workspace:* '@trigger.dev/tsup': workspace:* '@types/node': '18' - resend: ^2.0.0 + resend: ^2.1.0 rimraf: ^3.0.2 tsup: 8.0.1 typescript: ^5.3.0 dependencies: '@trigger.dev/integration-kit': link:../../packages/integration-kit '@trigger.dev/sdk': link:../../packages/trigger-sdk - resend: 2.0.0 + resend: 2.1.0 devDependencies: '@trigger.dev/tsconfig': link:../../config-packages/tsconfig '@trigger.dev/tsup': link:../../config-packages/tsup @@ -23364,7 +23364,7 @@ packages: '@selderee/plugin-htmlparser2': 0.10.0 deepmerge: 4.3.1 dom-serializer: 2.0.0 - htmlparser2: 8.0.1 + htmlparser2: 8.0.2 selderee: 0.10.0 dev: false @@ -23405,15 +23405,6 @@ packages: entities: 2.2.0 dev: true - /htmlparser2/8.0.1: - resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.0.1 - entities: 4.4.0 - dev: false - /htmlparser2/8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} dependencies: @@ -30388,8 +30379,8 @@ packages: - debug dev: false - /resend/2.0.0: - resolution: {integrity: sha512-jAh0DN84ZjjmzGM2vMjJ1hphPBg1mG98dzopF7kJzmin62v8ESg4og2iCKWdkAboGOT2SeO5exbr/8Xh8gLddw==} + /resend/2.1.0: + resolution: {integrity: sha512-s6LlaEReTUvlbo6w3Eg1M1TMuwK9OKJ1GVgyptIV8smLPHhFZVqnwBTFPZHID9rcsih72t3iuyrtkQ3IIGwnow==} engines: {node: '>=18'} dependencies: '@react-email/render': 0.0.9 diff --git a/references/job-catalog/src/resend.ts b/references/job-catalog/src/resend.ts index f480b83370..764594d46e 100644 --- a/references/job-catalog/src/resend.ts +++ b/references/job-catalog/src/resend.ts @@ -132,4 +132,214 @@ client.defineJob({ }, }); +client.defineJob({ + id: "create-resend-audience", + name: "Create Resend Audience", + version: "0.1.0", + trigger: eventTrigger({ + name: "create.audience", + schema: z.object({ + name: z.string(), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.audiences.create("📧", { + name: payload.name, + }); + + await io.logger.info("Created audience", { response }); + }, +}); + +client.defineJob({ + id: "get-resend-audience", + name: "Get Resend Audience", + version: "0.1.0", + trigger: eventTrigger({ + name: "get.audience", + schema: z.object({ + id: z.string(), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.audiences.get("📧", payload.id); + + await io.logger.info("Got audience", { response }); + }, +}); + +client.defineJob({ + id: "remove-resend-audience", + name: "Remove Resend Audience", + version: "0.1.0", + trigger: eventTrigger({ + name: "remove.audience", + schema: z.object({ + id: z.string(), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.audiences.remove("📧", payload.id); + + await io.logger.info("Removed audience", { response }); + }, +}); + +client.defineJob({ + id: "list-resend-audiences", + name: "List Resend Audiences", + version: "0.1.0", + trigger: eventTrigger({ + name: "list.audiences", + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.audiences.list("📧"); + + await io.logger.info("Listed audiences", { response }); + }, +}); + +client.defineJob({ + id: "create resend contact", + name: "Create Resend Contact", + version: "0.1.0", + trigger: eventTrigger({ + name: "create.contact", + schema: z.object({ + audience_id: z.string(), + email: z.string(), + unsubscribed: z.boolean().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.contacts.create("📧", { + audience_id: payload.audience_id, + email: payload.email, + unsubscribed: payload.unsubscribed, + first_name: payload.first_name, + last_name: payload.last_name, + }); + + await io.logger.info("Created contact", { response }); + }, +}); + +client.defineJob({ + id: "get resend contact", + name: "Get Resend Contact", + version: "0.1.0", + trigger: eventTrigger({ + name: "get.contact", + schema: z.object({ + audience_id: z.string(), + id: z.string(), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.contacts.get("📧", { + audience_id: payload.audience_id, + id: payload.id, + }); + + await io.logger.info("Got contact", { response }); + }, +}); + +client.defineJob({ + id: "update resend contact", + name: "Update Resend Contact", + version: "0.1.0", + trigger: eventTrigger({ + name: "update.contact", + schema: z.object({ + audience_id: z.string(), + id: z.string(), + email: z.string().optional(), + unsubscribed: z.boolean().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.contacts.update("📧", { + audience_id: payload.audience_id, + id: payload.id, + unsubscribed: payload.unsubscribed, + first_name: payload.first_name, + last_name: payload.last_name, + }); + + await io.logger.info("Updated contact", { response }); + }, +}); + +client.defineJob({ + id: "remove resend contact", + name: "Remove Resend Contact", + version: "0.1.0", + trigger: eventTrigger({ + name: "remove.contact", + schema: z.object({ + audience_id: z.string(), + id: z.string(), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.contacts.remove("📧", { + audience_id: payload.audience_id, + id: payload.id, + }); + + await io.logger.info("Removed contact", { response }); + }, +}); + +client.defineJob({ + id: "list resend contacts", + name: "List Resend Contacts", + version: "0.1.0", + trigger: eventTrigger({ + name: "list.contacts", + schema: z.object({ + audience_id: z.string(), + }), + }), + integrations: { + resend, + }, + run: async (payload, io, ctx) => { + const response = await io.resend.contacts.list("📧", { + audience_id: payload.audience_id, + }); + + await io.logger.info("Listed contacts", { response }); + }, +}); + createExpressServer(client);