diff --git a/.changeset/25190-retire-body-taking-render-exports.md b/.changeset/25190-retire-body-taking-render-exports.md new file mode 100644 index 00000000000..2bbe9707b87 --- /dev/null +++ b/.changeset/25190-retire-body-taking-render-exports.md @@ -0,0 +1,18 @@ +--- +'@objectstack/service-messaging': patch +--- + +fix(service-messaging)!: retire `renderNotification`, the render export that takes a notification template body with no save moment + +The hole-placement rule for notification templates is judged where a template is saved: the `sys_notification_template` save hooks. `renderNotification` took a template body inline and rendered it at once, with no save moment, so the rule could not see it. It leaves the published entry of `@objectstack/service-messaging`. Nothing in this repository imported it from that entry. The renderer stays inside the package: the email and sms channels still render every stored template through it, unchanged. + +Clause-②: no (narrowing) +Metadata-protocol: none + + + +**BREAKING** (a runtime export is removed). It ships as `patch` because the metadata protocol does not move. There is no staged window. + +- FROM `renderNotification` TO a `sys_notification_template` row for the topic, channel and locale, which the delivering channel renders when the notification is emitted. + +Unchanged: `interpolate` (the plain-text face, which the placement rule judges at no door), `NotificationTemplateStore`, `TEMPLATE_OBJECT`, `DEFAULT_LOCALE` and the template types; and how every stored template renders. diff --git a/packages/services/service-messaging/src/index.ts b/packages/services/service-messaging/src/index.ts index 999cf3a8cb2..7855de88583 100644 --- a/packages/services/service-messaging/src/index.ts +++ b/packages/services/service-messaging/src/index.ts @@ -87,9 +87,16 @@ export type { } from './notification-text.js'; // Templates + renderer (ADR-0030 P3) +// +// [#25190] `renderNotification` is not on this entry: it took a template body +// inline and rendered its html face with no save moment, so the hole-placement +// rule the template object's save hooks run could not see it (ruling +// 6335348115 on #25082, F2 B3). A notification template is a +// `sys_notification_template` row, judged when it is saved and rendered by the +// channels that deliver it. `interpolate` stays: it renders a plain-text face, +// which that rule judges at no door. export { NotificationTemplateStore, - renderNotification, interpolate, TEMPLATE_OBJECT, DEFAULT_LOCALE, diff --git a/packages/services/service-messaging/src/render-export-retired.pin.test.ts b/packages/services/service-messaging/src/render-export-retired.pin.test.ts new file mode 100644 index 00000000000..2cc61a41c8d --- /dev/null +++ b/packages/services/service-messaging/src/render-export-retired.pin.test.ts @@ -0,0 +1,82 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #25190 — `renderNotification`, the renderer that took a template body inline, + * is retired from this package's published entry (ruling 6335348115 on #25082, + * F2 B3; ADR-0087 `runtime-export-retired`). The hole-placement rule is judged + * by the `sys_notification_template` save hooks, and a body handed straight to + * it was never saved, so it was the one door the rule could not see. + * `interpolate` stays on the entry: it renders a plain-text face, which the + * rule judges at no door. + * + * Pinned both ways: the entry no longer names it (the template store and the + * channels stay), and a stored template row still renders through a channel + * built from the entry — the html body with every hole encoded, the subject + * (the plain-text face) verbatim. + */ + +import { describe, it, expect } from 'vitest'; +import * as entry from './index.js'; + +describe('service-messaging entry: the body-taking renderer is retired (#25190)', () => { + it('the published entry does not export renderNotification', () => { + expect('renderNotification' in entry).toBe(false); + }); + + it('THE CONTROL: the entry still exports the template store, the plain-text face and the channels that render', () => { + expect(typeof entry.NotificationTemplateStore).toBe('function'); + expect(typeof entry.interpolate).toBe('function'); + expect(entry.TEMPLATE_OBJECT).toBe('sys_notification_template'); + expect(typeof entry.createEmailChannel).toBe('function'); + expect(typeof entry.createSmsChannel).toBe('function'); + }); + + it('a stored html template row still renders through the email channel', async () => { + const row = { + organization_id: 'org_1', + topic: 'deal.won', + channel: 'email', + locale: 'en', + is_active: true, + format: 'html', + subject: 'Won {{ payload.title }}', + body: '