first commit
This commit is contained in:
129
docs/api-stack/api/api-openapi/src/emitter/emitter.md
Normal file
129
docs/api-stack/api/api-openapi/src/emitter/emitter.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# emitter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`createOpenapiEmitter` is the aggregator the spec calls for: it ingests
|
||||
every plugin's manifest, every registered route (with its Zod-converted
|
||||
schemas), the active auth adapter's security scheme, and the host
|
||||
config, then produces a single OpenAPI 3.1 document covering the entire
|
||||
mounted plugin set. The emitter is the single source of truth served at
|
||||
`/openapi.json` and consumed by the docs UI sibling packages.
|
||||
|
||||
By centralising emission, the host eliminates drift: every plugin's
|
||||
routes flow through the same namespacing rules, the same strictness
|
||||
policy, and the same security inheritance. Plugins cannot hand-author
|
||||
OpenAPI fragments that disagree with what their Hono runtime actually
|
||||
serves.
|
||||
|
||||
## Features
|
||||
|
||||
- **Manifest-driven tags + components** — `displayName` becomes the tag,
|
||||
`<pluginName>.<TypeName>` becomes the component schema prefix.
|
||||
- **Deterministic operationIds** — every route receives
|
||||
`<plugin>.<method>.<oasPath>`. Collisions raise the moment they
|
||||
appear.
|
||||
- **Security inheritance** — every operation lists the active auth
|
||||
adapter's scheme except for routes listed in any plugin's
|
||||
`publicRoutes`, which receive `security: []`.
|
||||
- **Strictness gate** — routes missing request + response schemas warn
|
||||
(with an OTel counter) or refuse install, per `host.openapi.strictness`.
|
||||
- **Per-plugin filtered view** — `getDocumentForPlugin(name)` returns
|
||||
only operations tagged with that plugin; misses raise
|
||||
`OPENAPI.UNKNOWN_PLUGIN_IN_FILTER`.
|
||||
- **JSON + YAML serialisation** — `getJson()` / `getYaml()` produce
|
||||
serialised bodies for the HTTP endpoints.
|
||||
- **OTel emission + conversion-error counters** — visibility on
|
||||
document production and Zod→OAS conversion failures.
|
||||
- **Lifecycle dedup** — the constructed document is memoised; repeated
|
||||
`getDocument()` calls return the same reference.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createOpenapiEmitter } from "@suaveplan/api-openapi/emitter";
|
||||
|
||||
const emitter = createOpenapiEmitter({
|
||||
manifests: [
|
||||
{ name: "billing", displayName: "Billing", description: "Invoices" },
|
||||
],
|
||||
routes: [
|
||||
{
|
||||
pluginName: "billing",
|
||||
route: { method: "GET", path: "/invoices/:id", tags: [] },
|
||||
responseSchema: { type: "object" },
|
||||
},
|
||||
],
|
||||
authAdapter: {
|
||||
name: "Bearer",
|
||||
scheme: { type: "http", scheme: "bearer" },
|
||||
},
|
||||
hostConfig: {
|
||||
info: { title: "host", version: "1.0.0" },
|
||||
},
|
||||
});
|
||||
emitter.getJson();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createOpenapiEmitter } from "@suaveplan/api-openapi/emitter";
|
||||
import { z } from "zod";
|
||||
|
||||
const Invoice = z.object({ id: z.string() });
|
||||
const emitter = createOpenapiEmitter({
|
||||
manifests: [{ name: "billing", displayName: "Billing" }],
|
||||
routes: [
|
||||
{
|
||||
pluginName: "billing",
|
||||
route: { method: "GET", path: "/invoices/:id", tags: [] },
|
||||
// Thunk → emitter wraps a thrown converter error in
|
||||
// OPENAPI.SCHEMA_CONVERSION_FAILED and increments the
|
||||
// conversion counter.
|
||||
responseSchema: () => ({ $ref: "#/components/schemas/billing.Invoice" }),
|
||||
components: { Invoice: { type: "object" } },
|
||||
},
|
||||
],
|
||||
hostConfig: {
|
||||
info: { title: "host", version: "1.0.0" },
|
||||
environment: "production",
|
||||
openapi: { strictness: "refuse-install" },
|
||||
},
|
||||
});
|
||||
emitter.getDocumentForPlugin("billing");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createOpenapiEmitter(input) → OpenapiEmitter`.
|
||||
- `OpenapiEmitter.getDocument() → OpenAPIDocument` — memoised.
|
||||
- `OpenapiEmitter.getJson() / getYaml() → string`.
|
||||
- `OpenapiEmitter.getDocumentForPlugin(name) → OpenAPIDocument`.
|
||||
- `OpenapiEmitter.getPluginNames() → readonly string[]`.
|
||||
- `OpenapiEmitter.getStrictness() → StrictnessMode`.
|
||||
- `EmitterPluginManifest`, `EmitterRouteInput`, `EmitterAuthAdapter`,
|
||||
`EmitterHostConfig` — input bag types.
|
||||
- `__resetEmitterCounters()` — test-only seam.
|
||||
- Constants: `EMISSION_COUNTER_NAME`, `CONVERSION_ERROR_COUNTER_NAME`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Schemas may be supplied as a pre-converted object or as a thunk; the
|
||||
thunk form gives the emitter a chance to catch + wrap conversion
|
||||
errors with `OPENAPI.SCHEMA_CONVERSION_FAILED`. Pre-converted objects
|
||||
bypass the catch (the caller has already succeeded).
|
||||
- Strictness application is per route and bound to whichever slot is
|
||||
primary for the method — `request` for non-GET, `response` for GET —
|
||||
so the failure mode mirrors what API clients actually rely on.
|
||||
- Per-plugin filtered views are cached on first request. The filter
|
||||
matches by the resolved tag (`displayName` or `name`), not by the raw
|
||||
plugin name, so two plugins sharing a `displayName` are merged into a
|
||||
single filtered view (and the host's manifest validator should reject
|
||||
such collisions upstream).
|
||||
- Memoisation: `getDocument()` returns the same object reference on
|
||||
every call; mutations to the returned document leak across callers
|
||||
by design — callers MUST `structuredClone` if they need to mutate.
|
||||
Reference in New Issue
Block a user