# runner _(no submodules)_ ## Purpose The `SeedRunner` orchestrates a single seeding run against an injected `SeedStore`. It is the public entry point for actually applying a set of declarative `SeedDefinition` objects: it picks the seeds that apply to the requested environment, filters them by the caller's `only` / `exclude` lists, topologically sorts the survivors, and executes each in turn with the configured upsert strategy, batching, dry-run, and cancellation behaviour. The runner is intentionally store-agnostic. It does not know about SurrealDB, Postgres, or any particular driver — it just calls the `SeedStore` interface declared in `types/common.ts`. That keeps the core universal (no `node:` / `bun:` imports) and lets the same runner drive in-memory tests, local containers, and production databases without rewriting the orchestration layer. ## Features - **Dependency-ordered execution** via Kahn's-algorithm topological sort (uses the sibling `utils/topo-sort` module). - **Filtering**: `only` includes a whitelist; `exclude` removes from the result; the two compose (`only` first, then `exclude`). - **Three upsert strategies** (`skip`, `update`, `replace`) honoured at the batch level — the runner delegates to `SeedStore.findExisting`, `SeedStore.insert`, and `SeedStore.update` according to the strategy and the presence of `upsertKeys`. - **Batching** by `SeedRunConfig.batchSize`, defaulting to `DEFAULT_BATCH_SIZE` (100). Records are passed to the store in `Array.slice`-derived chunks; the store reports the actual insert / update counts. - **Dry-run mode** synthesises `completed` results without invoking the store — useful for CI smoke tests and for validating environment filters. - **Dependency-aware skipping**: when a seed fails, every dependent in the topo-order is recorded with `status: "skipped"` and `error: "Dependency failed"` rather than attempted. - **`AbortSignal` plumbing**: the signal is checked between seeds and forwarded into every `SeedStore` call. Aborted runs return `success: false` and mark every remaining seed `skipped` with `error: "Aborted"`. - **Injectable `LoggerLike`** (`@suaveplan/types`) with a no-op default. Emits structured records at `debug` / `info` / `warn` / `error` levels for every observable transition. - **Store-error wrapping**: exceptions thrown by `SeedStore` methods are wrapped as `SeedError` with code `SEED.STORE_ERROR` and the original exception attached on `error.cause`. `SeedResult.error` carries the human-readable message. ## Usage: Basic ```ts import { createInMemoryStore, defineSeed, SeedRegistry, SeedRunner, } from "@suaveplan/seed"; const registry = new SeedRegistry(); registry.register( defineSeed("users") .data([{ id: 1, name: "alice" }]) .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); ``` ## Usage: Advanced ```ts import { createInMemoryStore, defineSeed, SeedRegistry, SeedRunner, } from "@suaveplan/seed"; import type { LoggerLike } from "@suaveplan/types"; const registry = new SeedRegistry(); registry.registerMany([ defineSeed("tenants") .environments("staging", "production") .data(async () => fetchTenantsFromCsv()) .upsert("update", ["slug"]) .priority(0) .build(), defineSeed("admin-users") .dependsOn("tenants") .data(adminUsers) .upsert("update", ["tenantId", "email"]) .priority(10) .build(), defineSeed("audit-buckets") .dependsOn("tenants") .truncateFirst() .data([]) .priority(20) .build(), ]); const logger: LoggerLike = createStructuredLogger(); const runner = new SeedRunner(registry, productionStore, logger); const controller = new AbortController(); setTimeout(() => controller.abort(), 30_000); 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, duration: r.duration, }), }); if (!result.success) { throw new Error( `Seed run failed: ${result.seedsFailed} failed, ${result.seedsSkipped} skipped`, ); } ``` ## API Reference ### `class SeedRunner implements ISeedRunner` ```ts constructor( registry: SeedRegistry, store: SeedStore, logger?: LoggerLike, ) run(config: SeedRunConfig): Promise ``` ### `SeedRunConfig` | Field | Type | Default | | ----------------- | ----------------------------------- | ---------------------- | | `environment` | `SeedEnvironment` | (required) | | `only?` | `readonly string[]` | undefined (all seeds) | | `exclude?` | `readonly string[]` | undefined | | `dryRun?` | `boolean` | `false` | | `batchSize?` | `number` | `DEFAULT_BATCH_SIZE` | | `continueOnError?`| `boolean` | `false` | | `onProgress?` | `(r: SeedResult) => void` | undefined | | `signal?` | `AbortSignal` | undefined | ### `SeedRunResult` | Field | Type | Meaning | | ---------------- | ------------------- | -------------------------------------------------- | | `environment` | `SeedEnvironment` | The environment the run was scoped to | | `totalDuration` | `number` | Milliseconds via `performance.now()` | | `results` | `readonly SeedResult[]` | One entry per attempted (or skipped) seed | | `success` | `boolean` | True iff zero failures AND not aborted | | `seedsRun` | `number` | Results with status other than `"skipped"` | | `seedsSkipped` | `number` | Results with status `"skipped"` | | `seedsFailed` | `number` | Results with status `"failed"` | ### Logger events | Event | Level | Fields | | ------------------ | ------- | --------------------------------------------------------------------------------------- | | `seed.run.preparing` | debug | `environment`, `filteredCount`, `orderedCount` | | `seed.run.start` | info | `environment`, `totalSeeds` | | `seed.completed` | info | `name`, `table`, `recordsInserted`, `recordsUpdated`, `recordsSkipped`, `duration` | | `seed.skipped` | warn | `name`, `table`, `reason` (`"Dependency failed"` \| `"Aborted"`) | | `seed.failed` | error | `name`, `table`, `error`, `duration` | | `seed.run.complete` | info | `environment`, `success`, `seedsRun`, `seedsSkipped`, `seedsFailed`, `totalDuration` | ## Implementation Notes The actual batched insert/update/findExisting work lives in a sibling `execute.ts` module so this file stays under the 250-line soft limit. The split is purely organisational: `executeSeed(store, seed, config)` is invoked from `SeedRunner.run` and returns a single `SeedResult` per seed. Errors thrown by the store inside `executeSeed` are wrapped as `SeedError({ code: SEED.STORE_ERROR, cause })`; the runner records the message string on `SeedResult.error` while the structured fields (`code`, `seedName`, `cause`) remain reachable to consumers that catch the wrapped error from higher up. `SeedRunner` keeps the abort-on-signal check at the **top** of the execution loop so a signal raised between seeds short-circuits before the next seed's data factory is even invoked. `AbortSignal` is also forwarded into every `SeedStore` method via the optional `signal?` parameter on the interface, letting network-bound adapters honour cancellation mid-batch. The `InMemorySeedStore` shipped in `fixtures/fixtures.ts` accepts but ignores the signal — it has nothing to cancel. Defensive `if (!seed) continue;` checks against `seedMap.get(name)` were removed: `order` and `seedMap` are both built from the same `filtered` array, so every name `topoSort` returns is guaranteed to resolve. The TypeScript assertion (`as SeedDefinition`) records that invariant without the false-positive coverage gap a runtime check would introduce.