From a1de61f1a9afdaa1aa2e884008c328935f9aaaea Mon Sep 17 00:00:00 2001 From: Vitor Capretz Date: Mon, 18 May 2026 09:48:39 -0300 Subject: [PATCH] feat(contacts): add ContactImports class and related interfaces for managing contact imports --- src/contacts/contacts.ts | 3 + src/contacts/imports/contact-imports.spec.ts | 98 +++++++++++++++++++ src/contacts/imports/contact-imports.ts | 28 ++++++ src/contacts/imports/interfaces/index.ts | 1 + .../list-contact-imports.interface.ts | 37 +++++++ src/index.ts | 1 + 6 files changed, 168 insertions(+) create mode 100644 src/contacts/imports/contact-imports.spec.ts create mode 100644 src/contacts/imports/contact-imports.ts create mode 100644 src/contacts/imports/interfaces/index.ts create mode 100644 src/contacts/imports/interfaces/list-contact-imports.interface.ts diff --git a/src/contacts/contacts.ts b/src/contacts/contacts.ts index debac863..0b0345f0 100644 --- a/src/contacts/contacts.ts +++ b/src/contacts/contacts.ts @@ -1,5 +1,6 @@ import { buildPaginationQuery } from '../common/utils/build-pagination-query'; import type { Resend } from '../resend'; +import { ContactImports } from './imports/contact-imports'; import type { CreateContactOptions, CreateContactRequestOptions, @@ -31,10 +32,12 @@ import { ContactSegments } from './segments/contact-segments'; import { ContactTopics } from './topics/contact-topics'; export class Contacts { + readonly imports: ContactImports; readonly topics: ContactTopics; readonly segments: ContactSegments; constructor(private readonly resend: Resend) { + this.imports = new ContactImports(this.resend); this.topics = new ContactTopics(this.resend); this.segments = new ContactSegments(this.resend); } diff --git a/src/contacts/imports/contact-imports.spec.ts b/src/contacts/imports/contact-imports.spec.ts new file mode 100644 index 00000000..9aeddb47 --- /dev/null +++ b/src/contacts/imports/contact-imports.spec.ts @@ -0,0 +1,98 @@ +import createFetchMock from 'vitest-fetch-mock'; +import { Resend } from '../../resend'; +import { mockSuccessResponse } from '../../test-utils/mock-fetch'; +import type { + ListContactImportsOptions, + ListContactImportsResponseSuccess, +} from './interfaces/list-contact-imports.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('ContactImports', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('list', () => { + it('lists contact imports', async () => { + const response: ListContactImportsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + object: 'contact_import', + id: '479e3145-dd38-476b-932c-529ceb705947', + status: 'completed', + created_at: '2026-05-15T18:32:37.823Z', + completed_at: '2026-05-15T18:33:42.916Z', + counts: { + total: 1200, + created: 800, + updated: 300, + skipped: 75, + failed: 25, + }, + }, + ], + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.contacts.imports.list(), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "completed_at": "2026-05-15T18:33:42.916Z", + "counts": { + "created": 800, + "failed": 25, + "skipped": 75, + "total": 1200, + "updated": 300, + }, + "created_at": "2026-05-15T18:32:37.823Z", + "id": "479e3145-dd38-476b-932c-529ceb705947", + "object": "contact_import", + "status": "completed", + }, + ], + "has_more": false, + "object": "list", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/contacts/imports', + ); + }); + + it('lists contact imports with pagination and status', async () => { + const options: ListContactImportsOptions = { + limit: 10, + status: 'completed', + }; + const response: ListContactImportsResponseSuccess = { + object: 'list', + has_more: false, + data: [], + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await resend.contacts.imports.list(options); + + expect(fetchMock.mock.calls[0][0]).toBe( + 'https://api.resend.com/contacts/imports?limit=10&status=completed', + ); + }); + }); +}); diff --git a/src/contacts/imports/contact-imports.ts b/src/contacts/imports/contact-imports.ts new file mode 100644 index 00000000..59170590 --- /dev/null +++ b/src/contacts/imports/contact-imports.ts @@ -0,0 +1,28 @@ +import { buildPaginationQuery } from '../../common/utils/build-pagination-query'; +import type { Resend } from '../../resend'; +import type { + ListContactImportsOptions, + ListContactImportsResponse, + ListContactImportsResponseSuccess, +} from './interfaces/list-contact-imports.interface'; + +export class ContactImports { + constructor(private readonly resend: Resend) {} + + async list( + options: ListContactImportsOptions = {}, + ): Promise { + const searchParams = new URLSearchParams(buildPaginationQuery(options)); + + if (options.status !== undefined) { + searchParams.set('status', options.status); + } + + const queryString = searchParams.toString(); + const url = queryString + ? `/contacts/imports?${queryString}` + : '/contacts/imports'; + + return this.resend.get(url); + } +} diff --git a/src/contacts/imports/interfaces/index.ts b/src/contacts/imports/interfaces/index.ts new file mode 100644 index 00000000..368f3b83 --- /dev/null +++ b/src/contacts/imports/interfaces/index.ts @@ -0,0 +1 @@ +export * from './list-contact-imports.interface'; diff --git a/src/contacts/imports/interfaces/list-contact-imports.interface.ts b/src/contacts/imports/interfaces/list-contact-imports.interface.ts new file mode 100644 index 00000000..3dafa855 --- /dev/null +++ b/src/contacts/imports/interfaces/list-contact-imports.interface.ts @@ -0,0 +1,37 @@ +import type { + PaginatedData, + PaginationOptions, +} from '../../../common/interfaces/pagination-options.interface'; +import type { Response } from '../../../interfaces'; + +export type ContactImportStatus = + | 'queued' + | 'in_progress' + | 'completed' + | 'failed'; + +export type ListContactImportsOptions = PaginationOptions & { + status?: ContactImportStatus; +}; + +export interface ContactImportCounts { + total: number; + created: number; + updated: number; + skipped: number; + failed: number; +} + +export interface ContactImport { + object: 'contact_import'; + id: string; + status: ContactImportStatus; + created_at: string; + completed_at: string | null; + counts: ContactImportCounts; +} + +export type ListContactImportsResponseSuccess = PaginatedData; + +export type ListContactImportsResponse = + Response; diff --git a/src/index.ts b/src/index.ts index 919e7820..b51bd8a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export * from './batch/interfaces'; export * from './broadcasts/interfaces'; export * from './common/interfaces'; export * from './contact-properties/interfaces'; +export * from './contacts/imports/interfaces'; export * from './contacts/interfaces'; export * from './contacts/segments/interfaces'; export * from './contacts/topics/interfaces';