Initial Commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# `tenant-scoped`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module exposes `wrapTenantScoped(adminProxy, tenantId)` — a
|
||||
worker-side type-level facade that re-presents an admin-surface
|
||||
`AdminDatabaseLike` as a `TenantScopedDatabaseLike`. The facade
|
||||
matters because the two surface types are deliberately
|
||||
non-assignable in `@suaveplan/api-types`: a request handler declared
|
||||
to accept `TenantScopedDatabaseLike` MUST NOT accidentally receive
|
||||
an unscoped admin proxy, and vice versa.
|
||||
|
||||
**Tenant scoping is enforced on the main side.** The actual SQL
|
||||
rewrite / row-level security / connection routing all live in the
|
||||
host's adapter. This wrapper does NOT add policy; it asserts a
|
||||
shape so the type system can guarantee the slot in the request
|
||||
context has been narrowed to a tenant.
|
||||
|
||||
## Features
|
||||
|
||||
- Returns a fresh facade object on every call so the same admin
|
||||
proxy can back many concurrent request scopes.
|
||||
- Exposes `tenantId` on the returned object for audit logging.
|
||||
- Preserves the optional `prepare` / `transaction` capabilities only
|
||||
if the admin proxy advertised them; absent capabilities throw
|
||||
`DB_PROXY.UNSAFE_NOT_PERMITTED`.
|
||||
- `close` is intentionally omitted because
|
||||
`TenantScopedDatabaseLike` does not expose it — the admin proxy
|
||||
retains lifecycle ownership.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { wrapTenantScoped } from "@suaveplan/api-db-worker-proxy/tenant-scoped";
|
||||
import type { TenantId } from "@suaveplan/api-types/tenant";
|
||||
|
||||
const tenantId = "acme" as TenantId;
|
||||
const scoped = wrapTenantScoped(adminProxy, tenantId);
|
||||
const rows = await scoped.query("SELECT * FROM invoices");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Use the facade inside a request middleware that materialises
|
||||
`RequestContext.db`. The middleware constructs the wrapper from the
|
||||
already-resolved `tenantId` so route handlers only ever see the
|
||||
tenant-scoped type.
|
||||
|
||||
```ts
|
||||
function attachDb(adminProxy: AdminDatabaseLike) {
|
||||
return async (c: Context, next: Next): Promise<void> => {
|
||||
const tenantId = c.get("tenantId") as TenantId;
|
||||
const scoped = wrapTenantScoped(adminProxy, tenantId);
|
||||
c.set("db", scoped);
|
||||
await next();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `wrapTenantScoped(adminProxy: AdminDatabaseLike, tenantId: TenantId): TenantScopedDatabaseLike`.
|
||||
- Throws `DB_PROXY.UNSAFE_NOT_PERMITTED` from `prepare` / `transaction`
|
||||
when the underlying admin proxy doesn't expose that capability.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The wrapper rebinds `prepare` and `transaction` so the underlying
|
||||
admin proxy's `this`-context survives the indirection (the methods
|
||||
are looked up off the proxy each call, not captured eagerly, so
|
||||
admin-side disposal is observed on the next call rather than at wrap
|
||||
time).
|
||||
|
||||
The `tenantId` field is set on the returned object purely so audit
|
||||
loggers and `RequestContext` consumers can read it directly. The
|
||||
field name and type match `TenantScopedDatabaseLike.tenantId`
|
||||
exactly; both are `TenantId` (a branded string).
|
||||
Reference in New Issue
Block a user