# 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__` 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` 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` to genesis `SuaveplanError` (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.