Files
skills/docs/persistence/database/seed/src/fixtures/fixtures.md
2026-05-29 08:20:56 +01:00

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

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[] — 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.