Skip to content

Commit

Permalink
Refactor module to support plugins
Browse files Browse the repository at this point in the history
Closes #15
Closes #4
  • Loading branch information
blakeembrey committed Nov 3, 2016
1 parent 41d4887 commit c923715
Show file tree
Hide file tree
Showing 9 changed files with 559 additions and 174 deletions.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ npm install -g immigration

## Usage

Only three commands and various config options. Created to run migration scripts without any boilerplate.
From `immigration --help`:

```
immigration [command] [options]
Expand All @@ -26,26 +26,36 @@ Options:
-c, --count [num] The number of migrations to execute (default: all)
-e, --extension [ext] Supported file extensions (default: ".js")
-a, --all Explicitly execute all migrations (execute without count or begin)
-n, --new Execute the new migrations (used for "up" migrations) *
-s, --since Rollback migrations for duration (E.g. "30m") (used for "down" migrations) *
Commands:
up [name] Migrate up
down [name] Migrate down
up [name] Run up migration scripts
down [name] Run down migration scripts
create [title] Create a new migration file
list List available migrations
executed List the run migrations *
log [name] Mark a migration as run (without explicitly executing up) *
unlog [name] Remove a migration marked as run (without explicitly executing down) *
tidy Unlog unknown migration names from the plugin *
* Requires plugin (E.g. "--use [ immigration/fs ]")
```

Migrations can export two functions: `up` and `down`. These functions can accept a callback or return a promise for asynchronous actions, such as altering a database.

Plugins can be used with `immigration` for persistence of migration state. The built-in plugin is `fs`, but others can be added. The only requirement is that they export function called `init` which, when called, returns an object with `lock`, `unlock`, `log` and `unlog` functions.

### CLI

```
immigration up -a
immigration down -c 1
immigration down -c1
```

## Attribution

Loosely based on Rails and [node-migrate](https://github.com/tj/node-migrate), but purposely missing complexity that didn't work for my own deployments (E.g. writing to file for state).
Loosely based on Rails and [node-migrate](https://github.com/tj/node-migrate), but I tried to keep the implementation simpler and more configurable.

## License

Expand Down
1 change: 1 addition & 0 deletions fs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/fs')
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,14 @@
"arrify": "^1.0.1",
"bluebird": "^3.1.1",
"chalk": "^1.1.1",
"lockfile": "^1.0.2",
"log-update": "^1.0.2",
"make-error-cause": "^1.2.1",
"minimist": "^1.2.0",
"ms": "^0.7.2",
"pad-left": "^2.0.0",
"promise-finally": "^2.2.1",
"resolve-from": "^2.0.0",
"subarg": "^1.0.0",
"thenify": "^3.1.1",
"touch": "^1.0.0"
}
Expand Down
197 changes: 159 additions & 38 deletions src/bin.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,140 @@
#!/usr/bin/env node

import minimist = require('minimist')
import subarg = require('subarg')
import arrify = require('arrify')
import immigration = require('./index')

interface Argv {
begin?: string
directory?: string
count?: number
extension?: string | string[]
all?: boolean
help?: boolean
}
import chalk = require('chalk')
import Promise = require('any-promise')
import ms = require('ms')
import { resolve } from 'path'
import logUpdate = require('log-update')
import * as immigration from './index'

function run (): Promise<any> {
interface Argv {
begin?: string
directory?: string
count?: number
extension?: string | string[]
all?: boolean
new?: boolean
help?: boolean
since?: string
use?: immigration.PluginOptions
}

const argv = subarg<Argv>(process.argv.slice(2), {
string: ['begin', 'directory', 'extension', 'since'],
boolean: ['help', 'all', 'new'],
alias: {
u: ['use'],
d: ['directory'],
b: ['begin'],
c: ['count'],
e: ['extension'],
a: ['all'],
n: ['new'],
h: ['help'],
s: ['since']
}
})

const cmd = argv._[0]
const name = argv._[1] as string | undefined

if (cmd === 'create') {
return immigration.create({
name: name,
directory: argv.directory,
extension: arrify(argv.extension).pop()
})
.then(() => console.log(`${chalk.green('✔')} Created`))
}

if (cmd === 'list') {
return immigration.list(resolve(argv.directory || 'migrations'))
.then((paths) => {
for (const path of paths) {
console.log(`${chalk.cyan('•')} ${immigration.toName(path)}`)
}
})
}

if (cmd === 'up' || cmd === 'down' || cmd === 'executed' || cmd === 'log' || cmd === 'unlog' || cmd === 'tidy') {
const plugin = argv.use ? immigration.createPlugin(argv.use, process.cwd()) : undefined
const migrate = new immigration.Migrate(plugin)

migrate.on('skipped', function (name: string) {
console.log(`${chalk.yellow('-')} ${name}`)
})

migrate.on('pending', function (name: string) {
logUpdate(`${chalk.cyan('○')} ${name}`)
})

const argv = minimist<Argv>(process.argv.slice(2), {
string: ['begin', 'directory', 'extension'],
boolean: ['help', 'all'],
alias: {
d: ['directory'],
b: ['begin'],
c: ['count'],
e: ['extension'],
a: ['all'],
h: ['help']
migrate.on('done', function (name: string, duration: number) {
logUpdate(`${chalk.green('✔')} ${name} ${chalk.magenta(ms(duration))}`)
logUpdate.done()
})

migrate.on('failed', function (name: string, duration: number) {
logUpdate(`${chalk.red('⨯')} ${name} ${chalk.magenta(ms(duration))}`)
logUpdate.done()
})

if (cmd === 'up' || cmd === 'down') {
return migrate.migrate(cmd as 'up' | 'down', {
all: argv.all,
name: name,
new: argv.new,
since: argv.since,
directory: argv.directory,
begin: argv.begin,
count: argv.count,
extension: argv.extension
})
.then(() => console.log(`\n${chalk.green('✔')} Migration finished successfully`))
}

if (!plugin) {
throw new TypeError(`This command requires a plugin to be specified`)
}

if (cmd === 'executed') {
return migrate.executed()
.then((executed) => {
for (const execution of executed) {
console.log(
`${chalk.cyan('•')} ${execution.name} ${execution.status} @ ` +
`${chalk.magenta(execution.date.toISOString())}`
)
}
})
}

if (cmd === 'log') {
if (!name) {
throw new TypeError(`Requires the migration name to "log"`)
}

return migrate.log(name, 'done', new Date())
.then(() => console.log(`${chalk.green('✔')} Migration logged`))
}

if (cmd === 'unlog') {
if (!name) {
throw new TypeError(`Requires the migration name to "unlog"`)
}

return migrate.unlog(name)
.then(() => console.log(`${chalk.green('✔')} Migration cleared`))
}

if (cmd === 'tidy') {
return migrate.tidy()
.then(() => console.log(`${chalk.green('✔')} Migrations tidied`))
}
}
})

if (argv.help || argv._.length === 0) {
console.error(`
immigration [command] [options]
Expand All @@ -36,26 +144,39 @@ Options:
-c, --count [num] The number of migrations to execute (default: all)
-e, --extension [ext] Supported file extensions (default: ".js")
-a, --all Explicitly execute all migrations (execute without count or begin)
-n, --new Execute the new migrations (used for "up" migrations) *
-s, --since Rollback migrations for duration (E.g. "30m") (used for "down" migrations) *
-u, --use Require a plugin and pass configuration options
Commands:
up [name] Migrate up
down [name] Migrate down
up [name] Run up migration scripts
down [name] Run down migration scripts
create [title] Create a new migration file
list List available migrations
executed List the run migrations *
log [name] Mark a migration as run (without explicitly executing up) *
unlog [name] Remove a migration marked as run (without explicitly executing down) *
tidy Unlog unknown migration names from the plugin *
* Requires plugin (E.g. "--use [ immigration/fs ]")
`)

process.exit(argv.help ? 0 : 1)
return Promise.resolve(process.exit(argv.help ? 0 : 1))
}

immigration(argv._[0], argv._[1], {
begin: arrify(argv.begin).pop(),
count: Number(arrify(argv.count).pop()),
directory: arrify(argv.directory).pop(),
extension: arrify(argv.extension),
all: !!argv.all,
log: true
})
.then(
() => process.exit(0),
() => process.exit(1)
)
// Remember to force process termination after migration.
run()
.then(() => process.exit(0))
.catch((error) => {
if (error instanceof immigration.ImmigrationError) {
console.error(`${chalk.red('⨯')} ${error.message} ${error.path ? `(${error.path})` : ''}`)

if (error.cause) {
console.error(error.cause.stack || error.cause)
}
} else {
console.error(error.stack || error)
}

process.exit(1)
})
76 changes: 76 additions & 0 deletions src/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import thenify = require('thenify')
import lockfile = require('lockfile')
import * as fs from 'fs'
import { join } from 'path'
import { Plugin, PluginOptions } from './index'

const readFile = thenify<string, string, string>(fs.readFile)
const writeFile = thenify<string, string, void>(fs.writeFile)
const lockFile = thenify(lockfile.lock)
const unlockFile = thenify(lockfile.unlock)

/**
* Options for the migration plugin.
*/
export interface Options extends PluginOptions {
path: string
}

/**
* Format of the JSON storage file.
*/
export interface FileJson {
[name: string]: { status: string, date: string }
}

/**
* Initialize the `fs` migration plugin.
*/
export function init (options: Options, dir: string): Plugin {
const path = join(dir, options.path || '.migrate.json')

function read (path: string) {
return readFile(path, 'utf8').then(
(contents) => JSON.parse(contents) as FileJson,
() => ({} as FileJson)
)
}

function log (name: string, status: string, date: Date) {
return read(path).then((file: FileJson) => {
file[name] = { status, date: date.toISOString() }

return writeFile(path, JSON.stringify(file, null, 2))
})
}

function unlog (name: string) {
return read(path).then((file: FileJson) => {
delete file[name]

return writeFile(path, JSON.stringify(file, null, 2))
})
}

function lock () {
return lockFile(`${path}.lock`)
}

function unlock () {
return unlockFile(`${path}.lock`)
}

function executed () {
return read(path).then((file: FileJson) => {
return Object.keys(file).map((key) => {
return {
name: key,
status: file[key].status,
date: new Date(file[key].date)
}
})
})
}

return { executed, lock, unlock, log, unlog }
}

0 comments on commit c923715

Please sign in to comment.