12 KiB
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:
SeedRunnerclass — orchestratesSeedRunConfigruns against an injectedSeedStore, with dependency-ordered execution (topological sort),only/excludefiltering,dryRun,continueOnError, per-batch processing (defaultDEFAULT_BATCH_SIZE = 100, configurable), three upsert strategies (skip/update/replace), per-seedtruncate, andonProgresscallback emission per completed seed. - ADDED:
SeedRegistryclass —register/registerMany/get/getAll/getForEnvironment(env)/has/remove/clear/size/names; in-memoryMap<name, SeedDefinition>with defensive-copy on read andSEED.DUPLICATE_NAMEraised on re-registration. - ADDED:
SeedBuilderfluent class +defineSeed(name)convenience — chainable methodstable/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(),countergetter. - ADDED:
InMemorySeedStore+createInMemoryStore()— fullSeedStoreshape (insert/update/findExisting/truncate/tableExists) plus test-affordancegetRecords(table)andclear(). - ADDED:
topoSort(seeds)utility — Kahn's algorithm with priority tiebreak; raisesSEED.UNREGISTERED_DEPENDENCYfor dangling names,SEED.CIRCULAR_DEPENDENCYfor cycles,SEED.DEPENDENCY_DEPTH_EXCEEDEDwhen iterations exceedMAX_DEPENDENCY_DEPTH × seedCount. - ADDED:
detectCycles(seeds)utility — DFS-based cycle reporter returningstring[]ofa -> b -> atraces; empty array on clean graph. - ADDED:
SeedDefinition/SeedRecord/SeedDataFactory/UpsertStrategy/SeedEnvironment/SeedStatus/SeedResult/SeedRunResult/SeedRunConfig/SeedStoretypes intypes/common.ts;ISeedBuilder/ISeedRegistry/ISeedRunnerinterface 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_STATUSenum object. - ADDED:
SEED_CODESregistry viadefineCodes("SEED", [...])— 7 codes:DUPLICATE_NAME,UNREGISTERED_DEPENDENCY,CIRCULAR_DEPENDENCY,DEPENDENCY_DEPTH_EXCEEDED,INVALID_CONFIG,STORE_ERROR,ABORTED. - ADDED:
SeedError extends SuaveplanError<SeedErrorCode>withseedName?/dependency?/path?discriminants for diagnostics. - ADDED: Optional injectable
LoggerLike(@suaveplan/types) onSeedRunnerconstructor — no-op default. Logsseed.run.start/seed.run.complete/seed.skipped/seed.failedstructured records when wired. - ADDED: Optional
AbortSignalplumbing onSeedRunConfig.signal— checked at every seed boundary; aborted runs return aSeedRunResultwithsuccess: falseand remaining seeds markedstatus: "skipped"witherror: "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,./surqlschema directory,migrationsPath. The enterprise audit calls these out as required for the package to flip to ✅ Enterprise, but they belong to a higher-tierapi-plugin-seedpackage that consumes@suaveplan/seedand provides the plugin shell. Keeping the core universal-tier is what makes it portable across server, browser-test, and worker contexts. Reason: same separationdrive↔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 anInMemorySeedStorefor testing, and the real-database integration belongs to whichever adapter package provides the productionSeedStore(e.g. a futureseed-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
i18nlands on the genesis enterprise path for errors broadly,SEED.*codes inherit the same treatment. Reason: not seed-specific. SeedDefinitionZod 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.mdtouching 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
topoSortfirst-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 at0.1.0on genesis with zero consumers, so there is no migration path to worry about.
Migration notes
None. Net-new package on genesis.