knex/lib/migrate.js

242 lines
7.5 KiB
JavaScript
Raw Normal View History

// Migrate
2013-09-13 16:58:38 -04:00
// -------
2013-10-24 21:54:35 -04:00
"use strict";
2013-09-13 16:58:38 -04:00
2013-10-24 21:54:35 -04:00
var fs = require('fs');
var path = require('path');
2013-09-04 20:36:56 -04:00
var _ = require('underscore');
2013-10-24 21:54:35 -04:00
var mkdirp = require('mkdirp');
var Promise = require('./promise').Promise;
2013-09-04 20:36:56 -04:00
2013-10-24 21:54:35 -04:00
// The new migration we're performing, typically called from the `knex.migrate`
// interface on the main `knex` object. Passes the `knex` instance performing
// the migration.
2013-09-05 16:36:49 -04:00
var Migrate = function(instance) {
this.knex = instance;
2013-09-04 20:36:56 -04:00
};
Migrate.prototype = {
2013-10-24 21:54:35 -04:00
// Initializes the migration, taking an optional `config` object,
// for things like the `tableName`.
init: function(config) {
this.config = _.defaults(config || {}, {
2013-09-04 20:36:56 -04:00
extension: 'js',
tableName: 'knex_migrations',
directory: process.cwd() + '/migrations'
});
2013-11-02 13:40:34 -04:00
return Promise.all([
2013-10-24 21:54:35 -04:00
this.ensureFolder(config),
this.ensureTable(config)
2013-11-02 13:40:34 -04:00
]).bind(this);
2013-10-24 21:54:35 -04:00
},
// Ensures that the proper table has been created,
// dependent on the migration config settings.
ensureTable: function(config) {
var migration = this;
return this.knex.schema.hasTable(config.tableName)
.then(function(exists) {
if (!exists) return migration.createMigrationTable(config.tableName);
});
},
2013-09-04 20:36:56 -04:00
2013-10-24 21:54:35 -04:00
// Ensures a folder for the migrations exist, dependent on the
// migration config settings.
ensureFolder: function(config) {
return Promise.promisify(fs.stat, fs)(config.directory)
.catch(function() {
return Promise.promisify(mkdirp)(config.directory);
2013-10-24 21:54:35 -04:00
});
2013-09-04 20:36:56 -04:00
},
// Create the migration table, if it doesn't already exist.
createMigrationTable: function(tableName) {
2013-10-24 21:54:35 -04:00
return this.knex.schema.createTable(tableName, function(t) {
2013-09-04 20:36:56 -04:00
t.increments();
t.string('name');
t.integer('batch');
t.dateTime('migration_time');
});
},
// Runs a specific migration, based on the migration version number.
run: function(version, config) {
version = parseVersion(version);
var migration = this;
2013-10-24 21:54:35 -04:00
return this.init(config)
.then(this.migrationData)
2013-09-04 20:36:56 -04:00
.spread(function(all, completed) {
2013-10-24 21:54:35 -04:00
if (!migration.hasRun(completed, version)) {
2013-09-04 20:36:56 -04:00
return migration.runBatch([getMigration(all, version, config)]);
} else {
throw new Error('Migration ' + version + ' already exists');
}
})
.bind()
2013-10-24 21:54:35 -04:00
.yield('Migration ' + version + ' successfully run');
2013-09-04 20:36:56 -04:00
},
// Migrate "up" to a specific migration id
// otherwise, migrates all migrations which have
// not been run yet.
up: function(version, config) {
return this.direction('up', version, config);
2013-09-04 20:36:56 -04:00
},
// Migrate "down" to a specific migration id,
// otherwise rolls back the last migration "batch".
down: function(version, config) {
return this.direction('down', version, config);
2013-09-04 20:36:56 -04:00
},
// Run a batch of current migrations, in sequence.
runBatch: function(migrations, direction) {
2013-10-24 21:54:35 -04:00
var knex = this.knex;
return Promise.then(function() {
return Promise.map(migrations, function(item) {
2013-10-24 21:54:35 -04:00
var migration = require(item);
if (!_.isFunction(migration.up) || !_.isFunction(migration.down)) {
throw new Error('Invalid migration: ' + item + ' must have both an up and down function');
}
return migration;
});
}).then(function(migrations) {
var current = Promise.fulfilled();
_.each(migrations, function(migration) {
current = current.then(function() {
migration[direction](knex, Promise.fulfilled());
});
2013-10-24 21:54:35 -04:00
});
return current;
});
2013-09-04 20:36:56 -04:00
},
// Retrieves and returns the current migration version
// we're on, as a promise. If there aren't any migrations run yet,
// return "none" as the value for the `currentVersion`.
currentVersion: function(config) {
return this.listCompleted(config).then(function(completed) {
var val = _.chain(completed).map(function(value) {
return value.split('_')[0];
}).max().value();
return (val === -Infinity ? 'none' : val);
});
},
// Creates a new migration, with a given name.
2013-10-24 21:54:35 -04:00
generate: function(name, config) {
if (!name) Promise.rejected(new Error('A name must be specified for the generated migration'));
return this.init(config).then(function() {
var readFile = Promise.promisify(fs.readFile, fs);
return readFile(this.config.stub || path.join(__dirname, 'stub', this.config.extension + '.stub'));
}).then(function(stub) {
2013-10-24 21:54:35 -04:00
name = dasherize(name);
2013-09-04 20:36:56 -04:00
if (name[0] === '-') name = name.slice(1);
var filename = yyyymmddhhmmss() + '_' + name + '.' + this.config.extension;
var writeFile = Promise.promisify(fs.writeFile, fs);
return writeFile(path.join(this.config.directory, filename), stub).yield(filename);
}).bind();
2013-09-04 20:36:56 -04:00
},
// Lists all available migration versions, as an array.
listAll: function(config) {
2013-10-24 21:54:35 -04:00
return this.init(config)
.then(function() {
return Promise.promisify(fs.readdir, fs)(this.config.directory);
2013-09-04 20:36:56 -04:00
})
.then(function(files) {
return _.reduce(files, function(memo, value) {
memo.push(value);
return memo;
}, []);
})
.bind();
2013-09-04 20:36:56 -04:00
},
// Lists all migrations that have been completed for the current db, as an array.
listCompleted: function(config) {
2013-10-24 21:54:35 -04:00
return this.init(config)
.then(function() {
return this.knex(this.config.tableName).orderBy('id').select('name');
})
.bind();
2013-09-04 20:36:56 -04:00
},
2013-10-24 21:54:35 -04:00
// Check if the current migration has run.
hasRun: function(versions, check) {
return _.some(versions, function(version) {
return (version.indexOf(check) === 0);
2013-09-04 20:36:56 -04:00
});
},
// Shared between the `up` and `down` migrations, this
// helps to create the batch of migrations that need to be run.
direction: function(direction, version, config) {
version = parseVersion(version || 'latest');
return this.ensureTable(config)
.then(this.migrationData)
.spread(function(all, completed, version) {
});
},
// Gets the migration list from the specified migration directory,
// as well as the list of completed migrations to check what
// should be run.
migrationData: function() {
return Promise.all([
this.listAll(),
this.listCompleted()
]);
2013-10-24 21:54:35 -04:00
}
2013-09-04 20:36:56 -04:00
2013-10-24 21:54:35 -04:00
};
2013-09-04 20:36:56 -04:00
2013-10-24 21:54:35 -04:00
// Gets the current migration.
var getMigration = function(all, version, config) {
var found = _.find(all, function(item) {
item.indexOf(version) === 0;
});
if (!found) throw new Error('Unable to locate the specified migration ' + version);
return path.join(config.directory, found);
};
2013-09-04 20:36:56 -04:00
2013-10-24 21:54:35 -04:00
// Parse the version, which really only needs to be the
// timestamp of the migration we wish to migrate to.
var parseVersion = function(version) {
if (version !== 'latest') {
version = version.slice(0, 14);
if (version.length !== 14) {
throw new Error('Invalid migration provided');
2013-09-04 20:36:56 -04:00
}
}
2013-10-24 21:54:35 -04:00
return version;
2013-09-04 20:36:56 -04:00
};
2013-10-24 21:54:35 -04:00
// Get a date object in the correct format, without requiring
// a full out library like "moment.js".
2013-09-04 20:36:56 -04:00
var yyyymmddhhmmss = function() {
var d = new Date();
2013-10-24 21:54:35 -04:00
return d.getFullYear().toString() +
padDate(d.getMonth() + 1) +
padDate(d.getDate()) +
padDate(d.getHours()) +
padDate(d.getMinutes()) +
padDate(d.getSeconds());
2013-09-04 20:36:56 -04:00
};
// Ensure that we have 2 places for each of the date segments
var padDate = function(segment) {
segment = segment.toString();
return segment[1] ? segment : '0' + segment;
};
2013-10-24 21:54:35 -04:00
// Dasherize the string.
var dasherize = function(str) {
return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
};
2013-09-04 20:36:56 -04:00
2013-10-24 21:54:35 -04:00
module.exports = Migrate;