Initial Commit
This commit is contained in:
121
packages/api-stack/api/api-tenant/src/middleware/middleware.md
Normal file
121
packages/api-stack/api/api-tenant/src/middleware/middleware.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# middleware
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `middleware` module ships `createTenantMiddleware(options)` — the
|
||||
Hono middleware that resolves the inbound tenant, applies the status
|
||||
gate, and writes the result under the single canonical
|
||||
`c.set("tenant", tenant)` key. suaveplan deliberately collapses
|
||||
upstream's three keys (`tenantId`, `tenant`, `tenantContext`) into
|
||||
one; the helper accessors live in `context.ts` and read from the
|
||||
same key. The module also augments Hono's `ContextVariableMap` so
|
||||
`c.get("tenant")` returns a typed `Tenant` with no cast.
|
||||
|
||||
Status gating raises typed `TenantError` values: `deleted →
|
||||
TENANT_NOT_FOUND`, `suspended → TENANT_SUSPENDED` (unless
|
||||
`allowSuspended`), `pending → TENANT_PENDING`. A throw from the
|
||||
underlying resolver is wrapped as `TENANT_RESOLUTION_FAILED` with the
|
||||
original error preserved on `.cause`. An optional `onError(error, c)`
|
||||
hook lets callers map errors to bespoke responses without losing the
|
||||
typed contract.
|
||||
|
||||
## Features
|
||||
|
||||
- Single canonical context key — `c.set("tenant", tenant)` — typed
|
||||
via the `ContextVariableMap` augmentation.
|
||||
- Status gate covers all four statuses; `allowSuspended` opt-in lets
|
||||
read-only paths serve suspended tenants.
|
||||
- Excluded paths (default `/health, /ready, /docs, /openapi`) bypass
|
||||
the resolver entirely so liveness probes never block on database
|
||||
health.
|
||||
- `onResolved(tenant, c)` callback fires after the gate passes — use
|
||||
it to attach OTel attributes or per-tenant logger context.
|
||||
- `onError(error, c)` maps `TenantError` to a `Response`; non-tenant
|
||||
errors re-throw so the framework's global handler runs.
|
||||
- `getTenant(c)` and `getTenantId(c)` typed helpers — read the
|
||||
resolved tenant without `as Tenant` casts.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
chainResolvers,
|
||||
createTenantMiddleware,
|
||||
headerResolver,
|
||||
} from "@suaveplan/api-tenant";
|
||||
import { Hono } from "hono";
|
||||
|
||||
const app = new Hono();
|
||||
app.use(
|
||||
"*",
|
||||
createTenantMiddleware({
|
||||
resolver: headerResolver("x-tenant-id", (id) =>
|
||||
db.tenants.get(id)
|
||||
),
|
||||
})
|
||||
);
|
||||
app.get("/me", (c) => c.json({ tenant: c.get("tenant") }));
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Composing a cached chained resolver with an error mapper:
|
||||
|
||||
```ts
|
||||
import {
|
||||
chainResolvers,
|
||||
createTenantMiddleware,
|
||||
headerResolver,
|
||||
subdomainResolver,
|
||||
TenantCache,
|
||||
TenantError,
|
||||
} from "@suaveplan/api-tenant";
|
||||
|
||||
const resolver = TenantCache.wrap(
|
||||
chainResolvers(
|
||||
headerResolver("x-tenant-id", lookupById),
|
||||
subdomainResolver("suaveplan.io", lookupBySlug)
|
||||
),
|
||||
{ ttlMs: 30_000 }
|
||||
);
|
||||
|
||||
app.use(
|
||||
"*",
|
||||
createTenantMiddleware({
|
||||
resolver,
|
||||
allowSuspended: false,
|
||||
excludePaths: ["/health", "/metrics"],
|
||||
onError: (error: TenantError, c) =>
|
||||
c.json({ code: error.code, message: error.message }, 404),
|
||||
onResolved: (tenant, c) => {
|
||||
c.header("x-tenant-id", tenant.id);
|
||||
},
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createTenantMiddleware(options): MiddlewareHandler`.
|
||||
- `interface TenantMiddlewareOptions` — `resolver`, `required?`,
|
||||
`excludePaths?`, `allowSuspended?`, `onError?`, `onResolved?`.
|
||||
- `getTenant(c): Tenant | undefined`.
|
||||
- `getTenantId(c): string | undefined`.
|
||||
- Module augmentation — `declare module "hono" { interface
|
||||
ContextVariableMap { tenant: Tenant } }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Status checks raise BEFORE `c.set` so a suspended tenant never lands
|
||||
under `c.get("tenant")` — downstream handlers cannot accidentally
|
||||
serve suspended data. Resolver throws are caught and re-wrapped as
|
||||
`TENANT_RESOLUTION_FAILED` instead of bubbling up as a raw `Error`
|
||||
because the framework's global error handler benefits from the
|
||||
discriminated `code` field. The `tenant: Tenant` ambient declaration
|
||||
is reachable from anywhere that imports any value from
|
||||
`@suaveplan/api-tenant`, which is enforced by the side-effect import
|
||||
in the package root barrel.
|
||||
Reference in New Issue
Block a user