Initial Commit
This commit is contained in:
71
openspec/archive/2026-05-18-enterprise-api/proposal.md
Normal file
71
openspec/archive/2026-05-18-enterprise-api/proposal.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Add @suaveplan/api — Hono orchestrator + middleware framework (5 of 5)
|
||||
|
||||
**Package**: `@suaveplan/api` (NEW, server-tier)
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package (split from upstream `@suaveplan/api` — this is the thin orchestrator slice)
|
||||
|
||||
- **Spec delta**: ./specs/api/spec.md
|
||||
- **Companions**: `add-api-plugin`, `add-api-tenant`, `add-api-openapi`, `add-api-migrations`
|
||||
- **Position in chain**: depends on all 4 companions (composes them into the runtime)
|
||||
|
||||
## Why
|
||||
|
||||
This is the final piece of the `@suaveplan/api` split. Upstream's single 8.7k-LOC package combined plugin + tenant + openapi + migrations + middleware + config + app orchestration into one. The 4 sibling changes peel off the first four concerns. What remains is the **thin app orchestrator** (`SuaveApp`/`AppBuilder`, ~739 LOC upstream), the **Hono middleware framework** (909 LOC upstream — request-id / cors / logger / error-handler + chain manager), the **app config layer** (595 LOC upstream — Zod-typed multi-source loader), and the **events bus integration** (the `@suaveplan/events`-typed event surface the plugin lifecycle emits to).
|
||||
|
||||
The genesis enterprise audit additionally fixes the upstream bugs found while reading: `AppBuilder.tenant(resolver)` plumbed but silently dropped (real bug); 3 AsyncLocalStorage instances (app + tenant + implicit globals); 3 global singletons (`globalApp`/`globalEventBus`/`globalSchemaRegistry`); error-handler statusCode closed-enum cast; hardcoded health-route paths; CORS exposing rate-limit headers without a rate-limiter wired; `insertBefore`/`insertAfter` silently fall back to push.
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package at `0.1.0`. ADDED:
|
||||
|
||||
- `SuaveApp` class — thin orchestrator that **composes** rather than imports siblings directly. Constructor accepts pre-built `PluginRegistry`, `OpenAPIRegistry`, `MiddlewareChain`, `TenantResolver?`, `MigrationRunner?` (all optional except registry). Removes upstream's deep coupling.
|
||||
- `AppState` literal union (`created`/`initializing`/`ready`/`running`/`stopping`/`stopped`) — the FSM. `state` getter exposes it; transitions are private.
|
||||
- `AppBuilder` fluent API — chainable `.name/.version/.env/.debug/.port/.host/.basePath/.database/.cors/.openapi/.use/.useAll/.middleware/.tenant/.skipConfigFile/.config/.migrations` → `.build()` / `.initialize()` / `.start()`. **Genesis fix:** `.tenant(resolver)` is actually wired into the middleware chain (upstream silently drops it).
|
||||
- `quickstart({name, port?, plugins?, debug?})` — one-call helper for trivial apps.
|
||||
- `AppConfigSchema` (Zod) — server / database / cors / logging / openapi / plugins / migrations sub-configs. Multi-source loader: file (json/ts/js — yaml/toml dropped from upstream's mis-advertised union) → env-specific file → env vars (`SUAVEPLAN_SERVER_PORT` → `server.port`).
|
||||
- `ConfigLoader` class + `loadConfig()` convenience + `defineConfig()` type helper + `defineEnvConfig()` for environment-keyed overrides.
|
||||
- `validateConfig(input): AppConfig` and `tryValidateConfig(input): ValidationResult<AppConfig>` (both Zod-backed).
|
||||
- `MiddlewareChain` class — name-addressable Hono middleware list with `MiddlewareOrder` slot constants (`FIRST=-1000` → `LAST=1000`, with `SECURITY=-500`, `CORS=-400`, `REQUEST_ID=-300`, `LOGGING=-200`, `TIMING=-100`, `AUTH=0`, `TENANT=100`, `VALIDATION=200`, `BUSINESS=500`, `RESPONSE=800`, `ERROR=900`). **Genesis fix:** `insertBefore(name, m)` and `insertAfter(name, m)` SHALL raise `ApiError(API_MIDDLEWARE_INSERT_TARGET_NOT_FOUND)` when the named middleware doesn't exist (upstream silently falls back to push).
|
||||
- 5 shipped middleware factories: `createRequestId`, `createCors`, `createLogger`, `createErrorHandler`, `createNotFoundHandler`. Plus helpers `combineMiddleware`, `conditionalMiddleware`, `pathMiddleware`, `excludePathMiddleware`.
|
||||
- **Error handler widens status code**: `createErrorHandler` SHALL pass the SuaveplanError's full `statusCode` through to Hono's response (no closed-enum cast — upstream caps at 400/401/403/404/500).
|
||||
- **CORS conditional headers**: `createCors` SHALL only expose `X-RateLimit-*` headers when the caller passes `withRateLimitHeaders: true`. Default omits them (upstream exposes unconditionally).
|
||||
- **Configurable health routes**: `SuaveApp.setupHealthRoutes(options?: { healthPath?, readyPath?, livePath?, enabled? })`. Defaults match upstream but can be disabled or renamed.
|
||||
- Event-bus integration: SuaveApp emits `app:starting`, `app:started`, `app:stopping`, `app:stopped`, `app:error` to the registry's typed `EventBus` (from `@suaveplan/events`).
|
||||
- **No global singletons.** Upstream `globalApp`/`getGlobalApp`/`setGlobalApp`/`clearGlobalApp` SHALL NOT exist on this package. Same for `globalEventBus` — every consumer constructs its own.
|
||||
- **AsyncLocalStorage consolidated.** ONE optional `RequestContext` ALS instance (`{ app: SuaveApp; requestId: string; tenant?: Tenant; user?: User }`) replaces upstream's 3 separate ALS instances. Opt-in via `app.config.useAsyncLocalStorage: true`. Default is off; consumers thread `c` from Hono.
|
||||
- `ApiError extends SuaveplanError<ApiErrorCode>` with codes `API_INVALID_CONFIG`, `API_INVALID_STATE`, `API_MIDDLEWARE_INSERT_TARGET_NOT_FOUND`, `API_SHUTDOWN_TIMEOUT`, `API_SERVER_START_FAILED`.
|
||||
- Subpath exports per feature module.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **All 4 sibling packages.** Plugin system (`add-api-plugin`), tenant (`add-api-tenant`), OpenAPI (`add-api-openapi`), and migrations (`add-api-migrations`) are separate dependencies — not bundled.
|
||||
- **Database adapter** (the upstream `DatabaseAdapter`/`ConnectionManager`/`QueryBuilder`). Owned by `@suaveplan/db` + dialect-specific adapters. The orchestrator wires a caller-supplied adapter into the plugin context via `registerService("db", ...)`.
|
||||
- **Auth middleware.** The `AUTH=0` order slot is reserved for `@suaveplan/api-plugin-auth` and friends — not shipped here.
|
||||
- **Validation middleware.** Plugin-level Zod validation is the `@hono/zod-openapi` integration via `api-openapi`; cross-cutting input validation belongs in middleware contributed by domain plugins.
|
||||
- **Rate limit middleware.** Tenant rate limit lives in `api-tenant`; global rate limit will be a separate `api-plugin-rate-limit` package.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Capability | Upstream source | Notes |
|
||||
|---|---|---|
|
||||
| `SuaveApp` class + FSM + `start`/`stop`/`initialize` | `../suaveplan/packages/server/api/src/app/app.ts` L66-L367 | port shape; drop globalApp; consolidate ALS; fix tenant-resolver wiring; widen health-route configurability |
|
||||
| `AppBuilder` fluent API | `../suaveplan/packages/server/api/src/app/app-builder.ts` L14-L217 | port + actually wire `.tenant(resolver)` into the middleware chain |
|
||||
| `quickstart` helper | `../suaveplan/packages/server/api/src/app/app-builder.ts` L229-L260 | verbatim port |
|
||||
| `AppContext` ALS | `../suaveplan/packages/server/api/src/app/app-context.ts` L1-L106 | consolidate into single optional `RequestContext` ALS; drop globalApp |
|
||||
| `AppConfigSchema` + sub-configs | `../suaveplan/packages/server/api/src/config/config.interface.ts` L1-L163 | port; add `migrations` sub-config; drop yaml/toml from `ConfigFileFormat` union |
|
||||
| `ConfigLoader` + multi-source merge + env-var parsing | `../suaveplan/packages/server/api/src/config/config-loader.ts` L1-L236 | port; replace upstream `deepMerge` with `@suaveplan/utils`'s |
|
||||
| `validateConfig` + `tryValidateConfig` + plugin config validator + typed getter | `../suaveplan/packages/server/api/src/config/config-validator.ts` L1-L191 | port shape; switch error from upstream `ConfigValidationError` to `defineCodes`-typed `ApiError(API_INVALID_CONFIG)` |
|
||||
| `MiddlewareChain` + `MiddlewareOrder` constants | `../suaveplan/packages/server/api/src/middleware/middleware-chain.ts` L1-L178 | port + fix `insertBefore`/`insertAfter` silent push fallback to raise typed error |
|
||||
| 5 middleware factories | `../suaveplan/packages/server/api/src/middleware/{cors,logger,error-handler,request-id}.ts` | port; widen error-handler statusCode; conditional `X-RateLimit-*` in CORS; ID generator alternatives kept |
|
||||
| Middleware composition helpers | `../suaveplan/packages/server/api/src/middleware/middleware.interface.ts` L75-L148 | verbatim port |
|
||||
| Event-bus integration | `../suaveplan/packages/server/api/src/events/event-bus.ts` L1-L385 | NOT migrated whole — only the `EventBus` integration surface lives here. The genesis package consumes `@suaveplan/events`'s typed bus directly; the `Hooks` transform-pipeline alter-ego is dropped (use the plugin `HookExecutor` for behaviour contribution). |
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
None today — depends on all 4 sibling packages. Will be consumed by every `@suaveplan/api-plugin-*` package (40+ in `openspec/changes/`) plus application code.
|
||||
|
||||
### Risk
|
||||
|
||||
- **Medium.** The thinnest of the 5 packages by responsibility but the most architectural impact — its tests cover every other layer's integration with Hono. The integration test suite is where most of the genesis bug fixes are pinned.
|
||||
207
openspec/archive/2026-05-18-enterprise-api/specs/api/spec.md
Normal file
207
openspec/archive/2026-05-18-enterprise-api/specs/api/spec.md
Normal file
@@ -0,0 +1,207 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api (enterprise-api)
|
||||
|
||||
**Package**: `@suaveplan/api`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: SuaveApp composes pre-built dependencies, not creates them
|
||||
|
||||
`SuaveApp` constructor SHALL accept `{ pluginRegistry: PluginRegistry; openAPIRegistry?: OpenAPIRegistry; middlewareChain?: MiddlewareChain; tenantResolver?: TenantResolver; migrationRunner?: MigrationRunner; config: AppConfig; logger?: LoggerLike }`. It SHALL NOT internally construct any of those — callers (or `AppBuilder`) wire them. Dependency inversion eliminates upstream's deep import coupling.
|
||||
|
||||
#### Scenario: SuaveApp does not instantiate PluginRegistry
|
||||
|
||||
- **WHEN** `new SuaveApp({ pluginRegistry: stubRegistry, config })` is constructed
|
||||
- **THEN** `app.registry === stubRegistry` (no internal reconstruction)
|
||||
|
||||
**Verified by:** `src/app/app.test.ts::"composes injected pluginRegistry without reconstruction"` AND `src/app/app.test.ts::"missing pluginRegistry raises API_INVALID_CONFIG"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AppBuilder.tenant(resolver) actually wires into middleware
|
||||
|
||||
When `AppBuilder.tenant(resolver).build()` is called, the produced `SuaveApp` SHALL register `createTenantMiddleware({ resolver, ... })` from `@suaveplan/api-tenant` into the middleware chain at `MiddlewareOrder.TENANT`. This is a deliberate fix for the upstream bug where the resolver is plumbed but silently dropped.
|
||||
|
||||
#### Scenario: tenant resolver reaches the middleware chain
|
||||
|
||||
- **WHEN** `defineApp().tenant(stubResolver).build()` is invoked and the middleware chain is inspected
|
||||
- **THEN** an entry with `meta.name === "tenant"` exists and its resolver === stubResolver
|
||||
|
||||
**Verified by:** `src/builder/builder.test.ts::"tenant(resolver) wires createTenantMiddleware"` AND `src/builder/builder.test.ts::"no tenant() call leaves chain without tenant middleware"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: AppState FSM transitions are monotonic
|
||||
|
||||
`app.state` SHALL move through `created → initializing → ready → running → stopping → stopped`. Calling `initialize()` from any state other than `created` SHALL raise `ApiError(API_INVALID_STATE)`. `start()` from `created` SHALL auto-initialize first. `stop()` from non-`running` SHALL be a no-op (idempotent).
|
||||
|
||||
#### Scenario: double-initialize raises API_INVALID_STATE
|
||||
|
||||
- **WHEN** `app.initialize()` is awaited, then `app.initialize()` is awaited again
|
||||
- **THEN** the second call rejects with `ApiError(API_INVALID_STATE)`
|
||||
|
||||
#### Scenario: stop is idempotent from stopped
|
||||
|
||||
- **WHEN** `app.stop()` is called twice in succession from `running`
|
||||
- **THEN** the second call resolves without error and leaves state at `stopped`
|
||||
|
||||
**Verified by:** `src/app/app.test.ts::"double initialize raises API_INVALID_STATE"` AND `src/app/app.test.ts::"stop is idempotent from stopped"` AND `src/app/app.test.ts::"start from created auto-initializes"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Config loader resolves file → env-file → env-vars
|
||||
|
||||
`loadConfig(options)` SHALL merge in the order: defaults → file (`.ts` → `.js` → `.json` extensions tried in that order) → environment-specific file (`{name}.{env}.json`) → environment variables (`SUAVEPLAN_<KEY_PATH>` parsed into nested object via `_` → `.`). Later sources override earlier. Yaml and toml file formats SHALL NOT be supported (upstream advertised them but threw at runtime — genesis drops the union entries).
|
||||
|
||||
#### Scenario: env var overrides file
|
||||
|
||||
- **WHEN** `suaveplan.config.json` contains `server.port: 3000` and `SUAVEPLAN_SERVER_PORT=4000` is set, then `loadConfig()` is awaited
|
||||
- **THEN** the result has `server.port: 4000`
|
||||
|
||||
#### Scenario: yaml extension not tried
|
||||
|
||||
- **WHEN** `loadConfig({ configName: "suaveplan.config" })` is called and only `suaveplan.config.yaml` exists
|
||||
- **THEN** the result is the defaults-only config (yaml is not loaded)
|
||||
|
||||
**Verified by:** `src/config/loader.test.ts::"env var overrides file"` AND `src/config/loader.test.ts::"env-specific file overrides base file"` AND `src/config/loader.test.ts::"yaml extension is not attempted"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: validateConfig raises typed API_INVALID_CONFIG
|
||||
|
||||
`validateConfig(input)` SHALL parse against `AppConfigSchema` and throw `ApiError(API_INVALID_CONFIG)` with the ZodError as `cause` on failure. `tryValidateConfig(input)` SHALL return `{success: false, errors: ConfigValidationErrorDetail[]}` on failure (no throw). `validatePluginConfig<T>(schema, input, pluginName)` SHALL throw `ApiError(API_INVALID_CONFIG)` with `context.pluginName` on failure.
|
||||
|
||||
#### Scenario: validateConfig with missing required field throws
|
||||
|
||||
- **WHEN** `validateConfig({})` is called (missing required `name` + `version` + `openapi`)
|
||||
- **THEN** an `ApiError(API_INVALID_CONFIG)` is thrown with `cause instanceof ZodError`
|
||||
|
||||
**Verified by:** `src/config/validator.test.ts::"validateConfig throws on missing fields"` AND `src/config/validator.test.ts::"tryValidateConfig returns errors on missing fields"` AND `src/config/validator.test.ts::"validatePluginConfig carries pluginName context"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: MiddlewareChain insertBefore/insertAfter raise on missing target
|
||||
|
||||
`chain.insertBefore(name, m)` and `chain.insertAfter(name, m)` SHALL raise `ApiError(API_MIDDLEWARE_INSERT_TARGET_NOT_FOUND)` when no middleware with the named target exists. Upstream silently falls back to `push` — genesis fails closed so callers learn about the mismatch immediately.
|
||||
|
||||
#### Scenario: insertBefore on missing name raises
|
||||
|
||||
- **WHEN** `chain.insertBefore("auth", myMiddleware)` is called and no "auth" middleware exists
|
||||
- **THEN** `ApiError(API_MIDDLEWARE_INSERT_TARGET_NOT_FOUND)` is thrown
|
||||
|
||||
#### Scenario: insertBefore on existing name splices
|
||||
|
||||
- **WHEN** "cors" exists at index 1 and `chain.insertBefore("cors", myMiddleware)` is called
|
||||
- **THEN** `myMiddleware` is at index 1, "cors" is at index 2
|
||||
|
||||
**Verified by:** `src/middleware/chain.test.ts::"insertBefore raises on missing target"` AND `src/middleware/chain.test.ts::"insertBefore splices on existing target"` AND `src/middleware/chain.test.ts::"insertAfter raises on missing target"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error handler passes full SuaveplanError statusCode through
|
||||
|
||||
`createErrorHandler()` SHALL respond with `error.statusCode` as the HTTP status — full range, not a closed enum. SuaveplanError subclasses with statusCode 409, 422, 503, 504, etc. SHALL respond with those codes. ZodError → 400 with `ValidationError.fromZodError(err).toJSON()`. HTTPException → its status. Unknown → 500 with `INTERNAL_ERROR`.
|
||||
|
||||
#### Scenario: 422 SuaveplanError responds 422
|
||||
|
||||
- **WHEN** a route throws a `SuaveplanError` with `statusCode: 422` and the error handler runs
|
||||
- **THEN** the response status is 422
|
||||
|
||||
#### Scenario: ZodError responds 400
|
||||
|
||||
- **WHEN** a route throws a `ZodError` and the error handler runs
|
||||
- **THEN** the response status is 400 with `error.code === "VALIDATION_ERROR"` in the body
|
||||
|
||||
**Verified by:** `src/middleware/error-handler.test.ts::"422 SuaveplanError responds 422"` AND `src/middleware/error-handler.test.ts::"ZodError responds 400 VALIDATION_ERROR"` AND `src/middleware/error-handler.test.ts::"unknown error responds 500 INTERNAL_ERROR"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CORS conditionally exposes X-RateLimit-* headers
|
||||
|
||||
`createCors(options)` SHALL only include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` in the default `exposeHeaders` when `options.withRateLimitHeaders === true`. Default omits them.
|
||||
|
||||
#### Scenario: default omits rate-limit headers
|
||||
|
||||
- **WHEN** `createCors({})` is built and its exposeHeaders is inspected
|
||||
- **THEN** none of `X-RateLimit-Limit/Remaining/Reset` is present
|
||||
|
||||
#### Scenario: withRateLimitHeaders adds them
|
||||
|
||||
- **WHEN** `createCors({ withRateLimitHeaders: true })` is built
|
||||
- **THEN** all three rate-limit headers are present
|
||||
|
||||
**Verified by:** `src/middleware/cors.test.ts::"default omits rate-limit headers"` AND `src/middleware/cors.test.ts::"withRateLimitHeaders adds them"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Health routes are configurable
|
||||
|
||||
`SuaveApp` SHALL accept `healthRoutes?: { healthPath?: string; readyPath?: string; livePath?: string; enabled?: boolean }`. Default matches upstream (`/health` + `/ready` enabled, no `/live`). `enabled: false` SHALL skip registration entirely.
|
||||
|
||||
#### Scenario: custom health path
|
||||
|
||||
- **WHEN** `new SuaveApp({ ..., healthRoutes: { healthPath: "/healthz" } })` is built and `/healthz` is GET'd
|
||||
- **THEN** the response is the standard health payload
|
||||
|
||||
#### Scenario: disabled skips registration
|
||||
|
||||
- **WHEN** `new SuaveApp({ ..., healthRoutes: { enabled: false } })` is built and `/health` is GET'd
|
||||
- **THEN** the response is 404 (route not registered)
|
||||
|
||||
**Verified by:** `src/app/health-routes.test.ts::"custom healthPath responds"` AND `src/app/health-routes.test.ts::"enabled false skips registration"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No global singletons exported
|
||||
|
||||
The package SHALL NOT export `globalApp`, `getGlobalApp`, `setGlobalApp`, `clearGlobalApp`, `globalEventBus`, or any other module-level mutable state. Each `SuaveApp` instance is independent; multi-app deployments compose freely.
|
||||
|
||||
#### Scenario: no global accessor in barrel
|
||||
|
||||
- **WHEN** the package barrel is inspected for `setGlobalApp` and `getGlobalApp`
|
||||
- **THEN** neither symbol is exported
|
||||
|
||||
**Verified by:** `src/app/app.spec-conformance.test.ts::"no global singleton exports"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Optional single RequestContext AsyncLocalStorage
|
||||
|
||||
When `app.config.useAsyncLocalStorage === true`, the orchestrator SHALL wrap every request handler in `requestStorage.run({ app, requestId, tenant?, user? }, next)`. `getCurrentRequestContext()` SHALL return that bag from any depth. Default is `false` — no ALS scope created. Replaces upstream's 3 separate ALS instances with 1.
|
||||
|
||||
#### Scenario: ALS off → getCurrentRequestContext undefined
|
||||
|
||||
- **WHEN** `useAsyncLocalStorage: false` is set and a request runs
|
||||
- **THEN** `getCurrentRequestContext()` called from inside the handler returns `undefined`
|
||||
|
||||
#### Scenario: ALS on → context threads through async stack
|
||||
|
||||
- **WHEN** `useAsyncLocalStorage: true` and a request runs
|
||||
- **THEN** `getCurrentRequestContext()` returns `{ app, requestId, tenant?, user? }` matching what was set up by the middleware
|
||||
|
||||
**Verified by:** `src/app/request-context.test.ts::"ALS off keeps getCurrentRequestContext undefined"` AND `src/app/request-context.test.ts::"ALS on threads context through async stack"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Every API_* code is thrown from a production path
|
||||
|
||||
`API_CODES` SHALL define exactly 5 codes (`API_INVALID_CONFIG`, `API_INVALID_STATE`, `API_MIDDLEWARE_INSERT_TARGET_NOT_FOUND`, `API_SHUTDOWN_TIMEOUT`, `API_SERVER_START_FAILED`). Every code SHALL be raised by at least one production call site.
|
||||
|
||||
**Verified by:** `src/api.spec-conformance.test.ts::"every API_* code is thrown from a production path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Server-tier package shape with integration tests against all 4 siblings
|
||||
|
||||
The package SHALL run in Bun + Node. `package.json` SHALL declare subpath exports per feature module. Every implementation `.ts` SHALL have a sibling `.md` ≥200 words. No `export *` in feature barrels. 100/100/100/100 coverage. Integration tests SHALL exercise: (a) `SuaveApp` boot with a real `PluginRegistry` containing 3 plugins with deps, (b) tenant middleware wiring through `AppBuilder.tenant`, (c) OpenAPI registry doc emission, (d) migration runner invocation before plugin init.
|
||||
|
||||
#### Scenario: full boot integration test
|
||||
|
||||
- **WHEN** `tests/integration/full-boot.test.ts` builds an app with all 4 siblings wired (real registry / real openapi / real tenant / real migrations against an in-memory sqlite) and `.start()` is awaited
|
||||
- **THEN** the app reaches state `running` with all plugins ready and the spec doc is served
|
||||
|
||||
**Verified by:** `tests/integration/full-boot.test.ts::"boots with all 4 siblings wired"` AND `tests/dep-graph.test.ts::"dependencies match the allowlist"` AND `tests/dep-graph.test.ts::"subpath exports cover every feature"` AND `tests/dep-graph.test.ts::"every impl .ts has a sibling .md"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
60
openspec/archive/2026-05-18-enterprise-api/tasks.md
Normal file
60
openspec/archive/2026-05-18-enterprise-api/tasks.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Tasks — enterprise-api
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] 1.1 Create `packages/api/api/` with `src/{app,builder,config,middleware,error-codes,types,event-integration}/` + `tests/setup/` + `tests/integration/`
|
||||
- [ ] 1.2 `package.json` v0.1.0. Deps: `@suaveplan/error` `@suaveplan/api-plugin` `@suaveplan/api-tenant` `@suaveplan/api-openapi` `@suaveplan/api-migrations` `@suaveplan/events` `@suaveplan/types` `@suaveplan/utils` `tslib`, `hono` `@hono/zod-openapi` `@hono/swagger-ui` (`catalog:server`), `zod` (`catalog:forms`), `@opentelemetry/api`. Dev: standard testing + `@suaveplan/testing-integration-sqlite` for the full-boot test.
|
||||
- [ ] 1.3 ~10 subpath exports (one per feature module + root)
|
||||
- [ ] 1.4 tsconfigs + vite + vitest (100/100/100/100, testTimeout 60000 for integration)
|
||||
- [ ] 1.5 `tests/setup/polyfills.ts`
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [ ] 2.1 `src/error-codes.ts` + `.md` + test — 5 API.* codes + `ApiError`
|
||||
- [ ] 2.2 `src/types/api.{ts,md,index.ts}` — `AppState`, `SuaveAppOptions`, `HealthRoutesOptions`, `RequestContext`
|
||||
- [ ] 2.3 `src/config/config.{ts,test.ts,md,index.ts}` — `AppConfigSchema` + sub-configs (server/database/cors/logging/openapi/plugins/migrations)
|
||||
- [ ] 2.4 `src/config/loader.{ts,test.ts,md}` — `ConfigLoader`, multi-source merge, env-var parsing
|
||||
- [ ] 2.5 `src/config/validator.{ts,test.ts,md}` — `validateConfig`, `tryValidateConfig`, `validatePluginConfig`, typed getter
|
||||
- [ ] 2.6 `src/middleware/chain.{ts,test.ts,md,index.ts}` — `MiddlewareChain` + `MiddlewareOrder` constants + insert raises on missing target
|
||||
- [ ] 2.7 `src/middleware/cors.{ts,test.ts,md}` — `createCors` with conditional rate-limit headers
|
||||
- [ ] 2.8 `src/middleware/logger.{ts,test.ts,md}` — `createLogger` + formatters (default/json/detailed)
|
||||
- [ ] 2.9 `src/middleware/error-handler.{ts,test.ts,md}` — `createErrorHandler` + `createNotFoundHandler`; full statusCode passthrough
|
||||
- [ ] 2.10 `src/middleware/request-id.{ts,test.ts,md}` — `createRequestId` + 3 generator alternatives
|
||||
- [ ] 2.11 `src/middleware/composition.{ts,test.ts,md}` — `combineMiddleware`, `conditionalMiddleware`, `pathMiddleware`, `excludePathMiddleware`
|
||||
- [ ] 2.12 `src/event-integration/event-bridge.{ts,test.ts,md,index.ts}` — wires the `@suaveplan/events` typed `EventBus` into `SuaveApp.events`
|
||||
- [ ] 2.13 `src/app/app.{ts,test.ts,md,index.ts}` — `SuaveApp` class, FSM, `initialize/start/stop`, optional ALS via `RequestContext`, configurable health routes
|
||||
- [ ] 2.14 `src/app/health-routes.{ts,test.ts,md}` — configurable health route registration
|
||||
- [ ] 2.15 `src/app/request-context.{ts,test.ts,md}` — single optional `requestStorage` ALS + `getCurrentRequestContext`
|
||||
- [ ] 2.16 `src/builder/builder.{ts,test.ts,md,index.ts}` — `AppBuilder`, `defineApp()`, `quickstart()`; **`.tenant(resolver)` actually wires middleware**
|
||||
- [ ] 2.17 `src/index.ts` — named re-exports (no global singletons)
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [ ] 3.1 Co-located `*.test.ts` per spec scenario
|
||||
- [ ] 3.2 `src/api.spec-conformance.test.ts` — every API.* thrown; no global singleton exports; `expectTypeOf` on `SuaveAppOptions`
|
||||
- [ ] 3.3 `tests/integration/full-boot.test.ts` — real PluginRegistry + 3 plugins (with deps) + real OpenAPIRegistry + real TenantResolver + real MigrationRunner against `@sqlite.org/sqlite-wasm`. Assert: state transitions, doc emission, tenant middleware wiring, migration ran before init.
|
||||
- [ ] 3.4 `tests/integration/builder-roundtrip.test.ts` — `defineApp().name().tenant(resolver).use(plugin).start()` → assert tenant middleware in chain
|
||||
- [ ] 3.5 `tests/integration/config-precedence.test.ts` — write fixture file + set env vars → assert load order
|
||||
- [ ] 3.6 `tests/property.test.ts` — fast-check MiddlewareChain insertBefore/After invariant: insertion produces consistent ordering
|
||||
- [ ] 3.7 `tests/dep-graph.test.ts`
|
||||
- [ ] 3.8 Runner imports + timeouts
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [ ] 4.1 Sibling `.md` per impl (≥200 words)
|
||||
- [ ] 4.2 `README.md` — quickstart, AppBuilder fluent example, plugin wiring example, config file format, migration runner integration, ALS opt-in pattern
|
||||
|
||||
## 5. Gates (dependent sweep mandatory — this is the orchestrator)
|
||||
|
||||
- [ ] 5.1 `bunx turbo lint typecheck test --filter=@suaveplan/api --force`
|
||||
- [ ] 5.2 Coverage 100/100/100/100
|
||||
- [ ] 5.3 `bun scripts/openspec-conformance.ts @suaveplan/api` exit 0
|
||||
- [ ] 5.4 Build green
|
||||
- [ ] 5.5 `bunx turbo test --filter=...@suaveplan/api --force` — dependent sweep (the 40+ `api-plugin-*` packages, when they exist)
|
||||
- [ ] 5.6 Banned-pattern + no `export *` + no direct runner imports
|
||||
|
||||
## 6. Archive
|
||||
|
||||
- [ ] 6.1 Fold spec → `openspec/specs/api/spec.md`
|
||||
- [ ] 6.2 mv → archive
|
||||
- [ ] 6.3 MANIFEST + STATUS refresh — flip `@suaveplan/api` to ✅
|
||||
Reference in New Issue
Block a user