diff --git a/README.md b/README.md index 442c8cc..bc559d4 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,15 @@ Refer to the [`examples`](examples) folder for the source code of this and other ### Contacts API - - [Contacts](examples/contacts/everything.ts) + - [Contacts CRUD](examples/contacts/everything.ts) ### Contact Lists API - - [Contact Lists](examples/contact-lists/everything.ts) + - [Contact Lists CRUD](examples/contact-lists/everything.ts) + +### Templates API + + - [Templates CRUD](examples/templates/everything.ts) ### Sending API @@ -76,10 +80,10 @@ Refer to the [`examples`](examples) folder for the source code of this and other ### Batch Sending API - - [Send a batch of transactional emails](examples/batch/transactional.ts) - - [Send a batch of bulk emails](examples/batch/bulk.ts) - - [Send a batch of sandbox emails](examples/batch/sandbox.ts) - - [Send a batch of emails from template](examples/batch/template.ts) + - [Transactional emails](examples/batch/transactional.ts) + - [Bulk emails](examples/batch/bulk.ts) + - [Sandbox emails](examples/batch/sandbox.ts) + - [Emails from template](examples/batch/template.ts) ### Email testing API @@ -92,6 +96,7 @@ Refer to the [`examples`](examples) folder for the source code of this and other ### Bulk sending API - [Send mail](examples/bulk/send-mail.ts) + - [Transport](examples/bulk/transport.ts) ### Nodemailer Transport diff --git a/examples/templates/everything.ts b/examples/templates/everything.ts new file mode 100644 index 0000000..32afe10 --- /dev/null +++ b/examples/templates/everything.ts @@ -0,0 +1,44 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = ""; + +const client = new MailtrapClient({ + token: TOKEN, + accountId: ACCOUNT_ID +}); + +async function templatesFlow() { + // Create a new template + const newTemplate = await client.templates.create({ + name: "Welcome Email", + subject: "Welcome to Our Service!", + category: "Promotional", + body_html: "

Welcome!

Thank you for joining our service.

", + body_text: "Welcome! Thank you for joining our service." + }); + console.log("Created template:", newTemplate); + + // Get all templates + const allTemplates = await client.templates.getList(); + console.log("All templates:", allTemplates); + + // Get a specific template + const template = await client.templates.get(newTemplate.id); + console.log("Template details:", template); + + // Update the template + const updatedTemplate = await client.templates.update(newTemplate.id, { + name: "Updated Welcome Email", + subject: "Welcome to Our Amazing Service!", + body_html: "

Welcome!

Thank you for joining our amazing service.

" + }); + console.log("Updated template:", updatedTemplate); + + // Delete the template + await client.templates.delete(newTemplate.id); + console.log("Template deleted successfully"); +} + +templatesFlow().catch(console.error); + diff --git a/src/__tests__/lib/api/Templates.test.ts b/src/__tests__/lib/api/Templates.test.ts new file mode 100644 index 0000000..8b490e8 --- /dev/null +++ b/src/__tests__/lib/api/Templates.test.ts @@ -0,0 +1,20 @@ +import axios from "axios"; + +import TemplatesBaseAPI from "../../../lib/api/Templates"; + +describe("lib/api/Templates: ", () => { + const accountId = 100; + const templatesAPI = new TemplatesBaseAPI(axios, accountId); + + describe("class TemplatesBaseAPI(): ", () => { + describe("init: ", () => { + it("initalizes with all necessary params.", () => { + expect(templatesAPI).toHaveProperty("create"); + expect(templatesAPI).toHaveProperty("getList"); + expect(templatesAPI).toHaveProperty("get"); + expect(templatesAPI).toHaveProperty("update"); + expect(templatesAPI).toHaveProperty("delete"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/Templates.test.ts b/src/__tests__/lib/api/resources/Templates.test.ts new file mode 100644 index 0000000..8d79859 --- /dev/null +++ b/src/__tests__/lib/api/resources/Templates.test.ts @@ -0,0 +1,297 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import TemplatesApi from "../../../../lib/api/resources/Templates"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; +import { + Template, + TemplateCreateParams, + TemplateUpdateParams, +} from "../../../../types/api/templates"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/Templates: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const templatesAPI = new TemplatesApi(axios, accountId); + + const createTemplateRequest: TemplateCreateParams = { + name: "Welcome Email", + subject: "Welcome to Our Service!", + category: "Promotional", + body_html: "

Welcome!

Thank you for joining our service.

", + body_text: "Welcome! Thank you for joining our service.", + }; + + const createTemplateResponse: Template = { + id: 1, + uuid: "813e39db-c74a-4830-b037-0e6ba8b1fe88", + name: "Welcome Email", + subject: "Welcome to Our Service!", + category: "Promotional", + body_html: "

Welcome!

Thank you for joining our service.

", + body_text: "Welcome! Thank you for joining our service.", + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + }; + + const updateTemplateRequest: TemplateUpdateParams = { + name: "Updated Welcome Email", + subject: "Welcome to Our Amazing Service!", + body_html: + "

Welcome!

Thank you for joining our amazing service.

", + }; + + const updateTemplateResponse: Template = { + id: 1, + uuid: "813e39db-c74a-4830-b037-0e6ba8b1fe88", + name: "Updated Welcome Email", + subject: "Welcome to Our Amazing Service!", + category: "Promotional", + body_html: + "

Welcome!

Thank you for joining our amazing service.

", + body_text: "Welcome! Thank you for joining our service.", + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + }; + + describe("class TemplatesApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(templatesAPI).toHaveProperty("create"); + expect(templatesAPI).toHaveProperty("update"); + expect(templatesAPI).toHaveProperty("delete"); + expect(templatesAPI).toHaveProperty("get"); + expect(templatesAPI).toHaveProperty("getList"); + }); + }); + }); + + beforeAll(() => { + axios.interceptors.response.use( + (response) => response.data, + handleSendingError + ); + mock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + describe("getList(): ", () => { + it("successfully gets all templates.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; + const expectedResponseData: Template[] = [ + { + id: 1, + uuid: "813e39db-c74a-4830-b037-0e6ba8b1fe88", + name: "Welcome Email", + subject: "Welcome to Our Service!", + category: "Promotional", + body_html: + "

Welcome!

Thank you for joining our service.

", + body_text: "Welcome! Thank you for joining our service.", + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + }, + { + id: 2, + uuid: "923e39db-c74a-4830-b037-0e6ba8b1fe89", + name: "Password Reset", + subject: "Reset Your Password", + category: "Transactional", + body_html: + "

Password Reset

Click here to reset your password.

", + body_text: "Password Reset. Click here to reset your password.", + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + }, + ]; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, expectedResponseData); + const result = await templatesAPI.getList(); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; + const expectedErrorMessage = "Request failed with status code 400"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(400, { error: expectedErrorMessage }); + + try { + await templatesAPI.getList(); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("get(): ", () => { + it("successfully gets a template by ID.", async () => { + const templateId = 1; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const expectedResponseData: Template = { + id: templateId, + uuid: "813e39db-c74a-4830-b037-0e6ba8b1fe88", + name: "Welcome Email", + subject: "Welcome to Our Service!", + category: "Promotional", + body_html: "

Welcome!

Thank you for joining our service.

", + body_text: "Welcome! Thank you for joining our service.", + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + }; + + expect.assertions(2); + + mock.onGet(endpoint).reply(200, expectedResponseData); + const result = await templatesAPI.get(templateId); + + expect(mock.history.get[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error when getting a template.", async () => { + const templateId = 999; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const expectedErrorMessage = "Template not found"; + + expect.assertions(2); + + mock.onGet(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await templatesAPI.get(templateId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("create(): ", () => { + it("successfully creates a template.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; + const expectedResponseData = createTemplateResponse; + + expect.assertions(2); + + mock + .onPost(endpoint, { email_template: createTemplateRequest }) + .reply(200, expectedResponseData); + const result = await templatesAPI.create(createTemplateRequest); + + expect(mock.history.post[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; + const expectedErrorMessage = "Request failed with status code 400"; + + expect.assertions(2); + + mock.onPost(endpoint).reply(400, { error: expectedErrorMessage }); + + try { + await templatesAPI.create(createTemplateRequest); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("update(): ", () => { + const templateId = 1; + + it("successfully updates a template.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const expectedResponseData = updateTemplateResponse; + + expect.assertions(2); + + mock + .onPatch(endpoint, { email_template: updateTemplateRequest }) + .reply(200, expectedResponseData); + const result = await templatesAPI.update( + templateId, + updateTemplateRequest + ); + + expect(mock.history.patch[0].url).toEqual(endpoint); + expect(result).toEqual(expectedResponseData); + }); + + it("fails with error.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + mock.onPatch(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await templatesAPI.update(templateId, updateTemplateRequest); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); + + describe("delete(): ", () => { + const templateId = 1; + + it("successfully deletes a template.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + + expect.assertions(1); + + mock.onDelete(endpoint).reply(204); + await templatesAPI.delete(templateId); + + expect(mock.history.delete[0].url).toEqual(endpoint); + }); + + it("fails with error.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const expectedErrorMessage = "Request failed with status code 404"; + + expect.assertions(2); + + mock.onDelete(endpoint).reply(404, { error: expectedErrorMessage }); + + try { + await templatesAPI.delete(templateId); + } 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 ca6edda..f92496a 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -11,6 +11,7 @@ import GeneralAPI from "../../lib/api/General"; import TestingAPI from "../../lib/api/Testing"; import ContactLists from "../../lib/api/ContactLists"; import Contacts from "../../lib/api/Contacts"; +import TemplatesBaseAPI from "../../lib/api/Templates"; const { ERRORS, CLIENT_SETTINGS } = CONFIG; const { TESTING_ENDPOINT, BULK_ENDPOINT, SENDING_ENDPOINT } = CLIENT_SETTINGS; @@ -782,5 +783,31 @@ describe("lib/mailtrap-client: ", () => { expect(contactListsClient).toBeInstanceOf(ContactLists); }); }); + + describe("get templates(): ", () => { + it("rejects with Mailtrap error, when `accountId` is missing.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + }); + expect.assertions(1); + + try { + client.templates; + } catch (error) { + expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING)); + } + }); + + it("returns templates API object when accountId is provided.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + accountId: 10, + }); + expect.assertions(1); + + const templatesClient = client.templates; + expect(templatesClient).toBeInstanceOf(TemplatesBaseAPI); + }); + }); }); }); diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 7a043c8..31e4785 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 TemplatesBaseAPI from "./api/Templates"; import CONFIG from "../config"; @@ -130,6 +131,14 @@ export default class MailtrapClient { return new ContactListsBaseAPI(this.axios, this.accountId); } + get templates() { + if (!this.accountId) { + throw new MailtrapError(ACCOUNT_ID_MISSING); + } + + return new TemplatesBaseAPI(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/Templates.ts b/src/lib/api/Templates.ts new file mode 100644 index 0000000..9d3f971 --- /dev/null +++ b/src/lib/api/Templates.ts @@ -0,0 +1,30 @@ +import { AxiosInstance } from "axios"; + +import TemplatesApi from "./resources/Templates"; + +export default class TemplatesBaseAPI { + private client: AxiosInstance; + + private accountId?: number; + + public get: TemplatesApi["get"]; + + public getList: TemplatesApi["getList"]; + + public create: TemplatesApi["create"]; + + public update: TemplatesApi["update"]; + + public delete: TemplatesApi["delete"]; + + constructor(client: AxiosInstance, accountId?: number) { + this.client = client; + this.accountId = accountId; + const templates = new TemplatesApi(this.client, this.accountId); + this.get = templates.get.bind(templates); + this.getList = templates.getList.bind(templates); + this.create = templates.create.bind(templates); + this.update = templates.update.bind(templates); + this.delete = templates.delete.bind(templates); + } +} diff --git a/src/lib/api/resources/Templates.ts b/src/lib/api/resources/Templates.ts new file mode 100644 index 0000000..de43117 --- /dev/null +++ b/src/lib/api/resources/Templates.ts @@ -0,0 +1,72 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + Template, + TemplateCreateParams, + TemplateUpdateParams, +} from "../../../types/api/templates"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class TemplatesApi { + private client: AxiosInstance; + + private accountId?: number; + + private templatesURL: string; + + constructor(client: AxiosInstance, accountId?: number) { + this.client = client; + this.accountId = accountId; + this.templatesURL = `${GENERAL_ENDPOINT}/api/accounts/${this.accountId}/email_templates`; + } + + /** + * Get a list of all templates. + */ + public async getList() { + const url = this.templatesURL; + + return this.client.get(url); + } + + /** + * Get a specific template by ID. + */ + public async get(templateId: number) { + const url = `${this.templatesURL}/${templateId}`; + + return this.client.get(url); + } + + /** + * Create a new template. + */ + public async create(params: TemplateCreateParams) { + const url = this.templatesURL; + const data = { email_template: params }; + + return this.client.post(url, data); + } + + /** + * Update an existing template. + */ + public async update(templateId: number, params: TemplateUpdateParams) { + const url = `${this.templatesURL}/${templateId}`; + const data = { email_template: params }; + + return this.client.patch(url, data); + } + + /** + * Delete a template. + */ + public async delete(templateId: number) { + const url = `${this.templatesURL}/${templateId}`; + + return this.client.delete(url); + } +} diff --git a/src/types/api/templates.ts b/src/types/api/templates.ts new file mode 100644 index 0000000..34b164c --- /dev/null +++ b/src/types/api/templates.ts @@ -0,0 +1,29 @@ +export interface Template { + id: number; + uuid: string; + name: string; + subject: string; + category: string; + body_html: string; + body_text?: string; + created_at: string; + updated_at: string; +} + +export interface TemplateCreateParams { + name: string; + subject: string; + category: string; + body_html: string; + body_text?: string; +} + +export interface TemplateUpdateParams { + name?: string; + subject?: string; + category?: string; + body_html?: string; + body_text?: string; +} + +export type TemplateList = Template[];