4.9 KiB
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
createand indexed overrides forcreateMany. - Counter reset to support deterministic test sequencing.
- Full
SeedStoreimplementation:insert,update,findExisting,truncate,tableExists. - Composite-key support: any number of
keyFieldsjoined with"::". - Test affordances:
getRecordsreturns a defensive copy,cleardrops every table. - Optional
AbortSignalparameter accepted on every method for contract compatibility (ignored — operations are synchronous in-memory). - Factory helpers
createFixtureFactoryandcreateInMemoryStorefor code that prefers function calls overnew.
Usage: Basic
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
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[]— emitcountrecords;overrides[i](if present) merges overdefaults(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; returnsrecords.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?)—trueiffinsertortruncatehas 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.