@wentools/schema-migrations
v0.1.0
Published
Type-safe schema migrations with entry-only validation strategy
Maintainers
Readme
@wentools/schema-migrations
Type-safe schema migrations with compile-time chain validation and entry-only runtime validation.
Why This Exists
Local-first apps store data in IndexedDB, localStorage, or similar. When your data shape changes, you need migrations — but most migration tools target SQL databases, not JS objects.
This library provides:
- Compile-time chain validation — TypeScript catches misordered or mismatched migrations before they run
- Entry-only validation — Zod validates once at the entry point, then trusts types through the chain (fast)
- Result-based errors — no thrown exceptions, every failure is typed and trackable
- Zero coupling to storage — works with any storage backend, you control when and where migrations run
Install
# JSR (recommended)
npx jsr add @wentools/schema-migrations
# Deno
deno add jsr:@wentools/schema-migrationsUsage
Define Schemas and Migrations
import { z } from 'zod'
import { createMigration, createMigrations } from '@wentools/schema-migrations'
const v0Schema = z.object({ name: z.string() })
const v1Schema = z.object({ name: z.string(), description: z.string() })
const v2Schema = z.object({ name: z.string(), description: z.string(), capacity: z.number() })
const eventMigrations = createMigrations(v0Schema, [
createMigration(v0Schema, v1Schema, (v0) => ({ ...v0, description: '' })),
createMigration(v1Schema, v2Schema, (v1) => ({ ...v1, capacity: 100 })),
])Reorder the migrations and TypeScript will error — each migration must accept the previous one's output.
Migrate Data
import { migrate } from '@wentools/schema-migrations'
// From version 0 to current
const result = migrate(eventMigrations, { version: 0, data: { name: 'Concert' } })
if (result.isOk()) {
console.log(result.value)
// { version: 2, data: { name: 'Concert', description: '', capacity: 100 } }
}
// From intermediate version
migrate(eventMigrations, { version: 1, data: { name: 'Concert', description: 'Live' } })
// To specific target version (not necessarily current)
migrate(eventMigrations, { version: 0, data: { name: 'Concert' } }, 1)
// Version defaults to 0 when omitted
migrate(eventMigrations, { data: { name: 'Concert' } })Stamp New Data
import { withVersion } from '@wentools/schema-migrations'
const newEvent = { name: 'Concert', description: '', capacity: 200 }
const versioned = withVersion(eventMigrations, newEvent)
// { version: 2, data: newEvent }Error Handling
const result = migrate(eventMigrations, { version: 0, data: { name: 123 } })
if (result.isErr()) {
switch (result.error.type) {
case 'version_out_of_range':
// version or targetVersion outside [0, currentVersion]
break
case 'invalid_input':
// data doesn't match claimed version's schema (Zod error in .cause)
break
case 'migration_threw':
// migration function threw (.step, .cause, .data for debugging)
break
}
}API
Functions
| Function | Description |
|----------|-------------|
| migrate(config, versionedData, targetVersion?) | Migrate data to target version (defaults to current) |
| withVersion(config, data) | Wrap data with current version number |
| createMigrations(initialSchema, steps) | Create migration config with compile-time chain validation |
| createMigration(fromSchema, toSchema, fn) | Create a typed migration step |
Primitives
| Function | Description |
|----------|-------------|
| migrateRange(config, data, from, to) | Migrate through version range with entry validation |
| migrateOneStep(fn, data, step) | Execute single migration with error tracking |
Types
| Type | Description |
|------|-------------|
| MigrationConfig<TSchema> | Config object with schemas, migrations, currentVersion, currentSchema |
| VersionedData<TData> | { version?: number; data: TData } |
| MigrateError | Union of all migration errors |
| VersionOutOfRangeError | Version outside valid range |
| InvalidInputError | Data fails schema validation |
| MigrateOneStepError | Migration function threw |
| MigrateRangeError | InvalidInputError | MigrateOneStepError |
Validation Strategy
This library uses entry-only validation: data is validated with Zod once at the starting version, then migrations run without intermediate validation. This is fast and sufficient — if your migration functions are correct (which TypeScript enforces at compile time), intermediate validation is redundant.
Requirements
- TypeScript 5.0+
- Zod 4.x (peer dependency)
- @wentools/result 0.1.x (peer dependency)
License
MIT
