13 KiB
13 KiB
Tasks: enterprise-migration
Change ID: enterprise-migration
Package: @suaveplan/migration
Target path: universal/database/migration
Prerequisite packages on Verdaccio: @suaveplan/error, @suaveplan/types, @suaveplan/db, @suaveplan/logger, @suaveplan/events
1. Scaffold
- Create directory
universal/database/migration/withsrc/,tests/,src/__benchmarks__/, and all feature subdirectories per the layout inproposal.md. - Write
package.jsonat version0.1.0with:"type": "module","sideEffects": falseexportsmap: top-level"."barrel plus subpaths for./planner/migration-planner,./runner/migration-runner,./runner/migration-executor,./runner/dry-run-reporter,./store/memory-store,./store/database-store,./lock/memory-lock,./lock/distributed-lock,./lock/redis-lock,./status/migration-status,./batch/batch-migrator,./adapters/surrealdb-adapterdependencies:@suaveplan/error workspace:^,@suaveplan/types workspace:^,@suaveplan/db workspace:^,@suaveplan/logger workspace:^,@suaveplan/events workspace:^,zod catalog:forms,@opentelemetry/api catalog:observability,tslib catalog:builddevDependencies:vitest catalog:testing,@vitest/coverage-istanbul catalog:testing,fast-check catalog:testing,mitata catalog:testing,@suaveplan/testing workspace:^,@suaveplan/db-sqlite workspace:^
- Write
tsconfig.jsonextending@suaveplan/typescript-config/library.json. - Write
vitest.config.tsusingcreateTestConfig()from@suaveplan/testing/config(pool:forks). - Add
lint,typecheck,test,test:coverage,build,docs:check,benchscripts topackage.json.
2. Types + errors
2a. Migration types (src/types/migration.types.ts + src/types/migration.types.md)
- Define all public TS interfaces:
Migration,MigrationContext,MigrationDirection,MigrationStatus,MigrationRecord,MigrationPlan,MigrationResult,ValidationError,ValidationResult,MigrationRunnerConfig,RunOptions,MigrationStoreAdapter,MigrationLockAdapter,DatabaseAdapter,DatabaseStoreConfig,DistributedLockAdapter,DistributedLockConfig,RedisLockClient,RedisLockConfig,BatchMigratorConfig,BatchMigrationResult,BatchTargetStatus,BatchTargetProgress,SurrealDbAdapterConfig,DryRunEntry,DryRunReport,RiskLevel,ExtendedMigrationStatus,MigrationSummary,MigrationHealthCheck,MigrationHistoryEntry,MigrationStatusEntry. - Write
src/types/migration.types.md(≥200 words).
2b. Error codes (src/error-codes.ts)
- Import
CodeMetadata,ErrorCodes,SuaveplanErrorfrom@suaveplan/error. - Define
MigrationErrorCodeunion — eight codes:MIGRATION_FAILED,MIGRATION_LOCK_TIMEOUT,MIGRATION_CHECKSUM_MISMATCH,MIGRATION_NOT_FOUND,MIGRATION_STORE_ERROR,MIGRATION_PLAN_INVALID,MIGRATION_ABORTED,MIGRATION_DRY_RUN. - Define
MIGRATION_ERROR_METADATAwithhttpStatus,severity,retryable,userMessageKey,category,descriptionper code. - Export
registerMigrationErrorCodes(). Idempotent; called eagerly at module scope. - Export
MigrationErrorextendingSuaveplanErrorcarryingMigrationErrorCodediscriminant.
2c. Zod schemas (src/types/schemas.ts + src/types/schemas.md)
- Write
MigrationRunnerConfigSchema,RunOptionsSchema,BatchMigratorConfigSchema,DatabaseStoreConfigSchema,DistributedLockConfigSchema,RedisLockConfigSchema. - Write
src/types/schemas.md(≥200 words).
2d. Events (src/migration-events.ts)
- Define
MigrationEventMapinterface:migration:before,migration:after,migration:failed,migration:plan. - Export
migrationEvents— a lazily-initialisedEventBus<MigrationEventMap>singleton from@suaveplan/events.
3. Constants (src/constants/index.ts)
- Export
DEFAULT_MIGRATION_TABLE = 'migrations',DEFAULT_LOCK_TTL_MS = 30_000,DEFAULT_BATCH_CONCURRENCY = 5.
4. Planner (src/planner/migration-planner/)
- Implement
sortMigrations(migrations)— sort ascending by migrationid(lexicographic). - Implement
checksumMigration(migration)— serialisemigration.up.toString()and produce a hex hash (use@suaveplan/crypto-utilsif available; otherwise use Web Cryptocrypto.subtle.digest('SHA-256', ...)). - Implement
planUp(all, applied):- Sort
allwithsortMigrations. - Filter to migrations whose
idis not inapplied(by id + checksum ifchecksumValidation). - Return
{ toApply, toRollback: [] }.
- Sort
- Implement
planDown(all, applied, steps?):- Return
stepsmost-recently-applied in reverse order for rollback. - Default
steps = 1.
- Return
- Write
migration-planner.test.ts(≤500 lines): happy path up/down, already applied skipped, checksum mismatch throws, empty sets, partial rollback with steps. - Write
migration-planner.md(≥200 words).
5. Store adapters
5a. Memory store (src/store/memory-store/)
- Implement
MemoryMigrationStoreimplementingMigrationStoreAdapter.- Internal
Map<string, MigrationRecord>. - Thread-safety: all operations are synchronous but wrapped in
Promise.resolve()to honour the async interface.
- Internal
- Write
memory-store.test.ts: record/getApplied/remove/clear. - Write
memory-store.md(≥200 words).
5b. Database store (src/store/database-store/)
- Implement
DatabaseMigrationStore(config: DatabaseStoreConfig):- On construction, creates the ledger table if it does not exist (via
CREATE TABLE IF NOT EXISTSonconfig.adapter). recordinserts;removedeletes;getAppliedqueries all rows;cleartruncates.- Wraps errors as
MigrationError(MIGRATION_STORE_ERROR).
- On construction, creates the ledger table if it does not exist (via
- Write
database-store.test.ts(integration:@suaveplan/db-sqliteSQLite adapter in-memory database). - Write
database-store.md(≥200 words).
6. Lock adapters
6a. Memory lock (src/lock/memory-lock/)
- Implement
MemoryMigrationLock— in-process boolean flag;acquirewith timeout using injectable clock. - Write
memory-lock.test.ts: acquire, double-acquire times out, release. - Write
memory-lock.md(≥200 words).
6b. Distributed lock (src/lock/distributed-lock/)
- Implement
DistributedMigrationLock(config)— delegates toconfig.adapter.acquire/release. - Implement
InMemoryDistributedLockAdapter— a test double implementingDistributedLockAdapterusing an in-process Map. - Write
distributed-lock.test.ts. - Write
distributed-lock.md(≥200 words).
6c. Redis lock (src/lock/redis-lock/)
- Implement
RedisDistributedLock(config)— usesconfig.client.set(key, value, { NX: true, PX: ttl })for acquire;config.client.del(key)for release. - Write
redis-lock.test.ts— DI theRedisLockClientinterface with a real in-memory stub (no vi.mock). - Write
redis-lock.md(≥200 words).
7. Executor (src/runner/migration-executor/)
- Implement
MigrationExecutor(options):execute(plan, direction, runOptions?):- Checks
runOptions?.signalbefore each migration; throwsMigrationError(MIGRATION_ABORTED)on abort. - For each migration in plan: emits
migration:before, callsmigration[direction](ctx), emitsmigration:afterwith duration, callsstore.record/remove. - On error: emits
migration:failed, records to result.failed, continues to next. - Dry-run: skips execution, records to
DryRunReporter. - OTel: per-migration span
migration.execute, histogrammigration.duration_ms. - Logs via injected
LoggerLike.
- Checks
- Write
migration-executor.test.ts(unit: injectMemoryMigrationStore+MemoryMigrationLock). - Write
migration-executor.md(≥200 words).
8. Runner (src/runner/migration-runner/)
- Implement
MigrationRunner(config):up(options?):- Acquire lock (if configured); throw
MigrationError(MIGRATION_LOCK_TIMEOUT)on failure. - Validate checksums if
checksumValidation: true. - Call
planUp; emitmigration:plan. - Delegate to
MigrationExecutor.execute(plan, 'up', options). - Release lock in
finally. - OTel: span
migration.runwith attributemigration.direction = 'up'.
- Acquire lock (if configured); throw
down(options?): analogous withplanDown.status()— comparesconfig.migrationsagainststore.getApplied(), returnsMigrationStatus[].validate()— checks checksums for all applied migrations, returnsValidationResult.
- Write
migration-runner.test.ts(≤500 lines): up/down happy path, abort mid-run, checksum mismatch, lock contention, dry-run. - Write
migration-runner.md(≥200 words).
9. Dry-run reporter (src/runner/dry-run-reporter/)
- Implement
DryRunReporter:record(entry)appends;report()computestotalRisk(highest entry risk), generates plain-textsummary. - Write
dry-run-reporter.test.ts. - Write
dry-run-reporter.md(≥200 words).
10. Status dashboard (src/status/migration-status/)
- Implement
MigrationStatusDashboard(config):getAll()— joins registered migrations with applied records.getSummary()— counts pending/applied/failed.healthCheck()— returns healthy if zero failed, at-risk if any pending for >24h.getHistory(id)— returns store records for a specific migration id.
- Write
migration-status.test.ts. - Write
migration-status.md(≥200 words).
11. Batch migrator (src/batch/batch-migrator/)
- Implement
BatchMigrator(config)— runsconfig.targetsrunners concurrently up toconfig.concurrency(default 5) using a semaphore; collects results. - Write
batch-migrator.test.ts: two targets, one fails; concurrency respected. - Write
batch-migrator.md(≥200 words).
12. SurrealDB adapter (src/adapters/surrealdb-adapter/)
- Implement
SurrealDbAdapter(config)wrapping a SurrealDBSurrealinstance (typed asunknownto avoid hard dep onsurrealdb.js— callers cast):query<T>(sql, params?)— callsdb.query(sql, params), flattens theQueryResult<T>[][]toT[].- Note: avoid
db.create()— useINSERT INTOquery (see memoryfeedback_surrealdb_create_content_drop).
- Write
surrealdb-adapter.test.ts— DI a mock{ query }conforming toSurrealDbAdapterConfig. - Write
surrealdb-adapter.md(≥200 words).
13. Property-based tests (tests/property.test.ts)
Use fast-check. All timeouts: { timeout: 30000 }.
planUpidempotency: apply all migrations, then callplanUpagain —toApplyis empty.sortMigrationsstability: for any permutation of N migrations,sortMigrationsreturns the same order.checksumMigrationdeterminism: for the same migration object, checksum called twice returns the same string.- Partial batch failure: BatchMigrator with K/N failing targets yields exactly K entries in
result.failed.
14. Benchmarks (src/__benchmarks__/migration.bench.ts)
Use mitata.
planUp_100: plan for 100 pending migrations (pure, no IO).runner_up_10_memory: run 10 no-op migrations againstMemoryMigrationStore.checksumMigration_1k: 1000 checksum computations.- Add
benchscript and placeholder benchmark table toREADME.md.
15. Docs
src/types/migration.types.mdsrc/types/schemas.mdsrc/planner/migration-planner/migration-planner.mdsrc/runner/migration-runner/migration-runner.mdsrc/runner/migration-executor/migration-executor.mdsrc/runner/dry-run-reporter/dry-run-reporter.mdsrc/store/memory-store/memory-store.mdsrc/store/database-store/database-store.mdsrc/lock/memory-lock/memory-lock.mdsrc/lock/distributed-lock/distributed-lock.mdsrc/lock/redis-lock/redis-lock.mdsrc/status/migration-status/migration-status.mdsrc/batch/batch-migrator/batch-migrator.mdsrc/adapters/surrealdb-adapter/surrealdb-adapter.md- Root
universal/database/migration/README.md.
16. Verify and publish
- Run
bunx turbo lint --filter=@suaveplan/migration— zero errors. - Run
bunx turbo typecheck --filter=@suaveplan/migration— zero errors. - Run
bunx turbo test --filter=@suaveplan/migration— all tests pass. - Run
bunx turbo test:coverage --filter=@suaveplan/migration— 100% on all touched modules. - Run
bun run docs:check --filter=@suaveplan/migration— zero errors. - Confirm no
TODO,FIXME,@deprecated, untypedany,ts-ignore, or.skipinsrc/ortests/. - Confirm
"sideEffects": falseinpackage.json. - Run
bun scripts/publish.ts --package @suaveplan/migration. - Update
CLAUDE.mdpackage listing to include@suaveplan/migration. - Archive: move
openspec/changes/enterprise-migration/toopenspec/archive/2026-04-24-enterprise-migration/and fold spec delta intoopenspec/specs/database/spec.md.