Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
# `CmsEngine`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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
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<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 `Map`s (`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.