8.3 KiB
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—SuaveplanErrorsubclass 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_INPUTis raised by the newcreateCmsPluginfactory when Zod boundary validation fails (genesis convention, mirrorsapi-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 constructedCmsEngineas thecmsslot on the hostServiceRegistry, matching the genesis api-plugin convention. - ADDED:
CmsPluginOptionsSchema,CmsPluginOptions,CmsPluginConfig— Zod-validated plugin options bundle (injectableClock/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 shipsrespondProblemJsonbut 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.mdis an aspirational future enhancement, not in the upstream baseline. - Counter-based ID replacement with ULID — kept as an injectable
IdGeneratorso callers wanting ULID supply it; default mirrors upstreamcms_<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 (expectTypeOfonCmsEngine's event map), determinism with injectedClock/IdGenerator, and the fullCMS_CODESenumeration.src/error-codes/error-codes.test.ts— every factory raises the expected code;causechain is preserved;instanceof CmsErrornarrows.src/routes/problem-json.test.ts— coversCmsError→ 4xx/409 mapping, genericSuaveplanError→ 500, non-error throwables → 500.src/schemas/options.test.ts— Zod boundary accept/reject, default-fill behavior.src/plugin/plugin.test.ts— factory returns validPluginDescriptor,onInitregisterscmsin theServiceRegistry,onCloseclears it, invalid options raiseCMS_CODES.INVALID_INPUT.src/otel/instruments.test.ts— instruments are emitted from the engine and observed via in-memoryMeterProvider.src/types/cms.types.test.ts—generateSlug,validateContentData,VALID_STATUS_TRANSITIONSshape.src/api-plugin-cms.spec-conformance.test.ts— everyCMS.*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.