diff --git a/README.md b/README.md index a776806..192fb3f 100644 --- a/README.md +++ b/README.md @@ -99,22 +99,29 @@ You can use custom contact properties in API calls. Please make sure to [add cus - [checkContactSuppression()](#checkcontactsuppression) - [removeContactSuppression()](#removecontactsuppression) - [createContactProperty()](#createcontactproperty) -- [getContactProperties()](#getcontactproperties) -- [getMailingLists()](#getmailinglists) +- [listContactProperties()](#listcontactproperties) +- [listMailingLists()](#listmailinglists) - [sendEvent()](#sendevent) - [sendTransactionalEmail()](#sendtransactionalemail) -- [getTransactionalEmails()](#gettransactionalemails) -- [getDedicatedSendingIps()](#getdedicatedsendingips) -- [getThemes()](#getthemes) +- [listTransactionalEmails()](#listtransactionalemails) +- [getTransactionalEmail()](#gettransactionalemail) +- [createTransactionalEmail()](#createtransactionalemail) +- [updateTransactionalEmail()](#updatetransactionalemail) +- [ensureTransactionalEmailDraft()](#ensuretransactionalemaildraft) +- [publishTransactionalEmail()](#publishtransactionalemail) +- [listDedicatedSendingIps()](#listdedicatedsendingips) +- [listThemes()](#listthemes) - [getTheme()](#gettheme) -- [getComponents()](#getcomponents) +- [listComponents()](#listcomponents) - [getComponent()](#getcomponent) -- [getCampaigns()](#getcampaigns) +- [listCampaigns()](#listcampaigns) - [createCampaign()](#createcampaign) - [getCampaign()](#getcampaign) - [updateCampaign()](#updatecampaign) - [getEmailMessage()](#getemailmessage) - [updateEmailMessage()](#updateemailmessage) +- [createUpload()](#createupload) +- [completeUpload()](#completeupload) --- @@ -531,7 +538,7 @@ HTTP 400 Bad Request --- -### getContactProperties() +### listContactProperties() Get a list of your account's contact properties. @@ -546,9 +553,9 @@ Get a list of your account's contact properties. #### Example ```javascript -const resp = await loops.getContactProperties(); +const resp = await loops.listContactProperties(); -const resp = await loops.getContactProperties("custom"); +const resp = await loops.listContactProperties("custom"); ``` #### Response @@ -617,7 +624,7 @@ This method will return a list of contact property objects containing `key`, `la --- -### getMailingLists() +### listMailingLists() Get a list of your account's mailing lists. [Read more about mailing lists](https://loops.so/docs/contacts/mailing-lists) @@ -630,7 +637,7 @@ None #### Example ```javascript -const resp = await loops.getMailingLists(); +const resp = await loops.listMailingLists(); ``` #### Response @@ -835,9 +842,9 @@ HTTP 400 Bad Request --- -### getTransactionalEmails() +### listTransactionalEmails() -Get a list of published transactional emails. +Get a paginated list of transactional emails, most recently created first. [API Reference](https://loops.so/docs/api-reference/list-transactional-emails) @@ -851,9 +858,9 @@ Get a list of published transactional emails. #### Example ```javascript -const resp = await loops.getTransactionalEmails(); +const resp = await loops.listTransactionalEmails(); -const resp = await loops.getTransactionalEmails({ perPage: 15 }); +const resp = await loops.listTransactionalEmails({ perPage: 15 }); ``` #### Response @@ -866,30 +873,29 @@ const resp = await loops.getTransactionalEmails({ perPage: 15 }); "perPage": 20, "totalPages": 2, "nextCursor": "clyo0q4wo01p59fsecyxqsh38", - "nextPage": "https://app.loops.so/api/v1/transactional?cursor=clyo0q4wo01p59fsecyxqsh38&perPage=20" + "nextPage": "https://app.loops.so/api/v1/transactional-emails?cursor=clyo0q4wo01p59fsecyxqsh38&perPage=20" }, "data": [ { "id": "clfn0k1yg001imo0fdeqg30i8", - "lastUpdated": "2023-11-06T17:48:07.249Z", + "name": "Sign up confirmation", + "draftEmailMessageId": null, + "publishedEmailMessageId": "msg_123", + "createdAt": "2023-11-06T17:48:07.249Z", + "updatedAt": "2023-11-06T17:48:07.249Z", "dataVariables": [] }, { "id": "cll42l54f20i1la0lfooe3z12", - "lastUpdated": "2025-02-02T02:56:28.845Z", + "name": "Password reset", + "draftEmailMessageId": "msg_456", + "publishedEmailMessageId": "msg_789", + "createdAt": "2025-02-02T02:56:28.845Z", + "updatedAt": "2025-02-02T02:56:28.845Z", "dataVariables": [ "confirmationUrl" ] }, - { - "id": "clw6rbuwp01rmeiyndm80155l", - "lastUpdated": "2024-05-14T19:02:52.000Z", - "dataVariables": [ - "firstName", - "lastName", - "inviteLink" - ] - }, ... ] } @@ -897,7 +903,110 @@ const resp = await loops.getTransactionalEmails({ perPage: 15 }); --- -### getDedicatedSendingIps() +### getTransactionalEmail() + +Retrieve a single transactional email by ID. + +[API Reference](https://loops.so/docs/api-reference/get-transactional-email) + +#### Parameters + +| Name | Type | Required | Notes | +| ----------------- | ------ | -------- | ------------------------------------ | +| `transactionalId` | string | Yes | The ID of the transactional email. | + +#### Example + +```javascript +const resp = await loops.getTransactionalEmail("trans_123"); +``` + +--- + +### createTransactionalEmail() + +Create a new transactional email. An empty draft email message is created automatically. + +[API Reference](https://loops.so/docs/api-reference/create-transactional-email) + +#### Parameters + +| Name | Type | Required | Notes | +| ------ | ------ | -------- | ---------------------------------- | +| `name` | string | Yes | The name of the transactional email. | + +#### Example + +```javascript +const resp = await loops.createTransactionalEmail({ name: "Welcome email" }); +``` + +--- + +### updateTransactionalEmail() + +Update a transactional email by ID. + +[API Reference](https://loops.so/docs/api-reference/update-transactional-email) + +#### Parameters + +| Name | Type | Required | Notes | +| ----------------- | ------ | -------- | ---------------------------------- | +| `transactionalId` | string | Yes | The ID of the transactional email. | +| `name` | string | Yes | The name of the transactional email. | + +#### Example + +```javascript +const resp = await loops.updateTransactionalEmail("trans_123", { + name: "Updated name", +}); +``` + +--- + +### ensureTransactionalEmailDraft() + +Ensure a transactional email has a draft email message. Use [`updateEmailMessage()`](#updateemailmessage) to edit the draft content. + +[API Reference](https://loops.so/docs/api-reference/ensure-transactional-email-draft) + +#### Parameters + +| Name | Type | Required | Notes | +| ----------------- | ------ | -------- | ---------------------------------- | +| `transactionalId` | string | Yes | The ID of the transactional email. | + +#### Example + +```javascript +const resp = await loops.ensureTransactionalEmailDraft("trans_123"); +``` + +--- + +### publishTransactionalEmail() + +Publish a transactional email's current draft. + +[API Reference](https://loops.so/docs/api-reference/publish-transactional-email) + +#### Parameters + +| Name | Type | Required | Notes | +| ----------------- | ------ | -------- | ---------------------------------- | +| `transactionalId` | string | Yes | The ID of the transactional email. | + +#### Example + +```javascript +const resp = await loops.publishTransactionalEmail("trans_123"); +``` + +--- + +### listDedicatedSendingIps() Get a list of Loops' dedicated sending IP addresses. This is intended for rare cases where you need to whitelist Loops' sending IPs. @@ -910,7 +1019,7 @@ None #### Example ```javascript -const resp = await loops.getDedicatedSendingIps(); +const resp = await loops.listDedicatedSendingIps(); ``` #### Response @@ -926,7 +1035,7 @@ Error handling is done through the `APIError` class, which provides `statusCode` --- -### getThemes() +### listThemes() Retrieve a paginated list of email themes, most recently created first. Requires the content API to be enabled for your team. @@ -942,9 +1051,9 @@ Retrieve a paginated list of email themes, most recently created first. Requires #### Example ```javascript -const resp = await loops.getThemes(); +const resp = await loops.listThemes(); -const resp = await loops.getThemes({ perPage: 15, cursor: "clyo0q4wo01p59fsecyxqsh38" }); +const resp = await loops.listThemes({ perPage: 15, cursor: "clyo0q4wo01p59fsecyxqsh38" }); ``` #### Response @@ -1017,7 +1126,7 @@ Error handling is done through the `APIError` class, which provides `statusCode` --- -### getComponents() +### listComponents() Retrieve a paginated list of email components. Requires the content API to be enabled for your team. @@ -1033,9 +1142,9 @@ Retrieve a paginated list of email components. Requires the content API to be en #### Example ```javascript -const resp = await loops.getComponents(); +const resp = await loops.listComponents(); -const resp = await loops.getComponents({ perPage: 15 }); +const resp = await loops.listComponents({ perPage: 15 }); ``` #### Response @@ -1098,7 +1207,7 @@ Error handling is done through the `APIError` class, which provides `statusCode` --- -### getCampaigns() +### listCampaigns() Retrieve a paginated list of campaigns. Requires the content API to be enabled for your team. @@ -1114,9 +1223,9 @@ Retrieve a paginated list of campaigns. Requires the content API to be enabled f #### Example ```javascript -const resp = await loops.getCampaigns(); +const resp = await loops.listCampaigns(); -const resp = await loops.getCampaigns({ perPage: 15 }); +const resp = await loops.listCampaigns({ perPage: 15 }); ``` #### Response @@ -1377,9 +1486,58 @@ HTTP 409 Conflict --- +### createUpload() + +Request a pre-signed URL to upload an image asset. Upload the file with an HTTP `PUT` to the returned `presignedUrl`, then call [`completeUpload()`](#completeupload). + +[API Reference](https://loops.so/docs/api-reference/create-upload) + +#### Parameters + +| Name | Type | Required | Notes | +| --------------- | ------- | -------- | ------------------------------------------------------------------------------------------ | +| `contentType` | string | Yes | MIME type (`image/jpeg`, `image/png`, `image/gif`, or `image/webp`). | +| `contentLength` | integer | Yes | File size in bytes. Must be a positive integer no greater than 4,000,000 bytes. | + +#### Example + +```javascript +const resp = await loops.createUpload({ + contentType: "image/png", + contentLength: 102400, +}); +``` + +--- + +### completeUpload() + +Finalize an asset after the file has been uploaded to the pre-signed URL. + +[API Reference](https://loops.so/docs/api-reference/complete-upload) + +#### Parameters + +| Name | Type | Required | Notes | +| ---- | ------ | -------- | ---------------------------------------------------------- | +| `id` | string | Yes | The `emailAssetId` returned from [`createUpload()`](#createupload). | + +#### Example + +```javascript +const resp = await loops.completeUpload("asset_123"); +``` + +--- + ## Version history -- `v6.4.0` (May 18, 2026) - Added [`getDedicatedSendingIps()`](#getdedicatedsendingips), and endpoints for creating and editing campaings ([`getThemes()`](#getthemes), [`getTheme()`](#gettheme), [`getComponents()`](#getcomponents), [`getComponent()`](#getcomponent), [`getCampaigns()`](#getcampaigns), [`createCampaign()`](#createcampaign), [`getCampaign()`](#getcampaign), [`updateCampaign()`](#updatecampaign), [`getEmailMessage()`](#getemailmessage), and [`updateEmailMessage()`](#updateemailmessage)). +- `v7.0.0` (Jun 8, 2026) + - Added transactional email management ([`getTransactionalEmail()`](#gettransactionalemail), [`createTransactionalEmail()`](#createtransactionalemail), [`updateTransactionalEmail()`](#updatetransactionalemail), [`ensureTransactionalEmailDraft()`](#ensuretransactionalemaildraft), [`publishTransactionalEmail()`](#publishtransactionalemail)) + - Added image uploads ([`createUpload()`](#createupload), [`completeUpload()`](#completeupload)). + - Renamed list methods to use a consistent `list` naming pattern (breaking change): [`listContactProperties()`](#listcontactproperties), [`listMailingLists()`](#listmailinglists), [`listTransactionalEmails()`](#listtransactionalemails), [`listDedicatedSendingIps()`](#listdedicatedsendingips), [`listThemes()`](#listthemes), [`listComponents()`](#listcomponents), and [`listCampaigns()`](#listcampaigns). Note `getCustomProperties` is now `listContactProperties`. + - Note: [`listTransactionalEmails()`](#listtransactionalemails) uses a new underlying API endpoint returns an updated response shape. +- `v6.4.0` (May 18, 2026) - Added `getDedicatedSendingIps()`, and endpoints for creating and editing campaings (`getThemes()`, [`getTheme()`](#gettheme), `getComponents()`, [`getComponent()`](#getcomponent), `getCampaigns()`, [`createCampaign()`](#createcampaign), [`getCampaign()`](#getcampaign), [`updateCampaign()`](#updatecampaign), [`getEmailMessage()`](#getemailmessage), and [`updateEmailMessage()`](#updateemailmessage)). - `v6.3.0` (Apr 8, 2026) - Added [`checkContactSuppression()`](#checkcontactsuppression) and [`removeContactSuppression()`](#removecontactsuppression) methods. - `v6.2.0` (Feb 9, 2026) - Support for the new arrays feature in sendTransactionalEmail. - `v6.1.2` (Jan 29, 2026) - Added `rawBody` to `APIError` in the case no JSON is received from the server (thanks to [@leipert](https://github.com/leipert)). @@ -1391,21 +1549,21 @@ HTTP 409 Conflict - `ValidationError` is now thrown when parameters are not added correctly. - `Error` is now returned if the API key is missing. - Added tests. -- `v4.1.0` (Feb 27, 2025) - Support for new [List transactional emails](#gettransactionalemails) endpoint. +- `v4.1.0` (Feb 27, 2025) - Support for new [List transactional emails](#listtransactionalemails) endpoint. - `v4.0.0` (Jan 16, 2025) - Added `APIError` to more easily understand API errors. [See usage example](#usage). - Added support for two new contact property endpoints: [List contact properties](#listcontactproperties) and [Create contact property](#createcontactproperty). - Deprecated and removed the `getCustomFields()` method (you can now use [`listContactProperties()`](#listcontactproperties) instead). -- `v3.4.1` (Dec 18, 2024) - Support for a new `description` attribute in [`getMailingLists()`](#getmailinglists). +- `v3.4.1` (Dec 18, 2024) - Support for a new `description` attribute in [`listMailingLists()`](#listmailinglists). - `v3.4.0` (Oct 29, 2024) - Added rate limit handling with [`RateLimitExceededError`](#handling-rate-limits). - `v3.3.0` (Sep 9, 2024) - Added [`testApiKey()`](#testapikey) method. - `v3.2.0` (Aug 23, 2024) - Added support for a new `mailingLists` attribute in [`findContact()`](#findcontact). -- `v3.1.1` (Aug 16, 2024) - Support for a new `isPublic` attribute in [`getMailingLists()`](#getmailinglists). +- `v3.1.1` (Aug 16, 2024) - Support for a new `isPublic` attribute in [`listMailingLists()`](#listmailinglists). - `v3.1.0` (Aug 12, 2024) - The SDK now accepts `null` as a value for contact properties in `createContact()`, `updateContact()` and `sendEvent()`, which allows you to reset/empty properties. - `v3.0.0` (Jul 2, 2024) - [`sendTransactionalEmail()`](#sendtransactionalemail) now accepts an object instead of separate parameters (breaking change). - `v2.2.0` (Jul 2, 2024) - Deprecated. Added new `addToAudience` option to [`sendTransactionalEmail()`](#sendtransactionalemail). - `v2.1.1` (Jun 20, 2024) - Added support for mailing lists in [`createContact()`](#createcontact), [`updateContact()`](#updatecontact) and [`sendEvent()`](#sendevent). -- `v2.1.0` (Jun 19, 2024) - Added support for new [List mailing lists](#getmailinglists) endpoint. +- `v2.1.0` (Jun 19, 2024) - Added support for new [List mailing lists](#listmailinglists) endpoint. - `v2.0.0` (Apr 19, 2024) - Added `userId` as a parameter to [`findContact()`](#findcontact). This includes a breaking change for the `findContact()` parameters. - `userId` values must now be strings (could have also been numbers previously). diff --git a/package.json b/package.json index c643e81..002d7cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loops", - "version": "6.4.0", + "version": "7.0.0", "author": "Dan Rowden ", "license": "MIT", "main": "./dist/index.cjs", diff --git a/src/__tests__/LoopsClient.test.ts b/src/__tests__/LoopsClient.test.ts index 2ee8203..477c509 100644 --- a/src/__tests__/LoopsClient.test.ts +++ b/src/__tests__/LoopsClient.test.ts @@ -816,7 +816,10 @@ describe("LoopsClient", () => { { id: "trans_123", name: "Welcome Email", - lastUpdated: "2023-01-02T00:00:00.000Z", + draftEmailMessageId: "msg_draft_123", + publishedEmailMessageId: "msg_pub_123", + createdAt: "2023-01-01T00:00:00.000Z", + updatedAt: "2023-01-02T00:00:00.000Z", dataVariables: ["name", "product"], }, ]; @@ -837,21 +840,21 @@ describe("LoopsClient", () => { text: () => Promise.resolve(JSON.stringify(mockResponse)), }); - const result = await client.getTransactionalEmails(); + const result = await client.listTransactionalEmails(); expect(result).toEqual(mockResponse); expect(fetch).toHaveBeenCalledWith( - expect.stringContaining("v1/transactional"), + expect.stringContaining("v1/transactional-emails?perPage=20"), expect.objectContaining({ method: "GET", }) ); - // Type checking result.data.forEach((email) => { expect(typeof email.id).toBe("string"); expect(typeof email.name).toBe("string"); - expect(typeof email.lastUpdated).toBe("string"); + expect(typeof email.createdAt).toBe("string"); + expect(typeof email.updatedAt).toBe("string"); expect(Array.isArray(email.dataVariables)).toBe(true); expect(email.dataVariables.length).toBe(2); }); @@ -864,6 +867,8 @@ describe("LoopsClient", () => { returnedResults: 0, perPage: 20, totalPages: 0, + nextCursor: null, + nextPage: null, }, data: [], }; @@ -873,11 +878,11 @@ describe("LoopsClient", () => { text: () => Promise.resolve(JSON.stringify(mockResponse)), }); - const result = await client.getTransactionalEmails(); + const result = await client.listTransactionalEmails(); expect(result.data).toEqual([]); expect(fetch).toHaveBeenCalledWith( - expect.stringContaining("v1/transactional"), + expect.stringContaining("v1/transactional-emails?perPage=20"), expect.objectContaining({ method: "GET", }) @@ -885,7 +890,207 @@ describe("LoopsClient", () => { }); }); - describe("getDedicatedSendingIps", () => { + describe("getTransactionalEmail", () => { + it("should get a transactional email by ID", async () => { + const mockResponse = { + id: "trans_123", + name: "Welcome Email", + draftEmailMessageId: null, + publishedEmailMessageId: "msg_pub_123", + createdAt: "2023-01-01T00:00:00.000Z", + updatedAt: "2023-01-02T00:00:00.000Z", + dataVariables: ["name"], + }; + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(JSON.stringify(mockResponse)), + }); + + const result = await client.getTransactionalEmail("trans_123"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining("v1/transactional-emails/trans_123"), + expect.objectContaining({ method: "GET" }) + ); + }); + }); + + describe("createTransactionalEmail", () => { + it("should create a transactional email", async () => { + const mockResponse = { + id: "trans_123", + name: "Welcome Email", + draftEmailMessageId: "msg_draft_123", + draftEmailMessageContentRevisionId: "rev_123", + publishedEmailMessageId: null, + createdAt: "2023-01-01T00:00:00.000Z", + updatedAt: "2023-01-01T00:00:00.000Z", + dataVariables: [], + }; + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(JSON.stringify(mockResponse)), + }); + + const result = await client.createTransactionalEmail({ + name: "Welcome Email", + }); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining("v1/transactional-emails"), + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ name: "Welcome Email" }), + }) + ); + }); + }); + + describe("updateTransactionalEmail", () => { + it("should update a transactional email", async () => { + const mockResponse = { + id: "trans_123", + name: "Updated Email", + draftEmailMessageId: "msg_draft_123", + publishedEmailMessageId: "msg_pub_123", + createdAt: "2023-01-01T00:00:00.000Z", + updatedAt: "2023-01-02T00:00:00.000Z", + dataVariables: ["name"], + }; + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(JSON.stringify(mockResponse)), + }); + + const result = await client.updateTransactionalEmail("trans_123", { + name: "Updated Email", + }); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining("v1/transactional-emails/trans_123"), + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ name: "Updated Email" }), + }) + ); + }); + }); + + describe("ensureTransactionalEmailDraft", () => { + it("should ensure a transactional email draft exists", async () => { + const mockResponse = { + id: "trans_123", + name: "Welcome Email", + draftEmailMessageId: "msg_draft_123", + draftEmailMessageContentRevisionId: "rev_123", + publishedEmailMessageId: "msg_pub_123", + createdAt: "2023-01-01T00:00:00.000Z", + updatedAt: "2023-01-02T00:00:00.000Z", + dataVariables: ["name"], + }; + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(JSON.stringify(mockResponse)), + }); + + const result = await client.ensureTransactionalEmailDraft("trans_123"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining("v1/transactional-emails/trans_123/draft"), + expect.objectContaining({ method: "POST" }) + ); + }); + }); + + describe("publishTransactionalEmail", () => { + it("should publish a transactional email draft", async () => { + const mockResponse = { + id: "trans_123", + name: "Welcome Email", + draftEmailMessageId: null, + publishedEmailMessageId: "msg_pub_123", + createdAt: "2023-01-01T00:00:00.000Z", + updatedAt: "2023-01-02T00:00:00.000Z", + dataVariables: ["name"], + }; + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(JSON.stringify(mockResponse)), + }); + + const result = await client.publishTransactionalEmail("trans_123"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining("v1/transactional-emails/trans_123/publish"), + expect.objectContaining({ method: "POST" }) + ); + }); + }); + + describe("createUpload", () => { + it("should create an upload", async () => { + const mockResponse = { + emailAssetId: "asset_123", + presignedUrl: "https://example.com/upload", + }; + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(JSON.stringify(mockResponse)), + }); + + const result = await client.createUpload({ + contentType: "image/png", + contentLength: 102400, + }); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining("v1/uploads"), + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ + contentType: "image/png", + contentLength: 102400, + }), + }) + ); + }); + }); + + describe("completeUpload", () => { + it("should complete an upload", async () => { + const mockResponse = { + emailAssetId: "asset_123", + finalUrl: "https://cdn.example.com/asset_123.png", + }; + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(JSON.stringify(mockResponse)), + }); + + const result = await client.completeUpload("asset_123"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining("v1/uploads/asset_123/complete"), + expect.objectContaining({ method: "POST" }) + ); + }); + }); + + describe("listDedicatedSendingIps", () => { it("should return a list of IP addresses", async () => { const mockResponse = ["1.2.3.4", "5.6.7.8"]; @@ -894,7 +1099,7 @@ describe("LoopsClient", () => { text: () => Promise.resolve(JSON.stringify(mockResponse)), }); - const result = await client.getDedicatedSendingIps(); + const result = await client.listDedicatedSendingIps(); expect(result).toEqual(mockResponse); expect(fetch).toHaveBeenCalledWith( @@ -904,7 +1109,7 @@ describe("LoopsClient", () => { }); }); - describe("getThemes", () => { + describe("listThemes", () => { it("should list themes with pagination", async () => { const mockResponse = { success: true, @@ -933,7 +1138,7 @@ describe("LoopsClient", () => { text: () => Promise.resolve(JSON.stringify(mockResponse)), }); - const result = await client.getThemes({ perPage: 10, cursor: "abc" }); + const result = await client.listThemes({ perPage: 10, cursor: "abc" }); expect(result).toEqual(mockResponse); expect(fetch).toHaveBeenCalledWith( @@ -970,7 +1175,7 @@ describe("LoopsClient", () => { }); }); - describe("getComponents", () => { + describe("listComponents", () => { it("should list components with pagination", async () => { const mockResponse = { success: true, @@ -996,7 +1201,7 @@ describe("LoopsClient", () => { text: () => Promise.resolve(JSON.stringify(mockResponse)), }); - const result = await client.getComponents(); + const result = await client.listComponents(); expect(result).toEqual(mockResponse); expect(fetch).toHaveBeenCalledWith( @@ -1030,7 +1235,7 @@ describe("LoopsClient", () => { }); }); - describe("getCampaigns", () => { + describe("listCampaigns", () => { it("should list campaigns", async () => { const mockResponse = { success: true, @@ -1060,7 +1265,7 @@ describe("LoopsClient", () => { text: () => Promise.resolve(JSON.stringify(mockResponse)), }); - const result = await client.getCampaigns(); + const result = await client.listCampaigns(); expect(result).toEqual(mockResponse); expect(fetch).toHaveBeenCalledWith( diff --git a/src/index.ts b/src/index.ts index 221c4d6..3c5ee23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -216,42 +216,71 @@ interface PaginationData { nextPage: string | null; } -interface TransactionalEmail { - /** The ID of the transactional email. */ - id: string; +interface ContactProperty { /** - * The name of the transactional email. + * The property's name. */ - name: string; + key: string; + /** + * The human-friendly label for this property. + */ + label: string; /** - * The date the email was last updated in ECMA-262 date-time format. - * @see https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date-time-string-format + * The type of property. */ - lastUpdated: string; + type: "string" | "number" | "boolean" | "date"; +} + +interface TransactionalEmailResource { + /** The ID of the transactional email. */ + id: string; + /** The name of the transactional email. */ + name: string; + /** The ID of the draft email message, or `null` if none. */ + draftEmailMessageId: string | null; + /** The ID of the published email message, or `null` if none. */ + publishedEmailMessageId: string | null; + /** The date the transactional email was created in ECMA-262 date-time format. */ + createdAt: string; + /** The date the transactional email was last updated in ECMA-262 date-time format. */ + updatedAt: string; /** - * Data variables in the transactional email. + * Data variable names used by the published email. + * Empty for unpublished transactional emails. */ dataVariables: string[]; } -interface ContactProperty { +interface TransactionalDraftResponse extends TransactionalEmailResource { /** - * The property's name. + * The `contentRevisionId` of the draft email message. + * Pass this as `expectedRevisionId` on your first update via `updateEmailMessage()`. */ - key: string; + draftEmailMessageContentRevisionId: string | null; +} + +interface ListTransactionalsResourceResponse { + pagination: PaginationData; + data: TransactionalEmailResource[]; +} + +interface CreateUploadResponse { /** - * The human-friendly label for this property. + * The ID of the created asset. + * Pass this to `completeUpload()` once the file has been uploaded. */ - label: string; + emailAssetId: string; /** - * The type of property. + * The pre-signed URL to upload the file to with an HTTP `PUT` request. + * Send the same `Content-Type` and `Content-Length` used in the create request. */ - type: "string" | "number" | "boolean" | "date"; + presignedUrl: string; } -interface ListTransactionalsResponse { - pagination: PaginationData; - data: TransactionalEmail[]; +interface CompleteUploadResponse { + emailAssetId: string; + /** The public URL of the uploaded asset. */ + finalUrl: string; } interface ThemeStyles { @@ -799,7 +828,7 @@ class LoopsClient { } /** - * Get contact properties. + * List contact properties. * * @param {"all" | "custom"} [list] Return all or just custom properties. * @@ -807,7 +836,7 @@ class LoopsClient { * * @returns {Object} List of contact properties (JSON) */ - async getCustomProperties( + async listContactProperties( list?: "all" | "custom" ): Promise { return this._makeQuery({ @@ -817,13 +846,13 @@ class LoopsClient { } /** - * Get mailing lists. + * List mailing lists. * * @see https://loops.so/docs/api-reference/list-mailing-lists * * @returns {Object} List of mailing lists (JSON) */ - async getMailingLists(): Promise { + async listMailingLists(): Promise { return this._makeQuery({ path: "v1/lists", }); @@ -934,7 +963,7 @@ class LoopsClient { } /** - * List published transactional emails. + * List transactional emails. * * @param {Object} params * @param {number} [params.perPage] How many results to return in each request. Must be between 10 and 50. Defaults to 20. @@ -944,31 +973,128 @@ class LoopsClient { * * @returns {Object} List of transactional emails (JSON) */ - async getTransactionalEmails({ + async listTransactionalEmails({ perPage, cursor, }: { perPage?: number; cursor?: string; - } = {}): Promise { - let params: { perPage: string; cursor?: string } = { + } = {}): Promise { + const params: { perPage: string; cursor?: string } = { perPage: (perPage || 20).toString(), }; if (cursor) params["cursor"] = cursor; return this._makeQuery({ - path: "v1/transactional", + path: "v1/transactional-emails", params, }); } /** - * Get dedicated sending IP addresses. + * Get a transactional email by ID. + * + * @param {string} transactionalId The ID of the transactional email. + * + * @see https://loops.so/docs/api-reference/get-transactional-email + * + * @returns {Object} Transactional email (JSON) + */ + async getTransactionalEmail( + transactionalId: string + ): Promise { + return this._makeQuery({ + path: `v1/transactional-emails/${transactionalId}`, + }); + } + + /** + * Create a transactional email. + * + * @param {Object} params + * @param {string} params.name The name of the transactional email. + * + * @see https://loops.so/docs/api-reference/create-transactional-email + * + * @returns {Object} Created transactional email with draft (JSON) + */ + async createTransactionalEmail({ + name, + }: { + name: string; + }): Promise { + return this._makeQuery({ + path: "v1/transactional-emails", + method: "POST", + payload: { name }, + }); + } + + /** + * Update a transactional email. + * + * @param {string} transactionalId The ID of the transactional email. + * @param {Object} params + * @param {string} params.name The name of the transactional email. + * + * @see https://loops.so/docs/api-reference/update-transactional-email + * + * @returns {Object} Updated transactional email (JSON) + */ + async updateTransactionalEmail( + transactionalId: string, + { name }: { name: string } + ): Promise { + return this._makeQuery({ + path: `v1/transactional-emails/${transactionalId}`, + method: "POST", + payload: { name }, + }); + } + + /** + * Ensure a transactional email has a draft email message. + * + * @param {string} transactionalId The ID of the transactional email. + * + * @see https://loops.so/docs/api-reference/ensure-transactional-email-draft + * + * @returns {Object} Transactional email with draft (JSON) + */ + async ensureTransactionalEmailDraft( + transactionalId: string + ): Promise { + return this._makeQuery({ + path: `v1/transactional-emails/${transactionalId}/draft`, + method: "POST", + }); + } + + /** + * Publish a transactional email draft. + * + * @param {string} transactionalId The ID of the transactional email. + * + * @see https://loops.so/docs/api-reference/publish-transactional-email + * + * @returns {Object} Published transactional email (JSON) + */ + async publishTransactionalEmail( + transactionalId: string + ): Promise { + return this._makeQuery({ + path: `v1/transactional-emails/${transactionalId}/publish`, + method: "POST", + }); + } + + /** + * List dedicated sending IP addresses. * * @see https://loops.so/docs/api-reference/get-dedicated-sending-ips * * @returns {string[]} List of IP addresses */ - async getDedicatedSendingIps(): Promise { + async listDedicatedSendingIps(): Promise { return this._makeQuery({ path: "v1/dedicated-sending-ips", }); @@ -985,7 +1111,7 @@ class LoopsClient { * * @returns {Object} List of themes (JSON) */ - async getThemes({ + async listThemes({ perPage, cursor, }: { @@ -1028,7 +1154,7 @@ class LoopsClient { * * @returns {Object} List of components (JSON) */ - async getComponents({ + async listComponents({ perPage, cursor, }: { @@ -1071,7 +1197,7 @@ class LoopsClient { * * @returns {Object} List of campaigns (JSON) */ - async getCampaigns({ + async listCampaigns({ perPage, cursor, }: { @@ -1218,6 +1344,47 @@ class LoopsClient { payload, }); } + + /** + * Create an upload. + * + * @param {Object} params + * @param {string} params.contentType The MIME type of the file to upload. Supported types are `image/jpeg`, `image/png`, `image/gif` and `image/webp`. + * @param {number} params.contentLength The size of the file in bytes. Must be a positive integer no greater than 4,000,000 bytes. + * + * @see https://loops.so/docs/api-reference/create-upload + * + * @returns {Object} Pre-signed upload URL (JSON) + */ + async createUpload({ + contentType, + contentLength, + }: { + contentType: string; + contentLength: number; + }): Promise { + return this._makeQuery({ + path: "v1/uploads", + method: "POST", + payload: { contentType, contentLength }, + }); + } + + /** + * Complete an upload. + * + * @param {string} id The `emailAssetId` returned when the upload was created. + * + * @see https://loops.so/docs/api-reference/complete-upload + * + * @returns {Object} Public URL of the uploaded asset (JSON) + */ + async completeUpload(id: string): Promise { + return this._makeQuery({ + path: `v1/uploads/${id}/complete`, + method: "POST", + }); + } } export { @@ -1247,8 +1414,11 @@ export { TransactionalAttachment, MailingList, PaginationData, - TransactionalEmail, - ListTransactionalsResponse, + TransactionalEmailResource, + TransactionalDraftResponse, + ListTransactionalsResourceResponse, + CreateUploadResponse, + CompleteUploadResponse, MailingLists, ThemeStyles, Theme,