Files
2026-05-27 23:29:53 +01:00

12 KiB
Raw Permalink Blame History

Add @suaveplan/seed — declarative database seeding & fixture framework

Package: @suaveplan/seed Status: PROPOSED Type: migration (upstream ../suaveplan/packages/server/seed)

  • Design: ./design.md
  • Spec delta: ./specs/seed/spec.md

Why

STATUS.md line 200 lists @suaveplan/seed as Pending. The upstream package is a small, runtime-pure orchestration core (3 features, 1 utility, fixtures, ~520 LOC of src/) that solves a problem genesis has no other answer for: declaratively seeding databases with dependency-ordered, idempotent, environment-filtered, batched, dry-run-capable runs against any storage adapter the caller injects. No genesis package covers this surface today, and several pending downstream packages — anything in the database or fixtures-oriented testing space — will want to depend on it. The upstream is well-shaped (98.85% test coverage), tier-pure (zero node:/bun: imports — only performance.now()), and dependency-light (single runtime dep on @suaveplan/error).

The migration is straightforward — preserve the public surface, drop the redundant core/ wrapper + seed- filename prefix per the no-redundant-prefixes rule, upgrade plain throw new Error(...) to SuaveplanError with a SEED.* code registry, rewrite the buggy first-pass in topoSort cleanly, and ship with injectable LoggerLike/tracer slots (no-op defaults) consistent with every other genesis package. The enterprise audit (../suaveplan/enterprise/seed.md) lists ambitious additions (SurrealDB schema migrations, JWT tenant scoping, RBAC, audit logging, rate limiting, webhooks) — these are explicitly out of scope for this change and belong in a follow-up add-api-plugin-seed proposal that wraps the core behind a plugin tier.

What changes

  • ADDED: SeedRunner class — orchestrates SeedRunConfig runs against an injected SeedStore, with dependency-ordered execution (topological sort), only/exclude filtering, dryRun, continueOnError, per-batch processing (default DEFAULT_BATCH_SIZE = 100, configurable), three upsert strategies (skip/update/replace), per-seed truncate, and onProgress callback emission per completed seed.
  • ADDED: SeedRegistry class — register / registerMany / get / getAll / getForEnvironment(env) / has / remove / clear / size / names; in-memory Map<name, SeedDefinition> with defensive-copy on read and SEED.DUPLICATE_NAME raised on re-registration.
  • ADDED: SeedBuilder fluent class + defineSeed(name) convenience — chainable methods table / environments(...envs) / dependsOn(...names) / data(records) / dataFactory(fn) / upsert(strategy, keys) / description(desc) / priority(n) / truncateFirst(enabled?) / build().
  • ADDED: FixtureFactory<T> + createFixtureFactory(defaults) — auto-incrementing counter, create(overrides?), createMany(count, overrides?), reset(), counter getter.
  • ADDED: InMemorySeedStore + createInMemoryStore() — full SeedStore shape (insert/update/findExisting/truncate/tableExists) plus test-affordance getRecords(table) and clear().
  • ADDED: topoSort(seeds) utility — Kahn's algorithm with priority tiebreak; raises SEED.UNREGISTERED_DEPENDENCY for dangling names, SEED.CIRCULAR_DEPENDENCY for cycles, SEED.DEPENDENCY_DEPTH_EXCEEDED when iterations exceed MAX_DEPENDENCY_DEPTH × seedCount.
  • ADDED: detectCycles(seeds) utility — DFS-based cycle reporter returning string[] of a -> b -> a traces; empty array on clean graph.
  • ADDED: SeedDefinition / SeedRecord / SeedDataFactory / UpsertStrategy / SeedEnvironment / SeedStatus / SeedResult / SeedRunResult / SeedRunConfig / SeedStore types in types/common.ts; ISeedBuilder / ISeedRegistry / ISeedRunner interface contracts co-located with each implementation.
  • ADDED: Constants — DEFAULT_ENVIRONMENTS (["dev","staging","test","production"] as const), MAX_DEPENDENCY_DEPTH = 50, DEFAULT_BATCH_SIZE = 100, SEED_STATUS enum object.
  • ADDED: SEED_CODES registry via defineCodes("SEED", [...]) — 7 codes: DUPLICATE_NAME, UNREGISTERED_DEPENDENCY, CIRCULAR_DEPENDENCY, DEPENDENCY_DEPTH_EXCEEDED, INVALID_CONFIG, STORE_ERROR, ABORTED.
  • ADDED: SeedError extends SuaveplanError<SeedErrorCode> with seedName? / dependency? / path? discriminants for diagnostics.
  • ADDED: Optional injectable LoggerLike (@suaveplan/types) on SeedRunner constructor — no-op default. Logs seed.run.start / seed.run.complete / seed.skipped / seed.failed structured records when wired.
  • ADDED: Optional AbortSignal plumbing on SeedRunConfig.signal — checked at every seed boundary; aborted runs return a SeedRunResult with success: false and remaining seeds marked status: "skipped" with error: "Aborted". (Upstream had no signal support.)
  • ADDED: Subpath exports ./runner/runner, ./registry/registry, ./builder/builder, ./fixtures/fixtures, ./utils/topo-sort/topo-sort, ./types/common, ./constants — tree-shakeable.

Out of scope

  • SurrealDB plugin wiring, definePlugin() lifecycle, ./surql schema directory, migrationsPath. The enterprise audit calls these out as required for the package to flip to ✅ Enterprise, but they belong to a higher-tier api-plugin-seed package that consumes @suaveplan/seed and provides the plugin shell. Keeping the core universal-tier is what makes it portable across server, browser-test, and worker contexts. Reason: same separation drive ↔ drive-template, network ↔ network-plugin-*.
  • JWT tenant scoping, RBAC/ABAC, rate limiting, idempotency headers, audit logging, webhook delivery. All API-tier concerns; none belong in a pure orchestration core. Reason: orthogonal to seeding mechanics; the seed runner takes data and a store and writes records — auth lives at the HTTP boundary that wraps it.
  • Real SurrealDB integration test suite via bun run test:with-db:filter. The core ships with an InMemorySeedStore for testing, and the real-database integration belongs to whichever adapter package provides the production SeedStore (e.g. a future seed-store-surrealdb). Reason: the core is store-agnostic by design; coupling tests to one DB violates that contract.
  • Benchmarks & CI regression gate. Useful but not blocking for a pure orchestration core whose hot path is bounded by the injected store's throughput. Defer to enterprise wave. Reason: meaningful benchmarks require a real store; the in-memory store benchmarks are testing JavaScript loops.
  • i18n message catalog for error messages. The error codes are stable; the human-readable messages are developer-facing English strings. When i18n lands on the genesis enterprise path for errors broadly, SEED.* codes inherit the same treatment. Reason: not seed-specific.
  • SeedDefinition Zod validation at the boundary. The fluent builder + TypeScript types are the public contract; the developer-facing API is constructed in-process, not over a wire. Adding runtime Zod would be defensive coding without a real attack surface. Reason: orthogonal to the API surface this package exposes.

Source mapping

Upstream source: ../suaveplan/packages/server/seed/src/ (23 files, 98.85% upstream coverage). Every public symbol in upstream src/index.ts migrates as-is; layout normalises per the no-prefix and feature-folder rules.

Parity Checklist

Capability Upstream Migrated Notes
SeedBuilder fluent class + defineSeed ✅ ✅ Class & function preserved; subpath ./builder/builder (renamed from ./core/seed-builder/seed-builder)
SeedRegistry with env-filter, dup-detect ✅ ⬆️ Enhanced throw new Error("already registered") → SeedError({ code: SEED.DUPLICATE_NAME, seedName })
SeedRunner with topo-order, filtering, batching, upsert, dry-run, continue-on-error, progress callback ✅ ✅ Behaviour preserved bit-for-bit; constructor gains optional LoggerLike, config gains optional signal
topoSort with priority tiebreak ✅ ⬆️ Enhanced Upstream has dead-code first pass (+ 0 no-op) and a second cleanup pass — rewritten cleanly as one Kahn's-algorithm pass; throw new Error(...) replaced with SeedError
detectCycles DFS reporter ✅ ✅ Unchanged logic; relocated to utils/topo-sort/topo-sort.ts
FixtureFactory<T> + createFixtureFactory ✅ ✅ Generic preserved; counter getter preserved
InMemorySeedStore + createInMemoryStore ✅ ✅ Full SeedStore surface + getRecords / clear test affordances preserved
All public types from types/common.ts ✅ ✅ Re-exported from root; subpath ./types/common added
4 constants (DEFAULT_ENVIRONMENTS, MAX_DEPENDENCY_DEPTH, DEFAULT_BATCH_SIZE, SEED_STATUS) ✅ ✅ Preserved; subpath ./constants added
Subpath exports per public module ✅ ⬆️ Enhanced Upstream exposes 4 subpaths under ./core/seed-* and ./fixtures/fixtures; genesis exposes 7 subpaths with the redundant core/ and seed- prefix dropped
Plain throw new Error(...) at boundaries ✅ ⬆️ Replaced All raised errors now extend SuaveplanError with stable SEED.* codes (rule §63 of CLAUDE.md)
LoggerLike injection 🚫 upstream ⬆️ Added Genesis convention; no-op default, zero runtime cost when unwired
AbortSignal plumbing 🚫 upstream ⬆️ Added SeedRunConfig.signal checked at every seed boundary; consistent with storage-http/drive precedent
Plugin wiring (definePlugin, lifecycle) ✅ enterprise audit asks for it 🚫 deferred Scoped to add-api-plugin-seed follow-up; the core stays universal-tier
SurrealDB schema migrations / ./surql ✅ enterprise audit asks for it 🚫 deferred Belongs to a future store adapter package; the core is store-agnostic
JWT tenant / RBAC / rate-limit / audit / webhooks / idempotency ✅ enterprise audit asks for it 🚫 deferred API-tier concerns; deferred to add-api-plugin-seed
OTel spans / Prometheus metrics ✅ enterprise audit asks for it 🚫 deferred Wire when the api-plugin-seed package lands and there's a real call site; the core emits structured logs via LoggerLike only
Benchmarks (src/__benchmarks__) + CI gate ✅ enterprise audit asks for it 🚫 deferred Requires real store; defer to enterprise wave
@suaveplan/api-schemas Zod boundary ✅ enterprise audit asks for it 🚫 deferred Not applicable to in-process developer API; revisit if seed config is ever serialised over a wire

Reference docs consulted

  • ✅ ../suaveplan/packages/server/seed/src/ — all 17 implementation files read
  • ✅ ../suaveplan/enterprise/seed.md — enterprise audit, in full
  • ✅ ../suaveplan/packages/server/seed/package.json — dep graph, public exports
  • 🚫 ../suaveplan/openspec/specs/seed/spec.md — upstream had no formal spec for this package; the parity contract is the source itself
  • 🚫 ../suaveplan/openspec/changes/*/proposal.md touching seed — none found

Impact

Affected dependents

No dependents today — @suaveplan/seed is being added net-new. Future consumers (api-plugin-seed, any package shipping a SeedStore adapter, integration test fixtures) will land in subsequent changes against the public contract this proposal locks in.

Risk

  • Low. Pure orchestration; no IO of its own. The buggy topoSort first-pass rewrite is the only non-mechanical change to upstream behaviour — covered by adding explicit unit tests against the upstream test suite to prove output-equivalent ordering before the rewrite.
  • Naming change to subpath exports (./core/seed-runner/seed-runner → ./runner/runner) is technically a public-API rename, but the package is at 0.1.0 on genesis with zero consumers, so there is no migration path to worry about.

Migration notes

None. Net-new package on genesis.