@decocms/apps-resend 8.0.0 → 8.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/__tests__/emails.test.ts +54 -0
- package/src/__tests__/send.test.ts +16 -12
- package/src/actions/send.ts +4 -1
- package/src/emails.ts +52 -0
- package/src/index.ts +20 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/apps-resend",
|
|
3
|
-
"version": "8.0.0",
|
|
3
|
+
"version": "8.1.0-next.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco app: Resend transactional email integration",
|
|
6
6
|
"repository": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"lint:unused": "knip"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@decocms/blocks": "8.0.0",
|
|
32
|
-
"@decocms/apps-commerce": "8.0.0"
|
|
31
|
+
"@decocms/blocks": "8.1.0-next.0",
|
|
32
|
+
"@decocms/apps-commerce": "8.1.0-next.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^19.0.0",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The v8 Resend client (/next/upstream-clients): one typed call through the
|
|
3
|
+
* instrumented fetch, errors without bodies or keys, no retries.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { createResendClient, ResendError } from "../index";
|
|
7
|
+
|
|
8
|
+
function fakeFetch(status: number, body: unknown) {
|
|
9
|
+
return vi.fn(
|
|
10
|
+
async (_input: string | URL | Request, _init?: RequestInit) =>
|
|
11
|
+
new Response(JSON.stringify(body), { status }),
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const email = {
|
|
16
|
+
from: "Store <hello@example.com>",
|
|
17
|
+
to: "shopper@example.com",
|
|
18
|
+
subject: "Your order",
|
|
19
|
+
html: "<p>Thanks</p>",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("createResendClient", () => {
|
|
23
|
+
it("sendEmail posts the email with the API key and returns its id", async () => {
|
|
24
|
+
const fetch = fakeFetch(200, { id: "email_1" });
|
|
25
|
+
const resend = createResendClient({ apiKey: "re_test" }, { fetch });
|
|
26
|
+
|
|
27
|
+
await expect(resend.sendEmail(email)).resolves.toEqual({ id: "email_1" });
|
|
28
|
+
|
|
29
|
+
const [url, init] = fetch.mock.calls[0];
|
|
30
|
+
expect(String(url)).toBe("https://api.resend.com/emails");
|
|
31
|
+
expect(init?.method).toBe("POST");
|
|
32
|
+
expect(init?.headers).toMatchObject({ authorization: "Bearer re_test" });
|
|
33
|
+
expect(JSON.parse(String(init?.body))).toEqual(email);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("throws the operation and status, never the body or the key", async () => {
|
|
37
|
+
const fetch = fakeFetch(422, { name: "validation_error", message: "re_test is bad" });
|
|
38
|
+
const resend = createResendClient({ apiKey: "re_test" }, { fetch });
|
|
39
|
+
|
|
40
|
+
const error = await resend.sendEmail(email).catch((e: unknown) => e);
|
|
41
|
+
|
|
42
|
+
expect(error).toBeInstanceOf(ResendError);
|
|
43
|
+
expect(error).toMatchObject({ operation: "sendEmail", status: 422 });
|
|
44
|
+
expect((error as Error).message).not.toContain("re_test");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does not retry", async () => {
|
|
48
|
+
const fetch = fakeFetch(503, {});
|
|
49
|
+
const resend = createResendClient({ apiKey: "re_test" }, { fetch });
|
|
50
|
+
|
|
51
|
+
await expect(resend.sendEmail(email)).rejects.toThrow(ResendError);
|
|
52
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -35,19 +35,23 @@ describe("sendEmail", () => {
|
|
|
35
35
|
expect(result.data).toEqual({ id: "email_abc123" });
|
|
36
36
|
expect(result.error).toBeNull();
|
|
37
37
|
|
|
38
|
-
expect(fetchSpy).toHaveBeenCalledWith(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
39
|
+
"https://api.resend.com/emails",
|
|
40
|
+
expect.objectContaining({
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: "Bearer re_test_123",
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
from: "Test <test@example.com>",
|
|
48
|
+
to: "user@example.com",
|
|
49
|
+
subject: "Hello",
|
|
50
|
+
html: "<p>World</p>",
|
|
51
|
+
}),
|
|
52
|
+
signal: expect.any(AbortSignal),
|
|
49
53
|
}),
|
|
50
|
-
|
|
54
|
+
);
|
|
51
55
|
});
|
|
52
56
|
|
|
53
57
|
it("falls back to defaults when fields are omitted", async () => {
|
package/src/actions/send.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { withFetchTimeout } from "@decocms/blocks/sdk/fetchTimeout";
|
|
1
2
|
import { getResendConfig } from "../client";
|
|
2
3
|
import type { CreateEmailOptions, CreateEmailResponse } from "../types";
|
|
3
4
|
|
|
5
|
+
const timeoutFetch = withFetchTimeout();
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Send an email via Resend API.
|
|
6
9
|
*
|
|
@@ -32,7 +35,7 @@ export async function sendEmail(
|
|
|
32
35
|
...(payload.headers && { headers: payload.headers }),
|
|
33
36
|
};
|
|
34
37
|
|
|
35
|
-
const response = await
|
|
38
|
+
const response = await timeoutFetch("https://api.resend.com/emails", {
|
|
36
39
|
method: "POST",
|
|
37
40
|
headers: {
|
|
38
41
|
Authorization: `Bearer ${config.apiKey}`,
|
package/src/emails.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Resend client: a thin client for the Resend REST API.
|
|
3
|
+
* See /next/upstream-clients.
|
|
4
|
+
*
|
|
5
|
+
* Every request goes through `createInstrumentedFetch` (provider `resend`).
|
|
6
|
+
* No retries or circuit breaker: sending an email isn't safe to repeat.
|
|
7
|
+
* Default senders, recipients and subjects belong to the site.
|
|
8
|
+
*/
|
|
9
|
+
import { createInstrumentedFetch } from "@decocms/blocks/fetch";
|
|
10
|
+
import type { CreateEmailOptions, CreateEmailResponseSuccess } from "./types";
|
|
11
|
+
|
|
12
|
+
export interface ResendClientConfig {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A Resend email; the API requires a sender. */
|
|
17
|
+
export type SendEmailOptions = CreateEmailOptions & { from: string };
|
|
18
|
+
|
|
19
|
+
export class ResendError extends Error {
|
|
20
|
+
constructor(
|
|
21
|
+
readonly operation: string,
|
|
22
|
+
readonly status: number,
|
|
23
|
+
) {
|
|
24
|
+
super(`resend ${operation} failed with HTTP ${status}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createResendClient(
|
|
29
|
+
config: ResendClientConfig,
|
|
30
|
+
options: { fetch?: typeof globalThis.fetch } = {},
|
|
31
|
+
) {
|
|
32
|
+
const request = createInstrumentedFetch({ provider: "resend", fetch: options.fetch });
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
/** Sends one email and returns its id. */
|
|
36
|
+
async sendEmail(email: SendEmailOptions): Promise<CreateEmailResponseSuccess> {
|
|
37
|
+
const response = await request("https://api.resend.com/emails", {
|
|
38
|
+
operation: "sendEmail",
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
42
|
+
"content-type": "application/json",
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify(email),
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) throw new ResendError("sendEmail", response.status);
|
|
47
|
+
return (await response.json()) as CreateEmailResponseSuccess;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ResendClient = ReturnType<typeof createResendClient>;
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@decocms/apps-resend`: the Resend client (`createResendClient`).
|
|
3
|
+
* See /next/upstream-clients.
|
|
4
|
+
*
|
|
5
|
+
* `sendEmail`, `configureResend` and `getResendConfig` are the v7 surface,
|
|
6
|
+
* kept for v7 sites.
|
|
7
|
+
*/
|
|
1
8
|
export { sendEmail } from "./actions/send";
|
|
2
9
|
export { configureResend, getResendConfig } from "./client";
|
|
10
|
+
export {
|
|
11
|
+
createResendClient,
|
|
12
|
+
type ResendClient,
|
|
13
|
+
type ResendClientConfig,
|
|
14
|
+
ResendError,
|
|
15
|
+
type SendEmailOptions,
|
|
16
|
+
} from "./emails";
|
|
3
17
|
export type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
18
|
+
CreateEmailOptions,
|
|
19
|
+
CreateEmailResponse,
|
|
20
|
+
CreateEmailResponseSuccess,
|
|
21
|
+
ErrorResponse,
|
|
22
|
+
ResendConfig,
|
|
23
|
+
ResendErrorCodeKey,
|
|
10
24
|
} from "./types";
|