first commit
This commit is contained in:
142
docs/persistence/database/seed/src/fixtures/fixtures.md
Normal file
142
docs/persistence/database/seed/src/fixtures/fixtures.md
Normal 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.
|
||||
Reference in New Issue
Block a user