first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
# @suaveplan/api-plugin-cms - Headless CMS primitives for @suaveplan/api: in-memory CmsEngine over @suaveplan/events with content models, entries, status lifecycle, version history, slug uniqueness, Zod-validated plugin options, RFC 7807 problem+json renderer, and an OTel instrument bundle. Server-tier — Bun + Node.
Headless content-management primitives for `@suaveplan/api`. Ships an
in-memory `CmsEngine` that 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 package factory wraps the engine in a typed
`PluginDescriptor` consumable by `@suaveplan/api-plugin`'s
`PluginRegistry`; the engine is exposed under the `cms` slot of the
host `ServiceRegistry`.
<!-- begin:auto-docs -->
## Documentation tree
- [engine](./src/engine/cms-engine.md) — `CmsEngine` is the headless content-management core for `@suaveplan/api-plugin-cms`. It manages flexible content models…
- [cms-engine](./src/engine/cms-engine.md) — `CmsEngine` is the headless content-management core for `@suaveplan/api-plugin-cms`. It manages flexible content models…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable error-code namespace `CMS.*` for the package and the concrete `CmsError` class that consume…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable error-code namespace `CMS.*` for the package and the concrete `CmsError` class that consume…
- [otel](./src/otel/instruments.md) — The package emits seven OpenTelemetry counters on every production mutation so operators can dashboard CMS activity and…
- [instruments](./src/otel/instruments.md) — The package emits seven OpenTelemetry counters on every production mutation so operators can dashboard CMS activity and…
- [plugin](./src/plugin/plugin.md) — Wraps `CmsEngine` in a typed `PluginDescriptor` consumable by `@suaveplan/api-plugin`'s `PluginRegistry`. The factory m…
- [plugin](./src/plugin/plugin.md) — Wraps `CmsEngine` in a typed `PluginDescriptor` consumable by `@suaveplan/api-plugin`'s `PluginRegistry`. The factory m…
- [routes](./src/routes/problem-json.md) — Render `CmsError` (and arbitrary throwables) as RFC 7807 problem+json HTTP responses without requiring `@suaveplan/api-…
- [problem-json](./src/routes/problem-json.md) — Render `CmsError` (and arbitrary throwables) as RFC 7807 problem+json HTTP responses without requiring `@suaveplan/api-…
- [schemas](./src/schemas/options.md) — Zod schema validating the `CmsPluginOptions` bundle accepted by `createCmsPlugin(...)`. Boundary validation at the plug…
- [options](./src/schemas/options.md) — Zod schema validating the `CmsPluginOptions` bundle accepted by `createCmsPlugin(...)`. Boundary validation at the plug…
- [services](./src/services/registry-augmentation.md) — Declares the `cms` slot on the `@suaveplan/api-plugin/service-registry`'s `ServiceRegistry` interface via TypeScript mo…
- [registry-augmentation](./src/services/registry-augmentation.md) — Declares the `cms` slot on the `@suaveplan/api-plugin/service-registry`'s `ServiceRegistry` interface via TypeScript mo…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/api-plugin-cms
```
## Quick start
```ts
import { createCmsPlugin } from "@suaveplan/api-plugin-cms";
import { createPluginRegistry } from "@suaveplan/api-plugin";
const registry = createPluginRegistry();
registry.register(createCmsPlugin({}));
await registry.initAll();
const cms = registry.context("cms").getService("cms");
const model = cms?.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 = cms?.createContent({
modelId: model!.id,
title: "Hello world",
data: { title: "Hello world", body: "Welcome." },
});
cms?.transitionStatus(entry!.id, "published");
```
## Rendering errors as RFC 7807 problem+json
Use `respondProblemJson(res, err)` from your route's error handler.
The renderer maps every `CMS.*` code to the documented HTTP status
(404 / 400 / 409 / 500).
```ts
import { respondProblemJson } from "@suaveplan/api-plugin-cms";
app.onError((err, c) => respondProblemJson(c, err));
```
## Modules
- [`engine/cms-engine`](./src/engine/cms-engine.md) — `CmsEngine`
core (model + content CRUD, status FSM, versions).
- [`error-codes/error-codes`](./src/error-codes/error-codes.md) —
`CMS_CODES`, `CmsError`, factory helpers.
- [`otel/instruments`](./src/otel/instruments.md) — OpenTelemetry
counters.
- [`plugin/plugin`](./src/plugin/plugin.md) — `createCmsPlugin`
factory.
- [`routes/problem-json`](./src/routes/problem-json.md) — RFC 7807
renderer.
- [`schemas/options`](./src/schemas/options.md) —
`CmsPluginOptionsSchema`.
- [`services/registry-augmentation`](./src/services/registry-augmentation.md)
— `ServiceRegistry` augmentation for the `cms` slot.
## License
MIT — © Suaveplan Team

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.

View File

@@ -0,0 +1,89 @@
# Error codes — `@suaveplan/api-plugin-cms`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the stable error-code namespace `CMS.*` for the
package and the concrete `CmsError` class that consumers `instanceof`
in their `catch` blocks. Every failure path raised by `CmsEngine` and
`createCmsPlugin` carries one of these codes; the
problem+json renderer maps them to HTTP statuses; the spec-conformance
test asserts every code is raised from at least one production source
file. Codes are minted via `defineCodes("CMS", [...])` so the literal
union flows through TypeScript and `error.code` narrows without a cast.
## Features
- Seven stable codes: `CONTENT_NOT_FOUND`, `MODEL_NOT_FOUND`,
`INVALID_TRANSITION`, `VALIDATION_FAILED`, `DUPLICATE_SLUG`,
`MODEL_IN_USE`, `INVALID_INPUT`.
- Six factory helpers (`contentNotFound`, `modelNotFound`,
`invalidStatusTransition`, `validationFailed`, `duplicateSlug`,
`modelInUse`) that build the right `CmsError` with the right
`context` fields populated.
- `CmsError` extends `SuaveplanError<CmsErrorCode>` so the `code` field
narrows and the `cause` chain is preserved across factory wrapping.
- A `cmsCode` mirror field types the same value with the narrowed
`CmsErrorCode` (the inherited `code` is typed as `string` on the
base class to keep the abstract surface stable).
## Usage: Basic
```ts
import {
CMS_CODES,
CmsError,
contentNotFound,
} from "@suaveplan/api-plugin-cms";
try {
engine.getContent("missing");
} catch (err) {
if (err instanceof CmsError && err.code === CMS_CODES.CONTENT_NOT_FOUND) {
return notFound();
}
throw err;
}
```
## Usage: Advanced
Build a richer error site by chaining a `cause` and supplying extra
context that the audit log can persist:
```ts
import { CmsError, CMS_CODES } from "@suaveplan/api-plugin-cms";
throw new CmsError(
CMS_CODES.VALIDATION_FAILED,
"user-driven validation failed",
{ cause: zodError, context: { tenantId, requestId } }
);
```
## API Reference
| Symbol | Description |
|---|---|
| `CMS_CODES` | Frozen map: `{ CONTENT_NOT_FOUND: "CMS.CONTENT_NOT_FOUND", ... }`. |
| `CmsErrorCode` | Union literal type of the seven values. |
| `class CmsError` | Concrete class; `instanceof` narrows `code` to `CmsErrorCode`. |
| `contentNotFound(id)` | → `CmsError(CONTENT_NOT_FOUND, ..., { context: { entryId } })`. |
| `modelNotFound(id)` | → `CmsError(MODEL_NOT_FOUND, ..., { context: { modelId } })`. |
| `invalidStatusTransition(from, to)` | → `CmsError(INVALID_TRANSITION, ..., { context: { from, to } })`. |
| `validationFailed(errors[])` | → `CmsError(VALIDATION_FAILED, ..., { context: { errors } })`. |
| `duplicateSlug(slug, modelId)` | → `CmsError(DUPLICATE_SLUG, ..., { context: { slug, modelId } })`. |
| `modelInUse(id)` | → `CmsError(MODEL_IN_USE, ..., { context: { modelId } })`. |
## Implementation Notes
`defineCodes` is the canonical suaveplan pattern; it returns a frozen
record where each key maps to a fully-qualified `<namespace>.<reason>`
string. The constructor of `CmsError` keeps the inherited `code` field
typed as `string` (limitation of `SuaveplanError`'s base generic) and
mirrors the narrowed value into `cmsCode`. The `context` field on the
base class is `readonly context?: ErrorContext` — optional — so all
access goes through `err.context?.<field>`.

View File

@@ -0,0 +1,77 @@
# OpenTelemetry instruments — `@suaveplan/api-plugin-cms`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The package emits seven OpenTelemetry counters on every production
mutation so operators can dashboard CMS activity and alert on drift
without wiring custom hooks. Instruments are resolved lazily through
`getCmsInstruments()` so test harnesses installing a custom
`MeterProvider` before importing the engine capture every observation.
The `resetCmsInstruments()` helper drops the cached bundle for tests
that need a clean slate; it is exported for testing only.
## Features
- One counter per `CmsEngine` event: `cms.models_created_total`,
`cms.content_created_total`, `cms.content_updated_total`,
`cms.content_published_total`, `cms.content_archived_total`,
`cms.content_deleted_total`.
- One derived counter `cms.validation_failures_total` increments
whenever `createContent` or `updateContent` raises
`CMS.VALIDATION_FAILED`.
- Lazy resolution honours the harness pattern: first import of the
package does not touch `metrics.getMeter` — the meter binds on the
first hot-path call.
## Usage: Basic
```ts
import { metrics } from "@opentelemetry/api";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
const provider = new MeterProvider({ readers: [...] });
metrics.setGlobalMeterProvider(provider);
// First mutation binds the cached meter to `provider`.
import { CmsEngine } from "@suaveplan/api-plugin-cms";
new CmsEngine().createModel({ /* ... */ });
```
## Usage: Advanced
In tests, install the in-memory exporter from
`tests/setup/polyfills.ts`, exercise the engine, then assert against
the flushed metrics:
```ts
import { flushTestMetrics } from "../../tests/setup/polyfills.js";
import { resetCmsInstruments } from "@suaveplan/api-plugin-cms";
beforeEach(() => resetCmsInstruments());
it("publishing increments the counter", async () => {
// ... engine.transitionStatus(..., "published")
const metrics = await flushTestMetrics();
// assert "cms.content_published_total" sum == 1
});
```
## API Reference
| Symbol | Description |
|---|---|
| `interface CmsInstruments` | Holds the seven counters + meter. |
| `getCmsInstruments()` | Resolve (or build + cache) the bundle. |
| `resetCmsInstruments()` | Test-only — clear the cached bundle. |
## Implementation Notes
The meter is acquired via `metrics.getMeter("@suaveplan/api-plugin-cms")`
on first call. Each counter is created with a human-readable
`description`. Production paths increment via `instr.<name>.add(1)` so
the wire emits a single delta per call; downstream pipelines aggregate
to a histogram of activity per minute if needed.

View File

@@ -0,0 +1,83 @@
# `createCmsPlugin`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Wraps `CmsEngine` in a typed `PluginDescriptor` consumable by
`@suaveplan/api-plugin`'s `PluginRegistry`. The factory matches the
suaveplan convention shipped by every other `api-plugin-*` package:
options are Zod-validated at the boundary so misuse is caught before
the plugin is registered; `onInit` constructs a `CmsEngine` and exposes
it via the host `ServiceRegistry`'s `cms` slot; `onClose` drops the
internal reference. Downstream plugins look the engine up via
`ctx.getService("cms")` and the slot is type-safe thanks to a
`declare module "@suaveplan/api-plugin/service-registry"` augmentation.
## Features
- Zod-validated boundary — invalid input raises
`CmsError(CMS.INVALID_INPUT)` with the underlying `ZodError`
attached as `cause`.
- Caller-supplied `Clock` and `IdGenerator` flow through to the
constructed `CmsEngine`, enabling deterministic test fixtures.
- ServiceRegistry slot named `cms` (module-augmented at package load).
- Lifecycle hooks (`onInit` / `onClose`) implement the canonical
api-plugin lifecycle; the descriptor is `enabledByDefault`-compatible
with `@suaveplan/api`'s `PluginRegistry`.
## Usage: Basic
```ts
import { createCmsPlugin } from "@suaveplan/api-plugin-cms";
import { createPluginRegistry } from "@suaveplan/api-plugin";
const registry = createPluginRegistry();
registry.register(createCmsPlugin({}));
await registry.initAll();
const cms = registry.context("cms").getService("cms");
cms?.createModel({ /* ... */ });
```
## Usage: Advanced
Inject a deterministic clock + id generator (useful for snapshot
tests of an entire request pipeline) and consume the registered engine
from another plugin:
```ts
import { createCmsPlugin } from "@suaveplan/api-plugin-cms";
let n = 0;
const plugin = createCmsPlugin({
clock: () => 1_700_000_000_000,
idGenerator: () => `det_${++n}`,
});
// In another plugin's onInit, after deps:
const cms = ctx.getService("cms");
cms?.events.on("content.published", broadcastWebhook);
```
## API Reference
| Symbol | Description |
|---|---|
| `interface CmsPluginOptions` | `{ clock?, idGenerator? }` — caller input. |
| `interface CmsPluginConfig` | Resolved post-validation shape. |
| `createCmsPlugin(options?)` | Returns `PluginDescriptor<CmsPluginConfig>`. |
## Implementation Notes
The factory Zod-parses options via `CmsPluginOptionsSchema`; both
`clock` and `idGenerator` are typed as `unknown` at the schema level
(Zod cannot usefully type arbitrary function signatures) and the
runtime `typeof === "function"` guard inside `createCmsPlugin` decides
whether to forward them to the engine. `onInit` mutates a closed-over
`CmsPluginState` so `onClose` can drop the reference without leaking
the engine instance into the registry. The `cms` slot is declared via
module augmentation in `services/registry-augmentation.ts` and the
barrel side-effect-imports that module so downstream type-only
imports of `@suaveplan/api-plugin/service-registry` see the slot.

View File

@@ -0,0 +1,90 @@
# RFC 7807 problem+json renderer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Render `CmsError` (and arbitrary throwables) as RFC 7807 problem+json
HTTP responses without requiring `@suaveplan/api-plugin-cms` to depend
on any HTTP framework. The renderer accepts a small structural
`HttpResponseLike` interface that is compatible with Hono, Fastify,
Bun's stdlib `Response`, and any framework whose response object
exposes `header(name, value)` plus `body(data, status)`. Status
mapping is encoded inline as a `Record<string, number>` lookup table
that the package-level spec-conformance test asserts is exhaustive
over `CMS_CODES`.
## Features
- Pure `formatProblemJson(err)` returns the JSON body for logging or
side-channel use.
- `respondProblemJson(res, err)` writes header + body and returns the
framework's `Response` for `return res.body(...)` style handlers.
- `PROBLEM_JSON_CONTENT_TYPE` constant — `application/problem+json` per
RFC 7807.
- Local `ProblemDetails` interface matching the IETF spec, with an
extension-index signature so callers can add fields per RFC §3.2.
- Status table:
- `CMS.CONTENT_NOT_FOUND` / `CMS.MODEL_NOT_FOUND` → 404
- `CMS.INVALID_TRANSITION` / `CMS.DUPLICATE_SLUG` / `CMS.MODEL_IN_USE` → 409
- `CMS.VALIDATION_FAILED` → 400
- `CMS.INVALID_INPUT` → 500
- Generic `SuaveplanError` instances render with HTTP 500 but with the
original code so log scrapers still see structure.
- Non-error throwables render `{ type: "about:blank", status: 500 }`.
## Usage: Basic
```ts
import { Hono } from "hono";
import { respondProblemJson } from "@suaveplan/api-plugin-cms";
const app = new Hono();
app.onError((err, c) => respondProblemJson(c, err));
```
## Usage: Advanced
Side-channel logging without writing to a response (e.g. forwarding
the body to an audit pipeline):
```ts
import { formatProblemJson } from "@suaveplan/api-plugin-cms";
try {
engine.transitionStatus(id, target);
} catch (err) {
const body = formatProblemJson(err);
await audit.log({
type: "cms.transition.failed",
severity: body.status >= 500 ? "error" : "warning",
...body,
});
throw err;
}
```
## API Reference
| Symbol | Description |
|---|---|
| `PROBLEM_JSON_CONTENT_TYPE` | `"application/problem+json"`. |
| `interface ProblemDetails` | RFC 7807 body shape with an extension index. |
| `interface HttpResponseLike` | `{ header(n, v); body(d, s): Response }`. |
| `respondProblemJson(res, err)` | Write header + body; return `Response`. |
| `formatProblemJson(err)` | Pure variant. |
## Implementation Notes
The status lookup is a `Readonly<Record<string, number>>` keyed by the
fully-qualified `CMS.*` code. If a `CmsError` is constructed with a
code not in the table (unreachable in normal use; possible only in
tests that bypass `defineCodes`), the renderer falls back to HTTP 500.
Generic `SuaveplanError` instances render with HTTP 500 + the
original `code` field so the response is still grep-friendly. The
`HttpResponseLike` shape was chosen over `Context` (Hono) to keep
this package free of framework dependencies — see
`@suaveplan/api-plugin-database`'s `routes/problem-json.ts` for the
same pattern.

View File

@@ -0,0 +1,69 @@
# `CmsPluginOptionsSchema`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schema validating the `CmsPluginOptions` bundle accepted by
`createCmsPlugin(...)`. Boundary validation at the plugin factory
ensures that misuse fails before the plugin reaches the registry, with
the underlying `ZodError` preserved as the `cause` chain on the
resulting `CmsError(CMS.INVALID_INPUT)`. The schema is intentionally
minimal — only `clock` and `idGenerator` are accepted today — and both
fields are passed through as `unknown` because Zod cannot usefully
type-check arbitrary function signatures. The runtime `typeof ===
"function"` guard inside `createCmsPlugin` is the actual gate.
## Features
- Boundary validation for `CmsPluginOptions`.
- `safeParse` style for opt-in error handling at the factory site.
- `z.input` / `z.output` derived types for downstream consumers that
want the pre-/post-validation shapes.
- The schema invokes `.transform(...)` so omitted fields are stripped
from the output rather than serialised as `undefined`.
## Usage: Basic
```ts
import { CmsPluginOptionsSchema } from "@suaveplan/api-plugin-cms";
const result = CmsPluginOptionsSchema.safeParse(input);
if (!result.success) throw new Error("bad options");
const { clock, idGenerator } = result.data;
```
## Usage: Advanced
Compose with a project-level config schema that also drives logging /
telemetry parameters:
```ts
import z from "zod";
import { CmsPluginOptionsSchema } from "@suaveplan/api-plugin-cms";
const ProjectConfigSchema = z.object({
cms: CmsPluginOptionsSchema,
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
});
```
## API Reference
| Symbol | Description |
|---|---|
| `CmsPluginOptionsSchema` | Zod schema. |
| `CmsPluginOptionsInput` | `z.input<typeof CmsPluginOptionsSchema>`. |
| `CmsPluginOptionsResolved` | `z.output<typeof CmsPluginOptionsSchema>`. |
## Implementation Notes
The schema is the only Zod surface exposed by the package, so the
conformance script's "schemas at boundaries" check is satisfied by the
single `.safeParse(...)` call inside `src/plugin/plugin.ts`. The
`unknown` typing of `clock` / `idGenerator` is deliberate; Zod
`z.function()` requires arg/return schemas at parse-time which would
force callers to construct a Zod-wrapped function — a worse ergonomics
trade than the inline `typeof` check inside the factory.

View File

@@ -0,0 +1,82 @@
# `ServiceRegistry` augmentation — `cms` slot
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Declares the `cms` slot on the
`@suaveplan/api-plugin/service-registry`'s `ServiceRegistry` interface
via TypeScript module augmentation. The augmentation has no runtime
output — it exists purely so consumers typed against the augmented
registry see `ctx.getService("cms")` resolve to `CmsEngine |
undefined` without any cast at the call site. The barrel
side-effect-imports this module so the augmentation is observed as
soon as any consumer imports `@suaveplan/api-plugin-cms`.
## Features
- Adds `cms: CmsEngine` to `ServiceRegistry`.
- Zero runtime cost — the file emits no JavaScript.
- Cumulative augmentation — TypeScript merges this declaration with
every other plugin's slot declarations, so apps wiring `cms`,
`auditService`, `db`, `searchIndexes`, etc. see one combined
registry shape.
## Usage: Basic
Consumers do not import this module directly; the barrel does it on
their behalf. The slot is then usable wherever a `PluginContext` is
in scope:
```ts
import { definePlugin } from "@suaveplan/api-plugin";
export const myPlugin = definePlugin({
name: "my-plugin",
version: "0.1.0",
dependencies: ["cms"],
onInit: (ctx) => {
const cms = ctx.getService("cms"); // typed as CmsEngine | undefined
cms?.events.on("content.published", (entry) => {
/* ... */
});
},
});
```
## Usage: Advanced
When stripping the package out at build time (e.g. for a browser
bundle), make sure to also strip the side-effect import of this module
from `src/index.ts`; otherwise the `declare module` block is harmless
but the `import` statement may cause a tree-shake regression.
```ts
// To consume the engine without the slot augmentation:
import { CmsEngine } from "@suaveplan/api-plugin-cms/engine/cms-engine";
```
## API Reference
This module exports nothing at runtime. Its only effect is the
ambient `ServiceRegistry` merge:
```ts
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry {
cms: CmsEngine;
}
}
```
## Implementation Notes
The augmentation file deliberately has no value-level imports — it
imports `type CmsEngine` so the JavaScript output is empty after
TypeScript erasure. Side-effect imports of empty files are still
emitted; the bundler keeps them because they may have ambient
side-effects (here, only typing). The barrel imports this module
unconditionally so any caller of `@suaveplan/api-plugin-cms` gets the
slot without an explicit reference.