132 lines
8.3 KiB
Markdown
132 lines
8.3 KiB
Markdown
# 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.
|