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: '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\