Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork163
Framework agnostic migration tool for Node.js
License
sequelize/umzug
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Umzug is a framework-agnostic migration tool for Node. It provides a clean API for running and rolling back tasks.
- Written in TypeScript
- Built-in typings
- Auto-completion right in your IDE
- Documentation right in your IDE
- Programmatic API for migrations
- Built-inCLI
- Database agnostic
- Supports logging of migration process
- Supports multiple storages for migration data
- Usage examples
Note: these are the docs for the latest version of umzug, which has several breaking changes from v2.x. Seethe upgrading section for a migration guide. For the previous stable version, please refer to thev2.x branch.
The following example uses a Sqlite database through sequelize and persists the migration data in the database itself through the sequelize storage. There are several more involved examples covering a few different scenarios in theexamples folder. Note that although this uses Sequelize, Umzug isn't coupled to Sequelize, it's just one of the (most commonly-used) supported storages.
// index.jsconst{Sequelize}=require('sequelize')const{Umzug, SequelizeStorage}=require('umzug')constsequelize=newSequelize({dialect:'sqlite',storage:'./db.sqlite'})constumzug=newUmzug({migrations:{glob:'migrations/*.js'},context:sequelize.getQueryInterface(),storage:newSequelizeStorage({sequelize}),logger:console,})// Checks migrations and run them if they are not already applied. To keep// track of the executed migrations, a table (and sequelize model) called SequelizeMeta// will be automatically created (if it doesn't exist already) and parsed.awaitumzug.up()
// migrations/00_initial.jsconst{Sequelize}=require('sequelize')asyncfunctionup({context:queryInterface}){awaitqueryInterface.createTable('users',{id:{type:Sequelize.INTEGER,allowNull:false,primaryKey:true,},name:{type:Sequelize.STRING,allowNull:false,},createdAt:{type:Sequelize.DATE,allowNull:false,},updatedAt:{type:Sequelize.DATE,allowNull:false,},})}asyncfunctiondown({context:queryInterface}){awaitqueryInterface.dropTable('users')}module.exports={up, down}
Note that we renamed thecontext argument toqueryInterface for clarity. Thecontext is whatever we specified when creating the Umzug instance inindex.js.
You can also write your migrations in typescript by using `ts-node` in the entrypoint:
// index.tsrequire('ts-node/register')import{Sequelize}from'sequelize';import{Umzug,SequelizeStorage}from'umzug';constsequelize=newSequelize({dialect:'sqlite',storage:'./db.sqlite'});constumzug=newUmzug({migrations:{glob:'migrations/*.ts'},context:sequelize.getQueryInterface(),storage:newSequelizeStorage({ sequelize}),logger:console,});// export the type helper exposed by umzug, which will have the `context` argument typed correctlyexporttypeMigration=typeofumzug._types.migration;(async()=>{awaitumzug.up();})();
// migrations/00_initial.tsimporttype{Migration}from'..';// types will now be available for `queryInterface`exportconstup:Migration=({context:queryInterface})=>queryInterface.createTable(...)exportconstdown:Migration=({context:queryInterface})=>queryInterface.dropTable(...)
Seethese tests for more examples of Umzug usage, including:
- passing
ignoreandcwdparameters to the glob instructions - customising migrations ordering
- finding migrations from multiple different directories
- using non-js file extensions via a custom resolver, e.g.
.sql
Umzug is available on npm by specifying the correct tag:
npm install umzug
It is possible to configure an Umzug instance by passing an object to the constructor.
const{Umzug}=require('umzug')constumzug=newUmzug({/* ... options ... */})
Detailed documentation for these options are in theUmzugOptions TypeScript interface, which can be found insrc/types.ts.
You can get a list of pending (i.e. not yet executed) migrations with thepending() method:
constmigrations=awaitumzug.pending()// returns an array of all pending migrations.
You can get a list of already executed migrations with theexecuted() method:
constmigrations=awaitumzug.executed()// returns an array of all already executed migrations
Theup method can be used to execute all pending migrations.
constmigrations=awaitumzug.up()// returns an array of all executed migrations
It is also possible to pass the name of a migration in order to just run the migrations from the current state to the passed migration name (inclusive).
awaitumzug.up({to:'20141101203500-task'})
To limit the number of migrations that are run,step can be used:
// This will run the next two migrationsawaitumzug.up({step:2})
Running specific migrations while ignoring the right order, can be done like this:
awaitumzug.up({migrations:['20141101203500-task','20141101203501-task-2']})
Thedown method can be used to revert the last executed migration.
constmigration=awaitumzug.down()// reverts the last migration and returns it.
To revert more than one migration, you can usestep:
// This will revert the last two migrationsawaitumzug.down({step:2})
It is possible to pass the name of a migration until which (inclusive) the migrations should be reverted. This allows the reverting of multiple migrations at once.
constmigrations=awaitumzug.down({to:'20141031080000-task'})// returns an array of all reverted migrations.
To revert all migrations, you can pass 0 as theto parameter:
awaitumzug.down({to:0})
Reverting specific migrations while ignoring the right order, can be done like this:
awaitumzug.down({migrations:['20141101203500-task','20141101203501-task-2']})
There are two ways to specify migrations: via files or directly via an array of migrations.
A migration file ideally exposes anup and adown async functions. They will perform the task of upgrading or downgrading the database.
module.exports={asyncup(){/* ... */},asyncdown(){/* ... */},}
Migration files can be located anywhere - they will typically be loaded according to a glob pattern provided to theUmzug constructor.
You can also specify directly a list of migrations to theUmzug constructor:
const{Umzug}=require('umzug')constumzug=newUmzug({migrations:[{// the name of the migration is mandatoryname:'00-first-migration',asyncup({context}){/* ... */},asyncdown({context}){/* ... */},},{name:'01-foo-bar-migration',asyncup({context}){/* ... */},asyncdown({context}){/* ... */},},],context:sequelize.getQueryInterface(),logger:console,})
Sometimes it's necessary to modify the parametersumzug will pass to your migration methods when the library calls theup anddown methods for each migration. This is the case when using migrations currently generated usingsequelize-cli. In this case you can use theresolve fuction during migration configuration to determine which parameters will be passed to the relevant method
import{Sequelize}from'sequelize'import{Umzug,SequelizeStorage}from'umzug'constsequelize=newSequelize(/* ... */)constumzug=newUmzug({migrations:{glob:'migrations/*.js',resolve:({name, path, context})=>{constmigration=require(path)return{// adjust the parameters Umzug will// pass to migration methods when called name,up:async()=>migration.up(context,Sequelize),down:async()=>migration.down(context,Sequelize),}},},context:sequelize.getQueryInterface(),storage:newSequelizeStorage({sequelize}),logger:console,})
To load migrations in another format, you can use theresolve function:
constfs=require('fs')const{Sequelize}=require('sequelize')const{Umzug}=require('umzug')constumzug=newUmzug({migrations:{glob:'migrations/*.up.sql',resolve:({name, path,context:sequelize})=>({ name,up:async()=>{constsql=fs.readFileSync(path).toString()returnsequelize.query(sql)},down:async()=>{// Get the corresponding `.down.sql` file to undo this migrationconstsql=fs.readFileSync(path.replace('.up.sql','.down.sql')).toString()returnsequelize.query(sql)},}),},context:newSequelize(/* ... */),logger:console,})
You can support mixed migration file types, and use umzug's default resolver for javascript/typescript:
constfs=require('fs')const{Sequelize}=require('sequelize')const{Umzug}=require('umzug')constumzug=newUmzug({migrations:{glob:'migrations/*.{js,ts,up.sql}',resolve:params=>{if(!params.path.endsWith('.sql')){returnUmzug.defaultResolver(params)}const{context:sequelize}=paramsreturn{name:params.name,up:async()=>{constsql=fs.readFileSync(params.path).toString()returnsequelize.query(sql)},down:async()=>{// Get the corresponding `.down.sql` file to undo this migrationconstsql=fs.readFileSync(params.path.replace('.up.sql','.down.sql')).toString()returnsequelize.query(sql)},}},},logger:console,context:newSequelize(/* ... */),})
The glob syntax allows loading migrations from multiple locations:
const{Sequelize}=require('sequelize')const{Umzug}=require('umzug')constumzug=newUmzug({migrations:{glob:'{first-folder/*.js,second-folder-with-different-naming-convention/*.js}',},context:newSequelize(/* ... */),logger:console,})
Note on migration file sorting:
- file matches, found usingtinyglobby, will be lexicographically sorted based on their paths
- so if your migrations are
one/m1.js,two/m2.js,three/m3.js, the resultant order will beone/m1.js,three/m3.js,two/m2.js - similarly, if your migrations are called
m1.js,m2.js, ...m10.js,m11.js, the resultant ordering will bem1.js,m10.js,m11.js, ...m2.js
- so if your migrations are
- The easiest way to deal with this is to ensure your migrations appear in a single folder, and their paths match lexicographically with the order they should run in
- If this isn't possible, the ordering can be customised using a new instance (previously, in the beta release for v3, this could be done with
.extend(...)- see below for example using a new instance)
The Umzug class should be imported as a named import, i.e.import { Umzug } from 'umzug'.
TheMigrationMeta type, which is returned byumzug.executed() andumzug.pending(), no longer has afile property - it has aname andoptionalpath - since migrations are not necessarily bound to files on the file system.
Themigrations.glob parameter replacespath,pattern andtraverseDirectories. It can be used, in combination withcwd andignore to do much more flexible file lookups. Seehttps://npmjs.com/package/tinyglobby for more information on the syntax.
Themigrations.resolve parameter replacescustomResolver. Explicit support forwrap andnameFormatter has been removed - these can be easily implemented in aresolve function.
The constructor optionlogging is replaced bylogger to allow forwarn anderror messages in future. NodeJS's globalconsole object can be passed to this. To disable logging, replacelogging: false withlogger: undefined.
Events have moved from the default nodejsEventEmitter toemittery. It has better design for async code, a less bloated API surface and strong types. But, it doesn't allow passing multiple arguments to callbacks, so listeners have to change slightly, as well as.addListener(...) and.removeListener(...) no longer being supported (.on(...) and.off(...) should now be used):
Before:
umzug.on('migrating',(name,m)=>console.log({name,path:m.path}))
After:
umzug.on('migrating',ev=>console.log({name:ev.name,path:ev.path}))
TheUmzug#execute method is removed. UseUmzug#up orUmzug#down.
The options forUmguz#up andUmzug#down have changed:
umzug.up({ to: 'some-name' })andumzug.down({ to: 'some-name' })are still valid.umzug.up({ from: '...' })andumzug.down({ from: '...' })are no longer supported. To run migrations out-of-order (which is not generally recommended), you can explicitly useumzug.up({ migrations: ['...'] })andumzug.down({ migrations: ['...'] }).- name matches must be exact.
umzug.up({ to: 'some-n' })will no longer match a migration calledsome-name. umzug.down({ to: 0 })is still valid butumzug.up({ to: 0 })is not.umzug.up({ migrations: ['m1', 'm2'] })is still valid but the shorthandumzug.up(['m1', 'm2'])has been removed.umzug.down({ migrations: ['m1', 'm2'] })is still valid but the shorthandumzug.down(['m1', 'm2'])has been removed.umzug.up({ migrations: ['m1', 'already-run'] })will throw an error, ifalready-runis not found in the list of pending migrations.umzug.down({ migrations: ['m1', 'has-not-been-run'] })will throw an error, ifhas-not-been-runis not found in the list of executed migrations.umzug.up({ migrations: ['m1', 'm2'], rerun: 'ALLOW' })will re-apply migrationsm1andm2even if they've already been run.umzug.up({ migrations: ['m1', 'm2'], rerun: 'SKIP' })will skip migrationsm1andm2if they've already been run.umzug.down({ migrations: ['m1', 'm2'], rerun: 'ALLOW' })will "revert" migrationsm1andm2even if they've never been run.umzug.down({ migrations: ['m1', 'm2'], rerun: 'SKIP' })will skip reverting migrationsm1andm2if they haven't been run or are already reverted.umzug.up({ migrations: ['m1', 'does-not-exist', 'm2'] })will throw an error if the migration name is not found. Note that the error will be thrown and no migrations run unlessall migration names are found - whether or notrerun: 'ALLOW'is added.
Thecontext parameter replacesparams, and is passed in as a property to migration functions as an options object, alongs sidename andpath. This means the signature for migrations, which in v2 was(context) => Promise<void>, has changed slightly in v3, to({ name, path, context }) => Promise<void>.
Theresolve function can also be used to upgrade your umzug version to v3 when you have existing v2-compatible migrations:
const{Umzug}=require('umzug')constumzug=newUmzug({migrations:{glob:'migrations/umzug-v2-format/*.js',resolve:({name, path, context})=>{// Adjust the migration from the new signature to the v2 signature, making easier to upgrade to v3constmigration=require(path)return{ name,up:async()=>migration.up(context),down:async()=>migration.down(context),}},},context:sequelize.getQueryInterface(),logger:console,})
Similarly, you no longer needmigrationSorting, you can instantiate a newUmzug instance to manipulate migration lists directly:
const{Umzug}=require('umzug')constparent=newUmzug({migrations:{glob:'migrations/**/*.js'},context:sequelize.getQueryInterface(),})constumzug=newUmzug({ ...parent.options,migrations:asyncctx=>(awaitparent.migrations()).sort((a,b)=>b.path.localeCompare(a.path)),})
Storages define where the migration data is stored.
UsingJSONStorage will create a JSON file which will contain an array with all the executed migrations. You can specify the path to the file. The default for that isumzug.json in the working directory of the process.
Detailed documentation for the options it can take are in theJSONStorageConstructorOptions TypeScript interface, which can be found insrc/storage/json.ts.
UsingmemoryStorage will store migrations with an in-memory array. This can be useful for proof-of-concepts or tests, since it doesn't interact with databases or filesystems.
It doesn't take any options, just import thememoryStorage function and call it to return a storage instance:
import{Umzug,memoryStorage}from'umzug'constumzug=newUmzug({migrations: ...,storage:memoryStorage(),logger:console,})
UsingSequelizeStorage will create a table in your SQL database calledSequelizeMeta containing an entry for each executed migration. You will have to pass a configured instance of Sequelize or an existing Sequelize model. Optionally you can specify the model name, table name, or column name. All major Sequelize versions are supported.
Detailed documentation for the options it can take are in the_SequelizeStorageConstructorOptions TypeScript interface, which can be found insrc/storage/sequelize.ts.
This library has been tested with sequelize v6. It may or may not work with lower versions - use at your own risk.
UsingMongoDBStorage will create a collection in your MongoDB database calledmigrations containing an entry for each executed migration. You will have either to pass a MongoDB Driver Collection ascollection property. Alternatively you can pass a established MongoDB Driver connection and a collection name.
Detailed documentation for the options it can take are in theMongoDBStorageConstructorOptions TypeScript interface, which can be found insrc/storage/mongodb.ts.
In order to use a custom storage, you can pass your storage instance to Umzug constructor.
classCustomStorage{constructor(){}logMigration(){}unlogMigration(){}executed(){}}constumzug=newUmzug({storage:newCustomStorage(/* ... */),logger:console,})
Your instance must adhere to theUmzugStorage interface. If you're using TypeScript you can ensure this at compile time, and get IDE type hints by importing it:
import{UmzugStorage}from'umzug'classCustomStorageimplementsUmzugStorage{/* ... */}
Umzug is anemittery event emitter. Each of the following events will be called with migration parameters as its payload (withcontext,name, and nullablepath properties). Events are a convenient place to implement application-specific logic that must run around each migration:
migrating- A migration is about to be executed.migrated- A migration has successfully been executed.reverting- A migration is about to be reverted.reverted- A migration has successfully been reverted.
These events run at the beginning and end ofup anddown calls. They'll receive an object containing acontext property:
beforeCommand- Before any command ('up' | 'down' | 'executed' | 'pending') is run.afterCommand- After any command ('up' | 'down' | 'executed' | 'pending') is run. Note: this will always run, even if the command throws an error.
TheFileLocker class usesbeforeAll andafterAll to implement a simple filesystem-based locking mechanism.
All events are type-safe, so IDEs will prevent typos and supply strong types for the event payloads.
When a migration throws an error, it will be wrapped in aMigrationError which captures the migration metadata (name, path etc.) as well as the original error message, andwill be rethrown. In most cases, this is expected behaviour, and doesn't require any special handling beyond standard error logging setups.
If you expect failures and want to try to recover from them, you will need to try-catch the call toumzug.up(). You can access the original error from the.cause property if necessary:
try{awaitumzug.up()}catch(e){if(einstanceofMigrationError){constoriginal=e.cause// do something with the original error here}throwe}
Under the hood,verror is used to wrap errors.
🚧🚧🚧 The CLI is new to Umzug v3. Feedback on it is welcome indiscussions 🚧🚧🚧
Umzug instances provide a.runAsCLI() method. When called, this method will automatically cause your program to become a complete CLI, with help text and such:
// migrator.jsconst{Umzug}=require('umzug')constumzug=newUmzug({/* ... */})exports.umzug=umzugif(require.main===module){umzug.runAsCLI()}
Note that this uses the@rushstack/ts-command-line package, which shows only the top-level message of any errors throw by default. Seehere for how you can see a full stack trace.
A script like the one above is now a runnable CLI program. You can runnode migrator.js --help to see how to use it. It will print something like:
usage: <script> [-h] <command> ...Umzug migratorPositional arguments: <command> up Applies pending migrations down Revert migrations pending Lists pending migrations executed Lists executed migrations create Create a migration fileOptional arguments: -h, --help Show this help message and exit.For detailed help about a specific command, use: <script> <command> -hnode migrator up andnode migrator down apply and revert migrations respectively. They're the equivalent of the.up() and.down() methods.
Usenode migrator up --help andnode migrator down --help for options (running "to" a specific migration, passing migration names to be run explicitly, and specifying the rerun behavior):
Up:
usage: <script> up [-h] [--to NAME] [--step COUNT] [--name MIGRATION] [--rerun {THROW,SKIP,ALLOW}] Performs all migrations. See --help for more optionsOptional arguments: -h, --help Show this help message and exit. --to NAME All migrations up to and including this one should be applied --step COUNT Apply this many migrations. If not specified, all will be applied. --name MIGRATION Explicity declare migration name(s) to be applied. Only these migrations will be applied. --rerun {THROW,SKIP,ALLOW} Specify what action should be taken when a migration that has already been applied is passed to --name. The default value is "THROW".Down:
usage: <script> down [-h] [--to NAME] [--step COUNT] [--name MIGRATION] [--rerun {THROW,SKIP,ALLOW}] Undoes previously-applied migrations. By default, undoes the most recent migration only. Use --help for more options. Useful in development to start from a clean slate. Use with care in production!Optional arguments: -h, --help Show this help message and exit. --to NAME All migrations up to and including this one should be reverted. Pass '0' to revert all. --step COUNT Revert this many migrations. If not specified, only the most recent migration will be reverted. --name MIGRATION Explicity declare migration name(s) to be reverted. Only these migrations will be reverted. --rerun {THROW,SKIP,ALLOW} Specify what action should be taken when a migration that has already been applied is passed to --name. The default value is "THROW".node migrator pending# list migrations yet to be runnode migrator executed# list migrations that have already runnode migrator pending --json# list pending migrations including names and paths, in a json array formatnode migrator executed --json# list executed migrations including names and paths, in a json array formatnode migrator pending --help# show help/optionsnode migrator executed --help# show help/options
usage: <script> pending [-h] [--json]Prints migrations returned by `umzug.pending()`. By default, prints migration names one per line.Optional arguments: -h, --help Show this help message and exit. --json Print pending migrations in a json format including names and paths. This allows piping output to tools like jq. Without this flag, the migration names will be printed one per line.usage: <script> executed [-h] [--json]Prints migrations returned by `umzug.executed()`. By default, prints migration names one per line.Optional arguments: -h, --help Show this help message and exit. --json Print executed migrations in a json format including names and paths. This allows piping output to tools like jq. Without this flag, the migration names will be printed one per line.Usually, migrations correspond to files on the filesystem. The CLI exposes a way to create migration files easily:
node migrator create --name my-migration.js
This will create a file with a name like2000.12.25T12.34.56.my-migration.js in the same directory as the most recent migration file. If it's the very first migration file, you need to specify the folder explicitly:
node migrator create --name my-migration.js --folder path/to/directory
The timestamp prefix can be customized to be date-only or omitted, but be aware that it's strongly recommended to ensure your migrations are lexicographically sortable so it's easy for humans and tools to determine what order they should run in - so the default prefix is recommended.
This will generate a migration file called<<timestamp>>.my-migration.js with the default migration template for.js files that ships with Umzug.
Umzug also ships with default templates for.ts,.cjs,.mjs and.sql files. Umzug will choose the template based on the extension you provide inname.
You can specify a custom template for your project when constructing an umzug instance via thetemplate option. It should be a function which receives a filepath string, and returns an array of[filepath, content] pairs. Usually, just one pair is needed, but a second could be used to include a "down" migration in a separate file:
constumzug=newUmzug({migrations:{/*...*/},create:{template:filepath=>[[filepath,fs.readFileSync('path/to/your/template/file').toString()],],},})
The create command includes some safety checks to make sure migrations aren't created with ambiguous ordering, and that they will be picked up by umzug when applying migrations. The first pair is expected to be the "up" migration file, and to be picked up by thepending command.
Usenode migrator create --help for more options:
usage: <script> create [-h] --name NAME [--prefix {TIMESTAMP,DATE,NONE}] [--folder PATH] [--allow-extension EXTENSION] [--skip-verify] [--allow-confusing-ordering] Generates a placeholder migration file using a timestamp as a prefix. By default, mimics the last existing migration, or guesses where to generate the file if no migration exists yet.Optional arguments: -h, --help Show this help message and exit. --name NAME The name of the migration file. e.g. my-migration.js, my-migration.ts or my-migration.sql. Note - a prefix will be added to this name, usually based on a timestamp. See --prefix --prefix {TIMESTAMP,DATE,NONE} The prefix format for generated files. TIMESTAMP uses a second-resolution timestamp, DATE uses a day-resolution timestamp, and NONE removes the prefix completely. The default value is "TIMESTAMP". --folder PATH Path on the filesystem where the file should be created. The new migration will be created as a sibling of the last existing one if this is omitted. --allow-extension EXTENSION Allowable extension for created files. By default .js, .ts and .sql files can be created. To create txt file migrations, for example, you could use '--name my-migration.txt --allow-extension .txt' This parameter may alternatively be specified via the UMZUG_ALLOW_EXTENSION environment variable. --skip-verify By default, the generated file will be checked after creation to make sure it is detected as a pending migration. This catches problems like creation in the wrong folder, or invalid naming conventions. This flag bypasses that verification step. --allow-confusing-ordering By default, an error will be thrown if you try to create a migration that will run before a migration that already exists. This catches errors which can cause problems if you change file naming conventions. If you use a custom ordering system, you can disable this behavior, but it's strongly recommended that you don't! If you're unsure, just ignore this option.Umzug includes an optional helper for generating migration files. It's often most convenient to create files using theCLI helper, but the equivalent API also exists on an umzug instance:
awaitumzug.create({name:'my-new-migration.js'})
See theLICENSE file
About
Framework agnostic migration tool for Node.js
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.