diff --git a/README.md b/README.md index f497b4c..e80c35d 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,10 @@ Refer to the [`examples`](examples) folder for the source code of this and other - [Remove account access](examples/general/accounts.ts) - [Permissions](examples/general/permissions.ts) +### Contacts API + + - [Contacts](examples/contacts/everything.ts) + ### Sending API - [Advanced](examples/sending/everything.ts) diff --git a/examples/contacts/everything.ts b/examples/contacts/everything.ts new file mode 100644 index 0000000..fa73047 --- /dev/null +++ b/examples/contacts/everything.ts @@ -0,0 +1,47 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = "" + +const client = new MailtrapClient({ + token: TOKEN, + accountId: ACCOUNT_ID +}); + +const contactData = { + email: "john.smith@example.com", + fields: { + first_name: "John", + last_name: "Smith" + }, +}; + +// Create contact first +client.contacts + .create(contactData) + .then(async (createResponse) => { + console.log("Contact created:", createResponse.data); + const contactId = createResponse.data.id; + + // Get contact by email + const getResponse = await client.contacts.get(contactData.email); + console.log("Contact retrieved:", getResponse.data); + + // Update contact + const updateResponse = await client.contacts + .update(contactId, { + email: contactData.email, + fields: { + first_name: "Johnny", + last_name: "Smith", + } + }) + console.log("Contact updated:", updateResponse.data); + + // Delete contact + await client.contacts.delete(contactId); + console.log("Contact deleted"); + }) + .catch(error => { + console.error("Error in contact lifecycle:", error); + }); diff --git a/src/__tests__/lib/api/Contacts.test.ts b/src/__tests__/lib/api/Contacts.test.ts new file mode 100644 index 0000000..0640293 --- /dev/null +++ b/src/__tests__/lib/api/Contacts.test.ts @@ -0,0 +1,19 @@ +import axios from "axios"; + +import Contacts from "../../../lib/api/Contacts"; + +describe("lib/api/Contacts: ", () => { + const accountId = 100; + const contactsAPI = new Contacts(axios, accountId); + + describe("class Contacts(): ", () => { + describe("init: ", () => { + it("initalizes with all necessary params.", () => { + expect(contactsAPI).toHaveProperty("create"); + expect(contactsAPI).toHaveProperty("get"); + expect(contactsAPI).toHaveProperty("update"); + expect(contactsAPI).toHaveProperty("delete"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/Contacts.test.ts b/src/__tests__/lib/api/resources/Contacts.test.ts new file mode 100644 index 0000000..cba424a --- /dev/null +++ b/src/__tests__/lib/api/resources/Contacts.test.ts @@ -0,0 +1,269 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import ContactsApi from "../../../../lib/api/resources/Contacts"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/Contacts: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const contactsAPI = new ContactsApi(axios, accountId); + + const createContactRequest = { + contact: { + email: "john.smith@example.com", + fields: { + first_name: "John", + last_name: "Smith", + }, + list_ids: [1, 2, 3], + }, + }; + + const createContactResponse = { + data: { + id: "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132", + status: "subscribed", + email: "john.smith@example.com", + fields: { + first_name: "John", + last_name: "Smith", + }, + list_ids: [1, 2, 3], + created_at: 1742820600230, + updated_at: 1742820600230, + }, + }; + + const updateContactRequest = { + contact: { + email: "customer1@example.com", + fields: { + first_name: "John", + last_name: "Smith", + zip_code: "11111", + }, + list_ids_included: [1, 2, 3], + list_ids_excluded: [4, 5, 6], + unsubscribed: false, + }, + }; + + const updateContactResponse = { + data: { + id: "01972696-84ef-783b-8a87-48067db2d16b", + email: "john.smith111@example.com", + created_at: 1748699088076, + updated_at: 1748700400794, + list_ids: [], + status: "subscribed", + fields: { + first_name: "Johnny", + last_name: "Smith", + }, + }, + }; + + describe("class ContactsApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(contactsAPI).toHaveProperty("create"); + expect(contactsAPI).toHaveProperty("update"); + expect(contactsAPI).toHaveProperty("delete"); + }); + }); + }); + + beforeAll(() => { + /** + * Init Axios interceptors for handling response.data, errors. + */ + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("get(): ", () => { + it("successfully gets a contact by email.", async () => { + const email = "john.smith@example.com"; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${email}`; + const expectedResponseData = { + data: { + id: "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132", + email: "john.smith@example.com", + created_at: 1748699088076, + updated_at: 1748699088076, + list_ids: [1, 2, 3], + status: "subscribed", + fields: { + first_name: "John", + last_name: "Smith", + zip_code: "11111", + }, + }, + }; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, expectedResponseData); + const result = await contactsAPI.get(email); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error when getting a contact.", async () => { + const email = "nonexistent@example.com"; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${email}`; + const expectedErrorMessage = "Contact not found"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactsAPI.get(email); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("create(): ", () => { + it("successfully creates a contact.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts`; + const expectedResponseData = createContactResponse; + + expect.assertions(2); + + mock + .onPost(endpoint, createContactRequest) + .reply(200, expectedResponseData); + const result = await contactsAPI.create(createContactRequest.contact); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts`; + const expectedErrorMessage = "Request failed with status code 400"; + + expect.assertions(2); + + mock.onPost(endpoint).reply(400, { error: expectedErrorMessage }); + + try { + await contactsAPI.create(createContactRequest.contact); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("update(): ", () => { + const contactId = "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132"; + + it("successfully updates a contact.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactId}`; + const expectedResponseData = updateContactResponse; + + expect.assertions(2); + + mock + .onPatch(endpoint, updateContactRequest) + .reply(200, expectedResponseData); + const result = await contactsAPI.update( + contactId, + updateContactRequest.contact + ); + + expect(mock.history.patch[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactId}`; + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + mock.onPatch(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactsAPI.update(contactId, updateContactRequest.contact); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("delete(): ", () => { + const contactId = "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132"; + + it("successfully deletes a contact.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactId}`; + const expectedResponseData = { + data: { + id: contactId, + status: "unsubscribed", + email: "john.smith@example.com", + fields: { + first_name: "John", + last_name: "Smith", + }, + list_ids: [], + created_at: 1740659901189, + updated_at: 1742903266889, + }, + }; + + expect.assertions(2); + + mock.onDelete(endpoint).reply(200, expectedResponseData); + const result = await contactsAPI.delete(contactId); + + expect(mock.history.delete[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/${contactId}`; + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + mock.onDelete(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactsAPI.delete(contactId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index 7de7fa7..dd3cc58 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -7,8 +7,8 @@ import { Mail, MailtrapClient } from "../.."; import MailtrapError from "../../lib/MailtrapError"; import CONFIG from "../../config"; -import TestingAPI from "../../lib/api/Testing"; import GeneralAPI from "../../lib/api/General"; +import TestingAPI from "../../lib/api/Testing"; const { ERRORS, CLIENT_SETTINGS } = CONFIG; const { TESTING_ENDPOINT, BULK_ENDPOINT, SENDING_ENDPOINT } = CLIENT_SETTINGS; diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index eb4dcc6..ee83c54 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -5,9 +5,11 @@ import axios, { AxiosInstance } from "axios"; import encodeMailBuffers from "./mail-buffer-encoder"; import handleSendingError from "./axios-logger"; +import MailtrapError from "./MailtrapError"; import GeneralAPI from "./api/General"; import TestingAPI from "./api/Testing"; +import ContactsBaseAPI from "./api/Contacts"; import CONFIG from "../config"; @@ -18,7 +20,6 @@ import { BatchSendResponse, BatchSendRequest, } from "../types/mailtrap"; -import MailtrapError from "./MailtrapError"; const { CLIENT_SETTINGS, ERRORS } = CONFIG; const { @@ -102,6 +103,13 @@ export default class MailtrapClient { return new GeneralAPI(this.axios, this.accountId); } + /** + * Getter for Contacts API. + */ + get contacts() { + return new ContactsBaseAPI(this.axios, this.accountId); + } + /** * Returns configured host. Checks if `bulk` and `sandbox` modes are activated simultaneously, * then reject with Mailtrap Error. diff --git a/src/lib/api/Contacts.ts b/src/lib/api/Contacts.ts new file mode 100644 index 0000000..889a048 --- /dev/null +++ b/src/lib/api/Contacts.ts @@ -0,0 +1,27 @@ +import { AxiosInstance } from "axios"; + +import ContactsApi from "./resources/Contacts"; + +export default class ContactsBaseAPI { + private client: AxiosInstance; + + private accountId?: number; + + public get: ContactsApi["get"]; + + public create: ContactsApi["create"]; + + public update: ContactsApi["update"]; + + public delete: ContactsApi["delete"]; + + constructor(client: AxiosInstance, accountId?: number) { + this.client = client; + this.accountId = accountId; + const contacts = new ContactsApi(this.client, this.accountId); + this.get = contacts.get.bind(contacts); + this.create = contacts.create.bind(contacts); + this.update = contacts.update.bind(contacts); + this.delete = contacts.delete.bind(contacts); + } +} diff --git a/src/lib/api/resources/Contacts.ts b/src/lib/api/resources/Contacts.ts new file mode 100644 index 0000000..ad1a18a --- /dev/null +++ b/src/lib/api/resources/Contacts.ts @@ -0,0 +1,59 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + ContactData, + ContactResponse, + ContactUpdateData, +} from "../../../types/api/contacts"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class ContactsApi { + private client: AxiosInstance; + + private contactsURL: string; + + constructor(client: AxiosInstance, accountId?: number) { + this.client = client; + this.contactsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts`; + } + + /** + * Get a contact by ID or email. + */ + public async get(idOrEmail: string) { + const url = `${this.contactsURL}/${idOrEmail}`; + + return this.client.get(url); + } + + /** + * Creates a new contact. + */ + public async create(contact: ContactData) { + return this.client.post( + this.contactsURL, + { contact } + ); + } + + /** + * Updates an existing contact by ID or email. + */ + public async update(identifier: string, contact: ContactUpdateData) { + const url = `${this.contactsURL}/${identifier}`; + return this.client.patch(url, { + contact, + }); + } + + /** + * Deletes a contact by ID or email. + */ + public async delete(identifier: string) { + const url = `${this.contactsURL}/${identifier}`; + return this.client.delete(url); + } +} diff --git a/src/types/api/contactlist.ts b/src/types/api/contactlist.ts new file mode 100644 index 0000000..38be07d --- /dev/null +++ b/src/types/api/contactlist.ts @@ -0,0 +1,8 @@ +export interface ContactList { + id: number; + name: string; +} + +export interface ContactListData { + name: string; +} diff --git a/src/types/api/contacts.ts b/src/types/api/contacts.ts new file mode 100644 index 0000000..74f5fa8 --- /dev/null +++ b/src/types/api/contacts.ts @@ -0,0 +1,25 @@ +export interface Contact { + id: string; + email: string; + created_at: number; + updated_at: number; + list_ids: number[]; + status: "subscribed" | "unsubscribed"; + fields: Record; +} + +export interface ContactData { + email: string; + fields?: Record; + list_ids?: number[]; + unsubscribed?: boolean; +} + +export interface ContactUpdateData extends ContactData { + list_ids_included?: number[]; + list_ids_excluded?: number[]; +} + +export interface ContactResponse { + data: Contact; +} diff --git a/src/types/mailtrap.ts b/src/types/mailtrap.ts index 4e8b506..6a7fe33 100644 --- a/src/types/mailtrap.ts +++ b/src/types/mailtrap.ts @@ -123,3 +123,13 @@ export interface BatchSendRequest { headers?: Record; }[]; } + +export interface ContactFields { + [key: string]: string | number | boolean | undefined; +} + +export interface ContactData { + email: string; + fields?: ContactFields; + list_ids?: number[]; +}