From 240f78362da6d733283ef4f7d0c68c3bf55b5dc5 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Mon, 5 Oct 2026 17:20:31 +0300 Subject: [PATCH 1/4] Move client.templates to /api/templates and drop the email_templates resource - client.templates now serves the paginated /api/templates surface: getList takes { token, per_page } and resolves { data, pagination }, get/create/update resolve { data }, bodies are flat. Pagination is reused from types/api/common. - The /api/email_templates resource is removed rather than kept behind a deprecated name: 5.0 is the release where a removal can land, and callers touching client.templates for the new shapes would have to edit every call site anyway. - Breaking for callers of client.templates, so this needs a major release; the README gains an "Upgrading to 5.0" note. --- README.md | 10 +- examples/templates/everything.ts | 19 ++-- .../lib/api/resources/Templates.test.ts | 104 +++++++++++------- src/lib/api/resources/Templates.ts | 48 +++++--- src/types/api/templates.ts | 47 ++++++-- 5 files changed, 149 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index ad0d92e3..97ef7f2e 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ Email Marketing: General API: -- Templates CRUD – [`templates/everything.ts`](examples/templates/everything.ts) +- Templates CRUD (paginated) – [`templates/everything.ts`](examples/templates/everything.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 +299,11 @@ 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; follow `pagination.next_token` for the rest. +- `get`, `create` and `update` resolve `{ data }` instead of the bare template. +- `create` and `update` send the fields as a flat body, so `category` is required on `create` while `body_html` and `body_text` are optional. diff --git a/examples/templates/everything.ts b/examples/templates/everything.ts index 32afe10c..a84c4869 100644 --- a/examples/templates/everything.ts +++ b/examples/templates/everything.ts @@ -17,26 +17,27 @@ 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); + // Get a page of templates (page-token pagination) + const list = await client.templates.getList({ per_page: 50, token: 1 }); + console.log("Templates:", list.data); + console.log("Pagination:", list.pagination); // 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/resources/Templates.test.ts b/src/__tests__/lib/api/resources/Templates.test.ts index 8d798593..cb4272a4 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,27 +113,50 @@ 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("fails with error.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; - const expectedErrorMessage = "Request failed with status code 400"; + const expectedErrorMessage = "token is out of range"; expect.assertions(2); - mock.onGet(endpoint).reply(400, { error: expectedErrorMessage }); + mock.onGet(endpoint).reply(422, { errors: expectedErrorMessage }); try { - await templatesAPI.getList(); + await templatesAPI.getList({ token: 99 }); } catch (error) { expect(error).toBeInstanceOf(MailtrapError); if (error instanceof MailtrapError) { @@ -145,18 +169,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 +183,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 +202,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 +242,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 +257,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 +286,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 +297,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/lib/api/resources/Templates.ts b/src/lib/api/resources/Templates.ts index 7c036d44..de66a598 100644 --- a/src/lib/api/resources/Templates.ts +++ b/src/lib/api/resources/Templates.ts @@ -2,9 +2,14 @@ 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; @@ -17,16 +22,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 !== undefined && { per_page: params.per_page }), + ...(params?.token !== undefined && { token: params.token }), + }; - return this.client.get(url); + return this.client.get(url, { + params: query, + }); } /** @@ -35,27 +47,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 +80,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/templates.ts b/src/types/api/templates.ts index 76e220f1..1bc98723 100644 --- a/src/types/api/templates.ts +++ b/src/types/api/templates.ts @@ -1,4 +1,6 @@ -export interface Template { +import { Pagination } from "./common"; + +export type Template = { id: number; uuid: string; name: string; @@ -8,20 +10,41 @@ export interface Template { body_text?: string; created_at: string; updated_at: string; -} +}; + +export type ListTemplatesParams = { + /** Page number to retrieve (page-token pagination). Defaults to 1. */ + token?: number; + /** Number of templates per page. Maximum 100, defaults to 50. */ + per_page?: number; +}; -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; From 5ddfb71e138ed1a953b2a15cd9cad28645b63812 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Tue, 6 Oct 2026 10:06:36 +0300 Subject: [PATCH 2/4] Address review: generic list failure case, experimental note --- README.md | 2 +- src/__tests__/lib/api/resources/Templates.test.ts | 6 +++--- src/lib/api/resources/Templates.ts | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 97ef7f2e..2b8103e5 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ Email Marketing: General API: -- Templates CRUD (paginated) – [`templates/everything.ts`](examples/templates/everything.ts) +- Templates CRUD (experimental) – [`templates/everything.ts`](examples/templates/everything.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) diff --git a/src/__tests__/lib/api/resources/Templates.test.ts b/src/__tests__/lib/api/resources/Templates.test.ts index cb4272a4..0088769b 100644 --- a/src/__tests__/lib/api/resources/Templates.test.ts +++ b/src/__tests__/lib/api/resources/Templates.test.ts @@ -149,14 +149,14 @@ describe("lib/api/resources/Templates: ", () => { }); it("fails with error.", async () => { - const expectedErrorMessage = "token is out of range"; + const expectedErrorMessage = "Request failed with status code 400"; expect.assertions(2); - mock.onGet(endpoint).reply(422, { errors: expectedErrorMessage }); + mock.onGet(endpoint).reply(400, { error: expectedErrorMessage }); try { - await templatesAPI.getList({ token: 99 }); + await templatesAPI.getList(); } catch (error) { expect(error).toBeInstanceOf(MailtrapError); if (error instanceof MailtrapError) { diff --git a/src/lib/api/resources/Templates.ts b/src/lib/api/resources/Templates.ts index de66a598..5d729237 100644 --- a/src/lib/api/resources/Templates.ts +++ b/src/lib/api/resources/Templates.ts @@ -15,6 +15,10 @@ import { const { CLIENT_SETTINGS } = CONFIG; const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; +/** + * Templates API. The `/api/templates` endpoints are experimental: their request + * and response shapes may change before general availability. + */ export default class TemplatesApi { private client: AxiosInstance; From bd0f12af8c33ee501b2bcd55054b980d5658ca9c Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Tue, 6 Oct 2026 17:33:53 +0300 Subject: [PATCH 3/4] Keep client.templates on /api/email_templates; add client.paginatedTemplates - Restore client.templates to the stable /api/email_templates endpoints, so this ships in 4.x with no breaking change. /api/templates is still experimental, and the 5.0 switch would have broken mailtrap-mcp on its next bump. - Move the /api/templates resource to the opt-in client.paginatedTemplates, marked experimental. - Type body_html and body_text as string | null, as the API returns them. - Accept a null token and per_page in getList, so pagination.next_token can be passed back as is. - Add examples/templates/paginated.ts, which follows next_token. --- README.md | 11 +- examples/templates/everything.ts | 19 +- examples/templates/paginated.ts | 51 +++ .../lib/api/PaginatedTemplates.test.ts | 20 ++ .../api/resources/PaginatedTemplates.test.ts | 339 ++++++++++++++++++ .../lib/api/resources/Templates.test.ts | 98 ++--- src/__tests__/lib/mailtrap-client.test.ts | 29 ++ src/lib/MailtrapClient.ts | 11 + src/lib/api/PaginatedTemplates.ts | 24 ++ src/lib/api/resources/PaginatedTemplates.ts | 91 +++++ src/lib/api/resources/Templates.ts | 52 +-- src/types/api/paginated-templates.ts | 58 +++ src/types/api/templates.ts | 47 +-- 13 files changed, 700 insertions(+), 150 deletions(-) create mode 100644 examples/templates/paginated.ts create mode 100644 src/__tests__/lib/api/PaginatedTemplates.test.ts create mode 100644 src/__tests__/lib/api/resources/PaginatedTemplates.test.ts create mode 100644 src/lib/api/PaginatedTemplates.ts create mode 100644 src/lib/api/resources/PaginatedTemplates.ts create mode 100644 src/types/api/paginated-templates.ts diff --git a/README.md b/README.md index 2b8103e5..559fa369 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,8 @@ Email Marketing: General API: -- Templates CRUD (experimental) – [`templates/everything.ts`](examples/templates/everything.ts) +- Templates CRUD – [`templates/everything.ts`](examples/templates/everything.ts) +- Paginated templates CRUD (experimental, `/api/templates`) – [`templates/paginated.ts`](examples/templates/paginated.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,11 +300,3 @@ 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; follow `pagination.next_token` for the rest. -- `get`, `create` and `update` resolve `{ data }` instead of the bare template. -- `create` and `update` send the fields as a flat body, so `category` is required on `create` while `body_html` and `body_text` are optional. diff --git a/examples/templates/everything.ts b/examples/templates/everything.ts index a84c4869..32afe10c 100644 --- a/examples/templates/everything.ts +++ b/examples/templates/everything.ts @@ -17,27 +17,26 @@ 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.data); + console.log("Created template:", newTemplate); - // Get a page of templates (page-token pagination) - const list = await client.templates.getList({ per_page: 50, token: 1 }); - console.log("Templates:", list.data); - console.log("Pagination:", list.pagination); + // 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.data.id); - console.log("Template details:", template.data); + const template = await client.templates.get(newTemplate.id); + console.log("Template details:", template); // Update the template - const updatedTemplate = await client.templates.update(newTemplate.data.id, { + 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.data); + console.log("Updated template:", updatedTemplate); // Delete the template - await client.templates.delete(newTemplate.data.id); + await client.templates.delete(newTemplate.id); console.log("Template deleted successfully"); } diff --git a/examples/templates/paginated.ts b/examples/templates/paginated.ts new file mode 100644 index 00000000..e88cea3f --- /dev/null +++ b/examples/templates/paginated.ts @@ -0,0 +1,51 @@ +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 = ""; + +const client = new MailtrapClient({ + token: TOKEN, + accountId: ACCOUNT_ID +}); + +async function paginatedTemplatesFlow() { + // Create a new template + const newTemplate = await client.paginatedTemplates.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.data); + + // List every template, one page at a time (page-token pagination) + let token: number | null = 1; + while (token !== null) { + const page = await client.paginatedTemplates.getList({ per_page: 50, token }); + console.log("Templates:", page.data); + token = page.pagination.next_token; + } + + // Get a specific template + const template = await client.paginatedTemplates.get(newTemplate.data.id); + console.log("Template details:", template.data); + + // Update the template + const updatedTemplate = await client.paginatedTemplates.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.data); + + // Delete the template + await client.paginatedTemplates.delete(newTemplate.data.id); + console.log("Template deleted successfully"); +} + +paginatedTemplatesFlow().catch(console.error); + diff --git a/src/__tests__/lib/api/PaginatedTemplates.test.ts b/src/__tests__/lib/api/PaginatedTemplates.test.ts new file mode 100644 index 00000000..997f23a8 --- /dev/null +++ b/src/__tests__/lib/api/PaginatedTemplates.test.ts @@ -0,0 +1,20 @@ +import axios from "axios"; + +import PaginatedTemplatesBaseAPI from "../../../lib/api/PaginatedTemplates"; + +describe("lib/api/PaginatedTemplates: ", () => { + const accountId = 100; + const paginatedTemplatesAPI = new PaginatedTemplatesBaseAPI(axios, accountId); + + describe("class PaginatedTemplatesBaseAPI(): ", () => { + describe("init: ", () => { + it("initializes with all necessary params.", () => { + expect(paginatedTemplatesAPI).toHaveProperty("create"); + expect(paginatedTemplatesAPI).toHaveProperty("getList"); + expect(paginatedTemplatesAPI).toHaveProperty("get"); + expect(paginatedTemplatesAPI).toHaveProperty("update"); + expect(paginatedTemplatesAPI).toHaveProperty("delete"); + }); + }); + }); +}); diff --git a/src/__tests__/lib/api/resources/PaginatedTemplates.test.ts b/src/__tests__/lib/api/resources/PaginatedTemplates.test.ts new file mode 100644 index 00000000..c36b562c --- /dev/null +++ b/src/__tests__/lib/api/resources/PaginatedTemplates.test.ts @@ -0,0 +1,339 @@ +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; + +import PaginatedTemplatesApi from "../../../../lib/api/resources/PaginatedTemplates"; +import handleSendingError from "../../../../lib/axios-logger"; +import MailtrapError from "../../../../lib/MailtrapError"; +import { + CreateTemplateParams, + ListTemplatesResponse, + Template, + UpdateTemplateParams, +} from "../../../../types/api/paginated-templates"; + +import CONFIG from "../../../../config"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +describe("lib/api/resources/PaginatedTemplates: ", () => { + let mock: AxiosMockAdapter; + const accountId = 100; + const templatesAPI = new PaginatedTemplatesApi(axios, accountId); + + const createTemplateRequest: CreateTemplateParams = { + 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: UpdateTemplateParams = { + 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 PaginatedTemplatesApi(): ", () => { + 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(): ", () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; + const expectedResponseData: ListTemplatesResponse = { + data: [ + { + 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", + }, + ], + 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 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}/templates/${templateId}`; + const expectedResponseData = { data: createTemplateResponse }; + + 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}/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 with a flat request body.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; + const expectedResponseData = { data: createTemplateResponse }; + + expect.assertions(3); + + mock + .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}/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 with a flat request body.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; + const expectedResponseData = { data: updateTemplateResponse }; + + expect.assertions(3); + + mock + .onPatch(endpoint, updateTemplateRequest) + .reply(200, expectedResponseData); + const result = await templatesAPI.update( + templateId, + updateTemplateRequest + ); + + 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}/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}/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}/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/api/resources/Templates.test.ts b/src/__tests__/lib/api/resources/Templates.test.ts index 0088769b..8d798593 100644 --- a/src/__tests__/lib/api/resources/Templates.test.ts +++ b/src/__tests__/lib/api/resources/Templates.test.ts @@ -5,10 +5,9 @@ import TemplatesApi from "../../../../lib/api/resources/Templates"; import handleSendingError from "../../../../lib/axios-logger"; import MailtrapError from "../../../../lib/MailtrapError"; import { - CreateTemplateParams, - ListTemplatesResponse, Template, - UpdateTemplateParams, + TemplateCreateParams, + TemplateUpdateParams, } from "../../../../types/api/templates"; import CONFIG from "../../../../config"; @@ -21,7 +20,7 @@ describe("lib/api/resources/Templates: ", () => { const accountId = 100; const templatesAPI = new TemplatesApi(axios, accountId); - const createTemplateRequest: CreateTemplateParams = { + const createTemplateRequest: TemplateCreateParams = { name: "Welcome Email", subject: "Welcome to Our Service!", category: "Promotional", @@ -41,7 +40,7 @@ describe("lib/api/resources/Templates: ", () => { updated_at: "2023-01-01T00:00:00Z", }; - const updateTemplateRequest: UpdateTemplateParams = { + const updateTemplateRequest: TemplateUpdateParams = { name: "Updated Welcome Email", subject: "Welcome to Our Amazing Service!", body_html: @@ -86,9 +85,9 @@ describe("lib/api/resources/Templates: ", () => { }); describe("getList(): ", () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; - const expectedResponseData: ListTemplatesResponse = { - data: [ + 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", @@ -113,42 +112,19 @@ describe("lib/api/resources/Templates: ", () => { created_at: "2023-01-01T00:00:00Z", updated_at: "2023-01-01T00:00:00Z", }, - ], - 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); + ]; + + expect.assertions(2); 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("fails with error.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; const expectedErrorMessage = "Request failed with status code 400"; expect.assertions(2); @@ -169,8 +145,18 @@ 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}/templates/${templateId}`; - const expectedResponseData = { data: createTemplateResponse }; + 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); @@ -183,7 +169,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}/templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; const expectedErrorMessage = "Template not found"; expect.assertions(2); @@ -202,26 +188,23 @@ describe("lib/api/resources/Templates: ", () => { }); describe("create(): ", () => { - it("successfully creates a template with a flat request body.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; - const expectedResponseData = { data: createTemplateResponse }; + it("successfully creates a template.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; + const expectedResponseData = createTemplateResponse; - expect.assertions(3); + expect.assertions(2); mock - .onPost(endpoint, createTemplateRequest) - .reply(201, expectedResponseData); + .onPost(endpoint, { email_template: createTemplateRequest }) + .reply(200, 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}/templates`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; const expectedErrorMessage = "Request failed with status code 400"; expect.assertions(2); @@ -242,14 +225,14 @@ describe("lib/api/resources/Templates: ", () => { describe("update(): ", () => { const templateId = 1; - it("successfully updates a template with a flat request body.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; - const expectedResponseData = { data: updateTemplateResponse }; + it("successfully updates a template.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const expectedResponseData = updateTemplateResponse; - expect.assertions(3); + expect.assertions(2); mock - .onPatch(endpoint, updateTemplateRequest) + .onPatch(endpoint, { email_template: updateTemplateRequest }) .reply(200, expectedResponseData); const result = await templatesAPI.update( templateId, @@ -257,14 +240,11 @@ 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}/templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; const expectedErrorMessage = "Request failed with status code 404"; expect.assertions(2); @@ -286,7 +266,7 @@ describe("lib/api/resources/Templates: ", () => { const templateId = 1; it("successfully deletes a template.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; expect.assertions(1); @@ -297,7 +277,7 @@ describe("lib/api/resources/Templates: ", () => { }); it("fails with error.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_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..23d2579b 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 PaginatedTemplatesBaseAPI from "../../lib/api/PaginatedTemplates"; import SuppressionsBaseAPI from "../../lib/api/Suppressions"; import SendingDomainsBaseAPI from "../../lib/api/SendingDomains"; import EmailLogsBaseAPI from "../../lib/api/EmailLogs"; @@ -878,6 +879,34 @@ describe("lib/mailtrap-client: ", () => { }); }); + describe("get paginatedTemplates(): ", () => { + it("rejects with Mailtrap error, when `accountId` is missing.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + }); + expect.assertions(1); + + try { + client.paginatedTemplates; + } catch (error) { + expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING)); + } + }); + + it("returns paginated templates API object when accountId is provided.", () => { + const client = new MailtrapClient({ + token: "MY_API_TOKEN", + accountId: 10, + }); + expect.assertions(1); + + const paginatedTemplatesClient = client.paginatedTemplates; + expect(paginatedTemplatesClient).toBeInstanceOf( + PaginatedTemplatesBaseAPI + ); + }); + }); + 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..42d4bf7d 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 PaginatedTemplatesBaseAPI from "./api/PaginatedTemplates"; import TestingAPI from "./api/Testing"; import WebhooksBaseAPI from "./api/Webhooks"; @@ -214,6 +215,16 @@ export default class MailtrapClient { return new TemplatesBaseAPI(this.axios, accountId); } + /** + * Getter for the paginated Templates API (`/api/templates`). The endpoints + * are experimental: their request and response shapes may change before + * general availability. + */ + get paginatedTemplates() { + const accountId = this.validateAccountIdPresence(); + return new PaginatedTemplatesBaseAPI(this.axios, accountId); + } + /** * Getter for Suppressions API. */ diff --git a/src/lib/api/PaginatedTemplates.ts b/src/lib/api/PaginatedTemplates.ts new file mode 100644 index 00000000..4d26b0b2 --- /dev/null +++ b/src/lib/api/PaginatedTemplates.ts @@ -0,0 +1,24 @@ +import { AxiosInstance } from "axios"; + +import PaginatedTemplatesApi from "./resources/PaginatedTemplates"; + +export default class PaginatedTemplatesBaseAPI { + public get: PaginatedTemplatesApi["get"]; + + public getList: PaginatedTemplatesApi["getList"]; + + public create: PaginatedTemplatesApi["create"]; + + public update: PaginatedTemplatesApi["update"]; + + public delete: PaginatedTemplatesApi["delete"]; + + constructor(client: AxiosInstance, accountId: number) { + const templates = new PaginatedTemplatesApi(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/PaginatedTemplates.ts b/src/lib/api/resources/PaginatedTemplates.ts new file mode 100644 index 00000000..c30c4b39 --- /dev/null +++ b/src/lib/api/resources/PaginatedTemplates.ts @@ -0,0 +1,91 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; +import { + CreateTemplateParams, + CreateTemplateResponse, + DeleteTemplateResponse, + GetTemplateResponse, + ListTemplatesParams, + ListTemplatesResponse, + UpdateTemplateParams, + UpdateTemplateResponse, +} from "../../../types/api/paginated-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 before general availability. + */ +export default class PaginatedTemplatesApi { + private client: AxiosInstance; + + private templatesURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.templatesURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; + } + + /** + * Lists the account's templates. The result is wrapped in a + * `{ data, pagination }` envelope; pagination is page-token based. + */ + 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, { + params: query, + }); + } + + /** + * 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: CreateTemplateParams) { + const url = this.templatesURL; + + return this.client.post( + url, + params + ); + } + + /** + * Update an existing template. + */ + public async update(templateId: number, params: UpdateTemplateParams) { + const url = `${this.templatesURL}/${templateId}`; + + return this.client.patch( + url, + params + ); + } + + /** + * 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 5d729237..7c036d44 100644 --- a/src/lib/api/resources/Templates.ts +++ b/src/lib/api/resources/Templates.ts @@ -2,23 +2,14 @@ import { AxiosInstance } from "axios"; import CONFIG from "../../../config"; import { - CreateTemplateParams, - CreateTemplateResponse, - DeleteTemplateResponse, - GetTemplateResponse, - ListTemplatesParams, - ListTemplatesResponse, - UpdateTemplateParams, - UpdateTemplateResponse, + Template, + TemplateCreateParams, + TemplateUpdateParams, } 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 before general availability. - */ export default class TemplatesApi { private client: AxiosInstance; @@ -26,23 +17,16 @@ export default class TemplatesApi { constructor(client: AxiosInstance, accountId: number) { this.client = client; - this.templatesURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; + this.templatesURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; } /** - * Lists the account's templates. The result is wrapped in a - * `{ data, pagination }` envelope; pagination is page-token based. + * Get a list of all templates. */ - public async getList(params?: ListTemplatesParams) { + public async getList() { const url = this.templatesURL; - const query = { - ...(params?.per_page !== undefined && { per_page: params.per_page }), - ...(params?.token !== undefined && { token: params.token }), - }; - return this.client.get(url, { - params: query, - }); + return this.client.get(url); } /** @@ -51,31 +35,27 @@ 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: CreateTemplateParams) { + public async create(params: TemplateCreateParams) { const url = this.templatesURL; + const data = { email_template: params }; - return this.client.post( - url, - params - ); + return this.client.post(url, data); } /** * Update an existing template. */ - public async update(templateId: number, params: UpdateTemplateParams) { + public async update(templateId: number, params: TemplateUpdateParams) { const url = `${this.templatesURL}/${templateId}`; + const data = { email_template: params }; - return this.client.patch( - url, - params - ); + return this.client.patch(url, data); } /** @@ -84,8 +64,6 @@ 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/paginated-templates.ts b/src/types/api/paginated-templates.ts new file mode 100644 index 00000000..e900e80b --- /dev/null +++ b/src/types/api/paginated-templates.ts @@ -0,0 +1,58 @@ +import { Pagination } from "./common"; + +export type Template = { + id: number; + uuid: string; + name: string; + subject: string; + category: 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 type CreateTemplateParams = { + 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; diff --git a/src/types/api/templates.ts b/src/types/api/templates.ts index 1bc98723..76e220f1 100644 --- a/src/types/api/templates.ts +++ b/src/types/api/templates.ts @@ -1,6 +1,4 @@ -import { Pagination } from "./common"; - -export type Template = { +export interface Template { id: number; uuid: string; name: string; @@ -10,41 +8,20 @@ export type Template = { body_text?: string; created_at: string; updated_at: string; -}; - -export type ListTemplatesParams = { - /** Page number to retrieve (page-token pagination). Defaults to 1. */ - token?: number; - /** Number of templates per page. Maximum 100, defaults to 50. */ - per_page?: number; -}; +} -export type CreateTemplateParams = { +export interface TemplateCreateParams { name: string; subject: string; category: string; - body_html?: 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; +export interface TemplateUpdateParams { + name?: string; + subject?: string; + category?: string; + body_html?: string; + body_text?: string; +} From e8db632794b9b95528aee5a63b95d60a4890543f Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Tue, 6 Oct 2026 17:49:51 +0300 Subject: [PATCH 4/4] Move client.templates to /api/templates and keep the old API as client.emailTemplates - client.templates calls the paginated /api/templates endpoints, so the agreed name needs no rename once the endpoints leave experimental. - The 4.x resource stays unchanged as client.emailTemplates on the stable /api/email_templates endpoints. Users who want the stable API rename one property and keep their code. - Say in the JSDoc and README that /api/templates shapes may change in a minor release while the endpoints are experimental. - examples/templates/everything.ts shows the paginated flow; email-templates.ts keeps the 4.x flow. --- README.md | 13 +- examples/templates/email-templates.ts | 44 +++++ examples/templates/everything.ts | 25 ++- examples/templates/paginated.ts | 51 ------ src/__tests__/lib/api/EmailTemplates.test.ts | 20 +++ .../lib/api/PaginatedTemplates.test.ts | 20 --- ...mplates.test.ts => EmailTemplates.test.ts} | 166 +++++++----------- .../lib/api/resources/Templates.test.ts | 120 +++++++++---- src/__tests__/lib/mailtrap-client.test.ts | 14 +- src/lib/MailtrapClient.ts | 15 +- src/lib/api/EmailTemplates.ts | 24 +++ src/lib/api/PaginatedTemplates.ts | 24 --- src/lib/api/resources/EmailTemplates.ts | 69 ++++++++ src/lib/api/resources/PaginatedTemplates.ts | 91 ---------- src/lib/api/resources/Templates.ts | 53 ++++-- src/types/api/email-templates.ts | 27 +++ src/types/api/paginated-templates.ts | 58 ------ src/types/api/templates.ts | 59 +++++-- 18 files changed, 451 insertions(+), 442 deletions(-) create mode 100644 examples/templates/email-templates.ts delete mode 100644 examples/templates/paginated.ts create mode 100644 src/__tests__/lib/api/EmailTemplates.test.ts delete mode 100644 src/__tests__/lib/api/PaginatedTemplates.test.ts rename src/__tests__/lib/api/resources/{PaginatedTemplates.test.ts => EmailTemplates.test.ts} (63%) create mode 100644 src/lib/api/EmailTemplates.ts delete mode 100644 src/lib/api/PaginatedTemplates.ts create mode 100644 src/lib/api/resources/EmailTemplates.ts delete mode 100644 src/lib/api/resources/PaginatedTemplates.ts create mode 100644 src/types/api/email-templates.ts delete mode 100644 src/types/api/paginated-templates.ts diff --git a/README.md b/README.md index 559fa369..67f64af0 100644 --- a/README.md +++ b/README.md @@ -273,8 +273,8 @@ Email Marketing: General API: -- Templates CRUD – [`templates/everything.ts`](examples/templates/everything.ts) -- Paginated templates CRUD (experimental, `/api/templates`) – [`templates/paginated.ts`](examples/templates/paginated.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) @@ -300,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/examples/templates/paginated.ts b/examples/templates/paginated.ts deleted file mode 100644 index e88cea3f..00000000 --- a/examples/templates/paginated.ts +++ /dev/null @@ -1,51 +0,0 @@ -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 = ""; - -const client = new MailtrapClient({ - token: TOKEN, - accountId: ACCOUNT_ID -}); - -async function paginatedTemplatesFlow() { - // Create a new template - const newTemplate = await client.paginatedTemplates.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.data); - - // List every template, one page at a time (page-token pagination) - let token: number | null = 1; - while (token !== null) { - const page = await client.paginatedTemplates.getList({ per_page: 50, token }); - console.log("Templates:", page.data); - token = page.pagination.next_token; - } - - // Get a specific template - const template = await client.paginatedTemplates.get(newTemplate.data.id); - console.log("Template details:", template.data); - - // Update the template - const updatedTemplate = await client.paginatedTemplates.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.data); - - // Delete the template - await client.paginatedTemplates.delete(newTemplate.data.id); - console.log("Template deleted successfully"); -} - -paginatedTemplatesFlow().catch(console.error); - 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/PaginatedTemplates.test.ts b/src/__tests__/lib/api/PaginatedTemplates.test.ts deleted file mode 100644 index 997f23a8..00000000 --- a/src/__tests__/lib/api/PaginatedTemplates.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import axios from "axios"; - -import PaginatedTemplatesBaseAPI from "../../../lib/api/PaginatedTemplates"; - -describe("lib/api/PaginatedTemplates: ", () => { - const accountId = 100; - const paginatedTemplatesAPI = new PaginatedTemplatesBaseAPI(axios, accountId); - - describe("class PaginatedTemplatesBaseAPI(): ", () => { - describe("init: ", () => { - it("initializes with all necessary params.", () => { - expect(paginatedTemplatesAPI).toHaveProperty("create"); - expect(paginatedTemplatesAPI).toHaveProperty("getList"); - expect(paginatedTemplatesAPI).toHaveProperty("get"); - expect(paginatedTemplatesAPI).toHaveProperty("update"); - expect(paginatedTemplatesAPI).toHaveProperty("delete"); - }); - }); - }); -}); diff --git a/src/__tests__/lib/api/resources/PaginatedTemplates.test.ts b/src/__tests__/lib/api/resources/EmailTemplates.test.ts similarity index 63% rename from src/__tests__/lib/api/resources/PaginatedTemplates.test.ts rename to src/__tests__/lib/api/resources/EmailTemplates.test.ts index c36b562c..bd2f87fc 100644 --- a/src/__tests__/lib/api/resources/PaginatedTemplates.test.ts +++ b/src/__tests__/lib/api/resources/EmailTemplates.test.ts @@ -1,27 +1,26 @@ import axios from "axios"; import AxiosMockAdapter from "axios-mock-adapter"; -import PaginatedTemplatesApi from "../../../../lib/api/resources/PaginatedTemplates"; +import EmailTemplatesApi from "../../../../lib/api/resources/EmailTemplates"; import handleSendingError from "../../../../lib/axios-logger"; import MailtrapError from "../../../../lib/MailtrapError"; import { - CreateTemplateParams, - ListTemplatesResponse, - Template, - UpdateTemplateParams, -} from "../../../../types/api/paginated-templates"; + 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/PaginatedTemplates: ", () => { +describe("lib/api/resources/EmailTemplates: ", () => { let mock: AxiosMockAdapter; const accountId = 100; - const templatesAPI = new PaginatedTemplatesApi(axios, accountId); + const emailTemplatesAPI = new EmailTemplatesApi(axios, accountId); - const createTemplateRequest: CreateTemplateParams = { + const createTemplateRequest: EmailTemplateCreateParams = { name: "Welcome Email", subject: "Welcome to Our Service!", category: "Promotional", @@ -29,7 +28,7 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { body_text: "Welcome! Thank you for joining our service.", }; - const createTemplateResponse: Template = { + const createTemplateResponse: EmailTemplate = { id: 1, uuid: "813e39db-c74a-4830-b037-0e6ba8b1fe88", name: "Welcome Email", @@ -41,14 +40,14 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { updated_at: "2023-01-01T00:00:00Z", }; - const updateTemplateRequest: UpdateTemplateParams = { + 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: Template = { + const updateTemplateResponse: EmailTemplate = { id: 1, uuid: "813e39db-c74a-4830-b037-0e6ba8b1fe88", name: "Updated Welcome Email", @@ -61,14 +60,14 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { updated_at: "2023-01-01T00:00:00Z", }; - describe("class PaginatedTemplatesApi(): ", () => { + describe("class EmailTemplatesApi(): ", () => { 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"); + expect(emailTemplatesAPI).toHaveProperty("create"); + expect(emailTemplatesAPI).toHaveProperty("update"); + expect(emailTemplatesAPI).toHaveProperty("delete"); + expect(emailTemplatesAPI).toHaveProperty("get"); + expect(emailTemplatesAPI).toHaveProperty("getList"); }); }); }); @@ -86,9 +85,9 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { }); describe("getList(): ", () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; - const expectedResponseData: ListTemplatesResponse = { - data: [ + 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", @@ -113,64 +112,19 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { created_at: "2023-01-01T00:00:00Z", updated_at: "2023-01-01T00:00:00Z", }, - ], - 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 }); - }); + const result = await emailTemplatesAPI.getList(); - 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(); + 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); @@ -178,7 +132,7 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { mock.onGet(endpoint).reply(400, { error: expectedErrorMessage }); try { - await templatesAPI.getList(); + await emailTemplatesAPI.getList(); } catch (error) { expect(error).toBeInstanceOf(MailtrapError); if (error instanceof MailtrapError) { @@ -191,13 +145,23 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { describe("get(): ", () => { it("successfully gets a template by ID.", async () => { const templateId = 1; - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; - const expectedResponseData = { data: createTemplateResponse }; + 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 templatesAPI.get(templateId); + const result = await emailTemplatesAPI.get(templateId); expect(mock.history.get[0].url).toEqual(endpoint); expect(result).toEqual(expectedResponseData); @@ -205,7 +169,7 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { it("fails with error when getting a template.", async () => { const templateId = 999; - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; const expectedErrorMessage = "Template not found"; expect.assertions(2); @@ -213,7 +177,7 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { mock.onGet(endpoint).reply(404, { error: expectedErrorMessage }); try { - await templatesAPI.get(templateId); + await emailTemplatesAPI.get(templateId); } catch (error) { expect(error).toBeInstanceOf(MailtrapError); if (error instanceof MailtrapError) { @@ -224,26 +188,23 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { }); describe("create(): ", () => { - it("successfully creates a template with a flat request body.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; - const expectedResponseData = { data: createTemplateResponse }; + it("successfully creates a template.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; + const expectedResponseData = createTemplateResponse; - expect.assertions(3); + expect.assertions(2); mock - .onPost(endpoint, createTemplateRequest) - .reply(201, expectedResponseData); - const result = await templatesAPI.create(createTemplateRequest); + .onPost(endpoint, { email_template: createTemplateRequest }) + .reply(200, expectedResponseData); + const result = await emailTemplatesAPI.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}/templates`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates`; const expectedErrorMessage = "Request failed with status code 400"; expect.assertions(2); @@ -251,7 +212,7 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { mock.onPost(endpoint).reply(400, { error: expectedErrorMessage }); try { - await templatesAPI.create(createTemplateRequest); + await emailTemplatesAPI.create(createTemplateRequest); } catch (error) { expect(error).toBeInstanceOf(MailtrapError); if (error instanceof MailtrapError) { @@ -264,29 +225,26 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { describe("update(): ", () => { const templateId = 1; - it("successfully updates a template with a flat request body.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; - const expectedResponseData = { data: updateTemplateResponse }; + it("successfully updates a template.", async () => { + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; + const expectedResponseData = updateTemplateResponse; - expect.assertions(3); + expect.assertions(2); mock - .onPatch(endpoint, updateTemplateRequest) + .onPatch(endpoint, { email_template: updateTemplateRequest }) .reply(200, expectedResponseData); - const result = await templatesAPI.update( + const result = await emailTemplatesAPI.update( templateId, updateTemplateRequest ); 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}/templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; const expectedErrorMessage = "Request failed with status code 404"; expect.assertions(2); @@ -294,7 +252,7 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { mock.onPatch(endpoint).reply(404, { error: expectedErrorMessage }); try { - await templatesAPI.update(templateId, updateTemplateRequest); + await emailTemplatesAPI.update(templateId, updateTemplateRequest); } catch (error) { expect(error).toBeInstanceOf(MailtrapError); if (error instanceof MailtrapError) { @@ -308,18 +266,18 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { const templateId = 1; it("successfully deletes a template.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; expect.assertions(1); mock.onDelete(endpoint).reply(204); - await templatesAPI.delete(templateId); + await emailTemplatesAPI.delete(templateId); expect(mock.history.delete[0].url).toEqual(endpoint); }); it("fails with error.", async () => { - const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates/${templateId}`; + const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/email_templates/${templateId}`; const expectedErrorMessage = "Request failed with status code 404"; expect.assertions(2); @@ -327,7 +285,7 @@ describe("lib/api/resources/PaginatedTemplates: ", () => { mock.onDelete(endpoint).reply(404, { error: expectedErrorMessage }); try { - await templatesAPI.delete(templateId); + await emailTemplatesAPI.delete(templateId); } catch (error) { expect(error).toBeInstanceOf(MailtrapError); if (error instanceof MailtrapError) { 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 23d2579b..9174f1f5 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -13,7 +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 PaginatedTemplatesBaseAPI from "../../lib/api/PaginatedTemplates"; +import EmailTemplatesBaseAPI from "../../lib/api/EmailTemplates"; import SuppressionsBaseAPI from "../../lib/api/Suppressions"; import SendingDomainsBaseAPI from "../../lib/api/SendingDomains"; import EmailLogsBaseAPI from "../../lib/api/EmailLogs"; @@ -879,7 +879,7 @@ describe("lib/mailtrap-client: ", () => { }); }); - describe("get paginatedTemplates(): ", () => { + describe("get emailTemplates(): ", () => { it("rejects with Mailtrap error, when `accountId` is missing.", () => { const client = new MailtrapClient({ token: "MY_API_TOKEN", @@ -887,23 +887,21 @@ describe("lib/mailtrap-client: ", () => { expect.assertions(1); try { - client.paginatedTemplates; + client.emailTemplates; } catch (error) { expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING)); } }); - it("returns paginated templates API object when accountId is provided.", () => { + it("returns email templates API object when accountId is provided.", () => { const client = new MailtrapClient({ token: "MY_API_TOKEN", accountId: 10, }); expect.assertions(1); - const paginatedTemplatesClient = client.paginatedTemplates; - expect(paginatedTemplatesClient).toBeInstanceOf( - PaginatedTemplatesBaseAPI - ); + const emailTemplatesClient = client.emailTemplates; + expect(emailTemplatesClient).toBeInstanceOf(EmailTemplatesBaseAPI); }); }); diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 42d4bf7d..54663af8 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -24,7 +24,7 @@ import SuppressionsBaseAPI from "./api/Suppressions"; import TrackingOptOutsBaseAPI from "./api/TrackingOptOuts"; import OrganizationsBaseAPI from "./api/Organizations"; import TemplatesBaseAPI from "./api/Templates"; -import PaginatedTemplatesBaseAPI from "./api/PaginatedTemplates"; +import EmailTemplatesBaseAPI from "./api/EmailTemplates"; import TestingAPI from "./api/Testing"; import WebhooksBaseAPI from "./api/Webhooks"; @@ -208,7 +208,9 @@ 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(); @@ -216,13 +218,12 @@ export default class MailtrapClient { } /** - * Getter for the paginated Templates API (`/api/templates`). The endpoints - * are experimental: their request and response shapes may change before - * general availability. + * Getter for Email Templates API (`/api/email_templates`), the stable + * templates API with the 4.x shapes. For the paginated API, see `templates`. */ - get paginatedTemplates() { + get emailTemplates() { const accountId = this.validateAccountIdPresence(); - return new PaginatedTemplatesBaseAPI(this.axios, accountId); + return new EmailTemplatesBaseAPI(this.axios, accountId); } /** 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/PaginatedTemplates.ts b/src/lib/api/PaginatedTemplates.ts deleted file mode 100644 index 4d26b0b2..00000000 --- a/src/lib/api/PaginatedTemplates.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { AxiosInstance } from "axios"; - -import PaginatedTemplatesApi from "./resources/PaginatedTemplates"; - -export default class PaginatedTemplatesBaseAPI { - public get: PaginatedTemplatesApi["get"]; - - public getList: PaginatedTemplatesApi["getList"]; - - public create: PaginatedTemplatesApi["create"]; - - public update: PaginatedTemplatesApi["update"]; - - public delete: PaginatedTemplatesApi["delete"]; - - constructor(client: AxiosInstance, accountId: number) { - const templates = new PaginatedTemplatesApi(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/PaginatedTemplates.ts b/src/lib/api/resources/PaginatedTemplates.ts deleted file mode 100644 index c30c4b39..00000000 --- a/src/lib/api/resources/PaginatedTemplates.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { AxiosInstance } from "axios"; - -import CONFIG from "../../../config"; -import { - CreateTemplateParams, - CreateTemplateResponse, - DeleteTemplateResponse, - GetTemplateResponse, - ListTemplatesParams, - ListTemplatesResponse, - UpdateTemplateParams, - UpdateTemplateResponse, -} from "../../../types/api/paginated-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 before general availability. - */ -export default class PaginatedTemplatesApi { - private client: AxiosInstance; - - private templatesURL: string; - - constructor(client: AxiosInstance, accountId: number) { - this.client = client; - this.templatesURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/templates`; - } - - /** - * Lists the account's templates. The result is wrapped in a - * `{ data, pagination }` envelope; pagination is page-token based. - */ - 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, { - params: query, - }); - } - - /** - * 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: CreateTemplateParams) { - const url = this.templatesURL; - - return this.client.post( - url, - params - ); - } - - /** - * Update an existing template. - */ - public async update(templateId: number, params: UpdateTemplateParams) { - const url = `${this.templatesURL}/${templateId}`; - - return this.client.patch( - url, - params - ); - } - - /** - * 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/paginated-templates.ts b/src/types/api/paginated-templates.ts deleted file mode 100644 index e900e80b..00000000 --- a/src/types/api/paginated-templates.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Pagination } from "./common"; - -export type Template = { - id: number; - uuid: string; - name: string; - subject: string; - category: 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 type CreateTemplateParams = { - 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; 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;