Database framework-agnostic package for DIY migrations solutions with zero dependencies, written in TypeScript.
With sql-migrations-core
you can write sql migrations as you want. Use ANY database framework and prepare CLI as you feel comfortable.
Install with npm
:
npm i sql-migrations-core
Or with yarn
:
yarn add sql-migrations-core
...Or with pnpm
:
pnpm add sql-migrations-core
Minimal configuration looks like this:
const migrationsCore = MigrationsCore.create({
path: path.join('..', 'path/to/migrations/dir'),
sqlActions: {
async createMigrationTable() {
// TODO implement
},
async getMigrationsNames() {
// TODO implement
return [];
},
async migrateDown(migrations) {
// TODO implement
},
async migrateUp(migrations) {
// TODO implement
},
},
});
There is a trade-off - you can use any db framework, but you need to implement SQL queries by yourself. For example, with kysely:
import { db } from './database';
// Probably you'll want to move SQL logic to repository.
// In this example I'm just showing a minimal SQL implementation.
const migrationsCore = MigrationsCore.create({
path: path.join("/migrations"),
sqlActions: {
async createMigrationTable(): Promise<void> {
await db.schema
.createTable("__migration")
.ifNotExists()
.addColumn("name", "varchar", (cb) => cb.notNull().unique())
.addColumn("migrated_at", "timestamp", (cb) =>
cb.notNull().defaultTo(sql`current_timestamp`)
)
.execute();
},
async getMigrationsNames(): Promise<string[]> {
const records = await db
.selectFrom("__migration")
.select("name")
.execute();
return records.map((r) => r.name);
},
async migrateDown(migrations: Migration[]): Promise<void> {
await db.transaction().execute(async (trx) => {
for (const migration of migrations) {
await trx
.deleteFrom("__migration")
.where("name", "=", migration.name)
.execute();
await sql.raw(migration.sql).execute(trx);
}
});
},
async migrateUp(migrations: Migration[]): Promise<void> {
await db.transaction().execute(async (trx) => {
for (const migration of migrations) {
await trx
.insertInto("__migration")
.values({ name: migration.name })
.execute();
await sql.raw(migration.sql).execute(trx);
}
});
},
},
});
See other examples here.
After initializing migrationsCore
you can use methods:
await migrationsCore.createFiles('example'); // creates blank sql files
await migrationsCore.up(); // one migration up
await migrationsCore.down(); // one migration down
await migrationsCore.sync(); // all pending up migrations
await migrationsCore.drop(); // all down migrations of executed migrations
await migrationsCore.toLatest(); // all pending up migrations from last executed
await migrationsCore.to('123-example'); // all up/down migrations between last executed and provided
await migrationsCore.status(); // get statuses of migrations (name and is synced)
Also, you can pass chunkSize
in some methods to run migrations by chunks in synchronous way:
await migrationsCore.sync(100); // runs migrations in chunks limit by 100
See API for more info.
Migrations core creates 2 migration files - up and down. It uses {timestamp}-${title}${postfix}
format (name of migration is {timestamp}-{title}
):
await migrationsCore.createFile('example');
// Will create something like this
// ./migrations/1718394484921-example.down.sql
// ./migrations/1718394484921-example.up.sql
There is no out-of-box CLI solution, so you need implement one by yourself. For example, create migration file:
import { migrationsCore } from './migrations-core';
import path from 'node:path';
const migrationsPath = path.join('./migrations');
migrationsCore.createFile(process.args[2]);
Usage:
node ./scripts/migrations-create.js example
See other CLI examples here.
There are several classes available to use:
MigrationsCore
- main class that gives access to all needed migration actions.LocalMigrations
- class used byMigrationsCore
. Hides all filesystem operations to access local migrations. For advanced usage.StoredMigrations
- class used byMigrationsCore
. Hides all database operations to run migrations and access metadata of executed migrations. For advanced usage.
There are two ways to create instance of MigrationsCore
:
- Using static
MigrationsCore.create()
method - Using
new MigrationsCore()
constructor
create()
method receives config object and returns instance of MigrationsCore
.
Actually it's constructor wrapper, which creates LocalMigrations
and StoredMigrations
by itself. Config options are passed to these objects. Config options:
path
- string, path to migrations directory. Used byLocalMigrations
.postfix
- object, optional, custom postfix for migrations files. Used byLocalMigrations
.postfix.up
- string, postfix for up migrations (default:.up.sql
).postfix.down
- string, postfix for down migrations (default:.down.sql
).sqlActions
- object, postfix for down migrations (default:.down.sql
). Used byStoredMigrations
.sqlActions.createMigrationTable
- function, used to create table before everyStoredMigrations
action. Recommended to use withIF NOT EXISTS
statement.sqlActions.migrateUp
- function, used to run up migrations. Receives array of migration objects.sqlActions.migrateDown
- function, used to run down migrations. Receives array of migration objects.sqlActions.getMigrationsNames
- function, used to get executed migrations names. Should return array of migration names.sqlActions.getLastMigrationName
- function, optional, used to get last executed migrations name. Should return name or null. If function not provided -getMigrationsNames
used instead.
Example:
const migrationsCore = MigrationsCore.create({
path: path.join(__dirname, '../migrations'),
postfix: {
up: '.custom.up.sql',
down: '.custom.down.sql',
},
sqlActions: {
createMigrationTable() { /* ... */ },
migrateUp(migrations) { /* ... */ },
migrateDown(migrations) { /* ... */ },
getMigrationsNames() { /* ... */ },
getLastMigrationName() { /* ... */ },
}
});
For options refer to Using create() method section.
Example:
const localMigrations = new LocalMigrations({
dirPath: path.join(__dirname, '../migrations'),
postfix: {
up: '.up.sql',
down: '.down.sql',
},
});
const storedMigrations = new StoredMigrations({
sqlActions: {
createMigrationTable() { /* ... */ },
migrateUp(migrations) { /* ... */ },
migrateDown(migrations) { /* ... */ },
getMigrationsNames() { /* ... */ },
getLastMigrationName() { /* ... */ },
}
});
const migrationsCore = new MigrationsCore(localMigrations, storedMigrations);
See migrations-core.interface.ts
See local-migrations.interface.ts