Driver-Agnostic Database Migrations
Ley is a lightweight, driver-agnostic database migration tool for Node.js, currently at version 0.8.1. It provides both a command-line interface (CLI) and a programmatic API for managing database schema changes. Ley's core differentiators include its agnosticism towards specific database drivers (supporting `pg`, `postgres`, `mysql`, `mysql2`, `better-sqlite3`, and custom drivers without bundling them), its lightweight nature, and its transactional approach to migrations, ensuring atomicity for each change. It emphasizes working directly with your chosen driver's API, avoiding new abstractions, and enforces an append-only, immutable task chain for migrations to maintain database integrity across environments. Releases are consistent, with recent updates focusing on ESM support and improved TypeScript integration.
Common errors
-
Ley: No migrations found in './migrations'
cause The configured 'migrations' directory is missing, empty, or the path is incorrect.fixEnsure the `migrations` directory exists in your project root or at the path specified in `ley.config.js` or via CLI options. Use `npx ley new <name>` to create your first migration file. -
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for <path>/ley.config.ts
cause Node.js cannot directly execute TypeScript files without a module loader.fixInstall `tsm` (`npm install --save-dev tsm`) and update your `package.json` scripts to invoke `ley` using the `tsm` loader, e.g., `"migrate:up": "node --loader tsm ley up"`. -
Error: Unknown driver: 'my-custom-driver-name' or TypeError: driver.connect is not a function
cause The database driver specified in `ley.config.js` or via CLI is not installed, or the provided driver object does not conform to Ley's driver interface.fixInstall the required database driver (e.g., `npm install pg`). If using a custom driver, ensure it exports an object with a `connect` method that returns an object conforming to Ley's `LeyClient` interface (e.g., `query`, `release`, `start`, `commit`, `rollback`). -
ReferenceError: exports is not defined in ES module scope
cause A migration file is using CommonJS `exports.up` syntax in a project or environment configured for Node.js ES Modules (e.g., `"type": "module"` in `package.json`).fixGenerate new migration files using `npx ley new --esm` and update existing ones to use ESM `export async function up(...) {}` syntax. Alternatively, if your project should be CJS, remove `"type": "module"` from `package.json`.
Warnings
- breaking In `v0.6.0`, the `--client` CLI option and `opts.client` programmatic option were removed. They have been replaced by `--driver` and `opts.driver` respectively, to better reflect the driver-agnostic nature of the tool.
- gotcha Ley enforces an append-only, immutable task chain for migrations. Attempting to modify or insert migrations out of their original sequence can lead to errors and inconsistent database states, especially when collaborating or deploying.
- gotcha Ley is driver-agnostic and does not bundle any database drivers. You must explicitly install and configure your desired database driver (e.g., `pg` for PostgreSQL, `mysql2` for MySQL) as a dependency in your project.
- gotcha For TypeScript migrations or configuration files (`ley.config.ts`), `tsm` is the recommended module loader. Using `ts-node` might lead to unexpected behavior or require additional configuration due to how `ley` resolves modules.
- gotcha When working with Node.js ES Modules, migration files and `ley.config.js` should use `import/export` syntax. If your project has `"type": "module"` in `package.json`, or you generate migrations with `ley new --esm`, ensure your files comply, otherwise, you may encounter CJS/ESM compatibility issues.
Install
-
npm install ley -
yarn add ley -
pnpm add ley
Imports
- run
const { run } = require('ley')import { run } from 'ley' - Ley CLI
node ley <command>
npx ley <command>
- Migration File Exports (ESM)
exports.up = async function (sql) { /* ... */ }export async function up(sql: Client) { /* ... */ } - Migration File Exports (CJS)
export async function up(sql: Client) { /* ... */ }exports.up = async function (sql) { /* ... */ }
Quickstart
{
"name": "my-ley-project",
"version": "1.0.0",
"description": "Ley quickstart example",
"type": "module", // Optional, but useful for ESM migrations
"scripts": {
"migrate:new": "npx ley new",
"migrate:up": "node --loader tsm ley up",
"migrate:down": "node --loader tsm ley down"
},
"devDependencies": {
"ley": "^0.8.1",
"pg": "^8.11.3",
"tsm": "^2.3.0"
}
}
// ley.config.ts
import { Pool } from 'pg';
import type { LeyDriver } from 'ley';
const driver: LeyDriver = {
connect: async () => {
const pool = new Pool({
connectionString: process.env.DATABASE_URL ?? 'postgresql://user:password@localhost:5432/mydb',
});
const client = await pool.connect();
return {
query: client.query.bind(client),
release: client.release.bind(client),
start: () => client.query('BEGIN'),
commit: () => client.query('COMMIT'),
rollback: () => client.query('ROLLBACK'),
};
},
};
export default {
driver: driver,
migrations: './migrations',
};
// migrations/0000_initial.ts (generated by 'npx ley new initial --esm' then edited)
import type { PoolClient } from 'pg';
export async function up(sql: PoolClient) {
await sql.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);
`);
await sql.query(`
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
`);
}
export async function down(sql: PoolClient) {
await sql.query(`DROP TABLE IF EXISTS users;`);
}