diff --git a/README.md b/README.md index 1693450..f05b74a 100644 --- a/README.md +++ b/README.md @@ -608,24 +608,33 @@ bento.V1.Tags.createTag({ ### Sequences -Retrieve sequences and their associated email templates. +Sequences power drip campaigns, onboarding flows, and other time-based journeys by chaining multiple email templates with configurable delays. The SDK mirrors the public Sequences API for fetching sequences, creating new sequence emails, and updating template content. Refer to the [Sequences API docs](https://docs.bentonow.com/sequences_api#get-sequences) for full request/response details. #### getSequences -Retrieves all sequences for the site, including their email templates. +Calls `GET /v1/fetch/sequences` and returns every sequence plus the embedded email templates and stats. Pass `{ page }` to paginate through large installs—the SDK appends `site_uuid` automatically. ```javascript -const sequences = await bento.V1.Sequences.getSequences(); -// Returns: -// [ +import { Analytics } from '@bentonow/bento-node-sdk'; + +const analytics = new Analytics({ + authentication: { + publishableKey: process.env.BENTO_PUBLISHABLE_KEY, + secretKey: process.env.BENTO_SECRET_KEY, + }, + siteUuid: process.env.BENTO_SITE_UUID, +}); + +const sequences = await analytics.V1.Sequences.getSequences({ page: 1 }); +// sequences => [ // { -// id: '123', +// id: 'seq-1', // type: 'sequence', // attributes: { // name: 'Welcome Sequence', // created_at: '2024-01-01T00:00:00Z', // email_templates: [ -// { id: 1, subject: 'Welcome!', stats: null }, +// { id: 1, subject: 'Welcome!', stats: { opened: 100, clicked: 50 } }, // { id: 2, subject: 'Getting Started', stats: null } // ] // } @@ -635,39 +644,61 @@ const sequences = await bento.V1.Sequences.getSequences(); #### createSequenceEmail -Creates a new email template inside a sequence. +Wraps [`POST /v1/fetch/sequences/:id/emails/templates`](https://docs.bentonow.com/sequences_api#create-sequence-email) so you can add messages to a sequence via code. Pass the sequence prefix ID (e.g., `sequence_abc123`) plus the subject/HTML and any optional delay/snippet/editor fields. ```javascript -const createdTemplate = await bento.V1.Sequences.createSequenceEmail('sequence_abc123', { +const createdTemplate = await analytics.V1.Sequences.createSequenceEmail('sequence_abc123', { subject: 'Welcome to Bento', html: '

Hello {{ visitor.first_name }}

', delay_interval: 'days', delay_interval_count: 7, inbox_snippet: 'Welcome to the sequence', + editor_choice: 'plain', +}); +``` + +#### updateSequenceEmail + +Sequence emails reuse the Email Templates resource, so updates happen through `analytics.V1.EmailTemplates.updateEmailTemplate` (the same helper documented in the [Email Templates](#email-templates) section). Only `subject` and `html` are patchable today, matching [`PATCH /v1/fetch/emails/templates/:id`](https://docs.bentonow.com/sequences_api#update-sequence-email). + +```javascript +await analytics.V1.EmailTemplates.updateEmailTemplate({ + id: 12345, + subject: 'Updated subject', + html: '

Updated HTML

', }); ``` ### Workflows -Retrieve workflows and their associated email templates. +Workflows (a.k.a. Flows) are Bento’s automation engine for welcome journeys, abandoned-cart nudges, re-engagement loops, and other event-driven campaigns. The SDK surfaces the public Workflows API so you can inspect every flow (including the embedded email templates and their stats) straight from Node. See the [Workflows API reference](https://docs.bentonow.com/workflows_api#get-workflows) for the canonical response schema. #### getWorkflows -Retrieves all workflows for the site, including their email templates. +Calls `GET /v1/fetch/workflows` and returns an array of workflows. Pass an optional `page` parameter to paginate through large accounts—the SDK automatically injects your `site_uuid` so you only need to provide the page number. ```javascript -const workflows = await bento.V1.Workflows.getWorkflows(); -// Returns: -// [ +import { Analytics } from '@bentonow/bento-node-sdk'; + +const analytics = new Analytics({ + authentication: { + publishableKey: process.env.BENTO_PUBLISHABLE_KEY, + secretKey: process.env.BENTO_SECRET_KEY, + }, + siteUuid: process.env.BENTO_SITE_UUID, +}); + +const workflows = await analytics.V1.Workflows.getWorkflows({ page: 2 }); +// workflows => [ // { -// id: '456', +// id: 'wf-1', // type: 'workflow', // attributes: { -// name: 'Onboarding Workflow', +// name: 'Abandoned Cart Recovery', // created_at: '2024-01-01T00:00:00Z', // email_templates: [ -// { id: 3, subject: 'Step 1', stats: null }, -// { id: 4, subject: 'Step 2', stats: null } +// { id: 3, subject: 'Reminder #1', stats: { opened: 42, clicked: 10 } }, +// { id: 4, subject: 'Reminder #2', stats: null } // ] // } // } @@ -676,38 +707,63 @@ const workflows = await bento.V1.Workflows.getWorkflows(); ### Email Templates -Retrieve and update email templates used in sequences and workflows. +Retrieve and update email templates used in sequences and workflows. Both helpers call the public Email Templates API (`GET /v1/fetch/emails/templates/:id` and `PATCH /v1/fetch/emails/templates/:id`), and the SDK automatically injects your `site_uuid` and authentication headers. See the [Email Templates API docs](https://docs.bentonow.com/email_templates_api#get-email-template) for the canonical contract. #### getEmailTemplate -Retrieves a single email template by ID. +Retrieves a single email template by ID and returns `null` when the Bento API responds with an empty payload. Use this to surface subject lines, HTML, and performance stats inside your own tooling. ```javascript -const template = await bento.V1.EmailTemplates.getEmailTemplate({ id: 123 }); -// Returns: -// { -// id: '123', -// type: 'email_template', -// attributes: { -// name: 'Welcome Email', -// subject: 'Welcome to our service!', -// html: '

Hello {{ name }}, welcome!

', -// created_at: '2024-01-01T00:00:00Z', -// stats: null -// } -// } +import { Analytics } from '@bentonow/bento-node-sdk'; + +const analytics = new Analytics({ + authentication: { + publishableKey: process.env.BENTO_PUBLISHABLE_KEY, + secretKey: process.env.BENTO_SECRET_KEY, + }, + siteUuid: process.env.BENTO_SITE_UUID, +}); + +const template = await analytics.V1.EmailTemplates.getEmailTemplate({ id: 123 }); +if (!template) { + console.log('Template not found'); +} else { + console.log(template.attributes.subject, template.attributes.stats); +} ``` #### updateEmailTemplate -Updates an email template's subject and/or HTML content. +Updates an email template's subject and/or HTML content via [`PATCH /v1/fetch/emails/templates/:id`](https://docs.bentonow.com/email_templates_api#update-email-template). Only pass the fields you want to change; omitted fields stay untouched. The helper returns the updated template (`null` if Bento responds empty) and bubbles up standard SDK errors such as `NotAuthorizedError`, `RateLimitedError`, or `RequestTimeoutError`. ```javascript -const updatedTemplate = await bento.V1.EmailTemplates.updateEmailTemplate({ - id: 123, - subject: 'Updated Subject Line', - html: '

Updated HTML content with {{ name }}

', +import { Analytics, NotAuthorizedError } from '@bentonow/bento-node-sdk'; + +const analytics = new Analytics({ + authentication: { + publishableKey: process.env.BENTO_PUBLISHABLE_KEY, + secretKey: process.env.BENTO_SECRET_KEY, + }, + siteUuid: process.env.BENTO_SITE_UUID, }); + +try { + const updatedTemplate = await analytics.V1.EmailTemplates.updateEmailTemplate({ + id: 123, + subject: 'Updated Subject Line', + html: '

Updated HTML content with {{ name }}

', + }); + + if (updatedTemplate) { + console.log(updatedTemplate.attributes.subject); + } +} catch (error) { + if (error instanceof NotAuthorizedError) { + console.error('Check your Bento credentials or site permissions.'); + } else { + throw error; + } +} ``` For detailed information on each module, refer to the [SDK Documentation](https://docs.bentonow.com/subscribers). diff --git a/__tests__/client/client.test.ts b/__tests__/client/client.test.ts index 694431b..5851a7d 100644 --- a/__tests__/client/client.test.ts +++ b/__tests__/client/client.test.ts @@ -1,7 +1,12 @@ import { expect, test, describe, beforeEach, afterEach, mock } from 'bun:test'; import { Analytics } from '../../src'; import { mockOptions } from '../helpers/mockClient'; -import { setupMockFetch, lastFetchSignal, resetMockFetchTracking } from '../helpers/mockFetch'; +import { + setupMockFetch, + lastFetchSignal, + lastFetchUrl, + resetMockFetchTracking, +} from '../helpers/mockFetch'; import { NotAuthorizedError, RateLimitedError, RequestTimeoutError } from '../../src'; describe('BentoClient', () => { @@ -215,6 +220,17 @@ describe('BentoClient', () => { expect(url.searchParams.get('site_uuid')).toBe(mockOptions.siteUuid); }); + test('omits undefined query parameters when forwarding SDK options', async () => { + setupMockFetch({ data: [] }); + + await analytics.V1.Sequences.getSequences({ page: undefined }); + + expect(lastFetchUrl).not.toBeNull(); + const url = new URL(lastFetchUrl!); + expect(url.searchParams.has('page')).toBe(false); + expect(url.searchParams.get('site_uuid')).toBe(mockOptions.siteUuid); + }); + test('initializes with custom timeout', () => { const customOptions = { ...mockOptions, diff --git a/__tests__/sequences/sequences.test.ts b/__tests__/sequences/sequences.test.ts index 73b11df..5491073 100644 --- a/__tests__/sequences/sequences.test.ts +++ b/__tests__/sequences/sequences.test.ts @@ -61,6 +61,17 @@ describe('BentoSequences', () => { expect(lastFetchMethod).toBe('GET'); }); + test('passes pagination parameters in query string', async () => { + setupMockFetch({ data: [] }); + + await analytics.V1.Sequences.getSequences({ page: 5 }); + + expect(lastFetchUrl).toContain('/fetch/sequences'); + const url = new URL(lastFetchUrl!); + expect(url.searchParams.get('page')).toBe('5'); + expect(url.searchParams.get('site_uuid')).toBe(mockOptions.siteUuid); + }); + test('returns empty array when response is empty object', async () => { setupMockFetch({}); diff --git a/__tests__/workflows/workflows.test.ts b/__tests__/workflows/workflows.test.ts index c37e72a..a488c3a 100644 --- a/__tests__/workflows/workflows.test.ts +++ b/__tests__/workflows/workflows.test.ts @@ -61,6 +61,17 @@ describe('BentoWorkflows', () => { expect(lastFetchMethod).toBe('GET'); }); + test('passes pagination parameters in query string', async () => { + setupMockFetch({ data: [] }); + + await analytics.V1.Workflows.getWorkflows({ page: 3 }); + + expect(lastFetchUrl).toContain('/fetch/workflows'); + const url = new URL(lastFetchUrl!); + expect(url.searchParams.get('page')).toBe('3'); + expect(url.searchParams.get('site_uuid')).toBe(mockOptions.siteUuid); + }); + test('returns empty array when response is empty object', async () => { setupMockFetch({}); diff --git a/src/sdk/client/index.ts b/src/sdk/client/index.ts index 3556f1e..16817c7 100644 --- a/src/sdk/client/index.ts +++ b/src/sdk/client/index.ts @@ -255,6 +255,7 @@ export class BentoClient { const queryParameters = new URLSearchParams(); for (const [key, value] of Object.entries(body)) { + if (value === undefined || value === null) continue; queryParameters.append(key, String(value)); } diff --git a/src/sdk/sequences/index.ts b/src/sdk/sequences/index.ts index 7b7f9ab..76d6a91 100644 --- a/src/sdk/sequences/index.ts +++ b/src/sdk/sequences/index.ts @@ -1,7 +1,7 @@ import type { BentoClient } from '../client'; import type { DataResponse } from '../client/types'; import type { EmailTemplate } from '../email-templates/types'; -import type { CreateSequenceEmailParameters, Sequence } from './types'; +import type { CreateSequenceEmailParameters, GetSequencesParameters, Sequence } from './types'; export class BentoSequences { private readonly _url = '/fetch/sequences'; @@ -11,10 +11,11 @@ export class BentoSequences { /** * Returns all of the sequences for the site, including their email templates. * + * @param parameters Optional pagination parameters (e.g., { page: 2 }) * @returns Promise\ */ - public async getSequences(): Promise { - const result = await this._client.get>(this._url); + public async getSequences(parameters: GetSequencesParameters = {}): Promise { + const result = await this._client.get>(this._url, parameters); if (!result || Object.keys(result).length === 0) return []; return result.data ?? []; diff --git a/src/sdk/sequences/types.ts b/src/sdk/sequences/types.ts index 0c1f94b..4469dbe 100644 --- a/src/sdk/sequences/types.ts +++ b/src/sdk/sequences/types.ts @@ -23,6 +23,10 @@ export type Sequence = BaseEntity; export type SequenceDelayInterval = 'minutes' | 'hours' | 'days' | 'months'; +export type GetSequencesParameters = { + page?: number; +}; + export type CreateSequenceEmailParameters = { subject: string; html: string; diff --git a/src/sdk/workflows/index.ts b/src/sdk/workflows/index.ts index 6d8b8a3..3a25449 100644 --- a/src/sdk/workflows/index.ts +++ b/src/sdk/workflows/index.ts @@ -1,6 +1,6 @@ import type { BentoClient } from '../client'; import type { DataResponse } from '../client/types'; -import type { Workflow } from './types'; +import type { GetWorkflowsParameters, Workflow } from './types'; export class BentoWorkflows { private readonly _url = '/fetch/workflows'; @@ -10,10 +10,11 @@ export class BentoWorkflows { /** * Returns all of the workflows for the site, including their email templates. * + * @param parameters Optional pagination parameters (e.g., { page: 2 }) * @returns Promise\ */ - public async getWorkflows(): Promise { - const result = await this._client.get>(this._url); + public async getWorkflows(parameters: GetWorkflowsParameters = {}): Promise { + const result = await this._client.get>(this._url, parameters); if (!result || Object.keys(result).length === 0) return []; return result.data ?? []; diff --git a/src/sdk/workflows/types.ts b/src/sdk/workflows/types.ts index e08de4e..0a26656 100644 --- a/src/sdk/workflows/types.ts +++ b/src/sdk/workflows/types.ts @@ -19,3 +19,7 @@ export type WorkflowAttributes = { }; export type Workflow = BaseEntity; + +export type GetWorkflowsParameters = { + page?: number; +};