@suaveplan/seed - Universal declarative database seeding and fixture framework: SeedBuilder fluent API, SeedRegistry with environment filters, SeedRunner with topological ordering, upsert strategies, batching, dry-run, AbortSignal, injectable LoggerLike, FixtureFactory, InMemorySeedStore, store-agnostic.
Universal declarative database seeding and fixture framework. Define seed data once, let the runner handle dependency ordering, environment filtering, idempotent upserts, batching, dry-runs, and clean cancellation. The runner is store-agnostic: any object satisfying the SeedStore interface plugs in, so the same seed definitions drive in-memory tests, local SurrealDB instances, and production databases without rewriting the data layer.
Documentation tree
- builder —
SeedBuilderis the fluent, chainable API used to construct immutableSeedDefinitionrecords. It exists so that call…- builder —
SeedBuilderis the fluent, chainable API used to construct immutableSeedDefinitionrecords. It exists so that call…
- builder —
- error-codes — The
@suaveplan/seedpackage centralises every failure mode it can produce behind a single registry built with `define…- error-codes — The
@suaveplan/seedpackage centralises every failure mode it can produce behind a single registry built with `define…
- error-codes — The
- fixtures — The
fixturesmodule supplies the two universal building blocks used to exercise seed definitions without a real…- fixtures — The
fixturesmodule supplies the two universal building blocks used to exercise seed definitions without a real…
- fixtures — The
- registry —
SeedRegistryis the canonical in-memory store forSeedDefinitionentries used by@suaveplan/seed. It owns inserti…- registry —
SeedRegistryis the canonical in-memory store forSeedDefinitionentries used by@suaveplan/seed. It owns inserti…
- registry —
- runner — The
SeedRunnerorchestrates a single seeding run against an injectedSeedStore. It is the public entry point for ac… - types — Shared type module for
@suaveplan/seed. Every public class and function in the package —SeedBuilder, `SeedRegistry…- common — Shared type module for
@suaveplan/seed. Every public class and function in the package —SeedBuilder, `SeedRegistry…
- common — Shared type module for
- topo-sort —
topo-sortis the dependency-resolution engine that lets@suaveplan/seedexecute seed definitions in an order where…- topo-sort —
topo-sortis the dependency-resolution engine that lets@suaveplan/seedexecute seed definitions in an order where…
- topo-sort —
Purpose
Ships a handful of database adapters (db, db-sqlite, surrealdb, migration) but no consistent way to populate them. Hand-rolled per-package seeders drift in shape, swallow errors, skip dependency ordering, and re-insert duplicates on every run. @suaveplan/seed consolidates the orchestration concerns — registration, topological execution, environment scoping, batched upserts, dry-runs, progress reporting, and cancellation — behind one pluggable contract. The package is intentionally universal: zero node:/bun: imports, no IO of its own, and a single runtime dependency on @suaveplan/error. Adapters that need a production store ship in dedicated seed-store-* packages and implement the SeedStore interface; the core runs unchanged in tests, server processes, and worker contexts.
Features
- Fluent
SeedBuilder+defineSeed(name)convenience — chainable API for declaring tables, dependencies, environments, upsert strategies, priorities, and per-seed truncation SeedRegistry— duplicate-name detection (typedSEED.DUPLICATE_NAME), defensive-copy reads, environment-aware queriesSeedRunner— Kahn's-algorithm topological sort with priority tiebreak,only/excludefiltering, three upsert strategies (skip/update/replace), configurable batching,dryRunmode,continueOnError, per-seedonProgresscallbackAbortSignalplumbing — abort mid-run; remaining seeds are reported asskippedwitherror: "Aborted"- Injectable
LoggerLike(@suaveplan/types) — structured events (seed.run.start,seed.completed,seed.skipped,seed.failed,seed.run.complete); no-op default keeps the runtime silent when unwired FixtureFactory<T>— deterministic per-record overrides with auto-incrementing index,createMany,resetInMemorySeedStore— fullSeedStoreshape withgetRecords/cleartest affordancesSeedError+SEED_CODESregistry — every public failure carries a stableSEED.*code and threadscauseper the@suaveplan/errorcontract- Tree-shakeable subpath exports — import
@suaveplan/seed/runner/runnerwithout pulling fixtures - 100% statement/branch/function/line coverage via Istanbul
Usage: Basic
import {
defineSeed,
SeedRegistry,
SeedRunner,
createInMemoryStore,
} from "@suaveplan/seed";
const registry = new SeedRegistry();
registry.register(
defineSeed("users")
.table("user")
.data([
{ id: 1, name: "alice" },
{ id: 2, name: "bob" },
])
.upsert("skip", ["id"])
.build(),
);
registry.register(
defineSeed("posts")
.dependsOn("users")
.data([
{ id: 1, authorId: 1, title: "Hello" },
{ id: 2, authorId: 2, title: "World" },
])
.upsert("skip", ["id"])
.build(),
);
const store = createInMemoryStore();
const runner = new SeedRunner(registry, store);
const result = await runner.run({ environment: "dev" });
console.log(result.success, result.seedsRun, result.seedsSkipped);
Usage: Advanced
import {
defineSeed,
SeedRegistry,
SeedRunner,
type LoggerLike,
} from "@suaveplan/seed";
import { mySurrealStore } from "./my-surreal-store";
const registry = new SeedRegistry();
registry.registerMany([
defineSeed("tenants")
.environments("dev", "staging")
.data(async () => fetchTenantsFromCsv())
.upsert("update", ["slug"])
.priority(0)
.build(),
defineSeed("admin-users")
.dependsOn("tenants")
.dataFactory(async () => seedFactoryFor(currentTenant))
.upsert("update", ["tenantId", "email"])
.priority(10)
.build(),
defineSeed("audit-log-buckets")
.dependsOn("tenants")
.truncateFirst()
.data([])
.priority(20)
.build(),
]);
const logger: LoggerLike = createStructuredLogger();
const runner = new SeedRunner(registry, mySurrealStore, logger);
const controller = new AbortController();
setTimeout(() => controller.abort(), 30_000); // hard 30s budget
const result = await runner.run({
environment: "staging",
only: ["tenants", "admin-users"],
batchSize: 500,
continueOnError: false,
signal: controller.signal,
onProgress: (r) =>
logger.info(`seed.progress`, { name: r.name, status: r.status }),
});
if (!result.success) {
throw new Error(
`Seed run failed: ${result.seedsFailed} failed, ${result.seedsSkipped} skipped`,
);
}
API Reference
Builder
defineSeed(name: string): SeedBuilder— convenience factoryclass SeedBuilder— chainable:table,environments,dependsOn,data,dataFactory,upsert,description,priority,truncateFirst, terminalbuild()
Registry
class SeedRegistry—register/registerMany/get/getAll/getForEnvironment(env)/has/remove/clear/size/names. RaisesSeedError({ code: SEED.DUPLICATE_NAME })on collision.
Runner
class SeedRunner(registry, store, logger?)—async run(config: SeedRunConfig): Promise<SeedRunResult>SeedRunConfig:environment,only?,exclude?,dryRun?,batchSize?,continueOnError?,onProgress?,signal?SeedRunResult:environment,totalDuration,results,success,seedsRun,seedsSkipped,seedsFailedSeedResult:name,table,status,recordsInserted,recordsUpdated,recordsSkipped,duration,error?
Fixtures
class FixtureFactory<T extends SeedRecord>+createFixtureFactory(defaults)—create(overrides?),createMany(count, overrides?),reset(),counterclass InMemorySeedStore+createInMemoryStore()— fullSeedStoreshape plusgetRecords(table)/clear()
Utils
topoSort(seeds): string[]— Kahn's algorithm with priority tiebreak; raisesSEED.UNREGISTERED_DEPENDENCY/SEED.CIRCULAR_DEPENDENCY/SEED.DEPENDENCY_DEPTH_EXCEEDEDdetectCycles(seeds): string[]— DFS cycle reporter; returns["a -> b -> a", ...]; empty array on clean graph
Errors
SeedError extends SuaveplanError<SeedErrorCode>with optionalseedName/dependencydiscriminantsSEED_CODES:DUPLICATE_NAME,UNREGISTERED_DEPENDENCY,CIRCULAR_DEPENDENCY,DEPENDENCY_DEPTH_EXCEEDED,INVALID_CONFIG,STORE_ERROR,ABORTED
Constants
DEFAULT_ENVIRONMENTS = ["dev", "staging", "test", "production"] as constMAX_DEPENDENCY_DEPTH = 50DEFAULT_BATCH_SIZE = 100SEED_STATUS = { PENDING, RUNNING, COMPLETED, FAILED, SKIPPED }
Implementation Notes
- Universal — no
node:/bun:imports anywhere insrc/. The only platform-aware call isperformance.now(), which every supported runtime provides natively. - Store-agnostic by design —
SeedRunnerreceives aSeedStorevia constructor injection. The core ships onlyInMemorySeedStore(for tests); production stores live in dedicatedseed-store-*packages. - Errors thread
cause— store exceptions are wrapped asSeedError({ code: SEED.STORE_ERROR, cause })so callers can inspect the underlying error without losing the stable code. - Topological sort uses Kahn's algorithm — one clean pass, priority breaks ties for independent seeds, a defensive iteration guard catches pathological cases before they hang.
- AbortSignal is checked between seeds — in-flight
SeedStorecalls receive the signal too (everySeedStoremethod accepts an optionalsignal?: AbortSignal). - Logger is opt-in —
LoggerLikedefaults to a no-op so the runner stays silent when unwired. Wire it to getseed.run.start/seed.completed/seed.skipped/seed.failed/seed.run.completeevents. - Enterprise extras are out of scope here — plugin lifecycle, SurrealDB schema migrations, JWT/RBAC, audit logging, rate limiting, webhooks, OTel spans, and Prometheus metrics all belong to a future
api-plugin-seedpackage that wraps this core.