# `CmsEngine` _(no submodules)_ ## Purpose `CmsEngine` is the headless content-management core for `@suaveplan/api-plugin-cms`. It manages flexible content models, content entries with a `draft → review → published → archived` status lifecycle, slug uniqueness within a model, and a per-entry version trail. The engine is in-memory by design — three `Map`s back its state — so it is suitable for development, testing, and small-scale single-process deployments. Production-scale persistence is a downstream concern; the engine ships a stable surface that a future Surreal-backed storage port can wrap without modifying any caller code. State changes flow through a typed `EventBus` and an OpenTelemetry counter bundle so observability is free at the call site. Two abstractions are injectable for determinism: `Clock` drives every timestamp and `IdGenerator` drives every persisted identifier. Both default to the upstream Suaveplan main behaviour, so test fixtures captured against main remain byte-reproducible against this port. ## Features - Content-model CRUD (`createModel`, `getModel`, `listModels`, `deleteModel`) with referential-integrity guard against orphaning entries. - Content-entry CRUD (`createContent`, `getContent`, `queryContent`, `updateContent`, `deleteContent`) with `(modelId, slug)` uniqueness and `validateContentData` enforcement on create and update. - `queryContent` supports filter by `modelId`, `status`, `locale`, case-insensitive `search` over `title`/`slug`, sort by an arbitrary field with `asc`/`desc`, and `offset`/`limit` paging. - Status transitions consult `VALID_STATUS_TRANSITIONS` and emit one of `content.published` / `content.drafted` / `content.archived` / fallback `content.updated`. - Version history retained per entry; `getVersions` returns the full trail, deletion drops it. - OTel counters fire from every mutating method. ## Usage: Basic ```ts import { CmsEngine } from "@suaveplan/api-plugin-cms"; const engine = new CmsEngine(); const model = engine.createModel({ name: "Blog post", slug: "post", contentType: "post", fields: [ { name: "title", type: "text", label: "Title", required: true }, { name: "body", type: "richtext", label: "Body" }, ], }); const entry = engine.createContent({ modelId: model.id, title: "Hello world", data: { title: "Hello world", body: "Welcome." }, }); engine.transitionStatus(entry.id, "published"); console.log(engine.queryContent({ status: "published" })); ``` ## Usage: Advanced Inject a `Clock` and `IdGenerator` for deterministic test fixtures, and subscribe to the typed event bus to fan-out into downstream systems (audit log, search indexer, webhook dispatcher): ```ts import { CmsEngine } from "@suaveplan/api-plugin-cms"; import { EventBus } from "@suaveplan/events"; let n = 0; const engine = new CmsEngine({ clock: () => 1_700_000_000_000, idGenerator: () => `ulid_${++n}`, events: new EventBus(), }); engine.events.on("content.published", (entry) => { queueWebhook("content.published", entry); }); ``` ## API Reference | Member | Description | |---|---| | `new CmsEngine(options?)` | Construct the engine. Optional `clock`, `idGenerator`, `events`. | | `engine.events` | The underlying `EventBus` — subscribe with `.on(...)`. | | `createModel(input)` | Register a new model. Emits `model.created`. | | `getModel(id)` | Look up by id. Throws `CMS.MODEL_NOT_FOUND` on miss. | | `listModels()` | Snapshot of every registered model. | | `deleteModel(id)` | Remove a model. Throws `CMS.MODEL_IN_USE` if entries exist. | | `createContent(input)` | Validate + persist + version + emit `content.created`. | | `getContent(id)` | Look up entry. Throws `CMS.CONTENT_NOT_FOUND` on miss. | | `queryContent(query?)` | Filter / sort / page entries. | | `updateContent(id, input)` | Merge data; re-validate; version; emit `content.updated`. | | `transitionStatus(id, target)` | Status FSM step. Throws `CMS.INVALID_TRANSITION` on illegal edge. | | `deleteContent(id)` | Drop entry + versions; emit `content.deleted`. | | `getVersions(entryId)` | Full version trail. | ## Implementation Notes The engine uses three `Map`s (`models`, `entries`, `versions`). The default `IdGenerator` produces `cms__` strings — verbatim from upstream so byte-level test fixtures survive. The default `Clock` is `() => Date.now()`. `EventBus` adapts to the single-payload suaveplan convention (`Record`); upstream used tuple events, but the listener-side argument shape is identical. OTel counters are resolved lazily inside `getCmsInstruments()` so test harnesses installing a custom `MeterProvider` first capture every observation.