11 KiB
Migration adapters completion
Package: @suaveplan/migration
Status: PROPOSED
Type: spec-conformance + feature
Why
The genesis port of @suaveplan/migration ships concrete MemoryMigrationLock, MemoryMigrationStore, DistributedMigrationLock, and RedisDistributedLock, but the surface around them is incomplete relative to the upstream package at ../suaveplan/packages/universal/migration. The runner already emits migration:before, migration:after, migration:failed, and migration:plan through an EventBus<MigrationEventMap> injected via config — but there is no shared migrationEvents instance that downstream tooling (status dashboards, observability sinks) can subscribe to, no canonical re-export of the lock / store interfaces from dedicated migration-lock / migration-store modules, no BatchTargetProgress / BatchTargetStatus / TargetConfigFactory types for the batch migrator, no GetAllRecordsFn companion for the status dashboard, no ValidationError (under that name) export, and no surfaced MIGRATION_CODES registration step that downstream consumers can rely on at module load.
Consumers of the migration package today have to hand-roll an EventBus<MigrationEventMap> per call site to observe lifecycle events, redeclare batch-progress shape types in their own code, and either ignore validation errors or extract them out of an internal symbol. This change closes those gaps so downstream services (the status dashboard, ops tooling, future SurrealDB / Postgres adapter packages) can rely on a single coherent migration surface.
What changes
- ADDED:
migration-eventsmodule — exports a typedEventBus<MigrationEventMap>instance namedmigrationEventsplus acreateMigrationEvents()factory; the existingMigrationEventMapinterface gains scenario-named keys"migration:started","migration:applied","migration:failed","migration:completed","migration:rolled-back"as aliases for the existingmigration:before/migration:after/migration:failed/migration:plankeys (existing keys preserved for one release). - ADDED:
MigrationLockLikeinterface inlock/migration-lock—acquire(lockId: string, ttlMs: number): Promise<boolean>,release(lockId: string): Promise<void>,extend(lockId: string, ttlMs: number): Promise<boolean>; the existing genesisMigrationLockAdaptershape (nolockId) is preserved as a back-compat alias. - ADDED:
MigrationStoreLikeinterface instore/migration-store—list(): Promise<MigrationRecord[]>,append(record: MigrationRecord): Promise<void>,markFailed(id: string, error: string): Promise<void>,markApplied(id: string): Promise<void>,getLatestVersion(): Promise<string | undefined>; the existingMigrationStoreAdaptershape (record/remove/clear) is preserved as a back-compat alias. - MODIFIED:
MemoryMigrationLockandMemoryMigrationStore— gain implementations of the newMigrationLockLike/MigrationStoreLikeinterfaces alongside their existing methods; default constructions remain wire-compatible. - ADDED:
BatchTargetStatus("pending" | "running" | "completed" | "failed"),BatchTargetProgress,TargetConfigFactorytypes — exported fromtypes/migration.typesand the package root. - ADDED:
GetAllRecordsFntype — exported fromstatus/migration-statusand the package root, used by the status dashboard for injectable history retrieval. - ADDED:
ValidationErrortype alias — re-exports the existingMigrationValidationErrorshape under the upstream-canonical name; both names co-exist for one release. - MODIFIED:
MIGRATION_CODESandMigrationErrorare guaranteed registered against the shareddefineCodesregistry from@suaveplan/erroron module load (side-effect import); the registration is idempotent and verifiable viaErrorCodes.has("MIGRATION.LOCK_HELD"). - ADDED:
migration-events.mdsibling docs and per-module.mdfor every new file.
Out of scope
- SurrealDB / Postgres
MigrationStoreLikeadapters — the surface here defines the contract; concrete adapters land in their own change folders (migration-adapter-surrealdb,migration-adapter-postgres) so each can pull in its native client and integration test fixture independently. - SQL migration text DSL — pre-baked SQL templates are tracked separately; this change only stabilises the runner contracts.
- Rollback-script auto-generation — synthesising a
downfrom anupis a separate research item; this change keeps the existing manualdownrequirement. - Hot-reload migration discovery — file-watcher-driven re-indexing is its own change; this proposal only stabilises the in-memory store and lock contracts.
Source mapping
This change derives from the upstream package at ../suaveplan/packages/universal/migration/src and the upstream enterprise audit at ../suaveplan/enterprise/migration.md.
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
MigrationLockLike interface (acquire(lockId, ttl), release(lockId), extend(lockId, ttl)) |
packages/universal/migration/src/lock/migration-lock.ts |
L1-L8 | Verbatim port of upstream contract; renamed from MigrationLockAdapter to MigrationLockLike to follow the genesis *Like naming for ambient interfaces (see LoggerLike / StorageLike) |
MigrationStoreLike interface (list, append, markFailed, markApplied, getLatestVersion) |
packages/universal/migration/src/store/migration-store.ts |
L1-L11 | Verbatim port; renamed from MigrationStoreAdapter |
migrationEvents shared EventBus instance |
packages/universal/migration/src/migration-events.ts |
L1-L42 | Verbatim port; replace upstream EventEmitter with @suaveplan/events::EventBus per genesis convention |
MigrationEventMap lifecycle keys (migration:started / migration:applied / migration:failed / migration:completed) |
packages/universal/migration/src/migration-events.ts |
L13-L41 | Add as additive keys alongside genesis migration:before / migration:after / migration:plan |
BatchTargetStatus type |
packages/universal/migration/src/batch/batch-migrator.ts |
L9 | Verbatim |
BatchTargetProgress interface |
packages/universal/migration/src/batch/batch-migrator.ts |
L11-L20 | Verbatim |
TargetConfigFactory type |
packages/universal/migration/src/batch/batch-migrator.ts |
L31-L34 | Verbatim |
GetAllRecordsFn type |
packages/universal/migration/src/status/migration-status.ts |
L61 | Verbatim |
ValidationError type re-export |
packages/universal/migration/src/types/migration.types.ts |
L155-L162 | Re-export the genesis MigrationValidationError shape under upstream canonical name |
MIGRATION_CODES registered via shared defineCodes registry |
packages/universal/migration/src/error-codes.ts |
(whole file) | Side-effect import already present in index.ts; this change adds a registerMigrationErrorCodes() named export and a verification scenario |
Requirements with no source line
| Requirement | Justification |
|---|---|
MemoryMigrationLock and MemoryMigrationStore implement the new *Like interfaces alongside the legacy genesis ones |
Keeps existing genesis call sites (acquire(timeout?), record(record)) compiling for one release while exposing the upstream contract; pure-additive method set on the same class. |
migration-events.md sibling doc |
Genesis-wide CLAUDE.md doc rule — every implementation file ships a .md. |
Idempotent registerMigrationErrorCodes() (callable more than once without throwing) |
Genesis @suaveplan/error registry rule (CLAUDE.md §Workflow rule 2) — re-registration during HMR / multi-bundle load must not crash. |
Verification approach
Each spec delta requirement carries a **Verified by:** line pointing at concrete it(...) titles. The test files that will exist after this change ships:
packages/database/migration/src/lock/migration-lock/migration-lock.test.ts—MigrationLockLikecontract tests forMemoryMigrationLock(acquire / release / extend / TTL expiry / lockId isolation)packages/database/migration/src/lock/migration-lock/migration-lock.spec-conformance.test.ts— pins back-compat alias to legacyMigrationLockAdapterpackages/database/migration/src/store/migration-store/migration-store.test.ts—MigrationStoreLikecontract tests forMemoryMigrationStore(list / append / markFailed / markApplied / getLatestVersion / idempotency)packages/database/migration/src/store/migration-store/migration-store.spec-conformance.test.ts— pins back-compat alias to legacyMigrationStoreAdapterpackages/database/migration/src/migration-events/migration-events.test.ts— typed event emission for every key inMigrationEventMap(started / applied / failed / completed / rolled-back)packages/database/migration/src/migration-events/migration-events.spec-conformance.test.ts— listener teardown, FIFO order, error in listener does not break buspackages/database/migration/src/batch/batch-migrator/batch-migrator.test.ts—BatchTargetProgresslifecycle,TargetConfigFactoryinvocation per target, partial failure isolationpackages/database/migration/src/status/migration-status/migration-status.test.ts—GetAllRecordsFninjectionpackages/database/migration/src/errors/migration-error.spec-conformance.test.ts—MIGRATION_CODESresolves throughErrorCodes.has()after package load AND after a secondregisterMigrationErrorCodes()call
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/migration to enumerate. Today grep -rl "@suaveplan/migration" packages/ --include="package.json" returns zero matches outside packages/database/migration/package.json itself, so the change is purely additive — existing genesis surface is preserved by the back-compat aliases.
@suaveplan/migration(self) — [x] tests run / [x] docs reviewed / [ ] no action needed- (no in-repo dependents) — verified by
bunx turbo ls --filter=...@suaveplan/migrationreturning a single package.
If tests run is unchecked for any dependent, the change MUST NOT be folded into the canonical spec — finish that work first or split the proposal.
Migration impact
MigrationLockAdapterconsumers — no break. The interface remains exported under its existing name;MigrationLockLikeis a strictly additive companion. New code SHOULD preferMigrationLockLikebecause itslockIdparameter enables a single lock instance to gate multiple migration targets (the upstream batch migrator pattern).MigrationStoreAdapterconsumers — no break. Alias preserved;MigrationStoreLikeis the canonical name going forward.MigrationEventMapconsumers — no break. Existing keys (migration:before,migration:after,migration:plan) preserved; new keys are additive. New code SHOULD prefer the lifecycle-named keys; the executor will continue to emit both (legacy + canonical) for one release, then drop legacy in a follow-up change.MigrationValidationErrorconsumers — no break.ValidationErroris a type alias for the same shape.