diff --git a/src/api-keys/api-keys.spec.ts b/src/api-keys/api-keys.spec.ts index 41a14bf8..2da88b91 100644 --- a/src/api-keys/api-keys.spec.ts +++ b/src/api-keys/api-keys.spec.ts @@ -1,9 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateApiKeyOptions, CreateApiKeyResponseSuccess, @@ -22,8 +19,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 +38,6 @@ describe('API Keys', () => { "token": "re_PKr4RCko_Lhm9ost2YjNCctnPjbLw8Nqk", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -55,8 +51,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 +70,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 +86,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 +105,6 @@ describe('API Keys', () => { "token": "re_PKr4RCko_Lhm9ost2YjNCctnPjbLw8Nqk", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -125,8 +119,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 +138,6 @@ describe('API Keys', () => { "token": "re_PKr4RCko_Lhm9ost2YjNCctnPjbLw8Nqk", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -155,8 +148,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 +172,6 @@ describe('API Keys', () => { "message": "Access must be "full_access" | "sending_access"", "name": "invalid_access", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -291,11 +283,6 @@ describe('API Keys', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -319,11 +306,6 @@ describe('API Keys', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -348,11 +330,6 @@ describe('API Keys', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -377,11 +354,6 @@ describe('API Keys', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -400,8 +372,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 +386,6 @@ describe('API Keys', () => { { "data": {}, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -425,8 +396,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 +415,6 @@ describe('API Keys', () => { "message": "Something went wrong", "name": "application_error", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -455,8 +425,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 +446,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..1ff01153 100644 --- a/src/audiences/audiences.spec.ts +++ b/src/audiences/audiences.spec.ts @@ -1,9 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateAudienceOptions, CreateAudienceResponseSuccess, @@ -24,8 +21,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 +40,6 @@ describe('Audiences', () => { "object": "audience", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -55,8 +51,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 +70,6 @@ describe('Audiences', () => { "message": "Missing "name" field", "name": "missing_required_field", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -110,11 +105,6 @@ describe('Audiences', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -138,11 +128,6 @@ describe('Audiences', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -167,11 +152,6 @@ describe('Audiences', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -196,11 +176,6 @@ describe('Audiences', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -222,8 +197,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 +216,6 @@ describe('Audiences', () => { "message": "Audience not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -255,8 +229,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 +250,6 @@ describe('Audiences', () => { "object": "audience", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -290,8 +263,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 +281,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..4e7e4c94 100644 --- a/src/batch/interfaces/create-batch-options.interface.ts +++ b/src/batch/interfaces/create-batch-options.interface.ts @@ -1,7 +1,7 @@ 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[]; @@ -40,4 +40,11 @@ export type CreateBatchSuccessResponse< : Record); export type CreateBatchResponse = - Response>; + | { + data: CreateBatchSuccessResponse; + error: null; + } + | { + data: null; + error: ErrorResponse; + }; diff --git a/src/broadcasts/broadcasts.spec.ts b/src/broadcasts/broadcasts.spec.ts index 1302b294..2826232c 100644 --- a/src/broadcasts/broadcasts.spec.ts +++ b/src/broadcasts/broadcasts.spec.ts @@ -1,10 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockFetchWithRateLimit, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateBroadcastOptions, CreateBroadcastResponseSuccess, @@ -26,8 +22,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 +38,6 @@ describe('Broadcasts', () => { "message": "Missing \`from\` field.", "name": "missing_required_field", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -53,8 +46,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 +68,6 @@ describe('Broadcasts', () => { "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -87,8 +77,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 +98,6 @@ describe('Broadcasts', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -118,8 +107,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 +130,6 @@ describe('Broadcasts', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -153,8 +141,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 +160,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 +197,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 +231,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 +247,6 @@ describe('Broadcasts', () => { "id": "b01e0de9-7c27-4a53-bf38-2e3f98389a65", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -306,11 +290,6 @@ describe('Broadcasts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -334,11 +313,6 @@ describe('Broadcasts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -363,11 +337,6 @@ describe('Broadcasts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -392,11 +361,6 @@ describe('Broadcasts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -418,8 +382,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 +403,6 @@ describe('Broadcasts', () => { "message": "Broadcast not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -461,8 +424,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 +453,6 @@ describe('Broadcasts', () => { "subject": "hello world", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -504,8 +466,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 +486,6 @@ describe('Broadcasts', () => { "object": "broadcast", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -534,8 +495,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 +513,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..7ad5cf4d 100644 --- a/src/contacts/contacts.spec.ts +++ b/src/contacts/contacts.spec.ts @@ -1,9 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateContactOptions, CreateContactResponseSuccess, @@ -36,8 +33,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 +51,6 @@ describe('Contacts', () => { "object": "contact", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -69,8 +65,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 +84,6 @@ describe('Contacts', () => { "message": "Missing \`email\` field.", "name": "missing_required_field", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -122,6 +117,7 @@ describe('Contacts', () => { }, ], }; + mockSuccessResponse(response, { headers: { Authorization: 'Bearer re_924b3rjh2387fbewf823' }, }); @@ -132,11 +128,6 @@ describe('Contacts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -178,11 +169,6 @@ describe('Contacts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -208,11 +194,6 @@ describe('Contacts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -238,11 +219,6 @@ describe('Contacts', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -264,8 +240,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 +263,6 @@ describe('Contacts', () => { "message": "Contact not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -304,8 +279,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 +306,6 @@ describe('Contacts', () => { "unsubscribed": false, }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -347,8 +321,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 +348,6 @@ describe('Contacts', () => { "unsubscribed": false, }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -391,8 +364,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 +383,6 @@ describe('Contacts', () => { "object": "contact", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -423,8 +395,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 +418,6 @@ describe('Contacts', () => { "object": "contact", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -457,8 +428,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 +444,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..ebfb3943 100644 --- a/src/domains/domains.spec.ts +++ b/src/domains/domains.spec.ts @@ -1,9 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateDomainOptions, CreateDomainResponseSuccess, @@ -70,8 +67,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 +132,6 @@ describe('Domains', () => { "status": "not_started", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -146,8 +142,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 +165,6 @@ describe('Domains', () => { "message": "Missing "name" field", "name": "missing_required_field", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -237,12 +232,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 +293,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 +315,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 +357,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 +410,6 @@ describe('Domains', () => { "status": "not_started", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -412,11 +450,6 @@ describe('Domains', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -440,11 +473,6 @@ describe('Domains', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -469,11 +497,6 @@ describe('Domains', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -498,11 +521,6 @@ describe('Domains', () => { expect(result).toEqual({ data: response, error: null, - rateLimiting: { - limit: 2, - remainingRequests: 2, - shouldResetAfter: 1, - }, }); expect(fetchMock).toHaveBeenCalledWith( @@ -524,8 +542,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 +561,6 @@ describe('Domains', () => { "message": "Domain not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -587,8 +604,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 +652,6 @@ describe('Domains', () => { "status": "not_started", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -649,8 +665,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 +687,6 @@ describe('Domains', () => { "object": "domain", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -684,8 +699,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 +716,6 @@ describe('Domains', () => { "object": "domain", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -715,8 +729,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 +747,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..bdbbb6a6 100644 --- a/src/emails/emails.spec.ts +++ b/src/emails/emails.spec.ts @@ -1,10 +1,6 @@ import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; -import { - mockErrorResponse, - mockFetchWithRateLimit, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; import type { CreateEmailOptions, CreateEmailResponseSuccess, @@ -24,8 +20,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 +37,6 @@ describe('Emails', () => { "message": "Missing \`from\` field.", "name": "missing_required_field", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -53,8 +46,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 +78,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 +111,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 +133,6 @@ describe('Emails', () => { "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -148,8 +142,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 +163,6 @@ describe('Emails', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -179,8 +172,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 +195,6 @@ describe('Emails', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -212,8 +204,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 +227,6 @@ describe('Emails', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -245,8 +236,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 +259,6 @@ describe('Emails', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -278,8 +268,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 +293,6 @@ describe('Emails', () => { "id": "124dc0f1-e36c-417c-a65c-e33773abc768", }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -315,8 +304,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 +323,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 +360,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 +395,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 +414,6 @@ describe('Emails', () => { "message": "Email not found", "name": "not_found", }, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -452,8 +437,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 +467,6 @@ describe('Emails', () => { ], }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -506,8 +488,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 +521,6 @@ describe('Emails', () => { ], }, "error": null, - "rateLimiting": { - "limit": 2, - "remainingRequests": 2, - "shouldResetAfter": 1, - }, } `); }); @@ -581,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', @@ -600,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', @@ -617,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', @@ -634,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/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/list-emails-options.interface.ts b/src/emails/interfaces/list-emails-options.interface.ts index 2ed99ddd..af531882 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,12 @@ export type ListEmailsResponseSuccess = { data: ListEmail[]; }; -export type ListEmailsResponse = Response; +export type ListEmailsResponse = + | { + data: ListEmailsResponseSuccess; + 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.', diff --git a/src/test-utils/mock-fetch.ts b/src/test-utils/mock-fetch.ts index 6a276a46..e27838eb 100644 --- a/src/test-utils/mock-fetch.ts +++ b/src/test-utils/mock-fetch.ts @@ -1,12 +1,6 @@ import type { MockParams } from 'vitest-fetch-mock'; -export interface MockFetchOptions extends MockParams { - rateLimiting?: { - limit?: number; - remaining?: number; - reset?: number; - }; -} +export interface MockFetchOptions extends MockParams {} /** * Mock fetch response with rate limiting headers included by default @@ -15,30 +9,10 @@ export function mockFetchWithRateLimit( body: string, 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 { headers = {}, status = 200, ...restOptions } = options; const allHeaders = { 'content-type': 'application/json', - ...rateLimitHeaders, ...headers, };