69 lines
3.7 KiB
Markdown
69 lines
3.7 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
|
|
|
# Capability Spec Delta: api (extend-api-foundation-host-services)
|
|
|
|
**Package**: `@suaveplan/api`
|
|
**Status**: PROPOSED
|
|
|
|
## ADDED Requirements
|
|
|
|
### Requirement: SuaveApp seeds canonical host services before plugin init
|
|
|
|
`SuaveApp.initialize()` SHALL call `this._registry.seedServices(...)` immediately before `this._registry.initialize()`. The seeded map SHALL include `app` (the OpenAPIHono instance), `openAPIRegistry`, `logger`, `eventBus`, `clock` always; AND optionally `migrationRunner` (when supplied via `SuaveAppOptions.migrationRunner`) AND `db` (when supplied via the new `SuaveAppOptions.db`). After init, every plugin's `ctx.getService("app")` / `ctx.getService("openAPIRegistry")` / etc. SHALL resolve to the corresponding instance with typed inference via the ServiceRegistry module augmentation.
|
|
|
|
#### Scenario: plugin resolves seeded app via ctx.getService
|
|
|
|
- **WHEN** a SuaveApp is built with a plugin whose `onInit(ctx) { recordApp(ctx.getService("app")) }` runs, and `app.initialize()` is awaited
|
|
- **THEN** `recordApp` was called with the OpenAPIHono instance the app holds at `this._hono` (referential equality, not a wrapper)
|
|
|
|
#### Scenario: missing migrationRunner option leaves the slot undefined
|
|
|
|
- **WHEN** `new SuaveApp({ ...options /* no migrationRunner */ })` is built and a plugin asks `ctx.getService("migrationRunner")`
|
|
- **THEN** the result is `undefined`
|
|
|
|
**Verified by:** `src/app/seed-services.test.ts::"plugin resolves seeded app via ctx.getService"` AND `src/app/seed-services.test.ts::"openAPIRegistry / logger / eventBus / clock are seeded"` AND `src/app/seed-services.test.ts::"missing migrationRunner leaves slot undefined"`
|
|
|
|
---
|
|
|
|
### Requirement: ServiceRegistry module augmentation declares canonical host slots
|
|
|
|
The package SHALL ship a side-effect-importable module at `./services/registry-augmentation` that declares:
|
|
|
|
```ts
|
|
declare module "@suaveplan/api-plugin/service-registry" {
|
|
interface ServiceRegistry {
|
|
app: OpenAPIHono;
|
|
openAPIRegistry: OpenAPIRegistry;
|
|
migrationRunner?: MigrationRunner;
|
|
db?: SurrealAdapter;
|
|
logger: LoggerLike;
|
|
eventBus: EventBusLike;
|
|
clock: Clock;
|
|
}
|
|
}
|
|
```
|
|
|
|
Consumers SHALL get full type inference on `ctx.getService("app")` (returns `OpenAPIHono | undefined`), etc. The augmentation SHALL be auto-loaded by the package root barrel (`src/index.ts` side-effect imports it) so any consumer of `@suaveplan/api` picks it up without an explicit import. Subpath consumers of `@suaveplan/api/services/registry-augmentation` get it directly.
|
|
|
|
#### Scenario: ctx.getService("app") type-narrows to OpenAPIHono | undefined
|
|
|
|
- **WHEN** `expectTypeOf(ctx.getService("app")).toEqualTypeOf<OpenAPIHono | undefined>()` is asserted in the conformance test
|
|
- **THEN** the type check passes
|
|
|
|
**Verified by:** `src/services/registry-augmentation.spec-conformance.test.ts::"every host slot type-narrows correctly via expectTypeOf"` AND `src/services/registry-augmentation.test.ts::"side-effect import auto-loads via root barrel"`
|
|
|
|
---
|
|
|
|
### Requirement: SuaveAppOptions.db is optional
|
|
|
|
`SuaveAppOptions` SHALL accept an optional `db?: SurrealAdapter` field. When supplied, it's seeded into `ServiceRegistry["db"]`; when omitted, the slot stays `undefined`. The constructor SHALL store it on `this._db` for later seeding. No change to existing options.
|
|
|
|
#### Scenario: db option seeded when provided
|
|
|
|
- **WHEN** `new SuaveApp({ ...config, db: surrealAdapter })` is built and a plugin queries `ctx.getService("db")`
|
|
- **THEN** the result is `surrealAdapter` (referential)
|
|
|
|
**Verified by:** `src/app/options.test.ts::"db option seeded when provided"` AND `src/app/options.test.ts::"omitting db leaves the slot undefined"`
|
|
|
|
<!-- markdownlint-enable MD024 -->
|