first commit
This commit is contained in:
70
docs/api-stack/api/api-tenant-provider-memory/README.md
Normal file
70
docs/api-stack/api/api-tenant-provider-memory/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# @suaveplan/api-tenant-provider-memory - In-memory reference implementation of @suaveplan/api-tenant-provider TenantProvider. Backed by an insertion-ordered Map<TenantId, TenantRecord>, ships stable base64url-JSON cursor pagination, active/suspended/archived filtering matching the abstract contract, out-of-band add/remove/clear admin helpers, idempotent dispose, Zod-validated TenantRecord seeds, and the TENANT_PROVIDER_MEMORY.* error registry (DUPLICATE_TENANT, TENANT_NOT_FOUND, INVALID_RECORD). Passes runTenantProviderConformance verbatim. Used by @suaveplan/api test hosts and small static-tenant deployments.
|
||||
|
||||
In-memory reference implementation of the
|
||||
`@suaveplan/api-tenant-provider` `TenantProvider` contract. Backed by
|
||||
an insertion-ordered `Map<TenantId, TenantRecord>`. Production-grade
|
||||
for small static-tenant deployments AND reference-grade for the
|
||||
downstream DB-backed adapters (Stage 8) that ship the same contract.
|
||||
|
||||
The API testing kit (`createTestHost`) uses this provider by
|
||||
default so unit tests never need a database.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [error-codes](./src/error-codes/error-codes.md) — The in-memory `TenantProvider` adapter has its own narrow error namespace, distinct from the abstract `TENANT_PROVIDER.…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — The in-memory `TenantProvider` adapter has its own narrow error namespace, distinct from the abstract `TENANT_PROVIDER.…
|
||||
- [provider](./src/provider/provider.md) — In-memory reference implementation of the `@suaveplan/api-tenant-provider` `TenantProvider` contract. It is both the de…
|
||||
- [provider](./src/provider/provider.md) — In-memory reference implementation of the `@suaveplan/api-tenant-provider` `TenantProvider` contract. It is both the de…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## What ships
|
||||
|
||||
| Subpath | Module | Doc |
|
||||
|---|---|---|
|
||||
| `./provider` | `createMemoryTenantProvider({ tenants? })` + admin helpers | [`src/provider/provider.md`](src/provider/provider.md) |
|
||||
| `./error-codes` | `TENANT_PROVIDER_MEMORY_CODES` + `TenantProviderMemoryError` | [`src/error-codes/error-codes.md`](src/error-codes/error-codes.md) |
|
||||
|
||||
## Surface
|
||||
|
||||
- `createMemoryTenantProvider(options?)` — factory returning a
|
||||
`MemoryTenantProvider` (the abstract `TenantProvider` interface plus
|
||||
out-of-band `add`/`remove`/`clear` admin helpers).
|
||||
- Active-only filtering by default; `list({ includeSuspended: true })`
|
||||
surfaces suspended tenants; `archived` is reachable only through
|
||||
`lookup`.
|
||||
- Cursor pagination via the canonical
|
||||
`@suaveplan/api-tenant-provider/pagination` codec — cursors produced
|
||||
by this provider round-trip with the abstract decoder.
|
||||
|
||||
## Conformance
|
||||
|
||||
The package's own test file calls
|
||||
`runTenantProviderConformance({ label: "memory", factory: ... })` so
|
||||
every invariant of the abstract contract is verified against this
|
||||
adapter on every CI run.
|
||||
|
||||
## Error namespace
|
||||
|
||||
`TENANT_PROVIDER_MEMORY.*` — three codes:
|
||||
|
||||
- `DUPLICATE_TENANT` — seed or `add()` collision.
|
||||
- `TENANT_NOT_FOUND` — `remove()` of unknown id.
|
||||
- `INVALID_RECORD` — schema failure at the boundary.
|
||||
|
||||
Errors that the abstract contract itself can raise
|
||||
(`TENANT_PROVIDER.INVALID_CURSOR`) flow through unchanged from
|
||||
`@suaveplan/api-tenant-provider`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- DB-backed enumeration — Stage 8 `api-tenant-provider-postgres` /
|
||||
`api-tenant-provider-surrealdb`.
|
||||
- Persistence across process restarts — every `createMemoryTenantProvider`
|
||||
call starts from the supplied seed.
|
||||
|
||||
## Stage
|
||||
|
||||
Stage 4 (Base adapters) of the API plugin system umbrella — see
|
||||
the design document.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# `TENANT_PROVIDER_MEMORY` error codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The in-memory `TenantProvider` adapter has its own narrow error
|
||||
namespace, distinct from the abstract `TENANT_PROVIDER.*` codes shipped
|
||||
by `@suaveplan/api-tenant-provider`. Keeping the registries separate
|
||||
means a caller can tell the difference between "the contract surfaced
|
||||
an error any provider can raise" (`TENANT_PROVIDER.INVALID_CURSOR`) and
|
||||
"the in-memory adapter rejected a mutation" (e.g.
|
||||
`TENANT_PROVIDER_MEMORY.DUPLICATE_TENANT`). The module exports the
|
||||
frozen `TENANT_PROVIDER_MEMORY_CODES` registry, the
|
||||
`TenantProviderMemoryError` class, and the `TenantProviderMemoryErrorCode`
|
||||
union.
|
||||
|
||||
## Features
|
||||
|
||||
- Three stable, dot-namespaced codes: `DUPLICATE_TENANT`,
|
||||
`TENANT_NOT_FOUND`, `INVALID_RECORD`.
|
||||
- `defineCodes("TENANT_PROVIDER_MEMORY", ...)` produces a `Readonly`
|
||||
string-keyed registry the runtime cannot mutate.
|
||||
- `TenantProviderMemoryError extends SuaveplanError<...>` so `instanceof`
|
||||
narrows the `code` field to the three-string union.
|
||||
- A duplicate `tenantProviderMemoryCode` field mirrors `code` for
|
||||
call-sites that want a non-ambiguous accessor.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
TENANT_PROVIDER_MEMORY_CODES,
|
||||
TenantProviderMemoryError,
|
||||
} from "@suaveplan/api-tenant-provider-memory/error-codes";
|
||||
|
||||
throw new TenantProviderMemoryError(
|
||||
TENANT_PROVIDER_MEMORY_CODES.DUPLICATE_TENANT,
|
||||
"tenant acme already exists"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Branch on the narrowed code to choose retry behaviour:
|
||||
|
||||
```ts
|
||||
try {
|
||||
provider.add(record);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof TenantProviderMemoryError &&
|
||||
err.code === TENANT_PROVIDER_MEMORY_CODES.DUPLICATE_TENANT
|
||||
) {
|
||||
return "noop";
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `TENANT_PROVIDER_MEMORY_CODES`: `Readonly<{ DUPLICATE_TENANT: "TENANT_PROVIDER_MEMORY.DUPLICATE_TENANT"; TENANT_NOT_FOUND: "TENANT_PROVIDER_MEMORY.TENANT_NOT_FOUND"; INVALID_RECORD: "TENANT_PROVIDER_MEMORY.INVALID_RECORD" }>`.
|
||||
- `TenantProviderMemoryErrorCode`: union of the three dotted strings.
|
||||
- `TenantProviderMemoryError(code, message, options?)`: stores `code`
|
||||
on the base class and `tenantProviderMemoryCode` on the subclass.
|
||||
- `options.cause` flows through `SuaveplanError`'s standard cause chain.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is built via `defineCodes("TENANT_PROVIDER_MEMORY", [...])`
|
||||
which freezes the resulting object at module-load time, ensuring no
|
||||
caller can extend the closed set. The subclass intentionally adds a
|
||||
mirror field rather than just inheriting `.code` so call-sites that
|
||||
catch a generic `SuaveplanError` and want to branch on adapter origin
|
||||
can do so without `instanceof`. All three codes are raised by at least
|
||||
one production path in `provider.ts`; no test-only codes exist.
|
||||
@@ -0,0 +1,111 @@
|
||||
# `createMemoryTenantProvider`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
In-memory reference implementation of the
|
||||
`@suaveplan/api-tenant-provider` `TenantProvider` contract. It is both
|
||||
the default backing for the API testing kit (so unit tests don't need
|
||||
a database) and a production-grade option for small deployments whose
|
||||
tenant list is small and static, or where a process-restart-bounded
|
||||
seed is acceptable. Backed by an insertion-ordered `Map<TenantId,
|
||||
TenantRecord>` for deterministic `list()` output and `O(1)`
|
||||
`lookup`/`count`.
|
||||
|
||||
## Features
|
||||
|
||||
- Conforms verbatim to `runTenantProviderConformance` from the
|
||||
abstract contract — same active/suspended/archived filtering rules,
|
||||
same `count()` semantics, same cursor-based pagination, same
|
||||
idempotent `dispose()`.
|
||||
- Optional `tenants` seed validated through `tenantRecordSchema` at the
|
||||
boundary; duplicate ids in the seed raise
|
||||
`TENANT_PROVIDER_MEMORY.DUPLICATE_TENANT`.
|
||||
- Out-of-band admin helpers — `add`, `remove`, `clear` — for
|
||||
runtime mutation of the backing store. These sit alongside the
|
||||
abstract `TenantProvider` interface so DB-backed adapters can ship a
|
||||
different (or no) mutation surface without breaking the contract.
|
||||
- Cursor pagination uses the canonical `encodeCursor` / `decodeCursor`
|
||||
helpers from `@suaveplan/api-tenant-provider/pagination`, so cursors
|
||||
produced by other adapters that share the encoding round-trip cleanly.
|
||||
- `ListOpts.pageSize` clamps a single iteration's yield count, mirroring
|
||||
the DB-adapter contract for fan-out callers that want to checkpoint
|
||||
long enumerations.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createMemoryTenantProvider } from "@suaveplan/api-tenant-provider-memory/provider";
|
||||
|
||||
const provider = createMemoryTenantProvider({
|
||||
tenants: [
|
||||
{ id: "acme", status: "active", createdAt: "2026-01-01T00:00:00Z" },
|
||||
{ id: "beta", status: "active", createdAt: "2026-01-02T00:00:00Z" },
|
||||
],
|
||||
});
|
||||
|
||||
for await (const tenant of provider.list()) {
|
||||
console.log(tenant.id);
|
||||
}
|
||||
await provider.dispose();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Cursor-based fan-out with checkpointing, plus runtime mutation:
|
||||
|
||||
```ts
|
||||
import { encodeCursor } from "@suaveplan/api-tenant-provider/pagination";
|
||||
import { createMemoryTenantProvider } from "@suaveplan/api-tenant-provider-memory/provider";
|
||||
|
||||
const provider = createMemoryTenantProvider();
|
||||
provider.add({ id: "acme", status: "active", createdAt: "2026-01-01T00:00:00Z" });
|
||||
provider.add({ id: "beta", status: "suspended", createdAt: "2026-01-02T00:00:00Z" });
|
||||
|
||||
// First page — active only
|
||||
for await (const tenant of provider.list({ pageSize: 1 })) {
|
||||
console.log(tenant.id);
|
||||
}
|
||||
|
||||
// Second page — resume from offset
|
||||
const cursor = encodeCursor({ offset: 1, version: 1 });
|
||||
for await (const tenant of provider.list({ cursor, includeSuspended: true })) {
|
||||
console.log(tenant.id);
|
||||
}
|
||||
|
||||
provider.remove("acme");
|
||||
provider.clear();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createMemoryTenantProvider(options?: CreateMemoryTenantProviderOptions): MemoryTenantProvider`.
|
||||
- `CreateMemoryTenantProviderOptions`: `{ tenants?: readonly TenantRecord[] }`.
|
||||
- `MemoryTenantProvider extends TenantProvider`: also exposes
|
||||
`add(record: TenantRecord): void`, `remove(tenantId: TenantId): void`,
|
||||
`clear(): void`.
|
||||
- `list(opts?)`: filters out `archived`, includes `suspended` only when
|
||||
`opts.includeSuspended` is `true`, applies `pageSize` clamp, resumes
|
||||
from `cursor` when set. Invalid cursors raise the contract's
|
||||
`TENANT_PROVIDER.INVALID_CURSOR`.
|
||||
- `count()`: returns the number of `active` records.
|
||||
- `lookup(id)`: returns the record (including `archived`) or `null`.
|
||||
- `dispose()`: idempotent — clears the backing store.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The backing `Map` preserves insertion order per the ECMAScript spec, so
|
||||
`list()` yields tenants in the order they were seeded or added — no
|
||||
sort is required. `dispose` clears the map rather than flagging a
|
||||
disposed bit, which means subsequent `lookup`/`count` calls behave as
|
||||
if the provider were freshly constructed with an empty seed; this is
|
||||
deliberate (the conformance harness requires `dispose` to remain
|
||||
callable). Admin helpers (`add`/`remove`) re-validate inputs through
|
||||
`tenantRecordSchema` so a runtime-mutated provider has the same
|
||||
boundary guarantees as one seeded via the constructor. The `pageSize`
|
||||
clamp is applied at iterator construction (when `Symbol.asyncIterator`
|
||||
is invoked) so a caller can construct multiple iterables from a single
|
||||
provider without sharing state.
|
||||
Reference in New Issue
Block a user