Files
suaveplan/packages/persistence/database/seed/src/fixtures/fixtures.ts
2026-05-27 23:47:53 +01:00

142 lines
4.1 KiB
TypeScript

import type { SeedRecord, SeedStore } from "../types/common.js";
/**
* Factory for generating test fixture data with auto-incrementing indices
* and customizable overrides.
*/
export class FixtureFactory<T extends SeedRecord> {
private readonly _defaults: (index: number) => T;
private _counter = 0;
constructor(defaults: (index: number) => T) {
this._defaults = defaults;
}
/** Creates a single record with optional overrides. */
create(overrides?: Partial<T>): T {
const record = this._defaults(this._counter++);
if (overrides) {
return { ...record, ...overrides };
}
return record;
}
/** Creates multiple records with optional per-record overrides. */
createMany(count: number, overrides?: Partial<T>[]): T[] {
const records: T[] = [];
for (let i = 0; i < count; i++) {
records.push(this.create(overrides?.[i]));
}
return records;
}
/** Resets the internal counter. */
reset(): void {
this._counter = 0;
}
/** Returns the current counter value. */
get counter(): number {
return this._counter;
}
}
/**
* Creates a fixture factory that generates records with incrementing IDs
* and customizable overrides.
*/
export function createFixtureFactory<T extends SeedRecord>(
defaults: (index: number) => T
): FixtureFactory<T> {
return new FixtureFactory(defaults);
}
/**
* Simple in-memory seed store for testing seed definitions without a real
* database. Implements the universal `SeedStore` contract; `AbortSignal`
* parameters are accepted for interface compatibility but ignored because
* every operation is synchronous in-memory.
*/
export class InMemorySeedStore implements SeedStore {
private readonly _tables = new Map<string, SeedRecord[]>();
async insert(
table: string,
records: readonly SeedRecord[],
_signal?: AbortSignal
): Promise<number> {
const existing = this._tables.get(table) ?? [];
existing.push(...records);
this._tables.set(table, existing);
return records.length;
}
async update(
table: string,
records: readonly SeedRecord[],
keyFields: readonly string[],
_signal?: AbortSignal
): Promise<number> {
const existing = this._tables.get(table) ?? [];
let updated = 0;
for (const record of records) {
const key = keyFields
.map((k) => String(record[k] ?? ""))
.join("::");
const idx = existing.findIndex(
(r) =>
keyFields.map((k) => String(r[k] ?? "")).join("::") === key
);
if (idx >= 0) {
existing[idx] = { ...existing[idx], ...record };
updated++;
}
}
return updated;
}
async findExisting(
table: string,
records: readonly SeedRecord[],
keyFields: readonly string[],
_signal?: AbortSignal
): Promise<SeedRecord[]> {
const existing = this._tables.get(table) ?? [];
const recordKeys = new Set(
records.map((r) =>
keyFields.map((k) => String(r[k] ?? "")).join("::")
)
);
return existing.filter((r) => {
const key = keyFields.map((k) => String(r[k] ?? "")).join("::");
return recordKeys.has(key);
});
}
async truncate(table: string, _signal?: AbortSignal): Promise<void> {
this._tables.set(table, []);
}
async tableExists(table: string, _signal?: AbortSignal): Promise<boolean> {
return this._tables.has(table);
}
/** Returns a defensive copy of all records in a table (for test assertions). */
getRecords(table: string): SeedRecord[] {
return [...(this._tables.get(table) ?? [])];
}
/** Clears all tables. */
clear(): void {
this._tables.clear();
}
}
/**
* Creates a simple in-memory seed store for testing. Records are stored in a
* map keyed by table name.
*/
export function createInMemoryStore(): InMemorySeedStore {
return new InMemorySeedStore();
}