-
Notifications
You must be signed in to change notification settings - Fork 1
/
migrate.js
executable file
·134 lines (112 loc) · 3.89 KB
/
migrate.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#!/usr/bin/env node
'use strict'
const DOC = `
Usage:
migrate up <host> <port> <db> [--dry-run]
migrate down <host> <port> <db> <version> [--dry-run]
migrate -h | --help | --version
`
const _ = require('lodash')
const co = require('co')
const docopt = require('docopt').docopt
const MongoClient = require('mongodb').MongoClient
const requireDir = require('require-dir')
const pkg = require('./package.json')
function loadMigrations () {
const allMigrations = requireDir(`${process.cwd()}/migrations`)
const migrationIdRe = /^([\d]+)/
let idSet = new Set()
function extractMigId (name) {
let id = name.match(migrationIdRe)[1]
if (!id) throw Error(`missing id in ${name}`)
return parseInt(id, 10)
}
const migrations = _(allMigrations)
.map((migration, name) => {
const id = extractMigId(name)
if (idSet.has(id)) throw Error(`duplicate id ${id} while processing ${name}`)
idSet.add(id)
if (!_.isFunction(migration.up)) throw Error(`missing 'up' function in ${name}`)
if (!_.isFunction(migration.down)) throw Error(`missing 'down' function in ${name}`)
// TODO: validate migration (ensure up and down funcs are defined)
return {
name,
id,
up: migration.up,
down: migration.down
}
})
.sortBy('id')
.value()
return migrations
}
function * main () {
const args = docopt(DOC, { version: pkg.version })
const host = args['<host>']
const port = args['<port>']
const database = args['<db>']
const db = yield MongoClient.connect(`mongodb://${host}:${port}/${database}`)
const migrationCollection = db.collection('__migrations')
const allMigrations = loadMigrations()
// load existing migration info
let migrationInfo = yield migrationCollection.findOne({_id: 'default'})
migrationInfo = _.defaults(migrationInfo, {
migId: 0
})
console.log(`current database version is '${migrationInfo.migId}'`)
// determine pending migrations
let pendingMigrations = []
let func = ''
if (args.up) {
func = 'up'
pendingMigrations = _.filter(allMigrations, m => m.id > migrationInfo.migId)
} else {
func = 'down'
const requestedMigId = parseInt(args['<version>'], 10)
pendingMigrations = _.filter(allMigrations, m => m.id > requestedMigId && m.id <= migrationInfo.migId)
_.reverse(pendingMigrations)
}
// if dry run, just display which migrations would be run
if (args['--dry-run']) {
console.log('dry run mode : the following migrations would be applied:')
_(pendingMigrations).each(m => console.log(` - ${m.name}`))
return yield db.close()
}
// TODO: make migration on a mirror of data for easy rollback ?
const migrationCtx = {
db,
updateCollection: function * (collection, updater) {
const col = db.collection(collection)
const chunkSize = 1000
let idx = 0
while (true) {
const docs = yield col.find({}).skip(idx++ * chunkSize).limit(chunkSize).toArray()
if (_.isEmpty(docs)) break
const newDocs = yield docs.map(updater)
for (var doc of newDocs) yield col.updateOne({ _id: doc._id }, doc)
}
}
}
if (!pendingMigrations.length) {
console.log('already up to date : no migration to apply!')
return yield db.close()
}
console.log('applying the following migrations:')
for (const m of pendingMigrations) {
if (func === 'up') {
console.log(` - applying ${m.name}...`)
yield m.up.bind(migrationCtx)()
yield migrationCollection.updateOne({ _id: 'default' }, { migId: m.id }, { upsert: true })
} else {
console.log(` - reverting ${m.name}...`)
yield m.down.bind(migrationCtx)()
yield migrationCollection.updateOne({ _id: 'default' }, { migId: m.id - 1 }, { upsert: true })
}
console.log(` done ${m.name}!`)
}
yield db.close()
}
// If module is launched on it's own
if (require.main === module) {
co(main).catch(console.error)
}