4.7 KiB
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 Maps 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<CmsEventMap> 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 andvalidateContentDataenforcement on create and update. queryContentsupports filter bymodelId,status,locale, case-insensitivesearchovertitle/slug, sort by an arbitrary field withasc/desc, andoffset/limitpaging.- Status transitions consult
VALID_STATUS_TRANSITIONSand emit one ofcontent.published/content.drafted/content.archived/ fallbackcontent.updated. - Version history retained per entry;
getVersionsreturns the full trail, deletion drops it. - OTel counters fire from every mutating method.
Usage: Basic
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):
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<CmsEventMap> — 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 Maps (models, entries, versions). The
default IdGenerator produces cms_<n>_<clock()> strings — verbatim
from upstream so byte-level test fixtures survive. The default Clock
is () => Date.now(). EventBus<CmsEventMap> adapts to the
single-payload suaveplan convention (Record<string, unknown>); 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.