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