From d7b59828a4db8f82e596bbabc784bd3549b9c869 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Fri, 5 Sep 2025 11:22:41 -0300 Subject: [PATCH 1/3] Revert "feat: rate limiting information for all responses (#562)" This reverts commit ee0d2a1e183fb5ebba4d6dbfbe3cd5965704bc74. --- src/api-keys/api-keys.spec.ts | 108 +++++------ .../create-api-key-options.interface.ts | 12 +- .../interfaces/list-api-keys.interface.ts | 12 +- .../interfaces/remove-api-keys.interface.ts | 12 +- src/audiences/audiences.spec.ts | 69 +++---- .../create-audience-options.interface.ts | 12 +- .../interfaces/get-audience.interface.ts | 12 +- .../interfaces/list-audiences.interface.ts | 12 +- .../interfaces/remove-audience.interface.ts | 12 +- src/batch/batch.spec.ts | 10 - .../create-batch-options.interface.ts | 39 ++-- src/broadcasts/broadcasts.spec.ts | 141 ++++++-------- .../create-broadcast-options.interface.ts | 12 +- .../interfaces/get-broadcast.interface.ts | 12 +- .../interfaces/list-broadcasts.interface.ts | 12 +- .../interfaces/remove-broadcast.interface.ts | 12 +- .../send-broadcast-options.interface.ts | 12 +- .../interfaces/update-broadcast.interface.ts | 12 +- src/contacts/contacts.spec.ts | 126 ++++++------- src/contacts/contacts.ts | 3 - .../create-contact-options.interface.ts | 12 +- .../interfaces/get-contact.interface.ts | 12 +- .../interfaces/list-contacts.interface.ts | 12 +- .../interfaces/remove-contact.interface.ts | 12 +- .../interfaces/update-contact.interface.ts | 12 +- src/domains/domains.spec.ts | 176 +++++++++++------- .../create-domain-options.interface.ts | 12 +- .../interfaces/get-domain.interface.ts | 12 +- .../interfaces/list-domains.interface.ts | 12 +- .../interfaces/remove-domain.interface.ts | 12 +- .../interfaces/update-domain.interface.ts | 12 +- .../interfaces/verify-domain.interface.ts | 12 +- src/emails/emails.spec.ts | 154 +++++++-------- .../cancel-email-options.interface.ts | 12 +- .../create-email-options.interface.ts | 12 +- .../interfaces/get-email-options.interface.ts | 12 +- .../update-email-options.interface.ts | 12 +- src/interfaces.ts | 31 +-- src/rate-limiting.ts | 44 ----- src/resend.ts | 31 +-- 40 files changed, 680 insertions(+), 588 deletions(-) delete mode 100644 src/rate-limiting.ts diff --git a/src/api-keys/api-keys.spec.ts b/src/api-keys/api-keys.spec.ts index 41a14bf8..2bc90d1c 100644 --- a/src/api-keys/api-keys.spec.ts +++ b/src/api-keys/api-keys.spec.ts @@ -1,9 +1,5 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; import type { CreateApiKeyOptions, CreateApiKeyResponseSuccess, @@ -22,8 +18,12 @@ describe('API Keys', () => { id: '430eed87-632a-4ea6-90db-0aace67ec228', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 201, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -37,11 +37,6 @@ describe('API Keys', () => { "token": "re_PKr4RCko_Lhm9ost2YjNCctnPjbLw8Nqk", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -55,8 +50,12 @@ describe('API Keys', () => { name: 'validation_error', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -70,11 +69,6 @@ describe('API Keys', () => { "message": "String must contain at least 1 character(s)", "name": "validation_error", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -91,8 +85,12 @@ describe('API Keys', () => { id: '430eed87-632a-4ea6-90db-0aace67ec228', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 201, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -106,11 +104,6 @@ describe('API Keys', () => { "token": "re_PKr4RCko_Lhm9ost2YjNCctnPjbLw8Nqk", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -125,8 +118,12 @@ describe('API Keys', () => { id: '430eed87-632a-4ea6-90db-0aace67ec228', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 201, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -140,11 +137,6 @@ describe('API Keys', () => { "token": "re_PKr4RCko_Lhm9ost2YjNCctnPjbLw8Nqk", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -155,8 +147,12 @@ describe('API Keys', () => { message: 'Access must be "full_access" | "sending_access"', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -175,11 +171,6 @@ describe('API Keys', () => { "message": "Access must be "full_access" | "sending_access"", "name": "invalid_access", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -400,8 +391,12 @@ describe('API Keys', () => { const response: RemoveApiKeyResponseSuccess = {}; it('removes an api key', async () => { - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -410,11 +405,6 @@ describe('API Keys', () => { { "data": {}, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -425,8 +415,12 @@ describe('API Keys', () => { message: 'Something went wrong', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 500, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -440,11 +434,6 @@ describe('API Keys', () => { "message": "Something went wrong", "name": "application_error", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -455,8 +444,12 @@ describe('API Keys', () => { message: 'API key not found', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -472,11 +465,6 @@ describe('API Keys', () => { "message": "API key not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); diff --git a/src/api-keys/interfaces/create-api-key-options.interface.ts b/src/api-keys/interfaces/create-api-key-options.interface.ts index 59274575..628600a8 100644 --- a/src/api-keys/interfaces/create-api-key-options.interface.ts +++ b/src/api-keys/interfaces/create-api-key-options.interface.ts @@ -1,5 +1,5 @@ import type { PostOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; export interface CreateApiKeyOptions { name: string; @@ -14,4 +14,12 @@ export interface CreateApiKeyResponseSuccess { id: string; } -export type CreateApiKeyResponse = Response; +export type CreateApiKeyResponse = + | { + data: CreateApiKeyResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/api-keys/interfaces/list-api-keys.interface.ts b/src/api-keys/interfaces/list-api-keys.interface.ts index c9728773..e69816f5 100644 --- a/src/api-keys/interfaces/list-api-keys.interface.ts +++ b/src/api-keys/interfaces/list-api-keys.interface.ts @@ -1,5 +1,5 @@ import type { PaginationOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { ApiKey } from './api-key'; export type ListApiKeysOptions = PaginationOptions; @@ -10,4 +10,12 @@ export type ListApiKeysResponseSuccess = { data: Pick[]; }; -export type ListApiKeysResponse = Response; +export type ListApiKeysResponse = + | { + data: ListApiKeysResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/api-keys/interfaces/remove-api-keys.interface.ts b/src/api-keys/interfaces/remove-api-keys.interface.ts index 3b6b77dc..04388cf8 100644 --- a/src/api-keys/interfaces/remove-api-keys.interface.ts +++ b/src/api-keys/interfaces/remove-api-keys.interface.ts @@ -1,6 +1,14 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; // biome-ignore lint/complexity/noBannedTypes: allow empty types export type RemoveApiKeyResponseSuccess = {}; -export type RemoveApiKeyResponse = Response; +export type RemoveApiKeyResponse = + | { + data: RemoveApiKeyResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/audiences/audiences.spec.ts b/src/audiences/audiences.spec.ts index ed9ff498..4b9b0f61 100644 --- a/src/audiences/audiences.spec.ts +++ b/src/audiences/audiences.spec.ts @@ -1,9 +1,5 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; import type { CreateAudienceOptions, CreateAudienceResponseSuccess, @@ -24,8 +20,12 @@ describe('Audiences', () => { object: 'audience', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -39,11 +39,6 @@ describe('Audiences', () => { "object": "audience", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -55,8 +50,12 @@ describe('Audiences', () => { message: 'Missing "name" field', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -70,11 +69,6 @@ describe('Audiences', () => { "message": "Missing "name" field", "name": "missing_required_field", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -222,8 +216,12 @@ describe('Audiences', () => { message: 'Audience not found', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -237,11 +235,6 @@ describe('Audiences', () => { "message": "Audience not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -255,8 +248,12 @@ describe('Audiences', () => { created_at: '2023-06-21T06:10:36.144Z', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -272,11 +269,6 @@ describe('Audiences', () => { "object": "audience", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -290,8 +282,12 @@ describe('Audiences', () => { id, deleted: true, }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -304,11 +300,6 @@ describe('Audiences', () => { "object": "audience", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); diff --git a/src/audiences/interfaces/create-audience-options.interface.ts b/src/audiences/interfaces/create-audience-options.interface.ts index 4bd6ae88..f8775dba 100644 --- a/src/audiences/interfaces/create-audience-options.interface.ts +++ b/src/audiences/interfaces/create-audience-options.interface.ts @@ -1,5 +1,5 @@ import type { PostOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Audience } from './audience'; export interface CreateAudienceOptions { @@ -13,4 +13,12 @@ export interface CreateAudienceResponseSuccess object: 'audience'; } -export type CreateAudienceResponse = Response; +export type CreateAudienceResponse = + | { + data: CreateAudienceResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/audiences/interfaces/get-audience.interface.ts b/src/audiences/interfaces/get-audience.interface.ts index f229a6f9..9c08efac 100644 --- a/src/audiences/interfaces/get-audience.interface.ts +++ b/src/audiences/interfaces/get-audience.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Audience } from './audience'; export interface GetAudienceResponseSuccess @@ -6,4 +6,12 @@ export interface GetAudienceResponseSuccess object: 'audience'; } -export type GetAudienceResponse = Response; +export type GetAudienceResponse = + | { + data: GetAudienceResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/audiences/interfaces/list-audiences.interface.ts b/src/audiences/interfaces/list-audiences.interface.ts index f898c597..54727fea 100644 --- a/src/audiences/interfaces/list-audiences.interface.ts +++ b/src/audiences/interfaces/list-audiences.interface.ts @@ -1,5 +1,5 @@ import type { PaginationOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Audience } from './audience'; export type ListAudiencesOptions = PaginationOptions; @@ -10,4 +10,12 @@ export type ListAudiencesResponseSuccess = { has_more: boolean; }; -export type ListAudiencesResponse = Response; +export type ListAudiencesResponse = + | { + data: ListAudiencesResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/audiences/interfaces/remove-audience.interface.ts b/src/audiences/interfaces/remove-audience.interface.ts index 97de36a4..e82e0b39 100644 --- a/src/audiences/interfaces/remove-audience.interface.ts +++ b/src/audiences/interfaces/remove-audience.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Audience } from './audience'; export interface RemoveAudiencesResponseSuccess extends Pick { @@ -6,4 +6,12 @@ export interface RemoveAudiencesResponseSuccess extends Pick { deleted: boolean; } -export type RemoveAudiencesResponse = Response; +export type RemoveAudiencesResponse = + | { + data: RemoveAudiencesResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/batch/batch.spec.ts b/src/batch/batch.spec.ts index 6fd398e2..219dc662 100644 --- a/src/batch/batch.spec.ts +++ b/src/batch/batch.spec.ts @@ -64,11 +64,6 @@ describe('Batch', () => { ], }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -212,11 +207,6 @@ describe('Batch', () => { ], }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); diff --git a/src/batch/interfaces/create-batch-options.interface.ts b/src/batch/interfaces/create-batch-options.interface.ts index a6013935..21a368f3 100644 --- a/src/batch/interfaces/create-batch-options.interface.ts +++ b/src/batch/interfaces/create-batch-options.interface.ts @@ -1,13 +1,13 @@ import type { PostOptions } from '../../common/interfaces'; import type { IdempotentRequest } from '../../common/interfaces/idempotent-request.interface'; import type { CreateEmailOptions } from '../../emails/interfaces/create-email-options.interface'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; export type CreateBatchOptions = CreateEmailOptions[]; export interface CreateBatchRequestOptions extends PostOptions, - IdempotentRequest { + IdempotentRequest { /** * @default 'strict' */ @@ -23,21 +23,28 @@ export type CreateBatchSuccessResponse< }[]; } & (Options['batchValidation'] extends 'permissive' ? { + /** + * Only present when header "x-batch-validation" is set to 'permissive'. + */ + errors: { /** - * Only present when header "x-batch-validation" is set to 'permissive'. + * The index of the failed email in the batch */ - errors: { - /** - * The index of the failed email in the batch - */ - index: number; - /** - * The error message for the failed email - */ - message: string; - }[]; // This always being an array depends on us doing https://github.com/resend/resend-api/pull/2025/files#r2303897690 - } + index: number; + /** + * The error message for the failed email + */ + message: string; + }[]; // This always being an array depends on us doing https://github.com/resend/resend-api/pull/2025/files#r2303897690 + } : Record); -export type CreateBatchResponse = - Response>; +export type CreateBatchResponse = + | { + data: CreateBatchSuccessResponse; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/broadcasts/broadcasts.spec.ts b/src/broadcasts/broadcasts.spec.ts index 1302b294..25456d86 100644 --- a/src/broadcasts/broadcasts.spec.ts +++ b/src/broadcasts/broadcasts.spec.ts @@ -1,10 +1,5 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockFetchWithRateLimit, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; import type { CreateBroadcastOptions, CreateBroadcastResponseSuccess, @@ -26,8 +21,10 @@ describe('Broadcasts', () => { message: 'Missing `from` field.', }; - mockErrorResponse(response, { + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, headers: { + 'content-type': 'application/json', Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', }, }); @@ -40,11 +37,6 @@ describe('Broadcasts', () => { "message": "Missing \`from\` field.", "name": "missing_required_field", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -53,8 +45,10 @@ describe('Broadcasts', () => { const response: CreateBroadcastResponseSuccess = { id: '71cdfe68-cf79-473a-a9d7-21f91db6a526', }; - mockSuccessResponse(response, { + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, headers: { + 'content-type': 'application/json', Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', }, }); @@ -73,11 +67,6 @@ describe('Broadcasts', () => { "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -87,8 +76,12 @@ describe('Broadcasts', () => { id: '124dc0f1-e36c-417c-a65c-e33773abc768', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateBroadcastOptions = { @@ -104,11 +97,6 @@ describe('Broadcasts', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -118,8 +106,12 @@ describe('Broadcasts', () => { id: '124dc0f1-e36c-417c-a65c-e33773abc768', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateBroadcastOptions = { @@ -137,11 +129,6 @@ describe('Broadcasts', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -153,8 +140,12 @@ describe('Broadcasts', () => { 'Invalid `from` field. The email address needs to follow the `email@example.com` or `Name ` format', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateBroadcastOptions = { @@ -168,19 +159,14 @@ describe('Broadcasts', () => { const result = resend.broadcasts.create(payload); await expect(result).resolves.toMatchInlineSnapshot(` -{ - "data": null, - "error": { - "message": "Invalid \`from\` field. The email address needs to follow the \`email@example.com\` or \`Name \` format", - "name": "invalid_parameter", - }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, -} -`); + { + "data": null, + "error": { + "message": "Invalid \`from\` field. The email address needs to follow the \`email@example.com\` or \`Name \` format", + "name": "invalid_parameter", + }, + } + `); }); it('returns an error when fetch fails', async () => { @@ -210,7 +196,7 @@ describe('Broadcasts', () => { }); it('returns an error when api responds with text payload', async () => { - mockFetchWithRateLimit('local_rate_limited', { + fetchMock.mockOnce('local_rate_limited', { status: 422, headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823', @@ -244,8 +230,10 @@ describe('Broadcasts', () => { id: randomBroadcastId, }; - mockSuccessResponse(response, { + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, headers: { + 'content-type': 'application/json', Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', }, }); @@ -258,11 +246,6 @@ describe('Broadcasts', () => { "id": "b01e0de9-7c27-4a53-bf38-2e3f98389a65", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -418,8 +401,12 @@ describe('Broadcasts', () => { message: 'Broadcast not found', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -435,11 +422,6 @@ describe('Broadcasts', () => { "message": "Broadcast not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -461,8 +443,12 @@ describe('Broadcasts', () => { sent_at: null, }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -486,11 +472,6 @@ describe('Broadcasts', () => { "subject": "hello world", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -504,8 +485,12 @@ describe('Broadcasts', () => { id, deleted: true, }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -520,11 +505,6 @@ describe('Broadcasts', () => { "object": "broadcast", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -534,8 +514,12 @@ describe('Broadcasts', () => { it('updates a broadcast', async () => { const id = 'b01e0de9-7c27-4a53-bf38-2e3f98389a65'; const response: UpdateBroadcastResponseSuccess = { id }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -548,11 +532,6 @@ describe('Broadcasts', () => { "id": "b01e0de9-7c27-4a53-bf38-2e3f98389a65", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); diff --git a/src/broadcasts/interfaces/create-broadcast-options.interface.ts b/src/broadcasts/interfaces/create-broadcast-options.interface.ts index 6b36357d..69d6940b 100644 --- a/src/broadcasts/interfaces/create-broadcast-options.interface.ts +++ b/src/broadcasts/interfaces/create-broadcast-options.interface.ts @@ -1,7 +1,7 @@ import type * as React from 'react'; import type { PostOptions } from '../../common/interfaces'; import type { RequireAtLeastOne } from '../../common/interfaces/require-at-least-one'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; interface EmailRenderOptions { /** @@ -73,4 +73,12 @@ export interface CreateBroadcastResponseSuccess { id: string; } -export type CreateBroadcastResponse = Response; +export type CreateBroadcastResponse = + | { + data: CreateBroadcastResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/broadcasts/interfaces/get-broadcast.interface.ts b/src/broadcasts/interfaces/get-broadcast.interface.ts index e456f200..2b7b2ac0 100644 --- a/src/broadcasts/interfaces/get-broadcast.interface.ts +++ b/src/broadcasts/interfaces/get-broadcast.interface.ts @@ -1,8 +1,16 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Broadcast } from './broadcast'; export interface GetBroadcastResponseSuccess extends Broadcast { object: 'broadcast'; } -export type GetBroadcastResponse = Response; +export type GetBroadcastResponse = + | { + data: GetBroadcastResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/broadcasts/interfaces/list-broadcasts.interface.ts b/src/broadcasts/interfaces/list-broadcasts.interface.ts index 9b58eaa8..8061427b 100644 --- a/src/broadcasts/interfaces/list-broadcasts.interface.ts +++ b/src/broadcasts/interfaces/list-broadcasts.interface.ts @@ -1,5 +1,5 @@ import type { PaginationOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Broadcast } from './broadcast'; export type ListBroadcastsOptions = PaginationOptions; @@ -19,4 +19,12 @@ export type ListBroadcastsResponseSuccess = { >[]; }; -export type ListBroadcastsResponse = Response; +export type ListBroadcastsResponse = + | { + data: ListBroadcastsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/broadcasts/interfaces/remove-broadcast.interface.ts b/src/broadcasts/interfaces/remove-broadcast.interface.ts index 1b45e8ce..438b4884 100644 --- a/src/broadcasts/interfaces/remove-broadcast.interface.ts +++ b/src/broadcasts/interfaces/remove-broadcast.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Broadcast } from './broadcast'; export interface RemoveBroadcastResponseSuccess extends Pick { @@ -6,4 +6,12 @@ export interface RemoveBroadcastResponseSuccess extends Pick { deleted: boolean; } -export type RemoveBroadcastResponse = Response; +export type RemoveBroadcastResponse = + | { + data: RemoveBroadcastResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/broadcasts/interfaces/send-broadcast-options.interface.ts b/src/broadcasts/interfaces/send-broadcast-options.interface.ts index 1d92a6ae..75f79393 100644 --- a/src/broadcasts/interfaces/send-broadcast-options.interface.ts +++ b/src/broadcasts/interfaces/send-broadcast-options.interface.ts @@ -1,5 +1,5 @@ import type { PostOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; interface SendBroadcastBaseOptions { /** @@ -21,4 +21,12 @@ export interface SendBroadcastResponseSuccess { id: string; } -export type SendBroadcastResponse = Response; +export type SendBroadcastResponse = + | { + data: SendBroadcastResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/broadcasts/interfaces/update-broadcast.interface.ts b/src/broadcasts/interfaces/update-broadcast.interface.ts index 644be559..fa776dd3 100644 --- a/src/broadcasts/interfaces/update-broadcast.interface.ts +++ b/src/broadcasts/interfaces/update-broadcast.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; export interface UpdateBroadcastResponseSuccess { id: string; @@ -16,4 +16,12 @@ export type UpdateBroadcastOptions = { previewText?: string; }; -export type UpdateBroadcastResponse = Response; +export type UpdateBroadcastResponse = + | { + data: UpdateBroadcastResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/contacts.spec.ts b/src/contacts/contacts.spec.ts index 24d96845..c5ddb055 100644 --- a/src/contacts/contacts.spec.ts +++ b/src/contacts/contacts.spec.ts @@ -1,9 +1,5 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; import type { CreateContactOptions, CreateContactResponseSuccess, @@ -36,8 +32,12 @@ describe('Contacts', () => { id: '3deaccfb-f47f-440a-8875-ea14b1716b43', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -50,11 +50,6 @@ describe('Contacts', () => { "object": "contact", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -69,8 +64,12 @@ describe('Contacts', () => { message: 'Missing `email` field.', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -84,11 +83,6 @@ describe('Contacts', () => { "message": "Missing \`email\` field.", "name": "missing_required_field", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -264,8 +258,12 @@ describe('Contacts', () => { message: 'Contact not found', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -283,11 +281,6 @@ describe('Contacts', () => { "message": "Contact not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -304,8 +297,12 @@ describe('Contacts', () => { unsubscribed: false, }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -327,11 +324,6 @@ describe('Contacts', () => { "unsubscribed": false, }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -347,8 +339,12 @@ describe('Contacts', () => { unsubscribed: false, }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -370,11 +366,6 @@ describe('Contacts', () => { "unsubscribed": false, }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -391,8 +382,12 @@ describe('Contacts', () => { id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', object: 'contact', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -406,11 +401,6 @@ describe('Contacts', () => { "object": "contact", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -423,8 +413,12 @@ describe('Contacts', () => { object: 'contact', deleted: true, }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -442,11 +436,6 @@ describe('Contacts', () => { "object": "contact", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -457,8 +446,12 @@ describe('Contacts', () => { object: 'contact', deleted: true, }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -469,20 +462,15 @@ describe('Contacts', () => { await expect( resend.contacts.remove(options), ).resolves.toMatchInlineSnapshot(` -{ - "data": { - "contact": "acme@example.com", - "deleted": true, - "object": "contact", - }, - "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, -} -`); + { + "data": { + "contact": "acme@example.com", + "deleted": true, + "object": "contact", + }, + "error": null, + } + `); }); }); }); diff --git a/src/contacts/contacts.ts b/src/contacts/contacts.ts index 641e0799..8786513b 100644 --- a/src/contacts/contacts.ts +++ b/src/contacts/contacts.ts @@ -62,7 +62,6 @@ export class Contacts { if (!options.id && !options.email) { return { data: null, - rateLimiting: null, error: { message: 'Missing `id` or `email` field.', name: 'missing_required_field', @@ -80,7 +79,6 @@ export class Contacts { if (!options.id && !options.email) { return { data: null, - rateLimiting: null, error: { message: 'Missing `id` or `email` field.', name: 'missing_required_field', @@ -103,7 +101,6 @@ export class Contacts { if (!payload.id && !payload.email) { return { data: null, - rateLimiting: null, error: { message: 'Missing `id` or `email` field.', name: 'missing_required_field', diff --git a/src/contacts/interfaces/create-contact-options.interface.ts b/src/contacts/interfaces/create-contact-options.interface.ts index 49304acd..ff73f25b 100644 --- a/src/contacts/interfaces/create-contact-options.interface.ts +++ b/src/contacts/interfaces/create-contact-options.interface.ts @@ -1,5 +1,5 @@ import type { PostOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Contact } from './contact'; export interface CreateContactOptions { @@ -16,4 +16,12 @@ export interface CreateContactResponseSuccess extends Pick { object: 'contact'; } -export type CreateContactResponse = Response; +export type CreateContactResponse = + | { + data: CreateContactResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/interfaces/get-contact.interface.ts b/src/contacts/interfaces/get-contact.interface.ts index 2ed90a5e..69b9f978 100644 --- a/src/contacts/interfaces/get-contact.interface.ts +++ b/src/contacts/interfaces/get-contact.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Contact, SelectingField } from './contact'; export type GetContactOptions = { @@ -13,4 +13,12 @@ export interface GetContactResponseSuccess object: 'contact'; } -export type GetContactResponse = Response; +export type GetContactResponse = + | { + data: GetContactResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/interfaces/list-contacts.interface.ts b/src/contacts/interfaces/list-contacts.interface.ts index bf210b8d..43fa3118 100644 --- a/src/contacts/interfaces/list-contacts.interface.ts +++ b/src/contacts/interfaces/list-contacts.interface.ts @@ -1,5 +1,5 @@ import type { PaginationOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Contact } from './contact'; export type ListContactsOptions = { @@ -12,4 +12,12 @@ export interface ListContactsResponseSuccess { has_more: boolean; } -export type ListContactsResponse = Response; +export type ListContactsResponse = + | { + data: ListContactsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/interfaces/remove-contact.interface.ts b/src/contacts/interfaces/remove-contact.interface.ts index dd7d117f..59d2e2c0 100644 --- a/src/contacts/interfaces/remove-contact.interface.ts +++ b/src/contacts/interfaces/remove-contact.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { SelectingField } from './contact'; export type RemoveContactsResponseSuccess = { @@ -11,4 +11,12 @@ export type RemoveContactOptions = SelectingField & { audienceId: string; }; -export type RemoveContactsResponse = Response; +export type RemoveContactsResponse = + | { + data: RemoveContactsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/interfaces/update-contact.interface.ts b/src/contacts/interfaces/update-contact.interface.ts index 51a903c4..c5a60ff5 100644 --- a/src/contacts/interfaces/update-contact.interface.ts +++ b/src/contacts/interfaces/update-contact.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Contact, SelectingField } from './contact'; export type UpdateContactOptions = { @@ -12,4 +12,12 @@ export type UpdateContactResponseSuccess = Pick & { object: 'contact'; }; -export type UpdateContactResponse = Response; +export type UpdateContactResponse = + | { + data: UpdateContactResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/domains/domains.spec.ts b/src/domains/domains.spec.ts index 6fbfe377..8e3bb6a1 100644 --- a/src/domains/domains.spec.ts +++ b/src/domains/domains.spec.ts @@ -1,9 +1,5 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; import type { CreateDomainOptions, CreateDomainResponseSuccess, @@ -70,8 +66,12 @@ describe('Domains', () => { ], region: 'us-east-1', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateDomainOptions = { name: 'resend.com' }; @@ -131,11 +131,6 @@ describe('Domains', () => { "status": "not_started", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -146,8 +141,12 @@ describe('Domains', () => { message: 'Missing "name" field', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateDomainOptions = { @@ -165,11 +164,6 @@ describe('Domains', () => { "message": "Missing "name" field", "name": "missing_required_field", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -237,12 +231,57 @@ describe('Domains', () => { resend.domains.create(payload), ).resolves.toMatchInlineSnapshot(` { - "data": null, - "error": { - "message": "Unable to fetch data. The request could not be resolved.", - "name": "application_error", + "data": { + "created_at": "2023-04-07T22:48:33.420498+00:00", + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222", + "name": "resend.com", + "records": [ + { + "name": "bounces", + "priority": 10, + "record": "SPF", + "status": "not_started", + "ttl": "Auto", + "type": "MX", + "value": "feedback-smtp.eu-west-1.com", + }, + { + "name": "bounces", + "record": "SPF", + "status": "not_started", + "ttl": "Auto", + "type": "TXT", + "value": ""v=spf1 include:com ~all"", + }, + { + "name": "nu22pfdfqaxdybogtw3ebaokmalv5mxg._domainkey", + "record": "DKIM", + "status": "not_started", + "ttl": "Auto", + "type": "CNAME", + "value": "nu22pfdfqaxdybogtw3ebaokmalv5mxg.dkim.com.", + }, + { + "name": "qklz5ozk742hhql3vmekdu3pr4f5ggsj._domainkey", + "record": "DKIM", + "status": "not_started", + "ttl": "Auto", + "type": "CNAME", + "value": "qklz5ozk742hhql3vmekdu3pr4f5ggsj.dkim.com.", + }, + { + "name": "eeaemodxoao5hxwjvhywx4bo5mswjw6v._domainkey", + "record": "DKIM", + "status": "not_started", + "ttl": "Auto", + "type": "CNAME", + "value": "eeaemodxoao5hxwjvhywx4bo5mswjw6v.dkim.com.", + }, + ], + "region": "eu-west-1", + "status": "not_started", }, - "rateLimiting": null, + "error": null, } `); }); @@ -253,8 +292,12 @@ describe('Domains', () => { message: 'Region must be "us-east-1" | "eu-west-1" | "sa-east-1"', }; - mockErrorResponse(errorResponse, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(errorResponse), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -271,11 +314,6 @@ describe('Domains', () => { "message": "Region must be "us-east-1" | "eu-west-1" | "sa-east-1"", "name": "invalid_region", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -318,8 +356,12 @@ describe('Domains', () => { region: 'us-east-1', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateDomainOptions = { @@ -367,11 +409,6 @@ describe('Domains', () => { "status": "not_started", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -524,8 +561,12 @@ describe('Domains', () => { message: 'Domain not found', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -539,11 +580,6 @@ describe('Domains', () => { "message": "Domain not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -587,8 +623,12 @@ describe('Domains', () => { ], }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -631,11 +671,6 @@ describe('Domains', () => { "status": "not_started", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -649,8 +684,12 @@ describe('Domains', () => { id, }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -667,11 +706,6 @@ describe('Domains', () => { "object": "domain", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -684,8 +718,12 @@ describe('Domains', () => { object: 'domain', id, }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -697,11 +735,6 @@ describe('Domains', () => { "object": "domain", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -715,8 +748,12 @@ describe('Domains', () => { id, deleted: true, }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); @@ -729,11 +766,6 @@ describe('Domains', () => { "object": "domain", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); diff --git a/src/domains/interfaces/create-domain-options.interface.ts b/src/domains/interfaces/create-domain-options.interface.ts index ce278e95..13ba9562 100644 --- a/src/domains/interfaces/create-domain-options.interface.ts +++ b/src/domains/interfaces/create-domain-options.interface.ts @@ -1,5 +1,5 @@ import type { PostOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Domain, DomainRecords, DomainRegion } from './domain'; export interface CreateDomainOptions { @@ -15,4 +15,12 @@ export interface CreateDomainResponseSuccess records: DomainRecords[]; } -export type CreateDomainResponse = Response; +export type CreateDomainResponse = + | { + data: CreateDomainResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/domains/interfaces/get-domain.interface.ts b/src/domains/interfaces/get-domain.interface.ts index 1b86eab3..6c79410f 100644 --- a/src/domains/interfaces/get-domain.interface.ts +++ b/src/domains/interfaces/get-domain.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Domain, DomainRecords } from './domain'; export interface GetDomainResponseSuccess @@ -7,4 +7,12 @@ export interface GetDomainResponseSuccess records: DomainRecords[]; } -export type GetDomainResponse = Response; +export type GetDomainResponse = + | { + data: GetDomainResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/domains/interfaces/list-domains.interface.ts b/src/domains/interfaces/list-domains.interface.ts index 8a66ff1d..aafff6c7 100644 --- a/src/domains/interfaces/list-domains.interface.ts +++ b/src/domains/interfaces/list-domains.interface.ts @@ -1,5 +1,5 @@ import type { PaginationOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Domain } from './domain'; export type ListDomainsOptions = PaginationOptions; @@ -10,4 +10,12 @@ export type ListDomainsResponseSuccess = { has_more: boolean; }; -export type ListDomainsResponse = Response; +export type ListDomainsResponse = + | { + data: ListDomainsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/domains/interfaces/remove-domain.interface.ts b/src/domains/interfaces/remove-domain.interface.ts index aa1cb266..7c11f64d 100644 --- a/src/domains/interfaces/remove-domain.interface.ts +++ b/src/domains/interfaces/remove-domain.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Domain } from './domain'; export type RemoveDomainsResponseSuccess = Pick & { @@ -6,4 +6,12 @@ export type RemoveDomainsResponseSuccess = Pick & { deleted: boolean; }; -export type RemoveDomainsResponse = Response; +export type RemoveDomainsResponse = + | { + data: RemoveDomainsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/domains/interfaces/update-domain.interface.ts b/src/domains/interfaces/update-domain.interface.ts index 9b36b90e..c07333eb 100644 --- a/src/domains/interfaces/update-domain.interface.ts +++ b/src/domains/interfaces/update-domain.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Domain } from './domain'; export interface UpdateDomainsOptions { @@ -12,4 +12,12 @@ export type UpdateDomainsResponseSuccess = Pick & { object: 'domain'; }; -export type UpdateDomainsResponse = Response; +export type UpdateDomainsResponse = + | { + data: UpdateDomainsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/domains/interfaces/verify-domain.interface.ts b/src/domains/interfaces/verify-domain.interface.ts index 6bb420b9..069b9193 100644 --- a/src/domains/interfaces/verify-domain.interface.ts +++ b/src/domains/interfaces/verify-domain.interface.ts @@ -1,8 +1,16 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { Domain } from './domain'; export type VerifyDomainsResponseSuccess = Pick & { object: 'domain'; }; -export type VerifyDomainsResponse = Response; +export type VerifyDomainsResponse = + | { + data: VerifyDomainsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/emails/emails.spec.ts b/src/emails/emails.spec.ts index 2acf2523..50b1f8f2 100644 --- a/src/emails/emails.spec.ts +++ b/src/emails/emails.spec.ts @@ -1,10 +1,5 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockFetchWithRateLimit, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; import type { CreateEmailOptions, CreateEmailResponseSuccess, @@ -24,8 +19,10 @@ describe('Emails', () => { message: 'Missing `from` field.', }; - mockErrorResponse(response, { + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, headers: { + 'content-type': 'application/json', Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', }, }); @@ -39,11 +36,6 @@ describe('Emails', () => { "message": "Missing \`from\` field.", "name": "missing_required_field", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -53,8 +45,10 @@ describe('Emails', () => { id: 'not-idempotent-123', }; - mockSuccessResponse(response, { + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, headers: { + 'content-type': 'application/json', Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', }, }); @@ -83,8 +77,10 @@ describe('Emails', () => { id: 'idempotent-123', }; - mockSuccessResponse(response, { + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, headers: { + 'content-type': 'application/json', Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', }, }); @@ -114,8 +110,10 @@ describe('Emails', () => { const response: CreateEmailResponseSuccess = { id: '71cdfe68-cf79-473a-a9d7-21f91db6a526', }; - mockSuccessResponse(response, { + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, headers: { + 'content-type': 'application/json', Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', }, }); @@ -134,11 +132,6 @@ describe('Emails', () => { "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -148,8 +141,12 @@ describe('Emails', () => { id: '124dc0f1-e36c-417c-a65c-e33773abc768', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateEmailOptions = { @@ -165,11 +162,6 @@ describe('Emails', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -179,8 +171,12 @@ describe('Emails', () => { id: '124dc0f1-e36c-417c-a65c-e33773abc768', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateEmailOptions = { @@ -198,11 +194,6 @@ describe('Emails', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -212,8 +203,12 @@ describe('Emails', () => { id: '124dc0f1-e36c-417c-a65c-e33773abc768', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateEmailOptions = { @@ -231,11 +226,6 @@ describe('Emails', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -245,8 +235,12 @@ describe('Emails', () => { id: '124dc0f1-e36c-417c-a65c-e33773abc768', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateEmailOptions = { @@ -264,11 +258,6 @@ describe('Emails', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -278,8 +267,12 @@ describe('Emails', () => { id: '124dc0f1-e36c-417c-a65c-e33773abc768', }; - mockSuccessResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateEmailOptions = { @@ -299,11 +292,6 @@ describe('Emails', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -315,8 +303,12 @@ describe('Emails', () => { 'Invalid `from` field. The email address needs to follow the `email@example.com` or `Name ` format', }; - mockErrorResponse(response, { - headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, }); const payload: CreateEmailOptions = { @@ -330,19 +322,14 @@ describe('Emails', () => { const result = resend.emails.send(payload); await expect(result).resolves.toMatchInlineSnapshot(` -{ - "data": null, - "error": { - "message": "Invalid \`from\` field. The email address needs to follow the \`email@example.com\` or \`Name \` format", - "name": "invalid_parameter", - }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, -} -`); + { + "data": null, + "error": { + "message": "Invalid \`from\` field. The email address needs to follow the \`email@example.com\` or \`Name \` format", + "name": "invalid_parameter", + }, + } + `); }); it('returns an error when fetch fails', async () => { @@ -372,7 +359,7 @@ describe('Emails', () => { }); it('returns an error when api responds with text payload', async () => { - mockFetchWithRateLimit('local_rate_limited', { + fetchMock.mockOnce('local_rate_limited', { status: 422, headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823', @@ -407,8 +394,10 @@ describe('Emails', () => { message: 'Email not found', }; - mockErrorResponse(response, { + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, headers: { + 'content-type': 'application/json', Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', }, }); @@ -424,11 +413,6 @@ describe('Emails', () => { "message": "Email not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -452,8 +436,10 @@ describe('Emails', () => { scheduled_at: null, }; - mockSuccessResponse(response, { + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, headers: { + 'content-type': 'application/json', Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', }, }); @@ -480,11 +466,6 @@ describe('Emails', () => { ], }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -506,8 +487,10 @@ describe('Emails', () => { scheduled_at: null, }; - mockSuccessResponse(response, { + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, headers: { + 'content-type': 'application/json', Authorization: 'Bearer re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', }, }); @@ -537,11 +520,6 @@ describe('Emails', () => { ], }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); diff --git a/src/emails/interfaces/cancel-email-options.interface.ts b/src/emails/interfaces/cancel-email-options.interface.ts index e7ff6ff8..c34fbf2d 100644 --- a/src/emails/interfaces/cancel-email-options.interface.ts +++ b/src/emails/interfaces/cancel-email-options.interface.ts @@ -1,8 +1,16 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; export interface CancelEmailResponseSuccess { object: 'email'; id: string; } -export type CancelEmailResponse = Response; +export type CancelEmailResponse = + | { + data: CancelEmailResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/emails/interfaces/create-email-options.interface.ts b/src/emails/interfaces/create-email-options.interface.ts index 509ff88f..11db678a 100644 --- a/src/emails/interfaces/create-email-options.interface.ts +++ b/src/emails/interfaces/create-email-options.interface.ts @@ -2,7 +2,7 @@ import type * as React from 'react'; import type { PostOptions } from '../../common/interfaces'; import type { IdempotentRequest } from '../../common/interfaces/idempotent-request.interface'; import type { RequireAtLeastOne } from '../../common/interfaces/require-at-least-one'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; interface EmailRenderOptions { /** @@ -101,7 +101,15 @@ export interface CreateEmailResponseSuccess { id: string; } -export type CreateEmailResponse = Response; +export type CreateEmailResponse = + | { + data: CreateEmailResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; export interface Attachment { /** Content of an attached file. */ diff --git a/src/emails/interfaces/get-email-options.interface.ts b/src/emails/interfaces/get-email-options.interface.ts index 4c840a4a..1d83248a 100644 --- a/src/emails/interfaces/get-email-options.interface.ts +++ b/src/emails/interfaces/get-email-options.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; export interface GetEmailResponseSuccess { bcc: string[] | null; @@ -28,4 +28,12 @@ export interface GetEmailResponseSuccess { object: 'email'; } -export type GetEmailResponse = Response; +export type GetEmailResponse = + | { + data: GetEmailResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/emails/interfaces/update-email-options.interface.ts b/src/emails/interfaces/update-email-options.interface.ts index b3184539..89d04637 100644 --- a/src/emails/interfaces/update-email-options.interface.ts +++ b/src/emails/interfaces/update-email-options.interface.ts @@ -1,4 +1,4 @@ -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; export interface UpdateEmailOptions { id: string; @@ -10,4 +10,12 @@ export interface UpdateEmailResponseSuccess { object: 'email'; } -export type UpdateEmailResponse = Response; +export type UpdateEmailResponse = + | { + data: UpdateEmailResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/interfaces.ts b/src/interfaces.ts index 15861c5e..f3843d9e 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,5 +1,3 @@ -import type { RateLimit } from './rate-limiting'; - export const RESEND_ERROR_CODES_BY_KEY = { missing_required_field: 422, invalid_idempotency_key: 400, @@ -21,32 +19,9 @@ export const RESEND_ERROR_CODES_BY_KEY = { export type RESEND_ERROR_CODE_KEY = keyof typeof RESEND_ERROR_CODES_BY_KEY; -export type RateLimitExceededErrorResponse = { +export interface ErrorResponse { message: string; - name: Extract; - /** - * Time in seconds. - */ - retryAfter: number; -}; - -export type ErrorResponse = - | { - message: string; - name: Exclude; - } - | RateLimitExceededErrorResponse; - -export type Response = - | { - data: Data; - rateLimiting: RateLimit; - error: null; - } - | { - data: null; - rateLimiting: RateLimit | null; - error: ErrorResponse; - }; + name: RESEND_ERROR_CODE_KEY; +} export type Tag = { name: string; value: string }; diff --git a/src/rate-limiting.ts b/src/rate-limiting.ts deleted file mode 100644 index 31ee1537..00000000 --- a/src/rate-limiting.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { RateLimitExceededErrorResponse } from './interfaces'; - -export type RateLimit = { - /** - * The maximum amount of requests that can be made in the time window of {@link RateLimit.shouldResetAfter}. - */ - limit: number; - /** - * The amount of requests that can still be made before hitting {@link RateLimit.limit}. - * - * Resets after the seconds in {@link RateLimit.shouldResetAfter} go by. - */ - remainingRequests: number; - /** - * The number of seconds after which the rate limiting will reset, - * and {@link RateLimit.remainingRequests} goes back to the value of - * {@link RateLimit.limit}. - * - * @see {@link RateLimitExceededErrorResponse.retryAfter} - */ - shouldResetAfter: number; -}; - -export function parseRateLimit(headers: Headers): RateLimit { - const limitHeader = headers.get('ratelimit-limit'); - const remainingHeader = headers.get('ratelimit-remaining'); - const resetHeader = headers.get('ratelimit-reset'); - - if (!limitHeader || !remainingHeader || !resetHeader) { - throw new Error( - "The rate limit headers are not present in the response, something must've gone wrong, please email us at support@resend.com", - ); - } - - const limit = Number.parseInt(limitHeader, 10); - const remaining = Number.parseInt(remainingHeader, 10); - const reset = Number.parseInt(resetHeader, 10); - - return { - limit, - remainingRequests: remaining, - shouldResetAfter: reset, - }; -} diff --git a/src/resend.ts b/src/resend.ts index 9b90c983..4942d151 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -9,8 +9,7 @@ import type { PatchOptions } from './common/interfaces/patch-option.interface'; import { Contacts } from './contacts/contacts'; import { Domains } from './domains/domains'; import { Emails } from './emails/emails'; -import type { ErrorResponse, Response } from './interfaces'; -import { parseRateLimit } from './rate-limiting'; +import type { ErrorResponse } from './interfaces'; const defaultBaseUrl = 'https://api.resend.com'; const defaultUserAgent = `resend-node:${version}`; @@ -54,28 +53,21 @@ export class Resend { }); } - async fetchRequest(path: string, options = {}): Promise> { + async fetchRequest( + path: string, + options = {}, + ): Promise<{ data: T; error: null } | { data: null; error: ErrorResponse }> { try { const response = await fetch(`${baseUrl}${path}`, options); - const rateLimiting = parseRateLimit(response.headers); - if (!response.ok) { try { const rawError = await response.text(); - const error: ErrorResponse = JSON.parse(rawError); - if (error.name === 'rate_limit_exceeded' && response.status === 429) { - const retryAfterHeader = response.headers.get('retry-after'); - if (retryAfterHeader) { - error.retryAfter = Number.parseInt(retryAfterHeader, 10); - } - } - return { data: null, rateLimiting, error }; + return { data: null, error: JSON.parse(rawError) }; } catch (err) { if (err instanceof SyntaxError) { return { data: null, - rateLimiting, error: { name: 'application_error', message: @@ -90,23 +82,18 @@ export class Resend { }; if (err instanceof Error) { - return { - data: null, - rateLimiting: rateLimiting, - error: { ...error, message: err.message }, - }; + return { data: null, error: { ...error, message: err.message } }; } - return { data: null, rateLimiting, error }; + return { data: null, error }; } } const data = await response.json(); - return { data, rateLimiting, error: null }; + return { data, error: null }; } catch { return { data: null, - rateLimiting: null, error: { name: 'application_error', message: 'Unable to fetch data. The request could not be resolved.', From 2e5ccb5cfcccbe2966dcbd724c687e4f642fb308 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Fri, 5 Sep 2025 12:21:15 -0300 Subject: [PATCH 2/3] fix remaining types, fetch mocking, and tests --- src/api-keys/api-keys.spec.ts | 21 +----------------- src/audiences/audiences.spec.ts | 21 +----------------- .../create-batch-options.interface.ts | 4 ++-- src/broadcasts/broadcasts.spec.ts | 21 +----------------- src/contacts/contacts.spec.ts | 22 ++----------------- src/domains/domains.spec.ts | 21 +----------------- src/emails/emails.spec.ts | 21 +----------------- .../list-emails-options.interface.ts | 13 +++++++++-- src/test-utils/mock-fetch.ts | 21 ------------------ 9 files changed, 20 insertions(+), 145 deletions(-) diff --git a/src/api-keys/api-keys.spec.ts b/src/api-keys/api-keys.spec.ts index 2bc90d1c..2da88b91 100644 --- a/src/api-keys/api-keys.spec.ts +++ b/src/api-keys/api-keys.spec.ts @@ -1,5 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateApiKeyOptions, CreateApiKeyResponseSuccess, @@ -282,11 +283,6 @@ describe('API Keys', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -310,11 +306,6 @@ describe('API Keys', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -339,11 +330,6 @@ describe('API Keys', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -368,11 +354,6 @@ describe('API Keys', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( diff --git a/src/audiences/audiences.spec.ts b/src/audiences/audiences.spec.ts index 4b9b0f61..1ff01153 100644 --- a/src/audiences/audiences.spec.ts +++ b/src/audiences/audiences.spec.ts @@ -1,5 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateAudienceOptions, CreateAudienceResponseSuccess, @@ -104,11 +105,6 @@ describe('Audiences', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -132,11 +128,6 @@ describe('Audiences', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -161,11 +152,6 @@ describe('Audiences', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -190,11 +176,6 @@ describe('Audiences', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( diff --git a/src/batch/interfaces/create-batch-options.interface.ts b/src/batch/interfaces/create-batch-options.interface.ts index 21a368f3..627bf693 100644 --- a/src/batch/interfaces/create-batch-options.interface.ts +++ b/src/batch/interfaces/create-batch-options.interface.ts @@ -39,9 +39,9 @@ export type CreateBatchSuccessResponse< } : Record); -export type CreateBatchResponse = +export type CreateBatchResponse = | { - data: CreateBatchSuccessResponse; + data: CreateBatchSuccessResponse; error: null; } | { diff --git a/src/broadcasts/broadcasts.spec.ts b/src/broadcasts/broadcasts.spec.ts index 25456d86..2826232c 100644 --- a/src/broadcasts/broadcasts.spec.ts +++ b/src/broadcasts/broadcasts.spec.ts @@ -1,5 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateBroadcastOptions, CreateBroadcastResponseSuccess, @@ -289,11 +290,6 @@ describe('Broadcasts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -317,11 +313,6 @@ describe('Broadcasts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -346,11 +337,6 @@ describe('Broadcasts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -375,11 +361,6 @@ describe('Broadcasts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( diff --git a/src/contacts/contacts.spec.ts b/src/contacts/contacts.spec.ts index c5ddb055..1ea77246 100644 --- a/src/contacts/contacts.spec.ts +++ b/src/contacts/contacts.spec.ts @@ -1,5 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateContactOptions, CreateContactResponseSuccess, @@ -116,6 +117,7 @@ describe('Contacts', () => { }, ], }; + mockSuccessResponse(response, { headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, }); @@ -126,11 +128,6 @@ describe('Contacts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -172,11 +169,6 @@ describe('Contacts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -202,11 +194,6 @@ describe('Contacts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -232,11 +219,6 @@ describe('Contacts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( diff --git a/src/domains/domains.spec.ts b/src/domains/domains.spec.ts index 8e3bb6a1..ebfb3943 100644 --- a/src/domains/domains.spec.ts +++ b/src/domains/domains.spec.ts @@ -1,5 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateDomainOptions, CreateDomainResponseSuccess, @@ -449,11 +450,6 @@ describe('Domains', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -477,11 +473,6 @@ describe('Domains', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -506,11 +497,6 @@ describe('Domains', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -535,11 +521,6 @@ describe('Domains', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( diff --git a/src/emails/emails.spec.ts b/src/emails/emails.spec.ts index 50b1f8f2..bdbbb6a6 100644 --- a/src/emails/emails.spec.ts +++ b/src/emails/emails.spec.ts @@ -1,5 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateEmailOptions, CreateEmailResponseSuccess, @@ -559,11 +560,6 @@ describe('Emails', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock.mock.calls[0][0]).toBe( 'https://api.resend.com/emails', @@ -578,11 +574,6 @@ describe('Emails', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock.mock.calls[0][0]).toBe( 'https://api.resend.com/emails?limit=10', @@ -595,11 +586,6 @@ describe('Emails', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock.mock.calls[0][0]).toBe( 'https://api.resend.com/emails?after=cursor123', @@ -612,11 +598,6 @@ describe('Emails', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock.mock.calls[0][0]).toBe( 'https://api.resend.com/emails?before=cursor123', diff --git a/src/emails/interfaces/list-emails-options.interface.ts b/src/emails/interfaces/list-emails-options.interface.ts index 2ed99ddd..8bc5be21 100644 --- a/src/emails/interfaces/list-emails-options.interface.ts +++ b/src/emails/interfaces/list-emails-options.interface.ts @@ -1,5 +1,5 @@ import type { PaginationOptions } from '../../common/interfaces'; -import type { Response } from '../../interfaces'; +import type { ErrorResponse } from '../../interfaces'; import type { GetEmailResponseSuccess } from './get-email-options.interface'; export type ListEmailsOptions = PaginationOptions; @@ -16,4 +16,13 @@ export type ListEmailsResponseSuccess = { data: ListEmail[]; }; -export type ListEmailsResponse = Response; +export type ListEmailsResponse = + | { + data: ListEmailsResponseSuccess; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; +; diff --git a/src/test-utils/mock-fetch.ts b/src/test-utils/mock-fetch.ts index 6a276a46..1a939f1e 100644 --- a/src/test-utils/mock-fetch.ts +++ b/src/test-utils/mock-fetch.ts @@ -1,11 +1,6 @@ import type { MockParams } from 'vitest-fetch-mock'; export interface MockFetchOptions extends MockParams { - rateLimiting?: { - limit?: number; - remaining?: number; - reset?: number; - }; } /** @@ -16,29 +11,13 @@ export function mockFetchWithRateLimit( options: MockFetchOptions = {}, ): void { const { - rateLimiting = {}, headers = {}, status = 200, ...restOptions } = options; - const defaultRateLimit = { - limit: 2, - remaining: 2, - reset: 1, // Fixed timestamp for consistent tests - }; - - const rateLimitHeaders = { - 'ratelimit-limit': String(rateLimiting.limit ?? defaultRateLimit.limit), - 'ratelimit-remaining': String( - rateLimiting.remaining ?? defaultRateLimit.remaining, - ), - 'ratelimit-reset': String(rateLimiting.reset ?? defaultRateLimit.reset), - }; - const allHeaders = { 'content-type': 'application/json', - ...rateLimitHeaders, ...headers, }; From 0f9e82285935a03141b7f480b2ee23d19d0fdbb2 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Fri, 5 Sep 2025 12:30:03 -0300 Subject: [PATCH 3/3] lint --- .../create-batch-options.interface.ts | 38 +++++++++---------- src/contacts/contacts.spec.ts | 2 +- .../list-emails-options.interface.ts | 3 +- src/test-utils/mock-fetch.ts | 9 +---- 4 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/batch/interfaces/create-batch-options.interface.ts b/src/batch/interfaces/create-batch-options.interface.ts index 627bf693..4e7e4c94 100644 --- a/src/batch/interfaces/create-batch-options.interface.ts +++ b/src/batch/interfaces/create-batch-options.interface.ts @@ -7,7 +7,7 @@ export type CreateBatchOptions = CreateEmailOptions[]; export interface CreateBatchRequestOptions extends PostOptions, - IdempotentRequest { + IdempotentRequest { /** * @default 'strict' */ @@ -23,28 +23,28 @@ export type CreateBatchSuccessResponse< }[]; } & (Options['batchValidation'] extends 'permissive' ? { - /** - * Only present when header "x-batch-validation" is set to 'permissive'. - */ - errors: { /** - * The index of the failed email in the batch + * Only present when header "x-batch-validation" is set to 'permissive'. */ - index: number; - /** - * The error message for the failed email - */ - message: string; - }[]; // This always being an array depends on us doing https://github.com/resend/resend-api/pull/2025/files#r2303897690 - } + errors: { + /** + * The index of the failed email in the batch + */ + index: number; + /** + * The error message for the failed email + */ + message: string; + }[]; // This always being an array depends on us doing https://github.com/resend/resend-api/pull/2025/files#r2303897690 + } : Record); export type CreateBatchResponse = | { - data: CreateBatchSuccessResponse; - error: null; - } + data: CreateBatchSuccessResponse; + error: null; + } | { - data: null; - error: ErrorResponse; - }; + data: null; + error: ErrorResponse; + }; diff --git a/src/contacts/contacts.spec.ts b/src/contacts/contacts.spec.ts index 1ea77246..7ad5cf4d 100644 --- a/src/contacts/contacts.spec.ts +++ b/src/contacts/contacts.spec.ts @@ -117,7 +117,7 @@ describe('Contacts', () => { }, ], }; - + mockSuccessResponse(response, { headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, }); diff --git a/src/emails/interfaces/list-emails-options.interface.ts b/src/emails/interfaces/list-emails-options.interface.ts index 8bc5be21..af531882 100644 --- a/src/emails/interfaces/list-emails-options.interface.ts +++ b/src/emails/interfaces/list-emails-options.interface.ts @@ -16,7 +16,7 @@ export type ListEmailsResponseSuccess = { data: ListEmail[]; }; -export type ListEmailsResponse = +export type ListEmailsResponse = | { data: ListEmailsResponseSuccess; error: null; @@ -25,4 +25,3 @@ export type ListEmailsResponse = data: null; error: ErrorResponse; }; -; diff --git a/src/test-utils/mock-fetch.ts b/src/test-utils/mock-fetch.ts index 1a939f1e..e27838eb 100644 --- a/src/test-utils/mock-fetch.ts +++ b/src/test-utils/mock-fetch.ts @@ -1,7 +1,6 @@ import type { MockParams } from 'vitest-fetch-mock'; -export interface MockFetchOptions extends MockParams { -} +export interface MockFetchOptions extends MockParams {} /** * Mock fetch response with rate limiting headers included by default @@ -10,11 +9,7 @@ export function mockFetchWithRateLimit( body: string, options: MockFetchOptions = {}, ): void { - const { - headers = {}, - status = 200, - ...restOptions - } = options; + const { headers = {}, status = 200, ...restOptions } = options; const allHeaders = { 'content-type': 'application/json',