# fixtures _(no submodules)_ ## 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 { orgId: string; userId: string; role: "member" | "admin"; } const memberships = new FixtureFactory((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` - `new FixtureFactory(defaults: (index: number) => T)` — wrap a defaults function. - `create(overrides?: Partial): T` — emit one record, advance the counter. - `createMany(count: number, overrides?: Partial[]): 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(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.