diff --git a/README.md b/README.md index ad0d92e3..67f64af0 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,8 @@ Email Marketing: General API: -- Templates CRUD – [`templates/everything.ts`](examples/templates/everything.ts) +- Templates CRUD, paginated (experimental) – [`templates/everything.ts`](examples/templates/everything.ts) +- Email templates CRUD (`/api/email_templates`) – [`templates/email-templates.ts`](examples/templates/email-templates.ts) - Suppressions (create, find & delete) – [`sending/suppressions.ts`](examples/sending/suppressions.ts) - Tracking Opt-outs (list, create & delete) – [`sending/tracking-opt-outs.ts`](examples/sending/tracking-opt-outs.ts) - Billing info – [`general/billing.ts`](examples/general/billing.ts) @@ -299,3 +300,12 @@ Everyone interacting in the Mailtrap project's codebases, issue trackers, chat r Versions of this package up to 2.0.2 were an [unofficial client](https://github.com/vchin/mailtrap-client) developed by [@vchin](https://github.com/vchin). Package version 3 is a completely new package. +### Upgrading to 5.0 + +`client.templates` now calls the paginated `/api/templates` endpoints instead of `/api/email_templates`: + +- `getList()` takes optional `{ token, per_page }` and resolves `{ data, pagination }` instead of a bare array. Pages hold at most 100 templates; pass `pagination.next_token` with the same `per_page` for the rest. +- `get`, `create` and `update` resolve `{ data }` instead of the bare template. `body_html` and `body_text` are `null` when the template has no such body. +- `create` and `update` send the fields as a flat body, so `category` is required on `create` while `body_html` and `body_text` are optional. + +The `/api/templates` endpoints are experimental, and their shapes may change in a minor release before general availability. To keep the 4.x behavior, rename `client.templates` to `client.emailTemplates`: it calls the stable `/api/email_templates` endpoints with the same methods and shapes. diff --git a/examples/templates/email-templates.ts b/examples/templates/email-templates.ts new file mode 100644 index 00000000..17442248 --- /dev/null +++ b/examples/templates/email-templates.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 emailTemplatesFlow() { + // Create a new template + const newTemplate = await client.emailTemplates.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.emailTemplates.getList(); + console.log("All templates:", allTemplates); + + // Get a specific template + const template = await client.emailTemplates.get(newTemplate.id); + console.log("Template details:", template); + + // Update the template + const updatedTemplate = await client.emailTemplates.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.emailTemplates.delete(newTemplate.id); + console.log("Template deleted successfully"); +} + +emailTemplatesFlow().catch(console.error); + diff --git a/examples/templates/everything.ts b/examples/templates/everything.ts index 32afe10c..a4c1fdcf 100644 --- a/examples/templates/everything.ts +++ b/examples/templates/everything.ts @@ -1,5 +1,8 @@ import { MailtrapClient } from "mailtrap"; +// The /api/templates endpoints are experimental: their request and response +// shapes may change before general availability. + const TOKEN = ""; const ACCOUNT_ID = ""; @@ -17,26 +20,30 @@ async function templatesFlow() { body_html: "

Welcome!

Thank you for joining our service.

", body_text: "Welcome! Thank you for joining our service." }); - console.log("Created template:", newTemplate); + console.log("Created template:", newTemplate.data); - // Get all templates - const allTemplates = await client.templates.getList(); - console.log("All templates:", allTemplates); + // List every template, one page at a time (page-token pagination) + let token: number | null = 1; + while (token !== null) { + const page = await client.templates.getList({ per_page: 50, token }); + console.log("Templates:", page.data); + token = page.pagination.next_token; + } // Get a specific template - const template = await client.templates.get(newTemplate.id); - console.log("Template details:", template); + const template = await client.templates.get(newTemplate.data.id); + console.log("Template details:", template.data); // Update the template - const updatedTemplate = await client.templates.update(newTemplate.id, { + const updatedTemplate = await client.templates.update(newTemplate.data.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); + console.log("Updated template:", updatedTemplate.data); // Delete the template - await client.templates.delete(newTemplate.id); + await client.templates.delete(newTemplate.data.id); console.log("Template deleted successfully"); } diff --git a/src/__tests__/lib/api/EmailTemplates.test.ts b/src/__tests__/lib/api/EmailTemplates.test.ts new file mode 100644 index 00000000..a27c40aa --- /dev/null +++ b/src/__tests__/lib/api/EmailTemplates.test.ts @@ -0,0 +1,20 @@ +import axios from "axios"; + +import EmailTemplatesBaseAPI from "../../../lib/api/EmailTemplates"; + +describe("lib/api/EmailTemplates: ", () => { + const accountId = 100; + const emailTemplatesAPI = new EmailTemplatesBaseAPI(axios, accountId); + + describe("class EmailTemplatesBaseAPI(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(emailTemplatesAPI).toHaveProperty("create"); + expect(emailTemplatesAPI).toHaveProperty("getList"); + expect(emailTemplatesAPI).toHaveProperty("get"); + expect(emailTemplatesAPI).toHaveProperty("update"); + expect(emailTemplatesAPI).toHaveProperty("delete"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/EmailTemplates.test.ts b/src/__tests__/lib/api/resources/EmailTemplates.test.ts new file mode 100644 index 00000000..bd2f87fc --- /dev/null +++ b/src/__tests__/lib/api/resources/EmailTemplates.test.ts @@ -0,0 +1,297 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import EmailTemplatesApi from "../../../../lib/api/resources/EmailTemplates"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; +import { + EmailTemplate, + EmailTemplateCreateParams, + EmailTemplateUpdateParams, +} from "../../../../types/api/email-templates"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/EmailTemplates: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const emailTemplatesAPI = new EmailTemplatesApi(axios, accountId); + + const createTemplateRequest: EmailTemplateCreateParams = { + 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: EmailTemplate = { + 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: EmailTemplateUpdateParams = { + name: "Updated Welcome Email", + subject: "Welcome to Our Amazing Service!", + body_html: + "

Welcome!

Thank you for joining our amazing service.

", + }; + + const updateTemplateResponse: EmailTemplate = { + 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 EmailTemplatesApi(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(emailTemplatesAPI).toHaveProperty("create"); + expect(emailTemplatesAPI).toHaveProperty("update"); + expect(emailTemplatesAPI).toHaveProperty("delete"); + expect(emailTemplatesAPI).toHaveProperty("get"); + expect(emailTemplatesAPI).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: EmailTemplate[] = [ + { + 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 emailTemplatesAPI.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 emailTemplatesAPI.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: EmailTemplate = { + 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 emailTemplatesAPI.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 emailTemplatesAPI.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 emailTemplatesAPI.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 emailTemplatesAPI.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 emailTemplatesAPI.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 emailTemplatesAPI.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 emailTemplatesAPI.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 emailTemplatesAPI.delete(templateId); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + expect(error.message).toEqual(expectedErrorMessage); + } + } + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/Templates.test.ts b/src/__tests__/lib/api/resources/Templates.test.ts index 8d798593..11f6f325 100644 --- a/src/__tests__/lib/api/resources/Templates.test.ts +++ b/src/__tests__/lib/api/resources/Templates.test.ts @@ -5,9 +5,10 @@ import TemplatesApi from "../../../../lib/api/resources/Templates"; import handleSendingError from "../../../../lib/axios-logger"; import MailtrapError from "../../../../lib/MailtrapError"; import { + CreateTemplateParams, + ListTemplatesResponse, Template, - TemplateCreateParams, - TemplateUpdateParams, + UpdateTemplateParams, } from "../../../../types/api/templates"; import CONFIG from "../../../../config"; @@ -20,7 +21,7 @@ describe("lib/api/resources/Templates: ", () => { const accountId = 100; const templatesAPI = new TemplatesApi(axios, accountId); - const createTemplateRequest: TemplateCreateParams = { + const createTemplateRequest: CreateTemplateParams = { name: "Welcome Email", subject: "Welcome to Our Service!", category: "Promotional", @@ -40,7 +41,7 @@ describe("lib/api/resources/Templates: ", () => { updated_at: "2023-01-01T00:00:00Z", }; - const updateTemplateRequest: TemplateUpdateParams = { + const updateTemplateRequest: UpdateTemplateParams = { name: "Updated Welcome Email", subject: "Welcome to Our Amazing Service!", body_html: @@ -85,9 +86,9 @@ describe("lib/api/resources/Templates: ", () => { }); describe("getList(): ", () => { - it("successfully gets all templates.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; - const expectedResponseData: Template[] = [ + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; + const expectedResponseData: ListTemplatesResponse = { + data: [ { id: 1, uuid: "813e39db-c74a-4830-b037-0e6ba8b1fe88", @@ -112,19 +113,64 @@ describe("lib/api/resources/Templates: ", () => { created_at: "2023-01-01T00:00:00Z", updated_at: "2023-01-01T00:00:00Z", }, - ]; - - expect.assertions(2); + ], + pagination: { + token: 1, + prev_token: null, + next_token: 2, + first_url: `${endpoint}?per_page=50&token=1`, + prev_url: null, + current_url: `${endpoint}?per_page=50&token=1`, + next_url: `${endpoint}?per_page=50&token=2`, + }, + }; + + it("successfully gets a page of templates.", async () => { + expect.assertions(3); mock.onGet(endpoint).reply(200, expectedResponseData); const result = await templatesAPI.getList(); expect(mock.history.get[0].url).toEqual(endpoint); + expect(mock.history.get[0].params).toEqual({}); expect(result).toEqual(expectedResponseData); }); + it("serializes per_page and token as query params.", async () => { + const params = { per_page: 25, token: 2 }; + + expect.assertions(2); + + mock.onGet(endpoint, { params }).reply(200, expectedResponseData); + const result = await templatesAPI.getList(params); + + expect(mock.history.get[0].params).toEqual(params); + expect(result).toEqual(expectedResponseData); + }); + + it("omits a null token, so pagination.next_token can be passed as is.", async () => { + expect.assertions(1); + + mock.onGet(endpoint).reply(200, expectedResponseData); + await templatesAPI.getList({ per_page: 25, token: null }); + + expect(mock.history.get[0].params).toEqual({ per_page: 25 }); + }); + + it("returns null bodies as null.", async () => { + const textOnly = { + data: { ...createTemplateResponse, body_html: null }, + }; + + expect.assertions(1); + + mock.onGet(`${endpoint}/1`).reply(200, textOnly); + const result = await templatesAPI.get(1); + + expect(result.data.body_html).toBeNull(); + }); + 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); @@ -145,18 +191,8 @@ describe("lib/api/resources/Templates: ", () => { 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", - }; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; + const expectedResponseData = { data: createTemplateResponse }; expect.assertions(2); @@ -169,7 +205,7 @@ describe("lib/api/resources/Templates: ", () => { it("fails with error when getting a template.", async () => { const templateId = 999; - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; const expectedErrorMessage = "Template not found"; expect.assertions(2); @@ -188,23 +224,26 @@ describe("lib/api/resources/Templates: ", () => { }); describe("create(): ", () => { - it("successfully creates a template.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; - const expectedResponseData = createTemplateResponse; + it("successfully creates a template with a flat request body.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; + const expectedResponseData = { data: createTemplateResponse }; - expect.assertions(2); + expect.assertions(3); mock - .onPost(endpoint, { email_template: createTemplateRequest }) - .reply(200, expectedResponseData); + .onPost(endpoint, createTemplateRequest) + .reply(201, expectedResponseData); const result = await templatesAPI.create(createTemplateRequest); expect(mock.history.post[0].url).toEqual(endpoint); + expect(JSON.parse(mock.history.post[0].data)).toEqual( + createTemplateRequest + ); expect(result).toEqual(expectedResponseData); }); it("fails with error.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; const expectedErrorMessage = "Request failed with status code 400"; expect.assertions(2); @@ -225,14 +264,14 @@ describe("lib/api/resources/Templates: ", () => { describe("update(): ", () => { const templateId = 1; - it("successfully updates a template.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; - const expectedResponseData = updateTemplateResponse; + it("successfully updates a template with a flat request body.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; + const expectedResponseData = { data: updateTemplateResponse }; - expect.assertions(2); + expect.assertions(3); mock - .onPatch(endpoint, { email_template: updateTemplateRequest }) + .onPatch(endpoint, updateTemplateRequest) .reply(200, expectedResponseData); const result = await templatesAPI.update( templateId, @@ -240,11 +279,14 @@ describe("lib/api/resources/Templates: ", () => { ); expect(mock.history.patch[0].url).toEqual(endpoint); + expect(JSON.parse(mock.history.patch[0].data)).toEqual( + updateTemplateRequest + ); expect(result).toEqual(expectedResponseData); }); it("fails with error.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; const expectedErrorMessage = "Request failed with status code 404"; expect.assertions(2); @@ -266,7 +308,7 @@ describe("lib/api/resources/Templates: ", () => { const templateId = 1; it("successfully deletes a template.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; expect.assertions(1); @@ -277,7 +319,7 @@ describe("lib/api/resources/Templates: ", () => { }); it("fails with error.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; const expectedErrorMessage = "Request failed with status code 404"; expect.assertions(2); diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index 70513f15..9174f1f5 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -13,6 +13,7 @@ import ContactLists from "../../lib/api/ContactLists"; import Contacts from "../../lib/api/Contacts"; import ContactExportsBaseAPI from "../../lib/api/ContactExports"; import TemplatesBaseAPI from "../../lib/api/Templates"; +import EmailTemplatesBaseAPI from "../../lib/api/EmailTemplates"; import SuppressionsBaseAPI from "../../lib/api/Suppressions"; import SendingDomainsBaseAPI from "../../lib/api/SendingDomains"; import EmailLogsBaseAPI from "../../lib/api/EmailLogs"; @@ -878,6 +879,32 @@ describe("lib/mailtrap-client: ", () => { }); }); + describe("get emailTemplates(): ", () => { + it("rejects with Mailtrap error, when `accountId` is missing.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + }); + expect.assertions(1); + + try { + client.emailTemplates; + } catch (error) { + expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING)); + } + }); + + it("returns email templates API object when accountId is provided.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + accountId: 10, + }); + expect.assertions(1); + + const emailTemplatesClient = client.emailTemplates; + expect(emailTemplatesClient).toBeInstanceOf(EmailTemplatesBaseAPI); + }); + }); + describe("get suppressions(): ", () => { it("rejects with Mailtrap error, when `accountId` is missing.", () => { const client = new MailtrapClient({ diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 7e4263e6..54663af8 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -24,6 +24,7 @@ import SuppressionsBaseAPI from "./api/Suppressions"; import TrackingOptOutsBaseAPI from "./api/TrackingOptOuts"; import OrganizationsBaseAPI from "./api/Organizations"; import TemplatesBaseAPI from "./api/Templates"; +import EmailTemplatesBaseAPI from "./api/EmailTemplates"; import TestingAPI from "./api/Testing"; import WebhooksBaseAPI from "./api/Webhooks"; @@ -207,13 +208,24 @@ export default class MailtrapClient { } /** - * Getter for Templates API. + * Getter for Templates API (`/api/templates`). The endpoints are + * experimental: their request and response shapes may change in a minor + * release before general availability. */ get templates() { const accountId = this.validateAccountIdPresence(); return new TemplatesBaseAPI(this.axios, accountId); } + /** + * Getter for Email Templates API (`/api/email_templates`), the stable + * templates API with the 4.x shapes. For the paginated API, see `templates`. + */ + get emailTemplates() { + const accountId = this.validateAccountIdPresence(); + return new EmailTemplatesBaseAPI(this.axios, accountId); + } + /** * Getter for Suppressions API. */ diff --git a/src/lib/api/EmailTemplates.ts b/src/lib/api/EmailTemplates.ts new file mode 100644 index 00000000..76126ee0 --- /dev/null +++ b/src/lib/api/EmailTemplates.ts @@ -0,0 +1,24 @@ +import { AxiosInstance } from "axios"; + +import EmailTemplatesApi from "./resources/EmailTemplates"; + +export default class EmailTemplatesBaseAPI { + public get: EmailTemplatesApi["get"]; + + public getList: EmailTemplatesApi["getList"]; + + public create: EmailTemplatesApi["create"]; + + public update: EmailTemplatesApi["update"]; + + public delete: EmailTemplatesApi["delete"]; + + constructor(client: AxiosInstance, accountId: number) { + const templates = new EmailTemplatesApi(client, 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/EmailTemplates.ts b/src/lib/api/resources/EmailTemplates.ts new file mode 100644 index 00000000..64fa9683 --- /dev/null +++ b/src/lib/api/resources/EmailTemplates.ts @@ -0,0 +1,69 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + EmailTemplate, + EmailTemplateCreateParams, + EmailTemplateUpdateParams, +} from "../../../types/api/email-templates"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class EmailTemplatesApi { + private client: AxiosInstance; + + private templatesURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.templatesURL = `${GENERAL_ENDPOINT}/api/accounts/${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: EmailTemplateCreateParams) { + 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: EmailTemplateUpdateParams) { + 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/lib/api/resources/Templates.ts b/src/lib/api/resources/Templates.ts index 7c036d44..d9b9649c 100644 --- a/src/lib/api/resources/Templates.ts +++ b/src/lib/api/resources/Templates.ts @@ -2,14 +2,24 @@ import { AxiosInstance } from "axios"; import CONFIG from "../../../config"; import { - Template, - TemplateCreateParams, - TemplateUpdateParams, + CreateTemplateParams, + CreateTemplateResponse, + DeleteTemplateResponse, + GetTemplateResponse, + ListTemplatesParams, + ListTemplatesResponse, + UpdateTemplateParams, + UpdateTemplateResponse, } from "../../../types/api/templates"; const { CLIENT_SETTINGS } = CONFIG; const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; +/** + * Templates API. The `/api/templates` endpoints are experimental: their request + * and response shapes may change in a minor release before general availability. + * For the stable `/api/email_templates` endpoints, use `EmailTemplatesApi`. + */ export default class TemplatesApi { private client: AxiosInstance; @@ -17,16 +27,23 @@ export default class TemplatesApi { constructor(client: AxiosInstance, accountId: number) { this.client = client; - this.templatesURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; + this.templatesURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; } /** - * Get a list of all templates. + * Lists the account's templates. The result is wrapped in a + * `{ data, pagination }` envelope; pagination is page-token based. */ - public async getList() { + public async getList(params?: ListTemplatesParams) { const url = this.templatesURL; + const query = { + ...(params?.per_page != null && { per_page: params.per_page }), + ...(params?.token != null && { token: params.token }), + }; - return this.client.get(url); + return this.client.get(url, { + params: query, + }); } /** @@ -35,27 +52,31 @@ export default class TemplatesApi { public async get(templateId: number) { const url = `${this.templatesURL}/${templateId}`; - return this.client.get(url); + return this.client.get(url); } /** * Create a new template. */ - public async create(params: TemplateCreateParams) { + public async create(params: CreateTemplateParams) { const url = this.templatesURL; - const data = { email_template: params }; - return this.client.post(url, data); + return this.client.post( + url, + params + ); } /** * Update an existing template. */ - public async update(templateId: number, params: TemplateUpdateParams) { + public async update(templateId: number, params: UpdateTemplateParams) { const url = `${this.templatesURL}/${templateId}`; - const data = { email_template: params }; - return this.client.patch(url, data); + return this.client.patch( + url, + params + ); } /** @@ -64,6 +85,8 @@ export default class TemplatesApi { public async delete(templateId: number) { const url = `${this.templatesURL}/${templateId}`; - return this.client.delete(url); + return this.client.delete( + url + ); } } diff --git a/src/types/api/email-templates.ts b/src/types/api/email-templates.ts new file mode 100644 index 00000000..ce99963b --- /dev/null +++ b/src/types/api/email-templates.ts @@ -0,0 +1,27 @@ +export interface EmailTemplate { + id: number; + uuid: string; + name: string; + subject: string; + category: string; + body_html: string; + body_text?: string; + created_at: string; + updated_at: string; +} + +export interface EmailTemplateCreateParams { + name: string; + subject: string; + category: string; + body_html: string; + body_text?: string; +} + +export interface EmailTemplateUpdateParams { + name?: string; + subject?: string; + category?: string; + body_html?: string; + body_text?: string; +} diff --git a/src/types/api/templates.ts b/src/types/api/templates.ts index 76e220f1..e900e80b 100644 --- a/src/types/api/templates.ts +++ b/src/types/api/templates.ts @@ -1,27 +1,58 @@ -export interface Template { +import { Pagination } from "./common"; + +export type Template = { id: number; uuid: string; name: string; subject: string; category: string; - body_html: string; - body_text?: string; + /** `null` when the template was created without an HTML body. */ + body_html: string | null; + /** `null` when the template was created without a text body. */ + body_text: string | null; created_at: string; updated_at: string; -} +}; + +export type ListTemplatesParams = { + /** + * Page number to retrieve (page-token pagination). Defaults to 1. Accepts + * `pagination.next_token` as is; `null` is the same as leaving it out. + */ + token?: number | null; + /** + * Number of templates per page. Maximum 100, defaults to 50. Pass the same + * value on every page. + */ + per_page?: number | null; +}; -export interface TemplateCreateParams { +export type CreateTemplateParams = { 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 UpdateTemplateParams = Partial; + +export type ListTemplatesResponse = { + data: Template[]; + pagination: Pagination; +}; + +export type GetTemplateResponse = { + data: Template; +}; + +export type CreateTemplateResponse = { + data: Template; +}; + +export type UpdateTemplateResponse = { + data: Template; +}; + +/** Delete returns `204 No Content` — there is no response body. */ +export type DeleteTemplateResponse = void;