Initial Commit
This commit is contained in:
390
openspec/archive/2026-04-24-enterprise-migration/proposal.md
Normal file
390
openspec/archive/2026-04-24-enterprise-migration/proposal.md
Normal file
@@ -0,0 +1,390 @@
|
||||
# OpenSpec Change: enterprise-migration
|
||||
|
||||
- **Change ID**: enterprise-migration
|
||||
- **Package**: `@suaveplan/migration`
|
||||
- **Category**: universal
|
||||
- **Target path**: `universal/database/migration`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/migration.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/migration` on the genesis branch from scratch at enterprise quality. The package provides a database-agnostic schema migration runner: a `MigrationRunner` that orchestrates up/down execution, a pluggable `MigrationStoreAdapter` for ledger persistence, a `MigrationLockAdapter` interface (memory + distributed), a dry-run reporter, a status dashboard, an optional batch migrator for multi-target rollouts, and a SurrealDB adapter. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for all public types, OTel instrumentation, `AbortSignal` cancellation, logger injection via `@suaveplan/logger`, event bus integration via `@suaveplan/events`, property-based tests (fast-check), and a mitata benchmark suite. The migration runner previously bundled inside `@suaveplan/surrealdb` is now a standalone package consuming `@suaveplan/db`'s `DatabaseAdapter` interface.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | Base class for `MigrationError`; `defineCodes` for `MIGRATION.*` codes |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `DatabaseLike` |
|
||||
| `@suaveplan/db` (`workspace:^`) | `DatabaseAdapter` interface consumed by `DatabaseMigrationStore` and `SurrealDbAdapter` |
|
||||
| `@suaveplan/logger` (`workspace:^`) | `LoggerLike` structured logging throughout the runner |
|
||||
| `@suaveplan/events` (`workspace:^`) | `EventBus` for migration lifecycle events (before/after each step) |
|
||||
| `zod` (`catalog:forms`) | Zod schemas for all public option types |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on runner, planner, executor |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`), `@suaveplan/testing` (`workspace:^`), `@suaveplan/db-sqlite workspace:^` (integration test store backend).
|
||||
|
||||
---
|
||||
|
||||
## Preserved public API surface (from main branch)
|
||||
|
||||
### Runner
|
||||
|
||||
```ts
|
||||
class MigrationRunner {
|
||||
constructor(config: MigrationRunnerConfig)
|
||||
up(options?: RunOptions): Promise<MigrationResult>
|
||||
down(options?: RunOptions & { steps?: number }): Promise<MigrationResult>
|
||||
status(): Promise<MigrationStatus[]>
|
||||
validate(): Promise<ValidationResult>
|
||||
}
|
||||
|
||||
interface MigrationRunnerConfig {
|
||||
migrations: Migration[]
|
||||
store: MigrationStoreAdapter
|
||||
lock?: MigrationLockAdapter
|
||||
logger?: LoggerLike
|
||||
dryRun?: boolean
|
||||
checksumValidation?: boolean
|
||||
}
|
||||
|
||||
interface RunOptions { signal?: AbortSignal; target?: string }
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
```ts
|
||||
interface Migration {
|
||||
id: string
|
||||
name: string
|
||||
up: (ctx: MigrationContext) => Promise<void>
|
||||
down?: (ctx: MigrationContext) => Promise<void>
|
||||
checksum?: string
|
||||
}
|
||||
|
||||
interface MigrationContext { db: DatabaseAdapter; logger: LoggerLike }
|
||||
|
||||
type MigrationDirection = 'up' | 'down'
|
||||
type MigrationStatus = 'pending' | 'applied' | 'failed' | 'rolled-back'
|
||||
|
||||
interface MigrationRecord { id: string; name: string; appliedAt: number; checksum?: string; direction: MigrationDirection }
|
||||
|
||||
interface MigrationPlan { toApply: Migration[]; toRollback: Migration[] }
|
||||
|
||||
interface MigrationResult { applied: string[]; skipped: string[]; failed: string[]; direction: MigrationDirection; dryRun: boolean }
|
||||
|
||||
interface ValidationError { migrationId: string; message: string }
|
||||
interface ValidationResult { valid: boolean; errors: ValidationError[] }
|
||||
```
|
||||
|
||||
### Store adapters
|
||||
|
||||
```ts
|
||||
interface MigrationStoreAdapter {
|
||||
getApplied(): Promise<MigrationRecord[]>
|
||||
record(record: MigrationRecord): Promise<void>
|
||||
remove(id: string): Promise<void>
|
||||
clear(): Promise<void>
|
||||
}
|
||||
|
||||
class MemoryMigrationStore implements MigrationStoreAdapter
|
||||
class DatabaseMigrationStore implements MigrationStoreAdapter {
|
||||
constructor(config: DatabaseStoreConfig)
|
||||
}
|
||||
|
||||
interface DatabaseStoreConfig { adapter: DatabaseAdapter; tableName?: string }
|
||||
interface DatabaseAdapter { query<T>(sql: string, params?: unknown[]): Promise<T[]> }
|
||||
```
|
||||
|
||||
### Lock adapters
|
||||
|
||||
```ts
|
||||
interface MigrationLockAdapter {
|
||||
acquire(timeout?: number): Promise<boolean>
|
||||
release(): Promise<void>
|
||||
isLocked(): Promise<boolean>
|
||||
}
|
||||
|
||||
class MemoryMigrationLock implements MigrationLockAdapter
|
||||
class DistributedMigrationLock implements MigrationLockAdapter {
|
||||
constructor(config: DistributedLockConfig)
|
||||
}
|
||||
|
||||
interface DistributedLockAdapter { acquire(key: string, ttl: number): Promise<boolean>; release(key: string): Promise<void> }
|
||||
interface DistributedLockConfig { adapter: DistributedLockAdapter; key?: string; ttl?: number }
|
||||
class InMemoryDistributedLockAdapter implements DistributedLockAdapter
|
||||
class RedisDistributedLock implements MigrationLockAdapter {
|
||||
constructor(config: RedisLockConfig)
|
||||
}
|
||||
interface RedisLockClient { set(key: string, value: string, options: object): Promise<string | null>; del(key: string): Promise<number> }
|
||||
interface RedisLockConfig { client: RedisLockClient; key?: string; ttl?: number }
|
||||
```
|
||||
|
||||
### Planner
|
||||
|
||||
```ts
|
||||
function planUp(all: Migration[], applied: MigrationRecord[]): MigrationPlan
|
||||
function planDown(all: Migration[], applied: MigrationRecord[], steps?: number): MigrationPlan
|
||||
function sortMigrations(migrations: Migration[]): Migration[]
|
||||
function checksumMigration(migration: Migration): string
|
||||
```
|
||||
|
||||
### Executor
|
||||
|
||||
```ts
|
||||
class MigrationExecutor {
|
||||
constructor(options: ExecutorOptions)
|
||||
execute(plan: MigrationPlan, direction: MigrationDirection, options?: RunOptions): Promise<MigrationResult>
|
||||
}
|
||||
```
|
||||
|
||||
### Dry-run reporter
|
||||
|
||||
```ts
|
||||
class DryRunReporter {
|
||||
constructor()
|
||||
record(entry: DryRunEntry): void
|
||||
report(): DryRunReport
|
||||
}
|
||||
|
||||
interface DryRunEntry { migrationId: string; direction: MigrationDirection; riskLevel: RiskLevel; reason?: string }
|
||||
interface DryRunReport { entries: DryRunEntry[]; totalRisk: RiskLevel; summary: string }
|
||||
type RiskLevel = 'low' | 'medium' | 'high'
|
||||
```
|
||||
|
||||
### Status dashboard
|
||||
|
||||
```ts
|
||||
class MigrationStatusDashboard {
|
||||
constructor(config: StatusConfig)
|
||||
getAll(): Promise<ExtendedMigrationStatus[]>
|
||||
getSummary(): Promise<MigrationSummary>
|
||||
healthCheck(): Promise<MigrationHealthCheck>
|
||||
getHistory(id: string): Promise<MigrationHistoryEntry[]>
|
||||
}
|
||||
```
|
||||
|
||||
### Batch migrator
|
||||
|
||||
```ts
|
||||
class BatchMigrator {
|
||||
constructor(config: BatchMigratorConfig)
|
||||
run(options?: RunOptions): Promise<BatchMigrationResult>
|
||||
}
|
||||
|
||||
interface BatchMigratorConfig { targets: Array<{ name: string; runner: MigrationRunner }>; concurrency?: number }
|
||||
interface BatchMigrationResult { results: Map<string, MigrationResult>; failed: string[]; succeeded: string[] }
|
||||
```
|
||||
|
||||
### SurrealDB adapter
|
||||
|
||||
```ts
|
||||
class SurrealDbAdapter implements DatabaseAdapter {
|
||||
constructor(config: SurrealDbAdapterConfig)
|
||||
query<T>(sql: string, params?: unknown[]): Promise<T[]>
|
||||
}
|
||||
|
||||
interface SurrealDbAdapterConfig { db: unknown /* Surreal instance */ }
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
```ts
|
||||
interface MigrationEventMap {
|
||||
'migration:before': { migration: Migration; direction: MigrationDirection }
|
||||
'migration:after': { migration: Migration; direction: MigrationDirection; durationMs: number }
|
||||
'migration:failed': { migration: Migration; direction: MigrationDirection; error: Error }
|
||||
'migration:plan': { plan: MigrationPlan; direction: MigrationDirection }
|
||||
}
|
||||
|
||||
const migrationEvents: EventBus<MigrationEventMap>
|
||||
```
|
||||
|
||||
### Error codes
|
||||
|
||||
```ts
|
||||
type MigrationErrorCode =
|
||||
| 'MIGRATION_FAILED'
|
||||
| 'MIGRATION_LOCK_TIMEOUT'
|
||||
| 'MIGRATION_CHECKSUM_MISMATCH'
|
||||
| 'MIGRATION_NOT_FOUND'
|
||||
| 'MIGRATION_STORE_ERROR'
|
||||
| 'MIGRATION_PLAN_INVALID'
|
||||
| 'MIGRATION_ABORTED'
|
||||
| 'MIGRATION_DRY_RUN'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
### 1. Structured `MigrationError`
|
||||
|
||||
`MigrationError` extends `SuaveplanError` from `@suaveplan/error` carrying a `MigrationErrorCode` discriminant. Internal throw sites use named factory helpers.
|
||||
|
||||
### 2. Zod schemas
|
||||
|
||||
- `MigrationRunnerConfigSchema`
|
||||
- `RunOptionsSchema`
|
||||
- `BatchMigratorConfigSchema`
|
||||
- `DatabaseStoreConfigSchema`
|
||||
- `DistributedLockConfigSchema`
|
||||
- `RedisLockConfigSchema`
|
||||
|
||||
### 3. Logger injection
|
||||
|
||||
`MigrationRunnerConfig.logger` is now typed as `LoggerLike` (from `@suaveplan/types`). All runner, planner, and executor operations emit structured log entries at `debug` / `info` / `error` levels with migration ID, direction, and duration.
|
||||
|
||||
### 4. Event bus integration
|
||||
|
||||
`MigrationRunnerConfig` gains an optional `events?: EventBus<MigrationEventMap>`. When provided the runner emits `migration:before`, `migration:after`, `migration:failed`, `migration:plan` events. The exported `migrationEvents` singleton is available for packages that do not control runner construction.
|
||||
|
||||
### 5. OTel instrumentation
|
||||
|
||||
| Operation | Signal |
|
||||
|---|---|
|
||||
| `runner.up` / `runner.down` | span `migration.run`, attribute `migration.direction`, `migration.count` |
|
||||
| per-migration execution | span `migration.execute`, attribute `migration.id`, histogram `migration.duration_ms` |
|
||||
| `runner.status` | counter `migration.status_query_total` |
|
||||
| `batchMigrator.run` | span `migration.batch`, attribute `migration.target_count` |
|
||||
|
||||
### 6. Checksum validation
|
||||
|
||||
`checksumMigration` hashes the serialised `up` function body. When `checksumValidation: true` (default on `MigrationRunner`), mismatched checksums between the registered migration and the store record throw `MigrationError(MIGRATION_CHECKSUM_MISMATCH)` before applying.
|
||||
|
||||
### 7. Property-based tests (fast-check)
|
||||
|
||||
- `planUp` idempotency: applying `planUp` to an already fully-applied set returns an empty `toApply` list.
|
||||
- `sortMigrations` stability: for any permutation of N migrations, `sortMigrations` returns the same order.
|
||||
- `checksumMigration` determinism: calling checksum on the same migration object twice returns the same string.
|
||||
- `BatchMigrator` partial failure: if K of N targets fail, the result has exactly K failed entries.
|
||||
|
||||
### 8. Benchmark suite
|
||||
|
||||
- `planUp_100`: plan for 100 pending migrations.
|
||||
- `runner_up_10_memory`: run 10 no-op migrations against `MemoryMigrationStore`.
|
||||
- `checksumMigration_1k`: 1000 checksum computations.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
universal/database/migration/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts # Public barrel
|
||||
error-codes.ts # MIGRATION.* codes
|
||||
migration-events.ts # migrationEvents singleton + MigrationEventMap
|
||||
constants/
|
||||
index.ts # DEFAULT_TABLE_NAME, DEFAULT_LOCK_TTL
|
||||
types/
|
||||
migration.types.ts # All public TS interfaces
|
||||
migration.types.md
|
||||
schemas.ts # Zod schemas
|
||||
schemas.md
|
||||
planner/
|
||||
migration-planner/
|
||||
migration-planner.ts # planUp, planDown, sortMigrations, checksumMigration
|
||||
migration-planner.md
|
||||
migration-planner.test.ts
|
||||
index.ts
|
||||
runner/
|
||||
migration-runner/
|
||||
migration-runner.ts # MigrationRunner
|
||||
migration-runner.md
|
||||
migration-runner.types.ts
|
||||
migration-runner.test.ts
|
||||
index.ts
|
||||
migration-executor/
|
||||
migration-executor.ts # MigrationExecutor
|
||||
migration-executor.md
|
||||
migration-executor.test.ts
|
||||
index.ts
|
||||
dry-run-reporter/
|
||||
dry-run-reporter.ts
|
||||
dry-run-reporter.md
|
||||
dry-run-reporter.test.ts
|
||||
index.ts
|
||||
store/
|
||||
memory-store/
|
||||
memory-store.ts # MemoryMigrationStore
|
||||
memory-store.md
|
||||
memory-store.test.ts
|
||||
index.ts
|
||||
database-store/
|
||||
database-store.ts # DatabaseMigrationStore
|
||||
database-store.md
|
||||
database-store.types.ts
|
||||
database-store.test.ts
|
||||
index.ts
|
||||
lock/
|
||||
memory-lock/
|
||||
memory-lock.ts
|
||||
memory-lock.md
|
||||
memory-lock.test.ts
|
||||
index.ts
|
||||
distributed-lock/
|
||||
distributed-lock.ts
|
||||
distributed-lock.md
|
||||
distributed-lock.types.ts
|
||||
distributed-lock.test.ts
|
||||
index.ts
|
||||
redis-lock/
|
||||
redis-lock.ts
|
||||
redis-lock.md
|
||||
redis-lock.types.ts
|
||||
redis-lock.test.ts
|
||||
index.ts
|
||||
status/
|
||||
migration-status/
|
||||
migration-status.ts # MigrationStatusDashboard
|
||||
migration-status.md
|
||||
migration-status.types.ts
|
||||
migration-status.test.ts
|
||||
index.ts
|
||||
batch/
|
||||
batch-migrator/
|
||||
batch-migrator.ts
|
||||
batch-migrator.md
|
||||
batch-migrator.types.ts
|
||||
batch-migrator.test.ts
|
||||
index.ts
|
||||
adapters/
|
||||
surrealdb-adapter/
|
||||
surrealdb-adapter.ts
|
||||
surrealdb-adapter.md
|
||||
surrealdb-adapter.types.ts
|
||||
surrealdb-adapter.test.ts
|
||||
index.ts
|
||||
__benchmarks__/
|
||||
migration.bench.ts
|
||||
tests/
|
||||
integration.test.ts # Full up/down cycle with SQLite store (db-sqlite)
|
||||
property.test.ts # fast-check property tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Downstream packages**: any package that manages database schema evolution. `@suaveplan/surrealdb` provides a `SurrealAdapter` that implements `DatabaseAdapter`; `@suaveplan/migration`'s `SurrealDbAdapter` wraps that for the migration store.
|
||||
|
||||
**Breaking changes from main-branch API**: none. New optional fields (`events`, `signal`) are additive.
|
||||
|
||||
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/db`, `@suaveplan/logger`, `@suaveplan/events` must be published to Verdaccio before this package can be built or tested.
|
||||
241
openspec/archive/2026-04-24-enterprise-migration/tasks.md
Normal file
241
openspec/archive/2026-04-24-enterprise-migration/tasks.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# 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
|
||||
|
||||
- [x] Create directory `universal/database/migration/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`.
|
||||
- [x] Write `package.json` at version `0.1.0` with:
|
||||
- `"type": "module"`, `"sideEffects": false`
|
||||
- `exports` map: 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-adapter`
|
||||
- `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/db workspace:^`, `@suaveplan/logger workspace:^`, `@suaveplan/events workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`
|
||||
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `mitata catalog:testing`, `@suaveplan/testing workspace:^`, `@suaveplan/db-sqlite workspace:^`
|
||||
- [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
|
||||
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config` (pool: `forks`).
|
||||
- [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench` scripts to `package.json`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types + errors
|
||||
|
||||
### 2a. Migration types (`src/types/migration.types.ts` + `src/types/migration.types.md`)
|
||||
|
||||
- [x] 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`.
|
||||
- [x] Write `src/types/migration.types.md` (≥200 words).
|
||||
|
||||
### 2b. Error codes (`src/error-codes.ts`)
|
||||
|
||||
- [x] Import `CodeMetadata`, `ErrorCodes`, `SuaveplanError` from `@suaveplan/error`.
|
||||
- [x] Define `MigrationErrorCode` union — eight codes: `MIGRATION_FAILED`, `MIGRATION_LOCK_TIMEOUT`, `MIGRATION_CHECKSUM_MISMATCH`, `MIGRATION_NOT_FOUND`, `MIGRATION_STORE_ERROR`, `MIGRATION_PLAN_INVALID`, `MIGRATION_ABORTED`, `MIGRATION_DRY_RUN`.
|
||||
- [x] Define `MIGRATION_ERROR_METADATA` with `httpStatus`, `severity`, `retryable`, `userMessageKey`, `category`, `description` per code.
|
||||
- [x] Export `registerMigrationErrorCodes()`. Idempotent; called eagerly at module scope.
|
||||
- [x] Export `MigrationError` extending `SuaveplanError` carrying `MigrationErrorCode` discriminant.
|
||||
|
||||
### 2c. Zod schemas (`src/types/schemas.ts` + `src/types/schemas.md`)
|
||||
|
||||
- [x] Write `MigrationRunnerConfigSchema`, `RunOptionsSchema`, `BatchMigratorConfigSchema`, `DatabaseStoreConfigSchema`, `DistributedLockConfigSchema`, `RedisLockConfigSchema`.
|
||||
- [x] Write `src/types/schemas.md` (≥200 words).
|
||||
|
||||
### 2d. Events (`src/migration-events.ts`)
|
||||
|
||||
- [x] Define `MigrationEventMap` interface: `migration:before`, `migration:after`, `migration:failed`, `migration:plan`.
|
||||
- [x] Export `migrationEvents` — a lazily-initialised `EventBus<MigrationEventMap>` singleton from `@suaveplan/events`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Constants (`src/constants/index.ts`)
|
||||
|
||||
- [x] Export `DEFAULT_MIGRATION_TABLE = 'migrations'`, `DEFAULT_LOCK_TTL_MS = 30_000`, `DEFAULT_BATCH_CONCURRENCY = 5`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Planner (`src/planner/migration-planner/`)
|
||||
|
||||
- [x] Implement `sortMigrations(migrations)` — sort ascending by migration `id` (lexicographic).
|
||||
- [x] Implement `checksumMigration(migration)` — serialise `migration.up.toString()` and produce a hex hash (use `@suaveplan/crypto-utils` if available; otherwise use Web Crypto `crypto.subtle.digest('SHA-256', ...)`).
|
||||
- [x] Implement `planUp(all, applied)`:
|
||||
- Sort `all` with `sortMigrations`.
|
||||
- Filter to migrations whose `id` is not in `applied` (by id + checksum if `checksumValidation`).
|
||||
- Return `{ toApply, toRollback: [] }`.
|
||||
- [x] Implement `planDown(all, applied, steps?)`:
|
||||
- Return `steps` most-recently-applied in reverse order for rollback.
|
||||
- Default `steps = 1`.
|
||||
- [x] Write `migration-planner.test.ts` (≤500 lines): happy path up/down, already applied skipped, checksum mismatch throws, empty sets, partial rollback with steps.
|
||||
- [x] Write `migration-planner.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 5. Store adapters
|
||||
|
||||
### 5a. Memory store (`src/store/memory-store/`)
|
||||
|
||||
- [x] Implement `MemoryMigrationStore` implementing `MigrationStoreAdapter`.
|
||||
- Internal `Map<string, MigrationRecord>`.
|
||||
- Thread-safety: all operations are synchronous but wrapped in `Promise.resolve()` to honour the async interface.
|
||||
- [x] Write `memory-store.test.ts`: record/getApplied/remove/clear.
|
||||
- [x] Write `memory-store.md` (≥200 words).
|
||||
|
||||
### 5b. Database store (`src/store/database-store/`)
|
||||
|
||||
- [x] Implement `DatabaseMigrationStore(config: DatabaseStoreConfig)`:
|
||||
- On construction, creates the ledger table if it does not exist (via `CREATE TABLE IF NOT EXISTS` on `config.adapter`).
|
||||
- `record` inserts; `remove` deletes; `getApplied` queries all rows; `clear` truncates.
|
||||
- Wraps errors as `MigrationError(MIGRATION_STORE_ERROR)`.
|
||||
- [x] Write `database-store.test.ts` (integration: `@suaveplan/db-sqlite` SQLite adapter in-memory database).
|
||||
- [x] Write `database-store.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 6. Lock adapters
|
||||
|
||||
### 6a. Memory lock (`src/lock/memory-lock/`)
|
||||
|
||||
- [x] Implement `MemoryMigrationLock` — in-process boolean flag; `acquire` with timeout using injectable clock.
|
||||
- [x] Write `memory-lock.test.ts`: acquire, double-acquire times out, release.
|
||||
- [x] Write `memory-lock.md` (≥200 words).
|
||||
|
||||
### 6b. Distributed lock (`src/lock/distributed-lock/`)
|
||||
|
||||
- [x] Implement `DistributedMigrationLock(config)` — delegates to `config.adapter.acquire/release`.
|
||||
- [x] Implement `InMemoryDistributedLockAdapter` — a test double implementing `DistributedLockAdapter` using an in-process Map.
|
||||
- [x] Write `distributed-lock.test.ts`.
|
||||
- [x] Write `distributed-lock.md` (≥200 words).
|
||||
|
||||
### 6c. Redis lock (`src/lock/redis-lock/`)
|
||||
|
||||
- [x] Implement `RedisDistributedLock(config)` — uses `config.client.set(key, value, { NX: true, PX: ttl })` for acquire; `config.client.del(key)` for release.
|
||||
- [x] Write `redis-lock.test.ts` — DI the `RedisLockClient` interface with a real in-memory stub (no vi.mock).
|
||||
- [x] Write `redis-lock.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 7. Executor (`src/runner/migration-executor/`)
|
||||
|
||||
- [x] Implement `MigrationExecutor(options)`:
|
||||
- `execute(plan, direction, runOptions?)`:
|
||||
- Checks `runOptions?.signal` before each migration; throws `MigrationError(MIGRATION_ABORTED)` on abort.
|
||||
- For each migration in plan: emits `migration:before`, calls `migration[direction](ctx)`, emits `migration:after` with duration, calls `store.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`, histogram `migration.duration_ms`.
|
||||
- Logs via injected `LoggerLike`.
|
||||
- [x] Write `migration-executor.test.ts` (unit: inject `MemoryMigrationStore` + `MemoryMigrationLock`).
|
||||
- [x] Write `migration-executor.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 8. Runner (`src/runner/migration-runner/`)
|
||||
|
||||
- [x] Implement `MigrationRunner(config)`:
|
||||
- `up(options?)`:
|
||||
1. Acquire lock (if configured); throw `MigrationError(MIGRATION_LOCK_TIMEOUT)` on failure.
|
||||
2. Validate checksums if `checksumValidation: true`.
|
||||
3. Call `planUp`; emit `migration:plan`.
|
||||
4. Delegate to `MigrationExecutor.execute(plan, 'up', options)`.
|
||||
5. Release lock in `finally`.
|
||||
6. OTel: span `migration.run` with attribute `migration.direction = 'up'`.
|
||||
- `down(options?)`: analogous with `planDown`.
|
||||
- `status()` — compares `config.migrations` against `store.getApplied()`, returns `MigrationStatus[]`.
|
||||
- `validate()` — checks checksums for all applied migrations, returns `ValidationResult`.
|
||||
- [x] Write `migration-runner.test.ts` (≤500 lines): up/down happy path, abort mid-run, checksum mismatch, lock contention, dry-run.
|
||||
- [x] Write `migration-runner.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 9. Dry-run reporter (`src/runner/dry-run-reporter/`)
|
||||
|
||||
- [x] Implement `DryRunReporter`: `record(entry)` appends; `report()` computes `totalRisk` (highest entry risk), generates plain-text `summary`.
|
||||
- [x] Write `dry-run-reporter.test.ts`.
|
||||
- [x] Write `dry-run-reporter.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 10. Status dashboard (`src/status/migration-status/`)
|
||||
|
||||
- [x] 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.
|
||||
- [x] Write `migration-status.test.ts`.
|
||||
- [x] Write `migration-status.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 11. Batch migrator (`src/batch/batch-migrator/`)
|
||||
|
||||
- [x] Implement `BatchMigrator(config)` — runs `config.targets` runners concurrently up to `config.concurrency` (default 5) using a semaphore; collects results.
|
||||
- [x] Write `batch-migrator.test.ts`: two targets, one fails; concurrency respected.
|
||||
- [x] Write `batch-migrator.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 12. SurrealDB adapter (`src/adapters/surrealdb-adapter/`)
|
||||
|
||||
- [x] Implement `SurrealDbAdapter(config)` wrapping a SurrealDB `Surreal` instance (typed as `unknown` to avoid hard dep on `surrealdb.js` — callers cast):
|
||||
- `query<T>(sql, params?)` — calls `db.query(sql, params)`, flattens the `QueryResult<T>[][]` to `T[]`.
|
||||
- Note: avoid `db.create()` — use `INSERT INTO` query (see memory `feedback_surrealdb_create_content_drop`).
|
||||
- [x] Write `surrealdb-adapter.test.ts` — DI a mock `{ query }` conforming to `SurrealDbAdapterConfig`.
|
||||
- [x] Write `surrealdb-adapter.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 13. Property-based tests (`tests/property.test.ts`)
|
||||
|
||||
Use `fast-check`. All timeouts: `{ timeout: 30000 }`.
|
||||
|
||||
- [x] **`planUp` idempotency**: apply all migrations, then call `planUp` again — `toApply` is empty.
|
||||
- [x] **`sortMigrations` stability**: for any permutation of N migrations, `sortMigrations` returns the same order.
|
||||
- [x] **`checksumMigration` determinism**: for the same migration object, checksum called twice returns the same string.
|
||||
- [x] **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`.
|
||||
|
||||
- [x] `planUp_100`: plan for 100 pending migrations (pure, no IO).
|
||||
- [x] `runner_up_10_memory`: run 10 no-op migrations against `MemoryMigrationStore`.
|
||||
- [x] `checksumMigration_1k`: 1000 checksum computations.
|
||||
- [x] Add `bench` script and placeholder benchmark table to `README.md`.
|
||||
|
||||
---
|
||||
|
||||
## 15. Docs
|
||||
|
||||
- [x] `src/types/migration.types.md`
|
||||
- [x] `src/types/schemas.md`
|
||||
- [x] `src/planner/migration-planner/migration-planner.md`
|
||||
- [x] `src/runner/migration-runner/migration-runner.md`
|
||||
- [x] `src/runner/migration-executor/migration-executor.md`
|
||||
- [x] `src/runner/dry-run-reporter/dry-run-reporter.md`
|
||||
- [x] `src/store/memory-store/memory-store.md`
|
||||
- [x] `src/store/database-store/database-store.md`
|
||||
- [x] `src/lock/memory-lock/memory-lock.md`
|
||||
- [x] `src/lock/distributed-lock/distributed-lock.md`
|
||||
- [x] `src/lock/redis-lock/redis-lock.md`
|
||||
- [x] `src/status/migration-status/migration-status.md`
|
||||
- [x] `src/batch/batch-migrator/batch-migrator.md`
|
||||
- [x] `src/adapters/surrealdb-adapter/surrealdb-adapter.md`
|
||||
- [x] Root `universal/database/migration/README.md`.
|
||||
|
||||
---
|
||||
|
||||
## 16. Verify and publish
|
||||
|
||||
- [x] Run `bunx turbo lint --filter=@suaveplan/migration` — zero errors.
|
||||
- [x] Run `bunx turbo typecheck --filter=@suaveplan/migration` — zero errors.
|
||||
- [x] Run `bunx turbo test --filter=@suaveplan/migration` — all tests pass.
|
||||
- [x] Run `bunx turbo test:coverage --filter=@suaveplan/migration` — 100% on all touched modules.
|
||||
- [x] Run `bun run docs:check --filter=@suaveplan/migration` — zero errors.
|
||||
- [x] Confirm no `TODO`, `FIXME`, `@deprecated`, untyped `any`, `ts-ignore`, or `.skip` in `src/` or `tests/`.
|
||||
- [x] Confirm `"sideEffects": false` in `package.json`.
|
||||
- [x] Run `bun scripts/publish.ts --package @suaveplan/migration`.
|
||||
- [x] Update `CLAUDE.md` package listing to include `@suaveplan/migration`.
|
||||
- [x] Archive: move `openspec/changes/enterprise-migration/` to `openspec/archive/2026-04-24-enterprise-migration/` and fold spec delta into `openspec/specs/database/spec.md`.
|
||||
Reference in New Issue
Block a user