├── .gitignore ├── README.md ├── db-migrate-store.js ├── migrations └── 1538913534285-add-last-namejs.js ├── package.json └── test └── migrations ├── create-test.js └── up-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mongodb-migration -------------------------------------------------------------------------------- /db-migrate-store.js: -------------------------------------------------------------------------------- 1 | const mongodb = require('mongodb') 2 | 3 | const MongoClient = mongodb.MongoClient 4 | 5 | const Bluebird = require('bluebird') 6 | 7 | Bluebird.promisifyAll(MongoClient) 8 | 9 | class dbStore { 10 | constructor() { 11 | 12 | this.url = 'mongodb://localhost/Sample' 13 | 14 | this.db = null 15 | this.mClient = null 16 | } 17 | 18 | connect() { 19 | return MongoClient.connect(this.url) 20 | .then(client => { 21 | this.mClient = client 22 | return client.db() 23 | }) 24 | } 25 | 26 | load(fn) { 27 | return this.connect() 28 | .then(db => db.collection('migrations').find().toArray()) 29 | .then(data => { 30 | if (!data.length) return fn(null, {}) 31 | 32 | const store = data[0] 33 | // Check if old format and convert if needed 34 | if (!Object.prototype.hasOwnProperty.call(store, 'lastRun') && 35 | Object.prototype.hasOwnProperty.call(store, 'pos')) { 36 | if (store.pos === 0) { 37 | store.lastRun = null 38 | } else { 39 | if (store.pos > store.migrations.length) 40 | return fn(new Error('Store file contains invalid pos property')) 41 | 42 | store.lastRun = store.migrations[store.pos - 1].title 43 | } 44 | 45 | // In-place mutate the migrations in the array 46 | store.migrations.forEach((migration, index) => { 47 | if (index < store.pos) 48 | migration.timestamp = Date.now() 49 | }) 50 | } 51 | 52 | // Check if does not have required properties 53 | if (!Object.prototype.hasOwnProperty.call(store, 'lastRun') || !Object.prototype.hasOwnProperty.call(store, 'migrations')) 54 | return fn(new Error('Invalid store file')) 55 | 56 | return fn(null, store) 57 | }) 58 | .catch(fn) 59 | } 60 | 61 | save(set, fn) { 62 | return this.connect() 63 | .then(db => db.collection('migrations') 64 | .update({}, 65 | { 66 | $set: { 67 | lastRun: set.lastRun, 68 | }, 69 | $push: { 70 | migrations: { $each: set.migrations }, 71 | }, 72 | }, 73 | { 74 | upsert: true, 75 | multi: true, 76 | } 77 | )) 78 | .then(result => fn(null, result)) 79 | .catch(fn) 80 | } 81 | } 82 | 83 | module.exports = dbStore -------------------------------------------------------------------------------- /migrations/1538913534285-add-last-namejs.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | const Bluebird = require('bluebird') 6 | const mongodb = require('mongodb') 7 | 8 | const MongoClient = mongodb.MongoClient 9 | const url = 'mongodb://localhost/Sample' 10 | Bluebird.promisifyAll(MongoClient) 11 | 12 | module.exports.up = next => { 13 | let mClient = null 14 | return MongoClient.connect(url) 15 | .then(client => { 16 | mClient = client 17 | return client.db(); 18 | }) 19 | .then(db => { 20 | const User = db.collection('users') 21 | 22 | return User 23 | .find({ lastName: { $exists: false } }) 24 | .forEach(result => { 25 | if (!result) return next('All docs have lastName') 26 | if (result.name) { 27 | const { name } = result 28 | 29 | result.lastName = name.split(' ')[1] 30 | result.firstName = name.split(' ')[0] 31 | } 32 | return db.collection('users').save(result) 33 | }) 34 | }) 35 | .then(() => { 36 | 37 | mClient.close() 38 | return next() 39 | }) 40 | .catch(err => next(err)) 41 | } 42 | 43 | module.exports.down = next => { 44 | let mClient = null 45 | return MongoClient 46 | .connect(url) 47 | .then(client => { 48 | mClient = client 49 | return client.db() 50 | }) 51 | .then(db => 52 | db.collection('users').update( 53 | { 54 | lastName: { $exists: true }, 55 | }, 56 | { 57 | $unset: { lastName: "" }, 58 | }, 59 | { multi: true } 60 | )) 61 | .then(() => { 62 | mClient.close() 63 | return next() 64 | }) 65 | .catch(err => next(err)) 66 | 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongodb-migration", 3 | "version": "1.0.0", 4 | "description": "Sample migration script", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "npm test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/thatshailesh/mongodb-migration.git" 12 | }, 13 | "author": "Shailesh Shekhawat", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/thatshailesh/mongodb-migration/issues" 17 | }, 18 | "homepage": "https://github.com/thatshailesh/mongodb-migration#readme", 19 | "dependencies": { 20 | "bluebird": "^3.5.2", 21 | "chai": "^4.2.0", 22 | "chance": "^1.0.16", 23 | "migrate": "^1.6.1", 24 | "mkdirp": "^0.5.1", 25 | "mongodb": "^3.1.6", 26 | "rimraf": "^2.6.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/migrations/create-test.js: -------------------------------------------------------------------------------- 1 | 2 | const Bluebird = require('bluebird') 3 | const { spawn } = require('child_process') 4 | const mkdirp = require('mkdirp') 5 | const rimraf = require('rimraf') 6 | 7 | const path = require('path') 8 | const fs = Bluebird.promisifyAll(require('fs')) 9 | 10 | describe('[Migrations]', () => { 11 | 12 | const run = (cmd, args = []) => { 13 | const process = spawn(cmd, args) 14 | let out = "" 15 | 16 | return new Bluebird((resolve, reject) => { 17 | process.stdout.on('data', data => { 18 | out += data.toString('utf8') 19 | }) 20 | 21 | process.stderr.on('data', data => { 22 | out += data.toString('utf8') 23 | }) 24 | 25 | process.on('error', err => { 26 | reject(err) 27 | }) 28 | 29 | process.on('close', code => { 30 | resolve(out, code) 31 | }) 32 | }) 33 | } 34 | const TMP_DIR = path.join(__dirname, '..', '..', 'tmp') 35 | const INIT = path.join(__dirname, '..', '..', 'node_modules/migrate/bin', 'migrate-init') 36 | const init = run.bind(null, INIT) 37 | 38 | 39 | const reset = () => { 40 | rimraf.sync(TMP_DIR) 41 | rimraf.sync(path.join(__dirname, '..', '..', '.migrate')) 42 | } 43 | 44 | beforeEach(reset) 45 | afterEach(reset) 46 | 47 | describe('init', () => { 48 | beforeEach(mkdirp.bind(mkdirp, TMP_DIR)) 49 | 50 | it('should create a migrations directory', done => { 51 | init() 52 | .then(() => fs.accessSync(path.join(TMP_DIR, '..', 'migrations'))) 53 | .then(() => done()) 54 | .catch(done) 55 | }) 56 | }) 57 | 58 | 59 | }) -------------------------------------------------------------------------------- /test/migrations/up-test.js: -------------------------------------------------------------------------------- 1 | const chance = require('chance')() 2 | const path = require('path') 3 | const { spawn } = require('child_process') 4 | const Bluebird = require('bluebird') 5 | const mongodb = require('mongodb') 6 | const rimraf = require('rimraf') 7 | 8 | const MongoClient = mongodb.MongoClient 9 | 10 | const url = 'mongodb://localhost/Sample' 11 | 12 | const generateUser = () => ({ 13 | email: chance.email(), 14 | name: `${chance.first()} ${chance.last()}`, 15 | }) 16 | 17 | const run = (cmd, args = []) => { 18 | const process = spawn(cmd, args) 19 | let out = "" 20 | 21 | return new Bluebird((resolve, reject) => { 22 | process.stdout.on('data', data => { 23 | out += data.toString('utf8') 24 | }) 25 | 26 | process.stderr.on('data', data => { 27 | out += data.toString('utf8') 28 | }) 29 | 30 | process.on('error', err => { 31 | reject(err) 32 | }) 33 | 34 | process.on('close', code => { 35 | resolve(out, code) 36 | }) 37 | }) 38 | } 39 | 40 | const migratePath = path.join(__dirname, '..', '..', 'node_modules/migrate/bin', 'migrate') 41 | const migrate = run.bind(null, migratePath) 42 | describe('[Migration: up]', () => { 43 | 44 | let db = null 45 | 46 | before(done => { 47 | MongoClient 48 | .connect(url) 49 | .then(client => { 50 | db = client.db() 51 | return db.collection('users').insert(generateUser()) 52 | }) 53 | .then(result => { 54 | if (!result) throw new Error('Failed to insert') 55 | return done() 56 | }).catch(done) 57 | }) 58 | it('should run up on specified migration', done => { 59 | migrate(['up', 'mention here the file name we created above', '--store=./db-migrate-store.js']) 60 | .then(() => { 61 | const promises = [] 62 | promises.push( 63 | db.collection('users').find().toArray() 64 | ) 65 | Bluebird.all(promises) 66 | .then(([users]) => { 67 | users.forEach(elem => { 68 | expect(elem).to.have.property('lastName') 69 | }) 70 | done() 71 | }) 72 | }).catch(done) 73 | }) 74 | after(done => { 75 | rimraf.sync(path.join(__dirname, '..', '..', '.migrate')) 76 | db.collection('users').deleteMany() 77 | .then(() => { 78 | rimraf.sync(path.join(__dirname, '..', '..', '.migrate')) 79 | return done() 80 | }).catch(done) 81 | }) 82 | }) --------------------------------------------------------------------------------