190 lines
11 KiB
Markdown
190 lines
11 KiB
Markdown
# @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.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## 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…
|
|
<!-- end:auto-docs -->
|
|
|
|
## 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
|
|
|
|
```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<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.
|
|
|