122 lines
3.9 KiB
Markdown
122 lines
3.9 KiB
Markdown
# 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.
|