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,142 @@
# fixtures
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `fixtures` module supplies the two universal building blocks used to
exercise seed definitions without a real database: `FixtureFactory` for
generating deterministic, counter-stamped record sets, and `InMemorySeedStore`
for satisfying the `SeedStore` contract entirely in process memory. Together
they let every test in the wider ecosystem author realistic seed data and run
the same `SeedRunner` paths a production adapter would run, without touching
any I/O or external service. The module deliberately has zero dependencies on
the runner, registry, or builder layers — it is a leaf consumed by tests and
by integration harnesses that want a drop-in store.
## Features
- Auto-incrementing fixture indices via a private monotonic counter.
- Per-record overrides for `create` and indexed overrides for `createMany`.
- Counter reset to support deterministic test sequencing.
- Full `SeedStore` implementation: `insert`, `update`, `findExisting`,
`truncate`, `tableExists`.
- Composite-key support: any number of `keyFields` joined with `"::"`.
- Test affordances: `getRecords` returns a defensive copy, `clear` drops
every table.
- Optional `AbortSignal` parameter accepted on every method for contract
compatibility (ignored — operations are synchronous in-memory).
- Factory helpers `createFixtureFactory` and `createInMemoryStore` for code
that prefers function calls over `new`.
## Usage: Basic
```ts
import {
createFixtureFactory,
createInMemoryStore,
} from "@suaveplan/seed/fixtures/fixtures";
const userFactory = createFixtureFactory((index) => ({
id: index,
name: `User ${index}`,
email: `user${index}@example.com`,
}));
const users = userFactory.createMany(3);
const store = createInMemoryStore();
await store.insert("users", users);
```
## Usage: Advanced
```ts
import {
FixtureFactory,
InMemorySeedStore,
} from "@suaveplan/seed/fixtures/fixtures";
interface Membership extends Record<string, unknown> {
orgId: string;
userId: string;
role: "member" | "admin";
}
const memberships = new FixtureFactory<Membership>((i) => ({
orgId: `org-${i % 2}`,
userId: `user-${i}`,
role: "member",
}));
const rows = memberships.createMany(4, [
undefined as never,
{ role: "admin" },
]);
const store = new InMemorySeedStore();
await store.insert("memberships", rows);
const updated = await store.update(
"memberships",
[{ orgId: "org-0", userId: "user-0", role: "admin" }],
["orgId", "userId"]
);
memberships.reset();
```
## API Reference
### `FixtureFactory<T>`
- `new FixtureFactory(defaults: (index: number) => T)` — wrap a defaults
function.
- `create(overrides?: Partial<T>): T` — emit one record, advance the counter.
- `createMany(count: number, overrides?: Partial<T>[]): T[]` — emit `count`
records; `overrides[i]` (if present) merges over `defaults(counter)`.
- `reset(): void` — set the counter back to 0.
- `get counter(): number` — read the current counter.
### `createFixtureFactory<T>(defaults)`
Convenience wrapper for `new FixtureFactory(defaults)`.
### `InMemorySeedStore implements SeedStore`
- `insert(table, records, signal?)` — append; returns `records.length`.
- `update(table, records, keyFields, signal?)` — composite-key upsert into
matching rows; returns the count actually updated.
- `findExisting(table, records, keyFields, signal?)` — return stored rows
whose composite key appears in the input set.
- `truncate(table, signal?)` — empty the table while keeping it present.
- `tableExists(table, signal?)` — `true` iff `insert` or `truncate` has ever
named the table.
- `getRecords(table)` — defensive copy of the table's rows.
- `clear()` — drop every table.
### `createInMemoryStore()`
Returns a new `InMemorySeedStore`.
## Implementation Notes
The composite key is built by mapping each `keyField` to `String(record[k] ??
"")` and joining with `"::"`. Nullish field values collapse to the empty
string, so records with `undefined` and `null` for the same field share a
key; callers that need to distinguish them must use a key field that is
always populated. `update` mutates the stored array in place via index
assignment so subsequent `getRecords` calls observe the merge; the spread
order is `{ ...existing[idx], ...record }`, meaning incoming fields win on
collision.
`AbortSignal` arguments are intentionally ignored: there is no I/O to
abort. The parameters exist only so the in-memory store satisfies the same
shape as a network-bound store and can be swapped in for one in tests.
`getRecords` returns a shallow copy of the underlying array so mutating the
returned list does not corrupt subsequent assertions. `clear` removes the
map entries themselves, after which `tableExists` returns `false` for every
table — distinguishing it from `truncate`, which preserves the entry while
emptying its contents.