Upgrade resend integration to use resend-node 2.0.0 (#724)

* Upgrade resend integration to use resend-node 2.0.0, fixes issue with Next.js 14

* Create mighty-dodos-study.md
This commit is contained in:
Eric Allam
2023-11-06 18:30:37 +00:00
committed by GitHub
parent 276181b91c
commit 42185bc36b
9 changed files with 441 additions and 58 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/resend": patch
---
Upgrade resend integration to use resend-node 2.0.0
+8 -4
View File
@@ -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 |
+3 -3
View File
@@ -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"
}
}
}
+81
View File
@@ -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<Awaited<ReturnType<Resend["batch"]["send"]>>["data"]>;
type CreateEmailResult = NonNullable<Awaited<ReturnType<Resend["batch"]["create"]>>["data"]>;
export class Batch {
constructor(private runTask: ResendRunTask) {}
send(
key: IntegrationTaskKey,
payload: Parameters<Resend["batch"]["send"]>[0],
options?: Parameters<Resend["batch"]["send"]>[1]
): Promise<SendEmailResult> {
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<Resend["batch"]["create"]>[0],
options?: Parameters<Resend["batch"]["create"]>[1]
): Promise<CreateEmailResult> {
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
);
}
}
+123
View File
@@ -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<Awaited<ReturnType<Resend["emails"]["send"]>>["data"]>;
type CreateEmailResult = NonNullable<Awaited<ReturnType<Resend["emails"]["create"]>>["data"]>;
type GetEmailResult = NonNullable<Awaited<ReturnType<Resend["emails"]["get"]>>["data"]>;
export class Emails {
constructor(private runTask: ResendRunTask) {}
send(
key: IntegrationTaskKey,
payload: Parameters<Resend["emails"]["send"]>[0],
options?: Parameters<Resend["emails"]["send"]>[1]
): Promise<SendEmailResult> {
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<Resend["emails"]["create"]>[0],
options?: Parameters<Resend["emails"]["create"]>[1]
): Promise<CreateEmailResult> {
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<GetEmailResult> {
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
);
}
}
+41 -33
View File
@@ -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<typeof ResendClient>;
type SendEmailData = Parameters<ResendType["sendEmail"]>[0];
type SendEmailResponse = Awaited<ReturnType<ResendType["sendEmail"]>>;
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<typeof Resend>["runTask"];
export class Resend implements TriggerIntegration {
/**
* @internal
@@ -120,33 +119,42 @@ export class Resend implements TriggerIntegration {
);
}
sendEmail(key: IntegrationTaskKey, params: SendEmailData): Promise<SendEmailResponse> {
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<typeof this.emails.send>) {
return this.emails.send(...args);
}
}
+52
View File
@@ -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");
}
+61 -12
View File
@@ -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:
+67 -6
View File
@@ -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 <hello@email.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 <hello@email.trigger.dev>",
},
{
to: payload.to,
subject: payload.subject,
text: payload.text,
from: "Trigger.dev <hello@email.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: {