diff --git a/api.md b/api.md index 1cd0fcc..9479f97 100644 --- a/api.md +++ b/api.md @@ -32,6 +32,7 @@ Types: - ElementalMetaNode - ElementalMetaNodeWithType - ElementalNode +- ElementalNodeNonChannel - ElementalQuoteNode - ElementalQuoteNodeWithType - ElementalTextNode diff --git a/src/client.ts b/src/client.ts index 02c3bf0..34cfa22 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1457,6 +1457,7 @@ export declare namespace Courier { export type ElementalMetaNode = API.ElementalMetaNode; export type ElementalMetaNodeWithType = API.ElementalMetaNodeWithType; export type ElementalNode = API.ElementalNode; + export type ElementalNodeNonChannel = API.ElementalNodeNonChannel; export type ElementalQuoteNode = API.ElementalQuoteNode; export type ElementalQuoteNodeWithType = API.ElementalQuoteNodeWithType; export type ElementalTextNode = API.ElementalTextNode; diff --git a/src/resources/journeys/templates.ts b/src/resources/journeys/templates.ts index eeb987a..51bfebe 100644 --- a/src/resources/journeys/templates.ts +++ b/src/resources/journeys/templates.ts @@ -17,6 +17,13 @@ export class Templates extends APIResource { * Create a notification template scoped to this journey. Defaults to `DRAFT` * state; pass `state: "PUBLISHED"` to publish on create. * + * The content tree must contain exactly one channel block whose `channel` matches + * the `channel` on the request — a journey-scoped template carries a single + * channel. Top-level elements, or a block for a different channel, return `400`. + * The template designer renders only the channel block matching the tab it draws, + * so content stored without one cannot be opened. An empty `elements` array is + * accepted. + * * @example * ```ts * const journeyTemplateGetResponse = @@ -29,7 +36,7 @@ export class Templates extends APIResource { * subscription: null, * content: { * version: '2022-01-01', - * elements: [{ type: 'text' }], + * elements: [{ type: 'channel' }], * }, * }, * }); diff --git a/src/resources/notifications/notifications.ts b/src/resources/notifications/notifications.ts index 4bf9a15..625a2e5 100644 --- a/src/resources/notifications/notifications.ts +++ b/src/resources/notifications/notifications.ts @@ -26,6 +26,16 @@ export class Notifications extends APIResource { * Create a notification template. Requires all fields in the notification object. * Templates are created in draft state by default. * + * Content must place its elements inside a channel block — + * `{ "type": "channel", "channel": "email", "elements": [...] }` — or the request + * returns `400`. The template designer renders only the channel block matching the + * tab it draws, so content stored without one cannot be opened. An empty + * `elements` array is accepted, and the requirement applies to creation only: + * `PUT /notifications/{id}` still accepts unwrapped content. Note this endpoint + * takes versioned content only — the `{ title, body }` shorthand accepted by + * `/send` is rejected here with an `invalid_request_error` on + * `notification.content.version`. + * * @example * ```ts * const notificationTemplateResponse = @@ -864,9 +874,12 @@ export interface NotificationTemplateSummary { } /** - * Request body for replacing a notification template. Same shape as create. All - * fields required (PUT = full replacement), except `alias`, whose omission means - * "leave the existing aliases alone". + * Request body for replacing a notification template. All fields are required, + * since `PUT` is a full replacement, except `alias`, whose omission leaves the + * existing aliases in place. Unlike `NotificationTemplateCreateRequest`, + * `notification.content` is not required to place its elements inside a channel + * block: the requirement applies to creation only, so templates already stored + * without one stay editable. */ export interface NotificationTemplateUpdateRequest { /** diff --git a/src/resources/shared.ts b/src/resources/shared.ts index df39e2f..0e37887 100644 --- a/src/resources/shared.ts +++ b/src/resources/shared.ts @@ -225,6 +225,13 @@ export interface ElementalChannelNode extends ElementalBaseNode { */ channel?: string; + /** + * An array of elements to apply to the channel. If `raw` has not been specified, + * `elements` is `required`. Channel elements cannot nest, so these are any node + * except another channel block. + */ + elements?: Array | null; + /** * Email only. Document-level base font size (CSS px, e.g. `16px`) for body content * — text, quote, list and action button labels. Heading styles (`h1`/`h2`/`h3`) @@ -422,6 +429,76 @@ export type ElementalNode = | ElementalQuoteNodeWithType | ElementalHTMLNodeWithType; +/** + * Any Elemental node except a channel block. Channel elements are only valid as + * top-level elements, so the `elements` nested inside one can never be another + * channel. Keeping this union channel-free also keeps the schema acyclic; a + * recursive `$ref` here breaks the generated Python models. + */ +export type ElementalNodeNonChannel = + | ElementalNodeNonChannel.UnionMember0 + | ElementalNodeNonChannel.UnionMember1 + | ElementalNodeNonChannel.UnionMember2 + | ElementalNodeNonChannel.UnionMember3 + | ElementalNodeNonChannel.UnionMember4 + | ElementalNodeNonChannel.UnionMember5 + | ElementalNodeNonChannel.UnionMember6; + +export namespace ElementalNodeNonChannel { + /** + * Represents a body of text to be rendered inside of the notification. + */ + export interface UnionMember0 extends Shared.ElementalTextNode { + type?: 'text'; + } + + /** + * The meta element contains information describing the notification that may be + * used by a particular channel or provider. One important field is the title field + * which will be used as the title for channels that support it. + */ + export interface UnionMember1 extends Shared.ElementalMetaNode { + type?: 'meta'; + } + + /** + * Used to embed an image into the notification. + */ + export interface UnionMember2 extends Shared.ElementalImageNode { + type?: 'image'; + } + + /** + * Allows the user to execute an action. Can be a button or a link. + */ + export interface UnionMember3 extends Shared.ElementalActionNode { + type?: 'action'; + } + + /** + * Renders a dividing line between elements. + */ + export interface UnionMember4 extends Shared.ElementalDividerNode { + type?: 'divider'; + } + + /** + * Renders a quote block. + */ + export interface UnionMember5 extends Shared.ElementalQuoteNode { + type?: 'quote'; + } + + /** + * Raw HTML string inside an Elemental document. When rendering a message, this + * node is turned into output only for the email channel; for other channels it + * produces no blocks. + */ + export interface UnionMember6 extends Shared.ElementalHTMLNode { + type?: 'html'; + } +} + /** * Renders a quote block. */ diff --git a/src/resources/tenants/templates/templates.ts b/src/resources/tenants/templates/templates.ts index cb1aa8d..e7c622e 100644 --- a/src/resources/tenants/templates/templates.ts +++ b/src/resources/tenants/templates/templates.ts @@ -104,6 +104,16 @@ export class Templates extends APIResource { * Creates or updates a notification template scoped to one tenant, letting a * tenant override the content the workspace template would send. * + * This is an upsert: it creates when the tenant has no template under + * `template_id`, and updates when it does. On the create half, content must place + * its elements inside a channel block — + * `{ "type": "channel", "channel": "email", "elements": [...] }` — or the request + * returns `400`. The template designer renders only the channel block matching the + * tab it draws, so content stored without one cannot be opened. An empty + * `elements` array is accepted, as is the `{ title, body }` shorthand, which has + * no elements to wrap. Updates are not checked, so tenant templates already stored + * without a wrapper stay editable. + * * @example * ```ts * const putTenantTemplateResponse = @@ -112,7 +122,7 @@ export class Templates extends APIResource { * template: { * content: { * version: '2022-01-01', - * elements: [{ type: 'text' }], + * elements: [{ type: 'channel' }], * }, * routing: { method: 'single', channels: ['email'] }, * }, diff --git a/tests/api-resources/journeys/templates.test.ts b/tests/api-resources/journeys/templates.test.ts index dfc79a6..24d9d0c 100644 --- a/tests/api-resources/journeys/templates.test.ts +++ b/tests/api-resources/journeys/templates.test.ts @@ -36,7 +36,7 @@ describe('resource templates', () => { notification: { brand: { id: 'id' }, content: { - elements: [{ type: 'text' }], + elements: [{ type: 'channel' }], version: '2022-01-01', scope: 'default', }, diff --git a/tests/api-resources/tenants/templates/templates.test.ts b/tests/api-resources/tenants/templates/templates.test.ts index 3735cb7..00b07fa 100644 --- a/tests/api-resources/tenants/templates/templates.test.ts +++ b/tests/api-resources/tenants/templates/templates.test.ts @@ -106,7 +106,7 @@ describe('resource templates', () => { const response = await client.tenants.templates.replace('template_id', { tenant_id: 'tenant_id', template: { - content: { elements: [{ type: 'text' }], version: '2022-01-01' }, + content: { elements: [{ type: 'channel' }], version: '2022-01-01' }, channels: { foo: { brand_id: 'brand_id',