From 755432e372beefae898442d226409e5ff1ea4f6c Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 6 Oct 2025 19:49:17 +0400 Subject: [PATCH 1/9] types: add ContactField types for API integration --- src/types/api/contact-fields.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/types/api/contact-fields.ts 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[]; +} From e8659b91f2e68bb007067a4cd344e80a255c2e6b Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 6 Oct 2025 19:49:35 +0400 Subject: [PATCH 2/9] feat: implement ContactFieldsApi for managing contact fields --- src/lib/api/resources/ContactFields.ts | 71 ++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/lib/api/resources/ContactFields.ts 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); + } +} From 8d00c4d37d544b6fec83b9da2ffaee8dd7de8d5a Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 6 Oct 2025 19:49:45 +0400 Subject: [PATCH 3/9] api: add ContactFieldsBaseAPI for managing contact fields operations --- src/lib/api/ContactFields.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/lib/api/ContactFields.ts 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); + } +} From d0a04b8569a3fe02aab9a481cfd3d2c032c7ad2d Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 6 Oct 2025 19:49:56 +0400 Subject: [PATCH 4/9] feat: add getter for ContactFieldsBaseAPI in MailtrapClient --- src/lib/MailtrapClient.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) 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. */ From 0fc3b30ef68e611884506c2b2361f0e9315cf8a6 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 6 Oct 2025 20:05:55 +0400 Subject: [PATCH 5/9] test: add unit tests for ContactFieldsApi methods including create, update, get, and delete operations --- .../lib/api/resources/ContactFields.test.ts | 315 ++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 src/__tests__/lib/api/resources/ContactFields.test.ts 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); + } + } + }); + }); +}); From 5d140242a666df15ae600d5c3bc9606598a30cb8 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 6 Oct 2025 20:06:06 +0400 Subject: [PATCH 6/9] test: add unit tests for ContactFields class initialization and method properties --- src/__tests__/lib/api/ContactFields.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/__tests__/lib/api/ContactFields.test.ts 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"); + }); + }); + }); +}); From 5e1b7fbfc0d756fd4ebd53ec6c431482d0953bd8 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 6 Oct 2025 20:07:09 +0400 Subject: [PATCH 7/9] docs: update README to include Contact Fields CRUD and examples --- README.md | 5 +++++ 1 file changed, 5 insertions(+) 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) From 55cb961dfbd23ee1d17e1a26e02580d022945098 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 6 Oct 2025 20:09:34 +0400 Subject: [PATCH 8/9] examples: add example script for ContactFields API usage --- examples/contact-fields/everything.ts | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 examples/contact-fields/everything.ts diff --git a/examples/contact-fields/everything.ts b/examples/contact-fields/everything.ts new file mode 100644 index 0000000..8a6b745 --- /dev/null +++ b/examples/contact-fields/everything.ts @@ -0,0 +1,45 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = "" + +// Initialize the client with your token and account ID +const client = new MailtrapClient({ + token: TOKEN, + accountId: ACCOUNT_ID, // Your account ID +}); + +// Example usage of ContactFields API +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); + } +} + +// Run the example +contactFieldsExample(); From 38b8e0239e20511e039a1e857faeda902cdcc1fe Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 6 Oct 2025 20:15:17 +0400 Subject: [PATCH 9/9] examples: clean up comments and formatting in ContactFields example script --- examples/contact-fields/everything.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/contact-fields/everything.ts b/examples/contact-fields/everything.ts index 8a6b745..375e2cb 100644 --- a/examples/contact-fields/everything.ts +++ b/examples/contact-fields/everything.ts @@ -3,13 +3,11 @@ import { MailtrapClient } from "mailtrap"; const TOKEN = ""; const ACCOUNT_ID = "" -// Initialize the client with your token and account ID const client = new MailtrapClient({ token: TOKEN, - accountId: ACCOUNT_ID, // Your account ID + accountId: ACCOUNT_ID, }); -// Example usage of ContactFields API async function contactFieldsExample() { try { // Get all contact fields @@ -41,5 +39,4 @@ async function contactFieldsExample() { } } -// Run the example contactFieldsExample();