npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kysera/migrations

v0.8.7

Published

Database migration management for Kysely with dry-run support and flexible rollback capabilities

Readme

@kysera/migrations

Lightweight, type-safe database migration management for Kysera with dry-run support, flexible rollback capabilities, and plugin system.

npm version License: MIT TypeScript

Features

Core Migration Management

  • Simple API - Intuitive migration creation and execution
  • Type-safe - Full TypeScript support with Kysely integration
  • State tracking - Automatic migration history in database
  • Sequential execution - Migrations run in order
  • Dry run mode - Preview changes before execution

Advanced Features

  • Rollback support - Roll back one or multiple migrations
  • Partial migration - Run up to specific migration
  • Status reporting - View executed and pending migrations
  • Error handling - Typed errors with MigrationError class
  • Transaction support - Optional transaction wrapping per migration
  • Duplicate detection - Validates unique migration names

Developer Experience (v0.5.0+)

  • defineMigrations() - Object-based migration definition
  • runMigrations() - One-liner to run pending migrations
  • rollbackMigrations() - One-liner for rollbacks
  • Migration metadata - Description, breaking flag, tags, timing

Plugin System (v0.5.0+)

  • Plugin hooks - Before/after migration events
  • Built-in plugins - Logging and metrics plugins
  • Extensible - Create custom plugins for your needs

Installation

# pnpm (recommended)
pnpm add @kysera/migrations kysely zod

# npm
npm install @kysera/migrations kysely zod

# yarn
yarn add @kysera/migrations kysely zod

# bun
bun add @kysera/migrations kysely zod

Note: Zod is a required peer dependency (not optional) for schema validation in the migration system.

Quick Start

Basic Usage

import { Kysely } from 'kysely'
import { createMigrationRunner, createMigration } from '@kysera/migrations'

// Define your migrations
const migrations = [
  createMigration(
    '001_create_users',
    async db => {
      await db.schema
        .createTable('users')
        .addColumn('id', 'serial', col => col.primaryKey())
        .addColumn('email', 'varchar(255)', col => col.notNull().unique())
        .addColumn('name', 'varchar(255)', col => col.notNull())
        .execute()
    },
    async db => {
      await db.schema.dropTable('users').execute()
    }
  ),

  createMigration(
    '002_create_posts',
    async db => {
      await db.schema
        .createTable('posts')
        .addColumn('id', 'serial', col => col.primaryKey())
        .addColumn('user_id', 'integer', col =>
          col.notNull().references('users.id').onDelete('cascade')
        )
        .addColumn('title', 'varchar(255)', col => col.notNull())
        .execute()
    },
    async db => {
      await db.schema.dropTable('posts').execute()
    }
  )
]

// Create migration runner
const db = new Kysely<Database>({
  /* ... */
})
const runner = createMigrationRunner(db, migrations)

// Run all pending migrations
await runner.up()

// Check status
await runner.status()

// Rollback last migration
await runner.down(1)

Minimalist API (v0.5.0+)

import { Kysely } from 'kysely'
import { defineMigrations, runMigrations } from '@kysera/migrations'

// Define migrations with object syntax
const migrations = defineMigrations({
  '001_create_users': {
    description: 'Create users table with email and name',
    up: async db => {
      await db.schema
        .createTable('users')
        .addColumn('id', 'serial', col => col.primaryKey())
        .addColumn('email', 'varchar(255)', col => col.notNull().unique())
        .execute()
    },
    down: async db => {
      await db.schema.dropTable('users').execute()
    }
  },

  '002_create_posts': {
    description: 'Create posts table',
    breaking: false,
    tags: ['schema'],
    up: async db => {
      await db.schema
        .createTable('posts')
        .addColumn('id', 'serial', col => col.primaryKey())
        .addColumn('title', 'varchar(255)', col => col.notNull())
        .execute()
    }
  }
})

// One-liner to run all migrations
await runMigrations(db, migrations)

Dry Run Mode

import { runMigrations } from '@kysera/migrations'

// Preview what would happen without making changes
const result = await runMigrations(db, migrations, { dryRun: true })

console.log('Would execute:', result.executed)
console.log('Would skip:', result.skipped)
// No actual changes made to database

API Reference

Types

Migration

interface Migration {
  name: string
  up: (db: Kysely<any>) => Promise<void>
  down?: (db: Kysely<any>) => Promise<void>
}

MigrationWithMeta

interface MigrationWithMeta extends Migration {
  description?: string // Shown in logs
  breaking?: boolean // Shows warning
  estimatedDuration?: number // In milliseconds
  tags?: string[] // For categorization
}

MigrationStatus

interface MigrationStatus {
  executed: string[]
  pending: string[]
  total: number
}

MigrationResult

interface MigrationResult {
  executed: string[] // Successfully executed
  skipped: string[] // Already executed or no down()
  failed: string[] // Failed migrations
  duration: number // Total time in ms
  dryRun: boolean // Whether dry run mode
}

MigrationRunnerOptions

interface MigrationRunnerOptions {
  dryRun?: boolean // Preview only (default: false)
  logger?: KyseraLogger // Logger instance from @kysera/core (default: silentLogger)
  useTransactions?: boolean // Wrap in transactions (default: false)
  stopOnError?: boolean // Stop on first error (default: true)
  verbose?: boolean // Show metadata (default: true)
}

MigrationDefinition

interface MigrationDefinition {
  up: (db: Kysely<any>) => Promise<void>
  down?: (db: Kysely<any>) => Promise<void>
  description?: string
  breaking?: boolean
  estimatedDuration?: number
  tags?: string[]
}

MigrationDefinitions

type MigrationDefinitions = Record<string, MigrationDefinition>

MigrationRunnerWithPluginsOptions

interface MigrationRunnerWithPluginsOptions extends MigrationRunnerOptions {
  plugins?: MigrationPlugin[]
}

MigrationErrorCode

type MigrationErrorCode =
  | 'MIGRATION_UP_FAILED'
  | 'MIGRATION_DOWN_FAILED'
  | 'MIGRATION_VALIDATION_FAILED'

Factory Functions

createMigration(name, up, down?)

Create a simple migration:

const migration = createMigration(
  '001_create_users',
  async db => {
    /* up */
  },
  async db => {
    /* down */
  }
)

createMigrationWithMeta(name, options)

Create a migration with metadata:

const migration = createMigrationWithMeta('001_create_users', {
  description: 'Create users table',
  breaking: true,
  tags: ['schema', 'users'],
  estimatedDuration: 5000,
  up: async db => {
    /* ... */
  },
  down: async db => {
    /* ... */
  }
})

createMigrationRunner(db, migrations, options?)

Create a MigrationRunner instance:

const runner = createMigrationRunner(db, migrations, {
  dryRun: false,
  logger: console.log,
  useTransactions: true
})

createMigrationRunnerWithPlugins(db, migrations, options?)

Create a MigrationRunner with plugin support (async factory):

const runner = await createMigrationRunnerWithPlugins(db, migrations, {
  plugins: [createLoggingPlugin(), createMetricsPlugin()],
  useTransactions: true
})

// Runner is ready with plugins initialized via onInit
await runner.up()

One-Liner Functions (v0.5.0+)

defineMigrations(definitions)

Define migrations using object syntax:

const migrations = defineMigrations({
  '001_users': {
    description: 'Create users',
    up: async db => {
      /* ... */
    },
    down: async db => {
      /* ... */
    }
  }
})

runMigrations(db, migrations, options?)

Run all pending migrations:

const result = await runMigrations(db, migrations)
const result = await runMigrations(db, migrations, { dryRun: true })

rollbackMigrations(db, migrations, steps?, options?)

Rollback migrations:

await rollbackMigrations(db, migrations) // Last 1
await rollbackMigrations(db, migrations, 3) // Last 3
await rollbackMigrations(db, migrations, 1, { dryRun: true })

getMigrationStatus(db, migrations, options?)

Get migration status:

const status = await getMigrationStatus(db, migrations)
console.log(`Executed: ${status.executed.length}`)
console.log(`Pending: ${status.pending.length}`)

MigrationRunner Methods

up(): Promise<MigrationResult>

Run all pending migrations:

const result = await runner.up()
console.log(`Executed: ${result.executed.length} migrations`)

down(steps?): Promise<MigrationResult>

Rollback last N migrations:

await runner.down(1) // Rollback last one
await runner.down(3) // Rollback last three

status(): Promise<MigrationStatus>

Get migration status:

const status = await runner.status()
// Logs status to console and returns object

reset(): Promise<MigrationResult>

Rollback all migrations:

await runner.reset() // Dangerous! Rolls back everything

upTo(targetName): Promise<MigrationResult>

Run migrations up to a specific one:

await runner.upTo('002_create_posts')
// Runs 001 and 002, stops before 003

getExecutedMigrations(): Promise<string[]>

Get list of executed migrations:

const executed = await runner.getExecutedMigrations()

markAsExecuted(name): Promise<void>

Manually mark a migration as executed:

await runner.markAsExecuted('001_create_users')

markAsRolledBack(name): Promise<void>

Manually mark a migration as rolled back:

await runner.markAsRolledBack('001_create_users')

Standalone Functions

setupMigrations(db)

Manually create the migrations tracking table:

import { setupMigrations } from '@kysera/migrations'

await setupMigrations(db)
// Creates migrations table if not exists

Plugin System (v0.5.0+)

Plugin Interface

Consistent with @kysera/repository Plugin interface:

interface MigrationPlugin {
  name: string
  version: string
  // Called once when runner is initialized (consistent with repository Plugin.onInit)
  onInit?(runner: MigrationRunner): Promise<void> | void
  beforeMigration?(migration: Migration, operation: 'up' | 'down'): Promise<void> | void
  afterMigration?(
    migration: Migration,
    operation: 'up' | 'down',
    duration: number
  ): Promise<void> | void
  // Unknown error type for consistency with repository Plugin.onError
  onMigrationError?(
    migration: Migration,
    operation: 'up' | 'down',
    error: unknown
  ): Promise<void> | void
}

Built-in Plugins

Logging Plugin

import { createLoggingPlugin } from '@kysera/migrations'

const loggingPlugin = createLoggingPlugin(console.log)
// or with custom logger
const loggingPlugin = createLoggingPlugin(msg => logger.info(msg))

Metrics Plugin

import { createMetricsPlugin } from '@kysera/migrations'

const metricsPlugin = createMetricsPlugin()

// After running migrations
const metrics = metricsPlugin.getMetrics()
console.log(metrics.migrations)
// [{ name: '001_users', operation: 'up', duration: 45, success: true }, ...]

Creating Custom Plugins

const notificationPlugin: MigrationPlugin = {
  name: 'notification-plugin',
  version: '1.0.0',

  // Called when runner is created via createMigrationRunnerWithPlugins()
  async onInit(runner) {
    console.log('Notification plugin initialized')
  },

  async beforeMigration(migration, operation) {
    await slack.send(`Starting ${operation} for ${migration.name}`)
  },

  async afterMigration(migration, operation, duration) {
    await slack.send(`Completed ${migration.name} in ${duration}ms`)
  },

  async onMigrationError(migration, operation, error) {
    // Error is unknown type - handle appropriately
    const message = error instanceof Error ? error.message : String(error)
    await pagerduty.alert(`Migration failed: ${message}`)
  }
}

Error Handling

MigrationError

Extends DatabaseError from @kysera/core for consistency:

import { MigrationError } from '@kysera/migrations'

try {
  await runner.up()
} catch (error) {
  if (error instanceof MigrationError) {
    console.log('Migration:', error.migrationName)
    console.log('Operation:', error.operation) // 'up' or 'down'
    console.log('Code:', error.code) // 'MIGRATION_UP_FAILED' or 'MIGRATION_DOWN_FAILED'
    console.log('Cause:', error.cause?.message)

    // Serialize for logging
    console.log(error.toJSON())
    // { name, message, code, detail, migrationName, operation, cause }
  }
}

BadRequestError

For validation errors (e.g., duplicate migration names):

import { BadRequestError } from '@kysera/migrations'

try {
  createMigrationRunner(db, [
    createMigration('001_users', ...),
    createMigration('001_users', ...), // Duplicate!
  ])
} catch (error) {
  if (error instanceof BadRequestError) {
    console.log(error.message) // "Duplicate migration name: 001_users"
    console.log(error.code)    // "BAD_REQUEST"
  }
}

NotFoundError

Uses NotFoundError from @kysera/core:

import { NotFoundError } from '@kysera/migrations'

try {
  await runner.upTo('nonexistent_migration')
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log(error.message) // "Migration not found"
  }
}

Best Practices

1. Use Numeric Prefixes

// Good - clear ordering
'001_create_users'
'002_create_posts'
'003_add_indexes'

// Bad - no guaranteed order
'create_users'
'create_posts'

2. Always Provide down() Methods

createMigration(
  '001_create_users',
  async db => {
    /* up */
  },
  async db => {
    /* down - always provide this! */
  }
)

3. Use Metadata for Complex Migrations

createMigrationWithMeta('005_big_refactor', {
  description: 'Refactors user permissions system',
  breaking: true, // Will show warning
  tags: ['breaking', 'permissions'],
  up: async db => {
    /* ... */
  }
})

4. Test with Dry Run First

// Preview in production
await runMigrations(db, migrations, { dryRun: true })

// Then run for real
await runMigrations(db, migrations)

5. Use Transactions for Safety

const runner = createMigrationRunner(db, migrations, {
  useTransactions: true // Each migration wrapped in transaction
})

Migration Script Example

// scripts/migrate.ts
import { Kysely, PostgresDialect } from 'kysely'
import { Pool } from 'pg'
import {
  runMigrations,
  rollbackMigrations,
  getMigrationStatus,
  defineMigrations
} from '@kysera/migrations'

const migrations = defineMigrations({
  // ... your migrations
})

async function main() {
  const db = new Kysely({
    dialect: new PostgresDialect({
      pool: new Pool({ connectionString: process.env.DATABASE_URL })
    })
  })

  const command = process.argv[2]

  try {
    switch (command) {
      case 'up':
        console.log('Running migrations...')
        const upResult = await runMigrations(db, migrations)
        console.log(`Executed: ${upResult.executed.length} migrations`)
        break

      case 'down':
        const steps = parseInt(process.argv[3] || '1')
        console.log(`Rolling back ${steps} migration(s)...`)
        await rollbackMigrations(db, migrations, steps)
        break

      case 'status':
        await getMigrationStatus(db, migrations)
        break

      case 'dry-run':
        console.log('Dry run mode...')
        await runMigrations(db, migrations, { dryRun: true })
        break

      default:
        console.log('Usage: pnpm migrate [up|down|status|dry-run] [steps]')
    }
  } finally {
    await db.destroy()
  }
}

main()
// package.json
{
  "scripts": {
    "migrate": "tsx scripts/migrate.ts up",
    "migrate:down": "tsx scripts/migrate.ts down",
    "migrate:status": "tsx scripts/migrate.ts status",
    "migrate:dry-run": "tsx scripts/migrate.ts dry-run"
  }
}

Multi-Database Support

PostgreSQL

createMigration('001_create_users', async db => {
  await db.schema
    .createTable('users')
    .addColumn('id', 'serial', col => col.primaryKey())
    .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(db.fn('now')))
    .execute()
})

MySQL

createMigration('001_create_users', async db => {
  await db.schema
    .createTable('users')
    .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
    .addColumn('created_at', 'datetime', col => col.notNull().defaultTo(db.fn('now')))
    .execute()
})

SQLite

createMigration('001_create_users', async db => {
  await db.schema
    .createTable('users')
    .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
    .addColumn('created_at', 'text', col => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))
    .execute()
})

Implementation Details

Version File Fallback Pattern

The migration system uses a robust version file fallback pattern to ensure compatibility across different build environments:

  1. Primary: Try to load version.ts (development/source code)
  2. Fallback: Try to load version.js (compiled/production code)
  3. Default: Use '0.0.0-dev' if both fail

This pattern ensures migrations work correctly in both development and production environments without requiring specific build configurations.

Changelog

v0.5.1

  • Breaking MigrationError now extends DatabaseError from @kysera/core with code property
  • Breaking onMigrationError hook now receives error: unknown (consistent with repository Plugin)
  • Breaking createMigrationRunnerWithPlugins() is now async (returns Promise<MigrationRunnerWithPlugins>)
  • Added onInit hook to MigrationPlugin interface (consistent with repository Plugin)
  • Added MigrationErrorCode type export
  • Added MigrationDefinition and MigrationDefinitions type exports
  • Added MigrationRunnerWithPluginsOptions interface export
  • Added DatabaseError and BadRequestError re-exports from @kysera/core
  • Added Version file fallback pattern for cross-environment compatibility
  • Changed Validation errors now throw BadRequestError instead of generic Error

v0.5.0

  • Added @kysera/core integration with typed errors
  • Added MigrationWithMeta support with description, breaking flag, tags
  • Added defineMigrations() for object-based syntax
  • Added runMigrations(), rollbackMigrations(), getMigrationStatus() one-liners
  • Added MigrationResult return type for all operations
  • Added Plugin system with MigrationPlugin interface
  • Added Built-in createLoggingPlugin() and createMetricsPlugin()
  • Added MigrationError class for better error handling
  • Added Duplicate migration name validation
  • Added useTransactions option for transaction wrapping
  • Added stopOnError option for error handling control
  • Fixed Inconsistent dry run behavior in reset() and upTo()
  • Fixed MigrationStatus now includes total count

v0.4.1

  • Initial release

Related Packages

License

MIT