From c4b5a80be37ef417e5aef43295ea6211cb913274 Mon Sep 17 00:00:00 2001 From: Bu Kinoshita Date: Wed, 15 Nov 2023 20:04:44 -0300 Subject: [PATCH 1/2] feat: Add contacts methods --- src/contacts/contacts.spec.ts | 227 ++++++++++++++++++ src/contacts/contacts.ts | 66 +++++ src/contacts/interfaces/contact.ts | 5 + .../create-contact-options.interface.ts | 21 ++ .../interfaces/get-contact.interface.ts | 12 + src/contacts/interfaces/index.ts | 4 + .../interfaces/list-contacts.interface.ts | 9 + .../interfaces/remove-contact.interface.ts | 9 + src/resend.ts | 6 +- 9 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 src/contacts/contacts.spec.ts create mode 100644 src/contacts/contacts.ts create mode 100644 src/contacts/interfaces/contact.ts create mode 100644 src/contacts/interfaces/create-contact-options.interface.ts create mode 100644 src/contacts/interfaces/get-contact.interface.ts create mode 100644 src/contacts/interfaces/index.ts create mode 100644 src/contacts/interfaces/list-contacts.interface.ts create mode 100644 src/contacts/interfaces/remove-contact.interface.ts diff --git a/src/contacts/contacts.spec.ts b/src/contacts/contacts.spec.ts new file mode 100644 index 00000000..aea492e4 --- /dev/null +++ b/src/contacts/contacts.spec.ts @@ -0,0 +1,227 @@ +import { enableFetchMocks } from 'jest-fetch-mock'; +import { Resend } from '../resend'; +import { GetContactResponseSuccess } from './interfaces'; +import { ErrorResponse } from '../interfaces'; + +enableFetchMocks(); + +describe('Contacts', () => { + afterEach(() => fetchMock.resetMocks()); + + describe('create', () => { + it('creates a contact', async () => { + fetchMock.mockOnce( + JSON.stringify({ + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', + name: 'Resend', + created_at: '2023-04-07T22:48:33.420498+00:00', + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }, + ); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.create({ + email: 'team@resend.com', + audience_id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', + }), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "created_at": "2023-04-07T22:48:33.420498+00:00", + "id": "3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222", + "name": "Resend", + }, + "error": null, +} +`); + }); + + it('throws error when missing name', async () => { + const errorResponse: ErrorResponse = { + name: 'missing_required_field', + message: 'Missing `email` field.', + }; + + fetchMock.mockOnce(JSON.stringify(errorResponse), { + status: 422, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.contacts.create({ email: '', audience_id: '' }); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Missing \`email\` field.", + "name": "missing_required_field", + }, +} +`); + }); + }); + + describe('list', () => { + it('lists contacts', async () => { + fetchMock.mockOnce( + JSON.stringify({ + data: [ + { + id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381e', + name: 'resend.com', + created_at: '2023-04-07T23:13:52.669661+00:00', + }, + { + id: 'ac7503ac-e027-4aea-94b3-b0acd46f65f9', + name: 'react.email', + created_at: '2023-04-07T23:13:20.417116+00:00', + }, + ], + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }, + ); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.contacts.list({ + audience_id: 'b6d24b8e-af0b-4c3c-be0c-359bbd97381a', + }), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "data": [ + { + "created_at": "2023-04-07T23:13:52.669661+00:00", + "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", + "name": "resend.com", + }, + { + "created_at": "2023-04-07T23:13:20.417116+00:00", + "id": "ac7503ac-e027-4aea-94b3-b0acd46f65f9", + "name": "react.email", + }, + ], + }, + "error": null, +} +`); + }); + }); + + describe('get', () => { + describe('when contact not found', () => { + it('returns error', async () => { + const errorResponse: ErrorResponse = { + name: 'not_found', + message: 'Contact not found', + }; + + fetchMock.mockOnce(JSON.stringify(errorResponse), { + status: 404, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.contacts.get({ + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + audience_id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', + }); + + await expect(result).resolves.toMatchInlineSnapshot(` +{ + "data": null, + "error": { + "message": "Contact not found", + "name": "not_found", + }, +} +`); + }); + }); + + it('get contact', async () => { + const contact: GetContactResponseSuccess = { + object: 'contact', + id: 'fd61172c-cafc-40f5-b049-b45947779a29', + name: 'resend.com', + created_at: '2023-06-21T06:10:36.144Z', + }; + + fetchMock.mockOnce(JSON.stringify(contact), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.contacts.get({ + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + audience_id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', + }), + ).resolves.toMatchInlineSnapshot(` +{ + "data": { + "created_at": "2023-06-21T06:10:36.144Z", + "id": "fd61172c-cafc-40f5-b049-b45947779a29", + "name": "resend.com", + "object": "contact", + }, + "error": null, +} +`); + }); + }); + + describe('remove', () => { + it('removes a contact', async () => { + fetchMock.mockOnce(JSON.stringify({}), { + status: 200, + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer re_924b3rjh2387fbewf823', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.contacts.remove({ + id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87223', + audience_id: '3d4a472d-bc6d-4dd2-aa9d-d3d50ce87222', + }), + ).resolves.toMatchInlineSnapshot(` +{ + "data": {}, + "error": null, +} +`); + }); + }); +}); diff --git a/src/contacts/contacts.ts b/src/contacts/contacts.ts new file mode 100644 index 00000000..6c08a9e8 --- /dev/null +++ b/src/contacts/contacts.ts @@ -0,0 +1,66 @@ +import { Resend } from '../resend'; +import { + CreateContactOptions, + CreateContactRequestOptions, + CreateContactResponse, + CreateContactResponseSuccess, + GetContactResponse, + GetContactResponseSuccess, + ListContactsResponse, + ListContactsResponseSuccess, + RemoveContactsResponse, + RemoveContactsResponseSuccess, +} from './interfaces'; + +export class Contacts { + constructor(private readonly resend: Resend) {} + + async create( + payload: CreateContactOptions, + options: CreateContactRequestOptions = {}, + ): Promise { + const data = await this.resend.post( + `/audiences/${payload.audience_id}/contacts`, + payload, + options, + ); + return data; + } + + async list({ + audience_id, + }: { + audience_id: string; + }): Promise { + const data = await this.resend.get( + `/audiences/${audience_id}/contacts`, + ); + return data; + } + + async get({ + audience_id, + id, + }: { + audience_id: string; + id: string; + }): Promise { + const data = await this.resend.get( + `/audiences/${audience_id}/contacts/${id}`, + ); + return data; + } + + async remove({ + audience_id, + id, + }: { + audience_id: string; + id: string; + }): Promise { + const data = await this.resend.delete( + `/audiences/${audience_id}/contacts/${id}`, + ); + return data; + } +} diff --git a/src/contacts/interfaces/contact.ts b/src/contacts/interfaces/contact.ts new file mode 100644 index 00000000..2de5c6db --- /dev/null +++ b/src/contacts/interfaces/contact.ts @@ -0,0 +1,5 @@ +export interface Contact { + created_at: string; + id: string; + name: string; +} diff --git a/src/contacts/interfaces/create-contact-options.interface.ts b/src/contacts/interfaces/create-contact-options.interface.ts new file mode 100644 index 00000000..03e4a9c7 --- /dev/null +++ b/src/contacts/interfaces/create-contact-options.interface.ts @@ -0,0 +1,21 @@ +import { PostOptions } from '../../common/interfaces'; +import { ErrorResponse } from '../../interfaces'; +import { Contact } from './contact'; + +export interface CreateContactOptions { + audience_id: string; + email: string; + unsubscribed?: boolean; + first_name?: string; + last_name?: string; +} + +export interface CreateContactRequestOptions extends PostOptions {} + +export interface CreateContactResponseSuccess + extends Pick {} + +export interface CreateContactResponse { + data: CreateContactResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/contacts/interfaces/get-contact.interface.ts b/src/contacts/interfaces/get-contact.interface.ts new file mode 100644 index 00000000..84edd5ce --- /dev/null +++ b/src/contacts/interfaces/get-contact.interface.ts @@ -0,0 +1,12 @@ +import { ErrorResponse } from '../../interfaces'; +import { Contact } from './contact'; + +export interface GetContactResponseSuccess + extends Pick { + object: 'contact'; +} + +export interface GetContactResponse { + data: GetContactResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/contacts/interfaces/index.ts b/src/contacts/interfaces/index.ts new file mode 100644 index 00000000..cb071660 --- /dev/null +++ b/src/contacts/interfaces/index.ts @@ -0,0 +1,4 @@ +export * from './create-contact-options.interface'; +export * from './list-contacts.interface'; +export * from './get-contact.interface'; +export * from './remove-contact.interface'; diff --git a/src/contacts/interfaces/list-contacts.interface.ts b/src/contacts/interfaces/list-contacts.interface.ts new file mode 100644 index 00000000..5c0673ee --- /dev/null +++ b/src/contacts/interfaces/list-contacts.interface.ts @@ -0,0 +1,9 @@ +import { ErrorResponse } from '../../interfaces'; +import { Contact } from './contact'; + +export type ListContactsResponseSuccess = Contact[]; + +export interface ListContactsResponse { + data: ListContactsResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/contacts/interfaces/remove-contact.interface.ts b/src/contacts/interfaces/remove-contact.interface.ts new file mode 100644 index 00000000..a081b771 --- /dev/null +++ b/src/contacts/interfaces/remove-contact.interface.ts @@ -0,0 +1,9 @@ +import { ErrorResponse } from '../../interfaces'; +import { Contact } from './contact'; + +export type RemoveContactsResponseSuccess = Pick; + +export interface RemoveContactsResponse { + data: RemoveContactsResponseSuccess | null; + error: ErrorResponse | null; +} diff --git a/src/resend.ts b/src/resend.ts index cbc4dba6..88076ca9 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -3,6 +3,7 @@ import { ApiKeys } from './api-keys/api-keys'; import { Audiences } from './audiences/audiences'; import { Batch } from './batch/batch'; import { GetOptions, PostOptions, PutOptions } from './common/interfaces'; +import { Contacts } from './contacts/contacts'; import { Domains } from './domains/domains'; import { Emails } from './emails/emails'; import { isResendErrorResponse } from './guards'; @@ -15,10 +16,11 @@ export class Resend { private readonly headers: Headers; readonly apiKeys = new ApiKeys(this); + readonly audiences = new Audiences(this); + readonly batch = new Batch(this); + readonly contacts = new Contacts(this); readonly domains = new Domains(this); readonly emails = new Emails(this); - readonly batch = new Batch(this); - readonly audiences = new Audiences(this); constructor(readonly key?: string) { if (!key) { From ab89916698a564f2e0eb4d8235d6c8f92fd0267a Mon Sep 17 00:00:00 2001 From: Bu Kinoshita Date: Thu, 16 Nov 2023 15:24:20 -0300 Subject: [PATCH 2/2] fix: Format --- src/audiences/audiences.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/audiences/audiences.spec.ts b/src/audiences/audiences.spec.ts index 96ba1934..9e6b577c 100644 --- a/src/audiences/audiences.spec.ts +++ b/src/audiences/audiences.spec.ts @@ -26,8 +26,8 @@ describe('Audiences', () => { ); const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - await expect(resend.audiences.create({ name: 'resend.com' })).resolves. -toMatchInlineSnapshot(` + await expect(resend.audiences.create({ name: 'resend.com' })).resolves + .toMatchInlineSnapshot(` { "data": { "created_at": "2023-04-07T22:48:33.420498+00:00", @@ -169,8 +169,8 @@ toMatchInlineSnapshot(` const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - await expect(resend.audiences.get('1234')).resolves. -toMatchInlineSnapshot(` + await expect(resend.audiences.get('1234')).resolves + .toMatchInlineSnapshot(` { "data": { "created_at": "2023-06-21T06:10:36.144Z",