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

@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 — SeedBuilder is the fluent, chainable API used to construct immutable SeedDefinition records. It exists so that call…
    • builder — SeedBuilder is the fluent, chainable API used to construct immutable SeedDefinition records. It exists so that call…
  • error-codes — The @suaveplan/seed package centralises every failure mode it can produce behind a single registry built with `define…
    • error-codes — The @suaveplan/seed package centralises every failure mode it can produce behind a single registry built with `define…
  • fixtures — The fixtures module supplies the two universal building blocks used to exercise seed definitions without a real…
    • fixtures — The fixtures module supplies the two universal building blocks used to exercise seed definitions without a real…
  • registry — SeedRegistry is the canonical in-memory store for SeedDefinition entries used by @suaveplan/seed. It owns inserti…
    • registry — SeedRegistry is the canonical in-memory store for SeedDefinition entries used by @suaveplan/seed. It owns inserti…
  • runner — The SeedRunner orchestrates a single seeding run against an injected SeedStore. It is the public entry point for ac…
    • execute — The executeSeed function applies one SeedDefinition to one SeedStore, returning a single SeedResult. It is the…
    • runner — The SeedRunner orchestrates a single seeding run against an injected SeedStore. 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…
  • topo-sort — topo-sort is the dependency-resolution engine that lets @suaveplan/seed execute seed definitions in an order where…
    • topo-sort — topo-sort is the dependency-resolution engine that lets @suaveplan/seed execute seed definitions in an order where…

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 (typed SEED.DUPLICATE_NAME), defensive-copy reads, environment-aware queries
  • SeedRunner — Kahn's-algorithm topological sort with priority tiebreak, only/exclude filtering, three upsert strategies (skip/update/replace), configurable batching, dryRun mode, continueOnError, per-seed onProgress callback
  • AbortSignal plumbing — abort mid-run; remaining seeds are reported as skipped with error: "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, reset
  • InMemorySeedStore — full SeedStore shape with getRecords/clear test affordances
  • SeedError + SEED_CODES registry — every public failure carries a stable SEED.* code and threads cause per the @suaveplan/error contract
  • Tree-shakeable subpath exports — import @suaveplan/seed/runner/runner without 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 factory
  • class SeedBuilder — chainable: table, environments, dependsOn, data, dataFactory, upsert, description, priority, truncateFirst, terminal build()

Registry

  • class SeedRegistry — register / registerMany / get / getAll / getForEnvironment(env) / has / remove / clear / size / names. Raises SeedError({ 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, seedsFailed
  • SeedResult: name, table, status, recordsInserted, recordsUpdated, recordsSkipped, duration, error?

Fixtures

  • class FixtureFactory<T extends SeedRecord> + createFixtureFactory(defaults) — create(overrides?), createMany(count, overrides?), reset(), counter
  • class InMemorySeedStore + createInMemoryStore() — full SeedStore shape plus getRecords(table) / clear()

Utils

  • topoSort(seeds): string[] — Kahn's algorithm with priority tiebreak; raises SEED.UNREGISTERED_DEPENDENCY / SEED.CIRCULAR_DEPENDENCY / SEED.DEPENDENCY_DEPTH_EXCEEDED
  • detectCycles(seeds): string[] — DFS cycle reporter; returns ["a -> b -> a", ...]; empty array on clean graph

Errors

  • SeedError extends SuaveplanError<SeedErrorCode> with optional seedName / dependency discriminants
  • SEED_CODES: DUPLICATE_NAME, UNREGISTERED_DEPENDENCY, CIRCULAR_DEPENDENCY, DEPENDENCY_DEPTH_EXCEEDED, INVALID_CONFIG, STORE_ERROR, ABORTED

Constants

  • DEFAULT_ENVIRONMENTS = ["dev", "staging", "test", "production"] as const
  • MAX_DEPENDENCY_DEPTH = 50
  • DEFAULT_BATCH_SIZE = 100
  • SEED_STATUS = { PENDING, RUNNING, COMPLETED, FAILED, SKIPPED }

Implementation Notes

  • Universal — no node:/bun: imports anywhere in src/. The only platform-aware call is performance.now(), which every supported runtime provides natively.
  • Store-agnostic by design — SeedRunner receives a SeedStore via constructor injection. The core ships only InMemorySeedStore (for tests); production stores live in dedicated seed-store-* packages.
  • Errors thread cause — store exceptions are wrapped as SeedError({ 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 SeedStore calls receive the signal too (every SeedStore method accepts an optional signal?: AbortSignal).
  • Logger is opt-in — LoggerLike defaults to a no-op so the runner stays silent when unwired. Wire it to get seed.run.start / seed.completed / seed.skipped / seed.failed / seed.run.complete events.
  • 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-seed package that wraps this core.