diff --git a/README.md b/README.md index b970354..e0de9d6 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Currently, with this SDK you can: - Contact management - Contacts CRUD - Lists CRUD + - Contact Fields CRUD - General - Templates CRUD - Suppressions management (find and delete) @@ -121,6 +122,10 @@ Refer to the [`examples`](examples) folder for the source code of this and other - [Contact Lists](examples/contact-lists/everything.ts) +### Contact Fields API + +- [Contact Fields](examples/contact-fields/everything.ts) + ### Sending API - [Advanced](examples/sending/everything.ts) diff --git a/examples/contact-fields/everything.ts b/examples/contact-fields/everything.ts new file mode 100644 index 0000000..375e2cb --- /dev/null +++ b/examples/contact-fields/everything.ts @@ -0,0 +1,42 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = "" + +const client = new MailtrapClient({ + token: TOKEN, + accountId: ACCOUNT_ID, +}); + +async function contactFieldsExample() { + try { + // Get all contact fields + const fields = await client.contactFields.getList(); + console.log("All contact fields:", fields); + + // Use the first field from the list for operations + const firstField = (fields as any)[0]; + if (!firstField) { + console.log("No contact fields found"); + return; + } + + // Get a specific contact field + const field = await client.contactFields.get(firstField.id); + console.log("Retrieved field:", field); + + // Update a contact field + const updatedField = await client.contactFields.update(firstField.id, { + name: "Updated First Name", + }); + console.log("Updated field:", updatedField); + + // Delete a contact field + await client.contactFields.delete(firstField.id); + console.log("Field deleted successfully"); + } catch (error) { + console.error("Error:", error); + } +} + +contactFieldsExample(); diff --git a/src/__tests__/lib/api/ContactFields.test.ts b/src/__tests__/lib/api/ContactFields.test.ts new file mode 100644 index 0000000..1156d0e --- /dev/null +++ b/src/__tests__/lib/api/ContactFields.test.ts @@ -0,0 +1,20 @@ +import axios from "axios"; + +import ContactFields from "../../../lib/api/ContactFields"; + +describe("lib/api/ContactFields: ", () => { + const accountId = 100; + const contactFieldsAPI = new ContactFields(axios, accountId); + + describe("class ContactFields(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(contactFieldsAPI).toHaveProperty("create"); + expect(contactFieldsAPI).toHaveProperty("get"); + expect(contactFieldsAPI).toHaveProperty("getList"); + expect(contactFieldsAPI).toHaveProperty("update"); + expect(contactFieldsAPI).toHaveProperty("delete"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/ContactFields.test.ts b/src/__tests__/lib/api/resources/ContactFields.test.ts new file mode 100644 index 0000000..83ba95a --- /dev/null +++ b/src/__tests__/lib/api/resources/ContactFields.test.ts @@ -0,0 +1,315 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import ContactFieldsApi from "../../../../lib/api/resources/ContactFields"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; + +import CONFIG from "../../../../config"; +import { ContactFieldOptions } from "../../../../types/api/contact-fields"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/ContactFields: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const contactFieldsAPI = new ContactFieldsApi(axios, accountId); + + const createContactFieldRequest: ContactFieldOptions = { + name: "Phone Number", + merge_tag: "phone", + data_type: "text", + }; + + const createContactFieldResponse = { + data: { + id: 4134638, + name: "Phone Number", + merge_tag: "phone", + data_type: "text", + created_at: 1742820600230, + updated_at: 1742820600230, + }, + }; + + const updateContactFieldRequest: ContactFieldOptions = { + name: "Mobile Phone", + merge_tag: "mobile_phone", + data_type: "text", + }; + + const updateContactFieldResponse = { + data: { + id: 4134638, + name: "Mobile Phone", + merge_tag: "mobile_phone", + data_type: "text", + created_at: 1742820600230, + updated_at: 1742820600230, + }, + }; + + const getContactFieldResponse = { + data: { + id: 4134638, + name: "Phone Number", + merge_tag: "phone", + data_type: "text", + created_at: 1742820600230, + updated_at: 1742820600230, + }, + }; + + const getContactFieldsResponse = { + data: [ + { + id: 3059351, + name: "First name", + merge_tag: "first_name", + data_type: "text", + created_at: 1742820600230, + updated_at: 1742820600230, + }, + { + id: 3059352, + name: "Last name", + merge_tag: "last_name", + data_type: "text", + created_at: 1742820600230, + updated_at: 1742820600230, + }, + ], + }; + + describe("class ContactFieldsApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(contactFieldsAPI).toHaveProperty("create"); + expect(contactFieldsAPI).toHaveProperty("get"); + expect(contactFieldsAPI).toHaveProperty("getList"); + expect(contactFieldsAPI).toHaveProperty("update"); + expect(contactFieldsAPI).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("getList(): ", () => { + it("successfully gets all contact fields.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields`; + const expectedResponseData = getContactFieldsResponse; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, expectedResponseData); + const result = await contactFieldsAPI.getList(); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error when getting contact fields.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields`; + const expectedErrorMessage = "Request failed with status code 500"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(500, { error: expectedErrorMessage }); + + try { + await contactFieldsAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("get(): ", () => { + const fieldId = 4134638; + + it("successfully gets a contact field by ID.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields/${fieldId}`; + const expectedResponseData = getContactFieldResponse; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, expectedResponseData); + const result = await contactFieldsAPI.get(fieldId); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error when getting a contact field.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields/${fieldId}`; + const expectedErrorMessage = "Contact field not found"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactFieldsAPI.get(fieldId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("create(): ", () => { + it("successfully creates a contact field.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields`; + const expectedResponseData = createContactFieldResponse; + + expect.assertions(2); + + mock + .onPost(endpoint, createContactFieldRequest) + .reply(200, expectedResponseData); + const result = await contactFieldsAPI.create(createContactFieldRequest); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error when creating a contact field.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields`; + const expectedErrorMessage = "Request failed with status code 422"; + + expect.assertions(2); + + mock.onPost(endpoint).reply(422, { error: expectedErrorMessage }); + + try { + await contactFieldsAPI.create(createContactFieldRequest); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("update(): ", () => { + const fieldId = 4134638; + + it("successfully updates a contact field.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields/${fieldId}`; + const expectedResponseData = updateContactFieldResponse; + + expect.assertions(2); + + mock + .onPatch(endpoint, updateContactFieldRequest) + .reply(200, expectedResponseData); + const result = await contactFieldsAPI.update( + fieldId, + updateContactFieldRequest + ); + + expect(mock.history.patch[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("successfully updates a contact field with partial data.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields/${fieldId}`; + const partialUpdateRequest = { name: "Work Phone" }; + const expectedResponseData = { + data: { + id: fieldId, + name: "Work Phone", + merge_tag: "phone", + data_type: "text", + created_at: 1742820600230, + updated_at: 1742820600230, + }, + }; + + expect.assertions(2); + + mock + .onPatch(endpoint, partialUpdateRequest) + .reply(200, expectedResponseData); + const result = await contactFieldsAPI.update( + fieldId, + partialUpdateRequest + ); + + expect(mock.history.patch[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error when updating a contact field.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields/${fieldId}`; + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + mock.onPatch(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactFieldsAPI.update(fieldId, updateContactFieldRequest); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("delete(): ", () => { + const fieldId = 4134638; + + it("successfully deletes a contact field.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields/${fieldId}`; + + expect.assertions(1); + + mock.onDelete(endpoint).reply(204); + await contactFieldsAPI.delete(fieldId); + + expect(mock.history.delete[0].url).toEqual(endpoint); + }); + + it("fails with error when deleting a contact field.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields/${fieldId}`; + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + mock.onDelete(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await contactFieldsAPI.delete(fieldId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index f13eba6..7e22a6f 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -11,6 +11,7 @@ import GeneralAPI from "./api/General"; import TestingAPI from "./api/Testing"; import ContactsBaseAPI from "./api/Contacts"; import ContactListsBaseAPI from "./api/ContactLists"; +import ContactFieldsBaseAPI from "./api/ContactFields"; import TemplatesBaseAPI from "./api/Templates"; import SuppressionsBaseAPI from "./api/Suppressions"; @@ -139,6 +140,15 @@ export default class MailtrapClient { return new ContactListsBaseAPI(this.axios, this.accountId); } + /** + * Getter for Contact Fields API. + */ + get contactFields() { + this.validateAccountIdPresence(); + + return new ContactFieldsBaseAPI(this.axios, this.accountId); + } + /** * Getter for Templates API. */ diff --git a/src/lib/api/ContactFields.ts b/src/lib/api/ContactFields.ts new file mode 100644 index 0000000..9b99c5d --- /dev/null +++ b/src/lib/api/ContactFields.ts @@ -0,0 +1,30 @@ +import { AxiosInstance } from "axios"; + +import ContactFieldsApi from "./resources/ContactFields"; + +export default class ContactFieldsBaseAPI { + private client: AxiosInstance; + + private accountId?: number; + + public create: ContactFieldsApi["create"]; + + public get: ContactFieldsApi["get"]; + + public getList: ContactFieldsApi["getList"]; + + public update: ContactFieldsApi["update"]; + + public delete: ContactFieldsApi["delete"]; + + constructor(client: AxiosInstance, accountId?: number) { + this.client = client; + this.accountId = accountId; + const contactFields = new ContactFieldsApi(this.client, this.accountId); + this.create = contactFields.create.bind(contactFields); + this.get = contactFields.get.bind(contactFields); + this.getList = contactFields.getList.bind(contactFields); + this.update = contactFields.update.bind(contactFields); + this.delete = contactFields.delete.bind(contactFields); + } +} diff --git a/src/lib/api/resources/ContactFields.ts b/src/lib/api/resources/ContactFields.ts new file mode 100644 index 0000000..8f4c343 --- /dev/null +++ b/src/lib/api/resources/ContactFields.ts @@ -0,0 +1,71 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + ContactFieldOptions, + ContactFieldResponse, + ContactFieldsResponse, +} from "../../../types/api/contact-fields"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class ContactFieldsApi { + private client: AxiosInstance; + + private contactFieldsURL: string; + + constructor(client: AxiosInstance, accountId?: number) { + this.client = client; + this.contactFieldsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/fields`; + } + + /** + * Get all contact fields. + */ + public async getList() { + const url = `${this.contactFieldsURL}`; + + return this.client.get(url); + } + + /** + * Get a contact field by `fieldId`. + */ + public async get(fieldId: number) { + const url = `${this.contactFieldsURL}/${fieldId}`; + + return this.client.get(url); + } + + /** + * Creates a new contact field. + */ + public async create(data: ContactFieldOptions) { + return this.client.post( + this.contactFieldsURL, + data + ); + } + + /** + * Updates an existing contact field by `fieldId`. + */ + public async update(fieldId: number, data: ContactFieldOptions) { + const url = `${this.contactFieldsURL}/${fieldId}`; + + return this.client.patch( + url, + data + ); + } + + /** + * Deletes a contact field by ID. + */ + public async delete(fieldId: number) { + const url = `${this.contactFieldsURL}/${fieldId}`; + + return this.client.delete(url); + } +} diff --git a/src/types/api/contact-fields.ts b/src/types/api/contact-fields.ts new file mode 100644 index 0000000..410368b --- /dev/null +++ b/src/types/api/contact-fields.ts @@ -0,0 +1,22 @@ +export interface ContactField { + id: number; + name: string; + merge_tag: string; + data_type: "text" | "number" | "boolean" | "date"; + created_at: number; + updated_at: number; +} + +export interface ContactFieldOptions { + name?: string; + merge_tag?: string; + data_type?: "text" | "number" | "boolean" | "date"; +} + +export interface ContactFieldResponse { + data: ContactField; +} + +export interface ContactFieldsResponse { + data: ContactField[]; +}