Skip to content

Commit 7293048

Browse files
committed
fix(plugin-email): keep the published sweep signature; the plugin calls an internal effective sweep
The effective-template boot sweep moves to a module-internal bootstrapEffectiveEmailTemplates, which EmailServicePlugin's boot wiring calls with the protocol and tenancy services. The exported bootstrapDeclaredEmailTemplates keeps its published four-parameter signature and delegates with no sources, reading the registry as before. The package entry exports no new symbol: EffectiveEmailTemplateSources is no longer re-exported. One sweep body, two entry points. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3191978 commit 7293048

5 files changed

Lines changed: 50 additions & 20 deletions

File tree

‎.changeset/21785-email-template-overlay-survives-boot.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ An email template edited through `PUT /api/v1/meta/email_template/:name` (the St
77
Clause-②: no
88

99
- The cause: on a deployment with a Default Organization the admin's save is an org-scoped overlay, and boot hydration keeps org-scoped overlays out of the registry the sweep read. An env-wide overlay was already kept.
10-
- `bootstrapDeclaredEmailTemplates` now projects the effective template, the layered list `protocol.getMetaItems` serves, read in `tenancy.defaultOrgId()`'s organization. That is the Default Organization under the `single` posture, and env-wide (no organization) whenever a walled posture is requested. A host without a `protocol` service reads the registry as before.
10+
- `EmailServicePlugin`'s boot sweep now projects the effective template: the layered list `protocol.getMetaItems` serves, read in the organization `tenancy.defaultOrgId()` names. That is the Default Organization under the `single` posture. A host without a `protocol` service reads the registry as before.
1111
- A failed effective read projects nothing for that boot, so the rows keep their last projection. It does not fall back to the package wording.
1212
- Seed-not-clobber is unchanged. A row an admin created (`managed_by: 'admin'`) or edited through the data API (`customized: true`) is still never overwritten.
13-
- `bootstrapDeclaredEmailTemplates` takes an optional fifth argument, `{ protocol, tenancy }` (`EffectiveEmailTemplateSources`, now exported). Existing callers are unaffected.
13+
- The published API is unchanged. The exported `bootstrapDeclaredEmailTemplates` keeps its signature and still reads the registry, so a caller outside the plugin sees the same behaviour as before.

‎packages/plugins/plugin-email/src/bootstrap-declared-email-templates.test.ts‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import { describe, expect, it, vi } from 'vitest';
1313
import {
1414
bootstrapDeclaredEmailTemplates,
15+
bootstrapEffectiveEmailTemplates,
1516
upsertDeclaredEmailTemplate,
1617
deactivateDeclaredEmailTemplate,
1718
mapTemplateToRow,
@@ -494,7 +495,7 @@ describe('bootstrapDeclaredEmailTemplates — the effective template (#21785)',
494495
{ organizationId: ORG, items: [declaredTemplate({ subject: OVERLAY_WORDING })] },
495496
);
496497

497-
const result = await bootstrapDeclaredEmailTemplates(engine as any, undefined, undefined, undefined, {
498+
const result = await bootstrapEffectiveEmailTemplates(engine as any, undefined, {
498499
protocol,
499500
tenancy: { defaultOrgId: async () => ORG },
500501
});
@@ -512,13 +513,13 @@ describe('bootstrapDeclaredEmailTemplates — the effective template (#21785)',
512513
{ organizationId: ORG, items: [declaredTemplate({ subject: OVERLAY_WORDING })] },
513514
);
514515

515-
await bootstrapDeclaredEmailTemplates(engine as any, undefined, undefined, undefined, {
516+
await bootstrapEffectiveEmailTemplates(engine as any, undefined, {
516517
protocol,
517518
tenancy: { defaultOrgId: async () => null },
518519
});
519520

520-
// No organization on the request: one plant's overlay is never what the
521-
// org-agnostic sending row carries.
521+
// No organization on the request: the tenancy contract named none, so the
522+
// read is env-wide and the declaration is what the row carries.
522523
expect(protocol.requests).toEqual([{ type: 'email_template' }]);
523524
expect(rowsOf(engine)[0].subject).toBe(PACKAGE_WORDING);
524525
});
@@ -533,10 +534,10 @@ describe('bootstrapDeclaredEmailTemplates — the effective template (#21785)',
533534
async getMetaItems(): Promise<never> { throw new Error('sys_metadata read failed'); },
534535
};
535536

536-
const result = await bootstrapDeclaredEmailTemplates(engine as any, undefined, { warn }, undefined, {
537+
const result = await bootstrapEffectiveEmailTemplates(engine as any, undefined, {
537538
protocol,
538539
tenancy: { defaultOrgId: async () => ORG },
539-
});
540+
}, { warn });
540541

541542
expect(result).toEqual({ seeded: 0, skipped: 0 });
542543
expect(rowsOf(engine)[0].subject).toBe(OVERLAY_WORDING);
@@ -562,10 +563,10 @@ describe('bootstrapDeclaredEmailTemplates — the effective template (#21785)',
562563
],
563564
});
564565

565-
const result = await bootstrapDeclaredEmailTemplates(engine as any, undefined, { warn }, undefined, {
566+
const result = await bootstrapEffectiveEmailTemplates(engine as any, undefined, {
566567
protocol,
567568
tenancy: { defaultOrgId: async () => ORG },
568-
});
569+
}, { warn });
569570

570571
expect(result).toEqual({ seeded: 0, skipped: 2 });
571572
expect(rowsOf(engine).map((r) => r.subject)).toEqual(['Admin original', 'Data-door wording']);

‎packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts‎

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ function uid(prefix: string): string {
115115
* (#21785). Both are optional: a host that registers no `protocol` has no
116116
* metadata door, so nothing can overlay a declaration there and the registry
117117
* read below is already the effective one.
118+
*
119+
* Module-internal: exported for EmailServicePlugin's boot wiring only and
120+
* deliberately NOT re-exported from the package entry — see
121+
* {@link bootstrapEffectiveEmailTemplates}.
118122
*/
119123
export interface EffectiveEmailTemplateSources {
120124
/** The `protocol` service. `getMetaItems` is the layered list `GET /meta/email_template` serves. */
@@ -154,9 +158,9 @@ const EFFECTIVE_READ_FAILED = Symbol('email-template-effective-read-failed');
154158
* org-less reader of org-overridable metadata does: `tenancy.defaultOrgId()`,
155159
* as the anonymous form doors read a form (`@objectstack/rest`). That answers
156160
* the Default Organization under `single` (ADR-0131: the organization IS the
157-
* environment there) and `null` whenever a walled posture was requested, where
158-
* the read is env-wide and no plant's overlay becomes everyone's mail. The
159-
* sending row stays org-agnostic: template resolution keys on
161+
* environment there) and `null` whenever a walled posture was requested (the
162+
* tenancy contract never guesses an organization there), where the read is
163+
* env-wide. The sending row stays org-agnostic: template resolution keys on
160164
* `(name, locale)` only, and per-organization template rows are a capability
161165
* no ruling has opened.
162166
*
@@ -359,17 +363,43 @@ export async function deactivateDeclaredEmailTemplate(
359363
}
360364

361365
/**
362-
* Materialize every declared email template into `sys_email_template`, as the
363-
* metadata door serves it (an overlay of the declaration included) when
364-
* `sources.protocol` is given — see {@link readDeclared}. Idempotent and safe
366+
* Materialize every declared email template into `sys_email_template` from the
367+
* ObjectQL registry (falling back to the metadata service). Idempotent and safe
365368
* to run on every boot.
369+
*
370+
* The published signature, unchanged. It delegates to
371+
* {@link bootstrapEffectiveEmailTemplates} with no sources, so an external
372+
* caller reads the registry exactly as before — the declarations plus the
373+
* env-wide overlays boot hydration registered. The plugin's own boot wiring
374+
* calls the effective form directly.
366375
*/
367376
export async function bootstrapDeclaredEmailTemplates(
368377
engine: IDataEngine,
369378
metadataService: any,
370379
logger?: Logger,
371380
object = EMAIL_TEMPLATE_OBJECT,
372-
sources?: EffectiveEmailTemplateSources,
381+
): Promise<BootstrapDeclaredEmailTemplatesResult> {
382+
return bootstrapEffectiveEmailTemplates(engine, metadataService, undefined, logger, object);
383+
}
384+
385+
/**
386+
* [#21785] The boot sweep EmailServicePlugin runs: materialize every
387+
* `email_template` into `sys_email_template` as the metadata door serves it —
388+
* an overlay of the declaration included — when `sources.protocol` is given,
389+
* and from the registry otherwise (see {@link readDeclared}). The one sweep
390+
* body; {@link bootstrapDeclaredEmailTemplates} is this with no sources.
391+
*
392+
* Module-internal: NOT re-exported from the package entry (`src/index.ts`),
393+
* and the package's `exports` map names only that entry, so this function and
394+
* {@link EffectiveEmailTemplateSources} add nothing to the published surface.
395+
* No caller outside this package needs them.
396+
*/
397+
export async function bootstrapEffectiveEmailTemplates(
398+
engine: IDataEngine,
399+
metadataService: any,
400+
sources: EffectiveEmailTemplateSources | undefined,
401+
logger?: Logger,
402+
object = EMAIL_TEMPLATE_OBJECT,
373403
): Promise<BootstrapDeclaredEmailTemplatesResult> {
374404
const declared = await readDeclared(engine, metadataService, 'email_template', sources, logger);
375405
if (declared === EFFECTIVE_READ_FAILED || declared.length === 0) return { seeded: 0, skipped: 0 };

‎packages/plugins/plugin-email/src/email-plugin.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import type {
3636
SettingsUnsubscribe,
3737
} from '@objectstack/spec/system';
3838
import {
39-
bootstrapDeclaredEmailTemplates,
39+
bootstrapEffectiveEmailTemplates,
4040
upsertDeclaredEmailTemplate,
4141
deactivateDeclaredEmailTemplate,
4242
mapTemplateToRow,
@@ -1102,7 +1102,7 @@ export class EmailServicePlugin implements Plugin {
11021102
try { tenancy = ctx.getService('tenancy'); } catch { /* optional */ }
11031103

11041104
try {
1105-
await bootstrapDeclaredEmailTemplates(engine, metadataService, ctx.logger as any, undefined, { protocol, tenancy });
1105+
await bootstrapEffectiveEmailTemplates(engine, metadataService, { protocol, tenancy }, ctx.logger as any);
11061106
} catch (err: any) {
11071107
ctx.logger.warn(
11081108
'EmailServicePlugin: declared email-template bootstrap failed (built-in templates still serve): '

‎packages/plugins/plugin-email/src/index.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ export {
105105
mapTemplateToRow,
106106
EMAIL_TEMPLATE_OBJECT,
107107
type BootstrapDeclaredEmailTemplatesResult,
108-
type EffectiveEmailTemplateSources,
109108
} from './bootstrap-declared-email-templates.js';
110109
export {
111110
sweepStrandedOutbox,

0 commit comments

Comments
 (0)