92 lines
12 KiB
Markdown
92 lines
12 KiB
Markdown
# 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.
|