Files
2026-05-27 23:29:53 +01:00

132 lines
8.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Add @suaveplan/api-plugin-cms
**Package**: `@suaveplan/api-plugin-cms`
**Status**: PROPOSED
**Type**: new-package
## Why
Headless-CMS primitives for `@suaveplan/api` applications. The upstream
package on main (`/mnt/s/web/suaveplan/packages/server/api-plugin-cms`)
ships a `CmsEngine` that manages flexible `ContentModel` definitions,
`ContentEntry` records with `draft → review → published → archived`
status transitions guarded by `VALID_STATUS_TRANSITIONS`, versioned
`ContentVersion` history, slug uniqueness, an RFC 7807 problem+json
helper, and a `CmsError` hierarchy registered into the shared error
metadata registry on module load. Genesis needs that surface ported at
full functional parity so downstream `@suaveplan/api` apps can author
content models, validate entries, and walk the status lifecycle without
reaching for main.
## What changes
- ADDED: `CmsEngine` — in-memory CMS core managing models, entries, and
version history; emits eight typed events through `@suaveplan/events`.
- ADDED: `CmsError`, `CMS_CODES`, `CmsErrorCode` — `SuaveplanError`
subclass and seven stable error codes (`CONTENT_NOT_FOUND`,
`MODEL_NOT_FOUND`, `INVALID_TRANSITION`, `VALIDATION_FAILED`,
`DUPLICATE_SLUG`, `MODEL_IN_USE`, `INVALID_INPUT`). The first six are
verbatim upstream; `INVALID_INPUT` is raised by the new
`createCmsPlugin` factory when Zod boundary validation fails (genesis
convention, mirrors `api-plugin-audit` / `api-plugin-database`).
- ADDED: `contentNotFound`, `modelNotFound`, `invalidStatusTransition`,
`validationFailed`, `duplicateSlug`, `modelInUse` — error-factory
helpers.
- ADDED: `respondProblemJson`, `formatProblemJson`,
`PROBLEM_JSON_CONTENT_TYPE`, `HttpResponseLike`, `ProblemDetails` —
RFC 7807 problem+json rendering.
- ADDED: `ContentStatus`, `ContentType`, `FieldType`, `ContentField`,
`FieldValidation`, `ContentModel`, `ContentEntry`, `ContentVersion`,
`CreateContentInput`, `UpdateContentInput`, `ContentQuery`,
`CmsEventMap` — content-model types.
- ADDED: `generateSlug`, `validateContentData`,
`VALID_STATUS_TRANSITIONS` — utility surface.
- ADDED: `createCmsPlugin` — `definePlugin()` factory that registers
the constructed `CmsEngine` as the `cms` slot on the host
`ServiceRegistry`, matching the genesis api-plugin convention.
- ADDED: `CmsPluginOptionsSchema`, `CmsPluginOptions`, `CmsPluginConfig`
— Zod-validated plugin options bundle (injectable
`Clock` / `IdGenerator`).
- ADDED: `CmsInstruments`, `getCmsInstruments`, `resetCmsInstruments` —
OTel meter bundle (content created / updated / published / archived /
deleted counters; model created counter; validation-failure counter).
## Out of scope
- SurrealDB storage adapter — upstream is in-memory only; matched.
- HTTP route mounting on `app` — upstream ships `respondProblemJson` but
no actual routes; matched.
- Auth/role gating, audit-log integration, webhook/preview tokens,
rich-text/media handling, full-text search hook — every item listed
under §4 of `enterprise/api-plugin-cms.md` is an aspirational future
enhancement, not in the upstream baseline.
- Counter-based ID replacement with ULID — kept as an injectable
`IdGenerator` so callers wanting ULID supply it; default mirrors
upstream `cms_<n>_<ts>` for byte-identical parity.
## Source mapping
This change derives from
`../suaveplan/enterprise/api-plugin-cms.md` and the upstream source at
`../suaveplan/packages/server/api-plugin-cms/`.
| Requirement | Source lines | Notes |
|---|---|---|
| `CmsEngine` lifecycle (`createModel`, `getModel`, `listModels`, `deleteModel`) | upstream `src/routes/cms-engine.ts` L38–L70 | verbatim port — same method bodies, adapted to `EventBus<CmsEventMap>` instead of upstream `EventEmitter` |
| Content entry CRUD (`createContent`, `getContent`, `queryContent`, `updateContent`, `deleteContent`) | upstream `src/routes/cms-engine.ts` L71–L210 | verbatim port — query filtering / sorting / paging preserved |
| Status-transition guard via `VALID_STATUS_TRANSITIONS` | upstream `src/routes/cms-engine.ts` L179–L202 + `src/types/cms.types.ts` L134–L142 | verbatim |
| Versioned history (`getVersions`, `saveVersion`) | upstream `src/routes/cms-engine.ts` L213–L230 | verbatim |
| `CmsError` + six factory helpers | upstream `src/errors/cms-errors.ts` L1–L71 | adapted from `BaseError<Code, Context>` to genesis `SuaveplanError<Code>` (genesis has no second-generic context — context lives on `options.context`) |
| Stable code registry (`CMS_CODES`) | upstream `src/errors/error-codes.ts` L20–L74 | adapted from upstream `ErrorCodes.registerMany(metadata)` (no equivalent in genesis `@suaveplan/error`) to `defineCodes("CMS", [...])`; HTTP-status metadata folded into the problem-json renderer (mirrors `api-plugin-database`) |
| Side-effect import of code registry on barrel load | upstream `src/index.ts` L3 | matched — `src/index.ts` side-effect-imports `./error-codes/error-codes.js` |
| `respondProblemJson`, `PROBLEM_JSON_CONTENT_TYPE` | upstream `src/routes/problem-json.ts` L1–L47 | adapted — genesis `@suaveplan/error` does not yet ship `HttpError` / `formatErrorAsProblemJson` / `ProblemDetails`, so a local `formatProblemJson` mirrors `api-plugin-database`'s pattern |
| `ContentStatus`, `ContentType`, `FieldType`, `ContentField`, `FieldValidation`, `ContentModel`, `ContentEntry`, `ContentVersion`, `CreateContentInput`, `UpdateContentInput`, `ContentQuery`, `CmsEventMap` | upstream `src/types/cms.types.ts` L1–L131 | verbatim |
| `generateSlug` | upstream `src/types/cms.types.ts` L145–L152 | verbatim |
| `validateContentData` + private `validateField` | upstream `src/types/cms.types.ts` L155–L230 | verbatim |
| `VALID_STATUS_TRANSITIONS` | upstream `src/types/cms.types.ts` L134–L142 | verbatim |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `createCmsPlugin` factory + `cms` ServiceRegistry slot | Every other genesis `api-plugin-*` ships a `definePlugin()` factory wiring its primary service into the registry. Matching the convention costs <50 lines and aligns with `enterprise/api-plugin-cms.md` §3 "Clarify identity → wrap in `definePlugin()`". |
| `CmsPluginOptionsSchema` (Zod) | Genesis-wide policy: every public option bundle is Zod-validated at the boundary (CLAUDE.md). |
| `Clock` / `IdGenerator` injection | Genesis-wide policy: no `new Date()` in hot paths, no non-deterministic ID generation in tests (CLAUDE.md "Code Organization"). Default factory mirrors upstream behavior byte-for-byte. |
| OTel instrument bundle | Genesis-wide policy: every plugin emits OTel metrics from its production hot path and asserts them with an in-memory `MeterProvider`. |
| 100% test coverage | Genesis-wide policy, not package-specific. |
## Verification approach
Every requirement in the spec delta is pinned by concrete `it(...)`
titles. Test files that will exist after this change ships:
- `src/engine/cms-engine.test.ts` — covers model + content CRUD,
query filtering / sorting / paging, status transitions, version
history, event emission, and every error-throwing path.
- `src/engine/cms-engine.spec-conformance.test.ts` — pins type-level
claims (`expectTypeOf` on `CmsEngine`'s event map), determinism with
injected `Clock` / `IdGenerator`, and the full `CMS_CODES` enumeration.
- `src/error-codes/error-codes.test.ts` — every factory raises the
expected code; `cause` chain is preserved; `instanceof CmsError`
narrows.
- `src/routes/problem-json.test.ts` — covers `CmsError` → 4xx/409
mapping, generic `SuaveplanError` → 500, non-error throwables → 500.
- `src/schemas/options.test.ts` — Zod boundary accept/reject,
default-fill behavior.
- `src/plugin/plugin.test.ts` — factory returns valid `PluginDescriptor`,
`onInit` registers `cms` in the `ServiceRegistry`, `onClose` clears it,
invalid options raise `CMS_CODES.INVALID_INPUT`.
- `src/otel/instruments.test.ts` — instruments are emitted from the
engine and observed via in-memory `MeterProvider`.
- `src/types/cms.types.test.ts` — `generateSlug`, `validateContentData`,
`VALID_STATUS_TRANSITIONS` shape.
- `src/api-plugin-cms.spec-conformance.test.ts` — every `CMS.*` code is
thrown from at least one production path; barrel side-effect-imports
the code registry.
- `tests/dep-graph.test.ts` — pins the eight runtime workspace deps and
forbids drift.
## Migration impact
None — additive only. New package; no existing consumers.