first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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.

View File

@@ -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.