index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. 'use strict';
  2. exports.__esModule = true;
  3. var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
  4. var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
  5. var _freeze = require('babel-runtime/core-js/object/freeze');
  6. var _freeze2 = _interopRequireDefault(_freeze);
  7. var _template2 = require('lodash/template');
  8. var _template3 = _interopRequireDefault(_template2);
  9. var _max2 = require('lodash/max');
  10. var _max3 = _interopRequireDefault(_max2);
  11. var _map2 = require('lodash/map');
  12. var _map3 = _interopRequireDefault(_map2);
  13. var _isUndefined2 = require('lodash/isUndefined');
  14. var _isUndefined3 = _interopRequireDefault(_isUndefined2);
  15. var _isEmpty2 = require('lodash/isEmpty');
  16. var _isEmpty3 = _interopRequireDefault(_isEmpty2);
  17. var _isBoolean2 = require('lodash/isBoolean');
  18. var _isBoolean3 = _interopRequireDefault(_isBoolean2);
  19. var _includes2 = require('lodash/includes');
  20. var _includes3 = _interopRequireDefault(_includes2);
  21. var _get2 = require('lodash/get');
  22. var _get3 = _interopRequireDefault(_get2);
  23. var _filter2 = require('lodash/filter');
  24. var _filter3 = _interopRequireDefault(_filter2);
  25. var _each2 = require('lodash/each');
  26. var _each3 = _interopRequireDefault(_each2);
  27. var _difference2 = require('lodash/difference');
  28. var _difference3 = _interopRequireDefault(_difference2);
  29. var _bind2 = require('lodash/bind');
  30. var _bind3 = _interopRequireDefault(_bind2);
  31. var _assign2 = require('lodash/assign');
  32. var _assign3 = _interopRequireDefault(_assign2);
  33. var _fs = require('fs');
  34. var _fs2 = _interopRequireDefault(_fs);
  35. var _path = require('path');
  36. var _path2 = _interopRequireDefault(_path);
  37. var _mkdirp = require('mkdirp');
  38. var _mkdirp2 = _interopRequireDefault(_mkdirp);
  39. var _bluebird = require('bluebird');
  40. var _bluebird2 = _interopRequireDefault(_bluebird);
  41. var _helpers = require('../helpers');
  42. var helpers = _interopRequireWildcard(_helpers);
  43. var _inherits = require('inherits');
  44. var _inherits2 = _interopRequireDefault(_inherits);
  45. function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
  46. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  47. // Migrator
  48. // -------
  49. function LockError(msg) {
  50. this.name = 'MigrationLocked';
  51. this.message = msg;
  52. }
  53. (0, _inherits2.default)(LockError, Error);
  54. var SUPPORTED_EXTENSIONS = (0, _freeze2.default)(['.co', '.coffee', '.eg', '.iced', '.js', '.litcoffee', '.ls', '.ts']);
  55. var CONFIG_DEFAULT = (0, _freeze2.default)({
  56. extension: 'js',
  57. tableName: 'knex_migrations',
  58. directory: './migrations',
  59. disableTransactions: false
  60. });
  61. // The new migration we're performing, typically called from the `knex.migrate`
  62. // interface on the main `knex` object. Passes the `knex` instance performing
  63. // the migration.
  64. var Migrator = function () {
  65. function Migrator(knex) {
  66. (0, _classCallCheck3.default)(this, Migrator);
  67. this.knex = knex;
  68. this.config = this.setConfig(knex.client.config.migrations);
  69. }
  70. // Migrators to the latest configuration.
  71. Migrator.prototype.latest = function latest(config) {
  72. var _this = this;
  73. this.config = this.setConfig(config);
  74. return this._migrationData().tap(validateMigrationList).spread(function (all, completed) {
  75. return _this._runBatch((0, _difference3.default)(all, completed), 'up');
  76. });
  77. };
  78. // Rollback the last "batch" of migrations that were run.
  79. Migrator.prototype.rollback = function rollback(config) {
  80. var _this2 = this;
  81. return _bluebird2.default.try(function () {
  82. _this2.config = _this2.setConfig(config);
  83. return _this2._migrationData().tap(validateMigrationList).then(function (val) {
  84. return _this2._getLastBatch(val);
  85. }).then(function (migrations) {
  86. return _this2._runBatch((0, _map3.default)(migrations, 'name'), 'down');
  87. });
  88. });
  89. };
  90. Migrator.prototype.status = function status(config) {
  91. this.config = this.setConfig(config);
  92. return _bluebird2.default.all([this.knex(this.config.tableName).select('*'), this._listAll()]).spread(function (db, code) {
  93. return db.length - code.length;
  94. });
  95. };
  96. // Retrieves and returns the current migration version we're on, as a promise.
  97. // If no migrations have been run yet, return "none".
  98. Migrator.prototype.currentVersion = function currentVersion(config) {
  99. this.config = this.setConfig(config);
  100. return this._listCompleted(config).then(function (completed) {
  101. var val = (0, _max3.default)((0, _map3.default)(completed, function (value) {
  102. return value.split('_')[0];
  103. }));
  104. return (0, _isUndefined3.default)(val) ? 'none' : val;
  105. });
  106. };
  107. Migrator.prototype.forceFreeMigrationsLock = function forceFreeMigrationsLock(config) {
  108. var _this3 = this;
  109. this.config = this.setConfig(config);
  110. var lockTable = this._getLockTableName();
  111. return this.knex.schema.hasTable(lockTable).then(function (exist) {
  112. return exist && _this3._freeLock();
  113. });
  114. };
  115. // Creates a new migration, with a given name.
  116. Migrator.prototype.make = function make(name, config) {
  117. var _this4 = this;
  118. this.config = this.setConfig(config);
  119. if (!name) {
  120. return _bluebird2.default.reject(new Error('A name must be specified for the generated migration'));
  121. }
  122. return this._ensureFolder(config).then(function (val) {
  123. return _this4._generateStubTemplate(val);
  124. }).then(function (val) {
  125. return _this4._writeNewMigration(name, val);
  126. });
  127. };
  128. // Lists all available migration versions, as a sorted array.
  129. Migrator.prototype._listAll = function _listAll(config) {
  130. this.config = this.setConfig(config);
  131. return _bluebird2.default.promisify(_fs2.default.readdir, { context: _fs2.default })(this._absoluteConfigDir()).then(function (migrations) {
  132. return (0, _filter3.default)(migrations, function (value) {
  133. var extension = _path2.default.extname(value);
  134. return (0, _includes3.default)(SUPPORTED_EXTENSIONS, extension);
  135. }).sort();
  136. });
  137. };
  138. // Ensures a folder for the migrations exist, dependent on the migration
  139. // config settings.
  140. Migrator.prototype._ensureFolder = function _ensureFolder() {
  141. var dir = this._absoluteConfigDir();
  142. return _bluebird2.default.promisify(_fs2.default.stat, { context: _fs2.default })(dir).catch(function () {
  143. return _bluebird2.default.promisify(_mkdirp2.default)(dir);
  144. });
  145. };
  146. // Ensures that a proper table has been created, dependent on the migration
  147. // config settings.
  148. Migrator.prototype._ensureTable = function _ensureTable() {
  149. var _this5 = this;
  150. var table = this.config.tableName;
  151. var lockTable = this._getLockTableName();
  152. return this.knex.schema.hasTable(table).then(function (exists) {
  153. return !exists && _this5._createMigrationTable(table);
  154. }).then(function () {
  155. return _this5.knex.schema.hasTable(lockTable);
  156. }).then(function (exists) {
  157. return !exists && _this5._createMigrationLockTable(lockTable);
  158. }).then(function () {
  159. return _this5.knex(lockTable).select('*');
  160. }).then(function (data) {
  161. return !data.length && _this5.knex(lockTable).insert({ is_locked: 0 });
  162. });
  163. };
  164. // Create the migration table, if it doesn't already exist.
  165. Migrator.prototype._createMigrationTable = function _createMigrationTable(tableName) {
  166. return this.knex.schema.createTableIfNotExists(tableName, function (t) {
  167. t.increments();
  168. t.string('name');
  169. t.integer('batch');
  170. t.timestamp('migration_time');
  171. });
  172. };
  173. Migrator.prototype._createMigrationLockTable = function _createMigrationLockTable(tableName) {
  174. return this.knex.schema.createTableIfNotExists(tableName, function (t) {
  175. t.integer('is_locked');
  176. });
  177. };
  178. Migrator.prototype._getLockTableName = function _getLockTableName() {
  179. return this.config.tableName + '_lock';
  180. };
  181. Migrator.prototype._isLocked = function _isLocked(trx) {
  182. var tableName = this._getLockTableName();
  183. return this.knex(tableName).transacting(trx).forUpdate().select('*').then(function (data) {
  184. return data[0].is_locked;
  185. });
  186. };
  187. Migrator.prototype._lockMigrations = function _lockMigrations(trx) {
  188. var tableName = this._getLockTableName();
  189. return this.knex(tableName).transacting(trx).update({ is_locked: 1 });
  190. };
  191. Migrator.prototype._getLock = function _getLock() {
  192. var _this6 = this;
  193. return this.knex.transaction(function (trx) {
  194. return _this6._isLocked(trx).then(function (isLocked) {
  195. if (isLocked) {
  196. throw new Error("Migration table is already locked");
  197. }
  198. }).then(function () {
  199. return _this6._lockMigrations(trx);
  200. });
  201. }).catch(function (err) {
  202. throw new LockError(err.message);
  203. });
  204. };
  205. Migrator.prototype._freeLock = function _freeLock() {
  206. var tableName = this._getLockTableName();
  207. return this.knex(tableName).update({ is_locked: 0 });
  208. };
  209. // Run a batch of current migrations, in sequence.
  210. Migrator.prototype._runBatch = function _runBatch(migrations, direction) {
  211. var _this7 = this;
  212. return this._getLock().then(function () {
  213. return _bluebird2.default.all((0, _map3.default)(migrations, (0, _bind3.default)(_this7._validateMigrationStructure, _this7)));
  214. }).then(function () {
  215. return _this7._latestBatchNumber();
  216. }).then(function (batchNo) {
  217. if (direction === 'up') batchNo++;
  218. return batchNo;
  219. }).then(function (batchNo) {
  220. return _this7._waterfallBatch(batchNo, migrations, direction);
  221. }).tap(function () {
  222. return _this7._freeLock();
  223. }).catch(function (error) {
  224. var cleanupReady = _bluebird2.default.resolve();
  225. if (error instanceof LockError) {
  226. // If locking error do not free the lock.
  227. helpers.warn('Can\'t take lock to run migrations: ' + error.message);
  228. helpers.warn('If you are sure migrations are not running you can release the ' + 'lock manually by deleting all the rows from migrations lock ' + 'table: ' + _this7._getLockTableName());
  229. } else {
  230. helpers.warn('migrations failed with error: ' + error.message);
  231. // If the error was not due to a locking issue, then remove the lock.
  232. cleanupReady = _this7._freeLock();
  233. }
  234. return cleanupReady.finally(function () {
  235. throw error;
  236. });
  237. });
  238. };
  239. // Validates some migrations by requiring and checking for an `up` and `down`
  240. // function.
  241. Migrator.prototype._validateMigrationStructure = function _validateMigrationStructure(name) {
  242. var migration = require(_path2.default.join(this._absoluteConfigDir(), name));
  243. if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {
  244. throw new Error('Invalid migration: ' + name + ' must have both an up and down function');
  245. }
  246. return name;
  247. };
  248. // Lists all migrations that have been completed for the current db, as an
  249. // array.
  250. Migrator.prototype._listCompleted = function _listCompleted() {
  251. var _this8 = this;
  252. var tableName = this.config.tableName;
  253. return this._ensureTable(tableName).then(function () {
  254. return _this8.knex(tableName).orderBy('id').select('name');
  255. }).then(function (migrations) {
  256. return (0, _map3.default)(migrations, 'name');
  257. });
  258. };
  259. // Gets the migration list from the specified migration directory, as well as
  260. // the list of completed migrations to check what should be run.
  261. Migrator.prototype._migrationData = function _migrationData() {
  262. return _bluebird2.default.all([this._listAll(), this._listCompleted()]);
  263. };
  264. // Generates the stub template for the current migration, returning a compiled
  265. // template.
  266. Migrator.prototype._generateStubTemplate = function _generateStubTemplate() {
  267. var stubPath = this.config.stub || _path2.default.join(__dirname, 'stub', this.config.extension + '.stub');
  268. return _bluebird2.default.promisify(_fs2.default.readFile, { context: _fs2.default })(stubPath).then(function (stub) {
  269. return (0, _template3.default)(stub.toString(), { variable: 'd' });
  270. });
  271. };
  272. // Write a new migration to disk, using the config and generated filename,
  273. // passing any `variables` given in the config to the template.
  274. Migrator.prototype._writeNewMigration = function _writeNewMigration(name, tmpl) {
  275. var config = this.config;
  276. var dir = this._absoluteConfigDir();
  277. if (name[0] === '-') name = name.slice(1);
  278. var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension;
  279. return _bluebird2.default.promisify(_fs2.default.writeFile, { context: _fs2.default })(_path2.default.join(dir, filename), tmpl(config.variables || {})).return(_path2.default.join(dir, filename));
  280. };
  281. // Get the last batch of migrations, by name, ordered by insert id in reverse
  282. // order.
  283. Migrator.prototype._getLastBatch = function _getLastBatch() {
  284. var tableName = this.config.tableName;
  285. return this.knex(tableName).where('batch', function (qb) {
  286. qb.max('batch').from(tableName);
  287. }).orderBy('id', 'desc');
  288. };
  289. // Returns the latest batch number.
  290. Migrator.prototype._latestBatchNumber = function _latestBatchNumber() {
  291. return this.knex(this.config.tableName).max('batch as max_batch').then(function (obj) {
  292. return obj[0].max_batch || 0;
  293. });
  294. };
  295. // If transaction config for a single migration is defined, use that.
  296. // Otherwise, rely on the common config. This allows enabling/disabling
  297. // transaction for a single migration at will, regardless of the common
  298. // config.
  299. Migrator.prototype._useTransaction = function _useTransaction(migration, allTransactionsDisabled) {
  300. var singleTransactionValue = (0, _get3.default)(migration, 'config.transaction');
  301. return (0, _isBoolean3.default)(singleTransactionValue) ? singleTransactionValue : !allTransactionsDisabled;
  302. };
  303. // Runs a batch of `migrations` in a specified `direction`, saving the
  304. // appropriate database information as the migrations are run.
  305. Migrator.prototype._waterfallBatch = function _waterfallBatch(batchNo, migrations, direction) {
  306. var _this9 = this;
  307. var knex = this.knex;
  308. var _config = this.config,
  309. tableName = _config.tableName,
  310. disableTransactions = _config.disableTransactions;
  311. var directory = this._absoluteConfigDir();
  312. var current = _bluebird2.default.bind({ failed: false, failedOn: 0 });
  313. var log = [];
  314. (0, _each3.default)(migrations, function (migration) {
  315. var name = migration;
  316. migration = require(directory + '/' + name);
  317. // We're going to run each of the migrations in the current "up".
  318. current = current.then(function () {
  319. if (_this9._useTransaction(migration, disableTransactions)) {
  320. return _this9._transaction(migration, direction, name);
  321. }
  322. return warnPromise(migration[direction](knex, _bluebird2.default), name);
  323. }).then(function () {
  324. log.push(_path2.default.join(directory, name));
  325. if (direction === 'up') {
  326. return knex(tableName).insert({
  327. name: name,
  328. batch: batchNo,
  329. migration_time: new Date()
  330. });
  331. }
  332. if (direction === 'down') {
  333. return knex(tableName).where({ name: name }).del();
  334. }
  335. });
  336. });
  337. return current.thenReturn([batchNo, log]);
  338. };
  339. Migrator.prototype._transaction = function _transaction(migration, direction, name) {
  340. return this.knex.transaction(function (trx) {
  341. return warnPromise(migration[direction](trx, _bluebird2.default), name, function () {
  342. trx.commit();
  343. });
  344. });
  345. };
  346. Migrator.prototype._absoluteConfigDir = function _absoluteConfigDir() {
  347. return _path2.default.resolve(process.cwd(), this.config.directory);
  348. };
  349. Migrator.prototype.setConfig = function setConfig(config) {
  350. return (0, _assign3.default)({}, CONFIG_DEFAULT, this.config || {}, config);
  351. };
  352. return Migrator;
  353. }();
  354. // Validates that migrations are present in the appropriate directories.
  355. exports.default = Migrator;
  356. function validateMigrationList(migrations) {
  357. var all = migrations[0];
  358. var completed = migrations[1];
  359. var diff = (0, _difference3.default)(completed, all);
  360. if (!(0, _isEmpty3.default)(diff)) {
  361. throw new Error('The migration directory is corrupt, the following files are missing: ' + diff.join(', '));
  362. }
  363. }
  364. function warnPromise(value, name, fn) {
  365. if (!value || typeof value.then !== 'function') {
  366. helpers.warn('migration ' + name + ' did not return a promise');
  367. if (fn && typeof fn === 'function') fn();
  368. }
  369. return value;
  370. }
  371. // Ensure that we have 2 places for each of the date segments.
  372. function padDate(segment) {
  373. segment = segment.toString();
  374. return segment[1] ? segment : '0' + segment;
  375. }
  376. // Get a date object in the correct format, without requiring a full out library
  377. // like "moment.js".
  378. function yyyymmddhhmmss() {
  379. var d = new Date();
  380. return d.getFullYear().toString() + padDate(d.getMonth() + 1) + padDate(d.getDate()) + padDate(d.getHours()) + padDate(d.getMinutes()) + padDate(d.getSeconds());
  381. }
  382. module.exports = exports['default'];