Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/22876-email-template-category-is-system-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
'@objectstack/spec': minor
'@objectstack/plugin-email': patch
'@objectstack/platform-objects': patch
---

feat(spec)!: retire an email template's `category` and `isSystem` — the grouping and the built-in flag nothing read (#22876)

**BREAKING** — `category` and `isSystem` on an email template (`EmailTemplateDefinitionSchema`, the `email_template` metadata type) are now refused at parse. Nothing read either key once templates stopped being copied into `sys_email_template` rows: the Studio listing never offered the filter facet `category` promised, and no door refused deleting a template that set `isSystem`. Delete both keys. A template is found by its `name`, so group templates with a dotted namespace prefix (`auth.password_reset`, `crm.welcome`). A built-in template is a code package's declaration: a Studio save overrides it for the environment, and discarding that save restores the package's copy.

Clause-②: no (narrowing)

Measured before removal: no reader of either key in `@objectstack/plugin-email`, `@objectstack/service-messaging`, `@objectstack/plugin-auth`, the metadata protocol, ObjectQL, the runtime or the REST layer, and none in the Studio console at its pinned commit beyond a fallback form input, which the console's own companion change removes. Both keys carried a schema default (`category: 'custom'`, `isSystem: false`), so every parsed template carried them and the liveness advisory fired on every template, with no way for an author to clear it.

## FROM → TO

| you wrote (17.7 and earlier) | write instead |
| --- | --- |
| `category: 'auth'` (or `'notification'`, `'workflow'`, `'marketing'`, `'custom'`) | nothing — delete the key; group by the `name` prefix |
| `isSystem: true` | nothing — delete the key |
| `isSystem: false` | nothing — delete the key |
| `import { EmailTemplateDefinitionCategorySchema } from '@objectstack/spec/system'` | nothing — the enum is removed with its key |
| `type EmailTemplateDefinitionCategory` | nothing — the type is removed with its key |

**The one-line fix:** delete `category` and `isSystem` from every email template.

**What an author who still writes either sees.** `tsc` fails at the authoring site (`EmailTemplateDefinition` types both keys as retired), and the parse — `defineEmailTemplateDefinition()`, `defineStack({ emailTemplates })`, `PUT /api/v1/meta/email_template/:name` — refuses each at its own path with the prescription:

> `email_template.category` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing read it: the Studio listing never offered the filter facet it promised, and delivery never depended on it. Delete the key. A template is found by its `name`, so group templates with a dotted namespace prefix instead (`auth.password_reset`, `crm.welcome`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.

> `email_template.isSystem` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing enforced it: no door refused deleting a template that set it. Delete the key. A built-in template is a code package's declaration: a Studio save overrides it for the environment, and discarding that save restores the package's copy. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.

## The retirement kit

- **Two `retiredKey()` tombstones** on `EmailTemplateDefinitionSchema`, a `strictObject` — so the refusal carries the prescription rather than a bare unknown-key report, and `tsc` fails first. `RETIRED_KEYS_BY_MAJOR[18]`: `system/EmailTemplateDefinition:category`, `system/EmailTemplateDefinition:isSystem`. The `category` enum, `EmailTemplateDefinitionCategorySchema`, had no other consumer and leaves with it: `RETIRED_DEFS_BY_MAJOR[18]`: `system/EmailTemplateDefinitionCategory`.
- **The D2 conversion `email-template-category-is-system-removed`** (protocol 18, retired from the load path) deletes both keys from every `emailTemplates[]` entry, one notice per key, as a lossless delete. Because both keys carried a default, every built artifact and every stored `email_template` row written under 17.x carries them; both load through the rehydration seams, which replay this conversion. The retired-default residue stage is not adopted: the replayed conversion heals those populations, and an authored source that still writes either key meets the prescription.
- **The D3 entry `email-template-category-is-system-retired`** asks the author whether anything they built read either key — a report or integration that filtered templates by `category`, or one that treated `isSystem: true` as protection against deletion. Nothing ever enforced that.
- **Studio:** the email-template form drops the `category` select and the `isSystem` checkbox.
- **Ledger:** both rows stay `dead`, re-noted as tombstones (a tombstone keeps the key in the walked shape).
- **No deprecation window**, per the project's startup-stage posture.

**`@objectstack/plugin-email`:** the six built-in auth templates (in every locale) no longer write `category: 'auth'` or `isSystem: true`. They render and send exactly as before.

**`@objectstack/platform-objects`:** the metadata-form translation bundles drop the two retired form inputs' labels (`email_template.fields.category`, `email_template.fields.isSystem`). The `sys_email_template` object and its `category` / `is_system` columns are unchanged.

⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for.

<!-- adr-0087: registered email-template-category-is-system-removed, email-template-category-is-system-retired -->
15 changes: 8 additions & 7 deletions content/docs/automation/email-templates.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { defineEmailTemplateDefinition } from '@objectstack/spec';
export const TaskDoneEmail = defineEmailTemplateDefinition({
name: 'crm.task_done',
label: 'Task Done Notification',
category: 'workflow',
locale: 'en-US',
subject: 'Task done: {{task.title}}',
bodyHtml: '<p>The task <strong>{{task.title}}</strong> on {{project.name}} was marked done.</p>',
Expand Down Expand Up @@ -56,10 +55,14 @@ enforces dotted snake_case (`auth.password_reset`, `crm.large_deal_won`).
Prefix it with your app or domain — the namespace is what keeps a tenant's
templates from colliding with the built-in authentication mail.

`category` (`auth` | `notification` | `workflow` | `marketing` | `custom`,
default `custom`) is a filter facet in Studio listings, not a delivery
behaviour. `active: false` makes `sendTemplate` return `TEMPLATE_INACTIVE`
rather than silently sending nothing.
The namespace is also how you group templates: there is no separate category
key. `category` and `isSystem` were removed (ADR-0049 enforce-or-remove) because
nothing read either one, and the schema now refuses both with a migration note.
A built-in template is the email plugin's declaration, which a Studio save
overrides for the environment and discarding that save restores.

`active: false` makes `sendTemplate` return `TEMPLATE_INACTIVE` rather than
silently sending nothing.

## Placeholders

Expand Down Expand Up @@ -113,7 +116,6 @@ import { defineEmailTemplateDefinition } from '@objectstack/spec';
export const passwordResetEn = defineEmailTemplateDefinition({
name: 'auth.password_reset',
label: 'Password Reset',
category: 'auth',
locale: 'en-US',
subject: 'Reset your password',
bodyHtml: '<p>Use {{{reset_url}}} within {{ttl_minutes}} minutes.</p>',
Expand All @@ -122,7 +124,6 @@ export const passwordResetEn = defineEmailTemplateDefinition({
export const passwordResetZh = defineEmailTemplateDefinition({
name: 'auth.password_reset',
label: 'Password Reset',
category: 'auth',
locale: 'zh-CN',
subject: '重置您的密码',
bodyHtml: '<p>请在 {{ttl_minutes}} 分钟内使用 {{{reset_url}}}。</p>',
Expand Down
6 changes: 3 additions & 3 deletions content/docs/permissions/system-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,16 @@ below, and it is left open deliberately rather than hidden — see
## Which `isSystem` this page is about

Three unrelated declarations share the identifier. **This page documents only the
first.** The others are ordinary metadata fields on a stored document and have
nothing to do with elevation. (A fourth, `Environment.isSystem` — a
first.** The others are metadata keys on a stored document (one of them now
retired) and have nothing to do with elevation. (A fourth, `Environment.isSystem` — a
platform-infrastructure environment, not user data — left this repo with the
`@objectstack/spec/cloud` subpath in #16325 and is declared in the cloud repo.)

| Declaration | What it is | This page? |
|:---|:---|:---:|
| `ExecutionContext.isSystem` — `packages/spec/src/kernel/execution-context.zod.ts#isSystem` | The elevation flag on an operation's context | ✅ |
| `Object.isSystem` — `packages/spec/src/data/object.zod.ts#isSystem` | Marks a **system object** (protected from deletion; defaults its org-wide sharing to `public` when no `sharingModel` is set) | ❌ |
| `EmailTemplate.isSystem` — `packages/spec/src/system/email-template.zod.ts#isSystem` | Built-in template; tenants may override but should not delete | ❌ |
| `EmailTemplate.isSystem` — `packages/spec/src/system/email-template.zod.ts#isSystem` | A retired key: a `retiredKey()` tombstone, which the parse refuses with a migration note | ❌ |

The collision is a genuine hazard rather than a naming nit: `Object.isSystem`
changes an object's **default sharing**, and `ExecutionContext.isSystem` changes
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/api/package-api-assembled.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Installed package row whose manifest is the assembled package body
| **actions** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; objectName?: string; … }[]` | optional | Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by its owning object's name (or 'global' when object-less), a colon, then the action name, and defineStack refuses two declarations that resolve to one key — both here, both on one object's actions, or one in each position, identical twins included (an embedded action is keyed by the object it is written on, not by its own objectName). One global and one object-bound action may share a name; on that object's route the object's own actions take precedence for by-name readers. composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) and names both source stacks on a collision. |
| **flows** | `{ name: string; label: string; description?: string; successMessage?: string; … }[]` | optional | Screen Flows |
| **jobs** | `{ name: string; label?: string; description?: string; schedule: object \| object \| object; … }[]` | optional | Background / Scheduled Jobs (run by IJobService on cron/interval/once schedules) |
| **emailTemplates** | `{ name: string; label: string; category?: Enum<'auth' \| 'notification' \| 'workflow' \| 'marketing' \| 'custom'>; locale?: string; … }[]` | optional | Email Templates resolved by IEmailService.sendTemplate(`{ template, locale }`) |
| **emailTemplates** | `{ name: string; label: string; locale?: string; subject: string; … }[]` | optional | Email Templates resolved by IEmailService.sendTemplate(`{ template, locale }`) |
| **docs** | `{ name: string; label?: string; description?: string; content: string; … }[]` | optional | Package documentation — flat Markdown items compiled from src/docs/*.md (ADR-0046) |
| **books** | `{ name: string; label?: string; description?: string; slug?: string; … }[]` | optional | Documentation navigation spines — ordered groups with derived membership (ADR-0046 §6) |
| **positions** | `{ name: string; label: string; description?: string; permissionSets?: string[]; … }[]` | optional | Positions — flat capability-distribution groups (ADR-0090 D3) |
Expand Down Expand Up @@ -371,7 +371,7 @@ Installed package row whose manifest is the assembled package body
| **actions** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; objectName?: string; … }[]` | optional | Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by its owning object's name (or 'global' when object-less), a colon, then the action name, and defineStack refuses two declarations that resolve to one key — both here, both on one object's actions, or one in each position, identical twins included (an embedded action is keyed by the object it is written on, not by its own objectName). One global and one object-bound action may share a name; on that object's route the object's own actions take precedence for by-name readers. composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) and names both source stacks on a collision. |
| **flows** | `{ name: string; label: string; description?: string; successMessage?: string; … }[]` | optional | Screen Flows |
| **jobs** | `{ name: string; label?: string; description?: string; schedule: object \| object \| object; … }[]` | optional | Background / Scheduled Jobs (run by IJobService on cron/interval/once schedules) |
| **emailTemplates** | `{ name: string; label: string; category?: Enum<'auth' \| 'notification' \| 'workflow' \| 'marketing' \| 'custom'>; locale?: string; … }[]` | optional | Email Templates resolved by IEmailService.sendTemplate(`{ template, locale }`) |
| **emailTemplates** | `{ name: string; label: string; locale?: string; subject: string; … }[]` | optional | Email Templates resolved by IEmailService.sendTemplate(`{ template, locale }`) |
| **docs** | `{ name: string; label?: string; description?: string; content: string; … }[]` | optional | Package documentation — flat Markdown items compiled from src/docs/*.md (ADR-0046) |
| **books** | `{ name: string; label?: string; description?: string; slug?: string; … }[]` | optional | Documentation navigation spines — ordered groups with derived membership (ADR-0046 §6) |
| **positions** | `{ name: string; label: string; description?: string; permissionSets?: string[]; … }[]` | optional | Positions — flat capability-distribution groups (ADR-0090 D3) |
Expand Down
10 changes: 5 additions & 5 deletions content/docs/references/index.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Protocol reference — every schema by module
navTitle: Protocol Reference
description: Every schema published by @objectstack/spec — 1524 schemas across 14 protocol modules
description: Every schema published by @objectstack/spec — 1523 schemas across 14 protocol modules
---

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
Expand Down Expand Up @@ -32,9 +32,9 @@ counts are sums of the rows they head. Regenerate with
| [Security Protocol](/docs/references/security) | 5 | 30 | Permission sets, row-level security, sharing rules, tenancy posture. |
| [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
| [System Protocol](/docs/references/system) | 35 | 278 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [System Protocol](/docs/references/system) | 35 | 277 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 16 | 166 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
| **Total** | **196** | **1524** | 14 protocol modules |
| **Total** | **196** | **1523** | 14 protocol modules |

---

Expand Down Expand Up @@ -317,7 +317,7 @@ Studio designer metadata — the authoring surfaces for the protocols above.

## System Protocol

**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **35 pages, 278 schemas**
**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **35 pages, 277 schemas**

The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance.

Expand All @@ -334,7 +334,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a
| [`disaster-recovery.zod.ts`](/docs/references/system/disaster-recovery) | `BackupConfig`, `BackupRetention`, `BackupStrategy`, `DisasterRecoveryPlan`, `FailoverConfig`, `FailoverMode`, `RPO`, `RTO` |
| [`doc.zod.ts`](/docs/references/system/doc) | `Doc` |
| [`email-config.zod.ts`](/docs/references/system/email-config) | `EmailAddressConfig`, `EmailProvider`, `EmailServiceConfig`, `StackEmailConfig`, `StackEmailProviderOptions` |
| [`email-template.zod.ts`](/docs/references/system/email-template) | `EmailTemplateDefinition`, `EmailTemplateDefinitionCategory`, `EmailTemplateDefinitionVariable` |
| [`email-template.zod.ts`](/docs/references/system/email-template) | `EmailTemplateDefinition`, `EmailTemplateDefinitionVariable` |
| [`encryption.zod.ts`](/docs/references/system/encryption) | `EncryptionAlgorithm`, `EncryptionConfig`, `FieldEncryption`, `KeyManagementProvider`, `KeyRotationPolicy` |
| [`environment-artifact.zod.ts`](/docs/references/system/environment-artifact) | `Sha256Digest` |
| [`http-server.zod.ts`](/docs/references/system/http-server) | `MiddlewareConfig`, `MiddlewareType`, `RouteHandlerMetadata` |
Expand Down
Loading
Loading