db-migrate 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,12 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
20
20
 
21
21
  # Usage
22
22
 
23
+ ## Upgrading from 0.11
24
+
25
+ db-migrate 0.11 and older are end of life. See the
26
+ [changelog of 1.0.0](CHANGELOG.md) for what changed, v1 migrations keep
27
+ working unchanged.
28
+
23
29
  ## Requirements
24
30
 
25
31
  Officially supported is Node.js 24 and newer. Older versions may work, but
@@ -61,6 +67,14 @@ db-migrate to use the `tunnel` setting of your database config:
61
67
 
62
68
  $ npm install db-migrate-plugin-tunnel-ssh
63
69
 
70
+ ## db-migrate for meta
71
+
72
+ [meta-db-migrate](https://github.com/db-migrate/meta-db-migrate) is db-migrate
73
+ rewritten in meta: a program ships with its migrations compiled in, as one
74
+ binary. It uses the same migrations table and the same state as db-migrate,
75
+ v2 migrations included, so a database migrated by one of them can be carried
76
+ on by the other.
77
+
64
78
  ## Resources and usage instructions
65
79
 
66
80
  Please follow the link below, for usage instructions examples and the full
package/api.js CHANGED
@@ -154,6 +154,14 @@ dbmigrate.prototype = {
154
154
  * @return boolean
155
155
  */
156
156
  addConfiguration: function (description, args, type) {
157
+ // the command line options are parsed already when the API is available
158
+ if (typeof this.internals.argv.describe !== 'function') {
159
+ log.warn(
160
+ 'addConfiguration has no effect, the options are parsed already.'
161
+ );
162
+ return false;
163
+ }
164
+
157
165
  var name = args.shift();
158
166
  this.internals.argv.describe(name, description);
159
167
 
@@ -368,7 +376,8 @@ dbmigrate.prototype = {
368
376
  */
369
377
  createDatabase: function (dbname, callback) {
370
378
  var executeDB = load('db');
371
- this.internals.argv._.push(dbname);
379
+ // in module mode there are no positional arguments yet
380
+ this.internals.argv._ = (this.internals.argv._ || []).concat(dbname);
372
381
  this.internals.mode = 'create';
373
382
  return Promise.resolve(executeDB(this.internals, this.config)).asCallback(
374
383
  callback
@@ -380,7 +389,8 @@ dbmigrate.prototype = {
380
389
  */
381
390
  dropDatabase: function (dbname, callback) {
382
391
  var executeDB = load('db');
383
- this.internals.argv._.push(dbname);
392
+ // in module mode there are no positional arguments yet
393
+ this.internals.argv._ = (this.internals.argv._ || []).concat(dbname);
384
394
  this.internals.mode = 'drop';
385
395
  return Promise.resolve(executeDB(this.internals, this.config)).asCallback(
386
396
  callback
package/connect.js CHANGED
@@ -2,15 +2,37 @@
2
2
  * This file is going to disappear.
3
3
  * Only still here for backwards compatibility.
4
4
  * */
5
- var recursive = require('final-fs').readdirRecursive;
6
5
  var fs = require('fs');
7
6
  var driver = require('./lib/driver');
8
7
  var path = require('path');
9
8
  var log = require('db-migrate-shared').log;
9
+ var resolveEnv = require('./lib/config').resolveEnv;
10
10
  const Promise = require('bluebird');
11
11
 
12
12
  Promise.promisifyAll(driver);
13
13
 
14
+ /**
15
+ * The config.json of a scope, with { "ENV": ... } resolved, or null.
16
+ */
17
+ function scopeConfig (dirPath, scope) {
18
+ const file = path.resolve(dirPath, scope, 'config.json');
19
+
20
+ if (!fs.existsSync(file)) {
21
+ return null;
22
+ }
23
+
24
+ log.info('loaded extra config for migration subfolder: "' + scope + '/config.json"');
25
+ return resolveEnv(JSON.parse(fs.readFileSync(file, 'utf8')));
26
+ }
27
+
28
+ /**
29
+ * A scope config with more than database or schema connects on its own,
30
+ * inheriting the settings of the environment it does not set itself.
31
+ */
32
+ const SWITCH_ONLY = ['database', 'schema'];
33
+ const ownConnection = conf =>
34
+ Object.keys(conf).some(key => SWITCH_ONLY.indexOf(key) === -1);
35
+
14
36
  exports.connect = async function (config, PassedClass) {
15
37
  var internals = {};
16
38
  var prefix = 'migration';
@@ -20,123 +42,47 @@ exports.connect = async function (config, PassedClass) {
20
42
  config = config.config;
21
43
  }
22
44
 
23
- const db2 = await driver.connectAsync(config, internals);
24
-
25
- return Promise.fromCallback(callback => {
26
- driver.connect(config, internals, function (err, db) {
27
- if (err) {
28
- callback(err);
29
- return;
30
- }
31
-
32
- const realClose = db.close;
33
- // close both lines with one disconnect action
34
- db.close = function (cb) {
35
- db2.close(function () {});
36
- db.close = realClose;
37
- db.close(cb);
38
- };
39
-
40
- var dirPath = path.resolve(
41
- internals.argv['migrations-dir'] || 'migrations'
42
- );
43
- if (internals.migrationMode) {
44
- if (internals.migrationMode !== 'all') {
45
- var switched = false;
46
- var newConf;
45
+ const dirPath = path.resolve(internals.argv['migrations-dir'] || 'migrations');
46
+ const scope = internals.migrationMode;
47
+ const conf = scope ? scopeConfig(dirPath, scope) : null;
48
+ let switchTo = null;
47
49
 
48
- try {
49
- newConf = require(path.resolve(
50
- internals.argv['migrations-dir'] || 'migrations',
51
- internals.migrationMode
52
- ) + '/config.json');
53
- log.info(
54
- 'loaded extra config for migration subfolder: "' +
55
- internals.migrationMode +
56
- '/config.json"'
57
- );
58
- switched = true;
59
- } catch (e) {}
60
-
61
- if (switched) {
62
- db.switchDatabase(newConf, function (err) {
63
- if (err) {
64
- return callback(err);
65
- }
66
- internals.locTitle = internals.migrationMode;
67
- callback(
68
- null,
69
- new PassedClass(
70
- db,
71
- dirPath,
72
- internals.mode !== 'static',
73
- internals,
74
- prefix,
75
- { db2 }
76
- )
77
- );
78
- });
79
- } else {
80
- internals.locTitle = internals.migrationMode;
81
- callback(
82
- null,
83
- new PassedClass(
84
- db,
85
- dirPath,
86
- internals.mode !== 'static',
87
- internals,
88
- prefix,
89
- { db2 }
90
- )
91
- );
92
- }
93
- } else {
94
- recursive(
95
- dirPath,
96
- false,
97
- internals.argv['migrations-dir'] || 'migrations'
98
- ).then(function (files) {
99
- var oldClose = db.close;
100
-
101
- files = files.filter(function (file) {
102
- return file !== 'migrations' && fs.statSync(file).isDirectory();
103
- });
50
+ if (conf && ownConnection(conf)) {
51
+ // a database of its own, with its own migrations and state tables
52
+ config = Object.assign({}, config, conf);
53
+ } else if (conf && (conf.database || conf.schema)) {
54
+ switchTo = conf;
55
+ }
104
56
 
105
- files.push('');
57
+ const db2 = await driver.connectAsync(config, internals);
58
+ const db = await driver.connectAsync(config, internals);
59
+
60
+ const realClose = db.close;
61
+ // close both lines with one disconnect action
62
+ db.close = function (cb) {
63
+ db2.close(function () {});
64
+ db.close = realClose;
65
+ db.close(cb);
66
+ };
67
+
68
+ if (switchTo) {
69
+ // both lines, so the state is kept in the database of the scope as well
70
+ await Promise.promisify(db.switchDatabase, { context: db })(switchTo);
71
+ await Promise.promisify(db2.switchDatabase, { context: db2 })(switchTo);
72
+ }
106
73
 
107
- db.close = function (cb) {
108
- migrationFiles(
109
- files,
110
- callback,
111
- config,
112
- internals,
113
- PassedClass,
114
- db,
115
- oldClose,
116
- prefix,
117
- db2,
118
- cb
119
- );
120
- };
74
+ if (scope) {
75
+ internals.locTitle = scope;
76
+ }
121
77
 
122
- db.close();
123
- });
124
- }
125
- } else {
126
- callback(
127
- null,
128
- new PassedClass(
129
- db,
130
- dirPath,
131
- internals.mode !== 'static',
132
- internals,
133
- prefix,
134
- { db2 }
135
- )
136
- );
137
- }
138
- });
139
- });
78
+ return new PassedClass(
79
+ db,
80
+ dirPath,
81
+ internals.mode !== 'static',
82
+ internals,
83
+ prefix,
84
+ { db2 }
85
+ );
140
86
  };
141
87
 
142
88
  exports.driver = function (config, callback) {
@@ -148,69 +94,3 @@ exports.driver = function (config, callback) {
148
94
 
149
95
  driver.connect(config, internals, callback);
150
96
  };
151
-
152
- function migrationFiles (
153
- files,
154
- callback,
155
- config,
156
- internals,
157
- PassedClass,
158
- db,
159
- close,
160
- prefix,
161
- db2,
162
- cb
163
- ) {
164
- var file;
165
- var switched = false;
166
- var newConf;
167
-
168
- if (files.length === 1) {
169
- db.close = close;
170
- }
171
-
172
- file = files.pop();
173
- log.info('Enter scope "' + (file !== '' ? file : '/') + '"');
174
-
175
- if (file !== '') {
176
- try {
177
- fs.statSync(path.resolve(file + '/config.json'));
178
- newConf = require(path.resolve(file + '/config.json'));
179
- log.info(
180
- 'loaded extra config for migration subfolder: "' +
181
- file +
182
- '/config.json"'
183
- );
184
- switched = true;
185
- } catch (e) {}
186
- }
187
-
188
- db.switchDatabase(switched ? newConf : config.database, function () {
189
- internals.matching = file.substr(
190
- file.indexOf(internals.argv['migrations-dir'] || 'migrations') +
191
- (internals.argv['migrations-dir'] || 'migrations').length +
192
- 1
193
- );
194
-
195
- if (internals.matching.length === 0) {
196
- internals.matching = '';
197
- }
198
-
199
- internals.locTitle = internals.matching;
200
- callback(
201
- null,
202
- new PassedClass(
203
- db,
204
- internals.argv['migrations-dir'],
205
- internals.mode !== 'static',
206
- internals,
207
- prefix,
208
- { db2 }
209
- )
210
- );
211
-
212
- if (typeof cb === 'function') {
213
- cb();
214
- }
215
- });
216
- }
package/lib/chain.js CHANGED
@@ -32,6 +32,43 @@ const describe = (method, args) =>
32
32
  .map(a => JSON.stringify(a))
33
33
  .join(', ')})`;
34
34
 
35
+ /**
36
+ * Instructions declaring an object, which can adopt objects created outside
37
+ * of v2 migrations into the schema, see adopt.
38
+ */
39
+ const ADOPTABLE = /^(create|add)/;
40
+
41
+ /**
42
+ * Adopting is for objects unknown to the schema only.
43
+ */
44
+ const assertUnknown = (m, args, schema) => {
45
+ const [t, a, b] = args;
46
+ let known = false;
47
+
48
+ switch (m) {
49
+ case 'createTable':
50
+ case 'createCollection':
51
+ known = !!schema.c[t];
52
+ break;
53
+ case 'addColumn':
54
+ known = !!(schema.c[t] && (schema.c[t][a] || schema.c[t].columns?.[a]));
55
+ break;
56
+ case 'addIndex':
57
+ known = !!(schema.i[t] && schema.i[t][a]);
58
+ break;
59
+ case 'addForeignKey':
60
+ known = !!(schema.f[t] && schema.f[t][b]);
61
+ break;
62
+ }
63
+
64
+ if (known) {
65
+ throw new Error(
66
+ `${describe(m, args)} is known to the schema already, adopt is for ` +
67
+ 'objects created outside of v2 migrations only.'
68
+ );
69
+ }
70
+ };
71
+
35
72
  let rollbackCounter = 0;
36
73
  let rollbackSignal = null;
37
74
  let signal = null;
@@ -107,6 +144,13 @@ class Chain {
107
144
  this.op = 0;
108
145
  this.recovery = null;
109
146
 
147
+ /**
148
+ * adopt: declares objects created outside of v2 migrations, e.g. by v1
149
+ * migrations, so v2 migrations can work with them. Only learns them,
150
+ * nothing is executed, and reverting it only forgets them again.
151
+ */
152
+ this.adopt = {};
153
+
110
154
  DEFAULT.concat(Object.keys(this.context.learnable || [])).forEach(
111
155
  method => {
112
156
  this[method] = function (...args) {
@@ -116,6 +160,15 @@ class Chain {
116
160
 
117
161
  return this.exec.apply(this, [method].concat(args));
118
162
  };
163
+
164
+ if (ADOPTABLE.test(method)) {
165
+ this.adopt[method] = async (...args) => {
166
+ this.transferInt();
167
+ assertUnknown(method, args, this.internals.schema);
168
+
169
+ return this.run(method, args, 'adopt');
170
+ };
171
+ }
119
172
  }
120
173
  );
121
174
  }
@@ -153,6 +206,21 @@ class Chain {
153
206
  }
154
207
 
155
208
  async exec (m, ...args) {
209
+ return this.run(m, args, null);
210
+ }
211
+
212
+ /**
213
+ * Learn an instruction only, without executing it, to revert an adopt.
214
+ */
215
+ async learnOnly (m, ...args) {
216
+ return this.run(m, args, 'learn');
217
+ }
218
+
219
+ /**
220
+ * mode: null to execute the instruction, 'adopt' or 'learn' to only learn
221
+ * it, skipping the links executing it on the database
222
+ */
223
+ async run (m, args, mode) {
156
224
  let ret;
157
225
  const stat = [];
158
226
  const op = ++this.op;
@@ -183,9 +251,22 @@ class Chain {
183
251
  continue;
184
252
  }
185
253
 
254
+ if (mode && this._step.executes) {
255
+ continue;
256
+ }
257
+
258
+ const mod = this.internals.modSchema;
259
+ const recorded = mod ? mod.s.length : 0;
186
260
  ret = await this._interface[m].apply(this._interface, args);
187
261
  stat.push(ret);
188
262
 
263
+ // an adopted object is only forgotten again when reverting
264
+ if (mode === 'adopt' && this._step.learns && mod) {
265
+ mod.s.slice(recorded).forEach(entry => {
266
+ entry.t = 2;
267
+ });
268
+ }
269
+
189
270
  if (this._step.hasStateUsage) {
190
271
  this._step.useState(ret);
191
272
  }
@@ -1,6 +1,7 @@
1
1
  var path = require('path');
2
2
  var log = require('db-migrate-shared').log;
3
3
  var migrationHook = require('./helper/migration-hook.js');
4
+ var eachScope = require('./helper/scopes.js');
4
5
 
5
6
  async function prepare (internals, config) {
6
7
  try {
@@ -40,7 +41,7 @@ async function prepare (internals, config) {
40
41
  }
41
42
  }
42
43
 
43
- module.exports = async function (internals, config) {
44
+ async function execute (internals, config) {
44
45
  const migrator = await prepare(internals, config);
45
46
 
46
47
  try {
@@ -49,4 +50,8 @@ module.exports = async function (internals, config) {
49
50
  } catch (err) {
50
51
  return internals.onComplete(migrator, internals, err);
51
52
  }
53
+ }
54
+
55
+ module.exports = function (internals, config) {
56
+ return eachScope(internals, () => execute(internals, config));
52
57
  };
@@ -199,12 +199,8 @@ function shouldCreateV2File (internals, config) {
199
199
  }
200
200
 
201
201
  async function createSqlFiles (internals, config) {
202
- let migrationsDir = internals.argv['migrations-dir'];
203
-
204
- if (internals.migrationMode && internals.migrationMode !== 'all') {
205
- migrationsDir =
206
- internals.argv['migrations-dir'] + '/' + internals.migrationMode;
207
- }
202
+ // the directory of the migration, its scope included already
203
+ const migrationsDir = internals.argv['migrations-dir'];
208
204
 
209
205
  const sqlDir = migrationsDir + '/sqls';
210
206
  try {
@@ -1,63 +1,60 @@
1
1
  'use strict';
2
2
 
3
3
  var log = require('db-migrate-shared').log;
4
- var assert = require('assert');
4
+ var Promise = require('bluebird');
5
5
 
6
+ /**
7
+ * Creates or drops a database, resolving once it is done.
8
+ */
6
9
  function executeDB (internals, config, callback) {
7
10
  var index = require('../../connect');
8
11
 
9
- if (internals.argv._.length > 0) {
10
- internals.argv.dbname = internals.argv._.shift().toString();
11
- } else {
12
- log.info('Error: You must enter a database name!');
13
- return;
12
+ if (internals.argv._.length === 0) {
13
+ return Promise.reject(
14
+ new Error('You must enter a database name!')
15
+ ).asCallback(callback);
14
16
  }
15
17
 
18
+ internals.argv.dbname = internals.argv._.shift().toString();
16
19
  delete config.getCurrent().settings.database;
17
20
 
18
- index.driver(config.getCurrent().settings, function (err, db) {
19
- assert.ifError(err);
20
-
21
- if (internals.mode === 'create') {
22
- db.createDatabase(
23
- internals.argv.dbname,
24
- {
25
- ifNotExists: true
26
- },
27
- function (err) {
28
- if (err) {
29
- if (err.error) err = err.error;
30
- log.error('Error: Failed to create database!', err);
31
- } else {
32
- log.info('Created database "' + internals.argv.dbname + '"');
33
- }
21
+ var create = internals.mode === 'create';
22
+ if (!create && internals.mode !== 'drop') {
23
+ return Promise.reject(
24
+ new Error(
25
+ (internals.mode ? 'Unknown db command "db:' + internals.mode + '"' : 'Missing db command') +
26
+ ', use db:create or db:drop'
27
+ )
28
+ ).asCallback(callback);
29
+ }
34
30
 
35
- db.close();
36
- if (typeof callback === 'function') callback(err);
37
- else process.exit(err ? 1 : 0);
31
+ return Promise.fromCallback(function (cb) {
32
+ index.driver(config.getCurrent().settings, cb);
33
+ })
34
+ .then(function (db) {
35
+ return Promise.fromCallback(function (cb) {
36
+ if (create) {
37
+ db.createDatabase(internals.argv.dbname, { ifNotExists: true }, cb);
38
+ } else {
39
+ db.dropDatabase(internals.argv.dbname, { ifExists: true }, cb);
38
40
  }
39
- );
40
- } else if (internals.mode === 'drop') {
41
- db.dropDatabase(
42
- internals.argv.dbname,
43
- {
44
- ifExists: true
45
- },
46
- function (err) {
47
- if (err) {
48
- if (err.error) err = err.error;
49
- log.error('Error: Failed to drop database!', err);
50
- } else {
51
- log.info('Deleted database "' + internals.argv.dbname + '"');
52
- }
53
-
41
+ })
42
+ .catch(function (err) {
43
+ throw err && err.error ? err.error : err;
44
+ })
45
+ .finally(function () {
54
46
  db.close();
55
- if (typeof callback === 'function') callback(err);
56
- else process.exit(err ? 1 : 0);
57
- }
47
+ });
48
+ })
49
+ .then(function () {
50
+ log.info(
51
+ (create ? 'Created' : 'Deleted') +
52
+ ' database "' +
53
+ internals.argv.dbname +
54
+ '"'
58
55
  );
59
- }
60
- });
56
+ })
57
+ .asCallback(callback);
61
58
  }
62
59
 
63
60
  module.exports = executeDB;
@@ -1,6 +1,7 @@
1
1
  var path = require('path');
2
2
  var log = require('db-migrate-shared').log;
3
3
  var migrationHook = require('./helper/migration-hook.js');
4
+ var eachScope = require('./helper/scopes.js');
4
5
 
5
6
  async function prepare (internals, config) {
6
7
  try {
@@ -33,7 +34,7 @@ async function prepare (internals, config) {
33
34
  }
34
35
  }
35
36
 
36
- module.exports = async function (internals, config) {
37
+ async function execute (internals, config) {
37
38
  const migrator = await prepare(internals, config);
38
39
 
39
40
  try {
@@ -42,4 +43,8 @@ module.exports = async function (internals, config) {
42
43
  } catch (err) {
43
44
  return internals.onComplete(migrator, internals, err);
44
45
  }
46
+ }
47
+
48
+ module.exports = function (internals, config) {
49
+ return eachScope(internals, () => execute(internals, config));
45
50
  };
@@ -1,6 +1,7 @@
1
1
  var path = require('path');
2
2
  var log = require('db-migrate-shared').log;
3
3
  var migrationHook = require('./helper/migration-hook.js');
4
+ var eachScope = require('./helper/scopes.js');
4
5
 
5
6
  async function prepare (internals, config) {
6
7
  try {
@@ -41,7 +42,7 @@ async function prepare (internals, config) {
41
42
  }
42
43
  }
43
44
 
44
- module.exports = async function (internals, config) {
45
+ async function execute (internals, config) {
45
46
  const migrator = await prepare(internals, config);
46
47
 
47
48
  try {
@@ -50,4 +51,8 @@ module.exports = async function (internals, config) {
50
51
  } catch (err) {
51
52
  return internals.onComplete(migrator, internals, err);
52
53
  }
54
+ }
55
+
56
+ module.exports = function (internals, config) {
57
+ return eachScope(internals, () => execute(internals, config));
53
58
  };
@@ -13,7 +13,7 @@ function registerPluginLoader (plugins) {
13
13
  'Attention, multiple overwrites registered for %s, we are ' +
14
14
  'only loading the first plugin %s!',
15
15
  name,
16
- plugin.name
16
+ plugin[0].name
17
17
  );
18
18
  }
19
19
 
@@ -16,6 +16,7 @@ const files = {
16
16
  "helper/load-config": require('./helper/load-config.js'),
17
17
  "helper/migration-hook": require('./helper/migration-hook.js'),
18
18
  "helper/register-events": require('./helper/register-events.js'),
19
+ "helper/scopes": require('./helper/scopes.js'),
19
20
  "index": require('./index.js'),
20
21
  "on-complete": require('./on-complete.js'),
21
22
  "run": require('./run.js'),
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const log = require('db-migrate-shared').log;
6
+
7
+ /**
8
+ * The scopes below a migrations directory, nested ones as "a/b". Folders
9
+ * named sqls hold the files of sql-file migrations, they are no scopes.
10
+ */
11
+ function listScopes (dir, prefix = '') {
12
+ return fs
13
+ .readdirSync(dir, { withFileTypes: true })
14
+ .filter(entry => entry.isDirectory() && entry.name !== 'sqls')
15
+ .sort((a, b) => a.name.localeCompare(b.name))
16
+ .reduce((scopes, entry) => {
17
+ const scope = prefix + entry.name;
18
+ return scopes.concat(
19
+ scope,
20
+ listScopes(path.join(dir, entry.name), scope + '/')
21
+ );
22
+ }, []);
23
+ }
24
+
25
+ /**
26
+ * Runs a command for its scope, or with the scope "all" once for the
27
+ * migrations directory itself and once for every scope, one after another.
28
+ * The results of all scopes are returned together.
29
+ */
30
+ module.exports = async function eachScope (internals, run) {
31
+ if (internals.migrationMode !== 'all') {
32
+ return run();
33
+ }
34
+
35
+ const dir = path.resolve(internals.argv['migrations-dir'] || 'migrations');
36
+ const scopes = [''].concat(listScopes(dir));
37
+ const forceExit = internals.argv['force-exit'];
38
+ let results = [];
39
+
40
+ try {
41
+ for (let i = 0; i < scopes.length; ++i) {
42
+ const scope = scopes[i];
43
+ log.info('Enter scope "' + (scope || '/') + '"');
44
+
45
+ internals.migrationMode = scope || undefined;
46
+ internals.matching = scope;
47
+ internals.locTitle = undefined;
48
+ // exiting is only up to the last scope
49
+ internals.argv['force-exit'] = forceExit && i === scopes.length - 1;
50
+
51
+ const result = await run();
52
+ results = results.concat(result === undefined ? [] : result);
53
+ }
54
+ } finally {
55
+ internals.migrationMode = 'all';
56
+ internals.argv['force-exit'] = forceExit;
57
+ }
58
+
59
+ return results;
60
+ };
@@ -79,8 +79,9 @@ module.exports = function (internals, isModule) {
79
79
  .boolean('coffee-file')
80
80
  .describe(
81
81
  'ignore-on-init',
82
- 'Create files that will run only if ignore-on-init in the env is set ' +
83
- 'to false (currently works only with SQL)'
82
+ 'With create, create sql-file migrations whose up is skipped when ' +
83
+ 'running with --ignore-on-init, e.g. to initialize a database ' +
84
+ 'from a dump (sql-file migrations only)'
84
85
  )
85
86
  .boolean('ignore-on-init')
86
87
  .describe(
@@ -146,7 +147,25 @@ module.exports = function (internals, isModule) {
146
147
  });
147
148
  }
148
149
 
149
- internals.argv = deepExtend(internals.argv, rc('db-migrate', {}));
150
+ // in module mode the arguments of the process belong to the application
151
+ var rcConfig = rc('db-migrate', {}, isModule ? {} : undefined);
152
+
153
+ // the long names of options are aliases on the command line only
154
+ [
155
+ ['state-table', 'state'],
156
+ ['migration-table', 'table']
157
+ ].forEach(function ([alias, name]) {
158
+ if (rcConfig[alias] !== undefined && rcConfig[name] === undefined) {
159
+ rcConfig[name] = rcConfig[alias];
160
+ }
161
+ });
162
+
163
+ internals.argv = deepExtend(internals.argv, rcConfig);
164
+
165
+ // positional arguments, the API adds its own ones in module mode
166
+ if (!Array.isArray(internals.argv._)) {
167
+ internals.argv._ = [];
168
+ }
150
169
  internals.argv.rcconfig = internals.argv.config;
151
170
  internals.argv.config = internals.argv.configFile || _config;
152
171
 
@@ -1,6 +1,7 @@
1
1
  var path = require('path');
2
2
  var log = require('db-migrate-shared').log;
3
3
  var migrationHook = require('./helper/migration-hook.js');
4
+ var eachScope = require('./helper/scopes.js');
4
5
 
5
6
  async function prepare (internals, config) {
6
7
  try {
@@ -40,7 +41,7 @@ async function prepare (internals, config) {
40
41
  }
41
42
  }
42
43
 
43
- module.exports = async function (internals, config) {
44
+ async function execute (internals, config) {
44
45
  const migrator = await prepare(internals, config);
45
46
 
46
47
  try {
@@ -49,4 +50,8 @@ module.exports = async function (internals, config) {
49
50
  } catch (err) {
50
51
  return internals.onComplete(migrator, internals, err);
51
52
  }
53
+ }
54
+
55
+ module.exports = function (internals, config) {
56
+ return eachScope(internals, () => execute(internals, config));
52
57
  };
@@ -1,6 +1,7 @@
1
1
  var path = require('path');
2
2
  var log = require('db-migrate-shared').log;
3
3
  var migrationHook = require('./helper/migration-hook.js');
4
+ var eachScope = require('./helper/scopes.js');
4
5
 
5
6
  async function prepare (internals, config) {
6
7
  try {
@@ -40,7 +41,7 @@ async function prepare (internals, config) {
40
41
  }
41
42
  }
42
43
 
43
- module.exports = async function (internals, config) {
44
+ async function execute (internals, config) {
44
45
  const migrator = await prepare(internals, config);
45
46
 
46
47
  try {
@@ -49,4 +50,8 @@ module.exports = async function (internals, config) {
49
50
  } catch (err) {
50
51
  return internals.onComplete(migrator, internals, err);
51
52
  }
53
+ }
54
+
55
+ module.exports = function (internals, config) {
56
+ return eachScope(internals, () => execute(internals, config));
52
57
  };
package/lib/config.js CHANGED
@@ -94,6 +94,9 @@ exports.loadFile = function (fileName, currentEnv, plugins) {
94
94
  return exports.loadObject(config, currentEnv);
95
95
  };
96
96
 
97
+ // resolves { "ENV": ... } entries of a config, in place
98
+ exports.resolveEnv = walkConfig;
99
+
97
100
  function walkConfig (level) {
98
101
  for (var configEntry in level) {
99
102
  if (level[configEntry] && level[configEntry].ENV) {
@@ -181,6 +181,15 @@ const execUnit = {
181
181
  );
182
182
  describeFailure(err, chain, context.internals);
183
183
 
184
+ if (context.internals.modSchema.s.some(entry => entry.t === 3)) {
185
+ log.error(
186
+ `Migration "${file.name}" failed and can not be rolled back, it ` +
187
+ 'ran a step with { irreversible: true }. The steps executed ' +
188
+ 'stay, the next run continues after them.'
189
+ );
190
+ throw err;
191
+ }
192
+
184
193
  log.error(
185
194
  `Migration "${file.name}" failed, rolling back: ${
186
195
  (err && err.message) || err
@@ -246,7 +255,16 @@ const execUnit = {
246
255
  });
247
256
  }
248
257
  // start migration, see up comments
249
- await TranslateState(context._driver, file, driver, context.internals, context._pdriver);
258
+ try {
259
+ await TranslateState(context._driver, file, driver, context.internals, context._pdriver);
260
+ } catch (err) {
261
+ // refused before changing anything, the state is left as it was
262
+ if (err.irreversible && abort !== true) {
263
+ await State.endMigration(context._pdriver, file, context.internals);
264
+ }
265
+
266
+ throw err;
267
+ }
250
268
  await State.endMigration(context._pdriver, file, context.internals);
251
269
  return Promise.promisify(context.deleteMigrationRecord.bind(context))(file);
252
270
  // end migration, see up comments
package/lib/file.js CHANGED
@@ -200,6 +200,11 @@ File.loadFromFileystem = function (dir, prefix, internals) {
200
200
  };
201
201
 
202
202
  File.loadFromDatabase = function (dir, prefix, driver, internals) {
203
+ // --ignore-completed-migrations starts at the first migration
204
+ if (internals.ignoreCompleted) {
205
+ return Promise.resolve([]);
206
+ }
207
+
203
208
  log.verbose(`[${prefix}] loading from database`);
204
209
  return driver[LOADER[prefix]]()
205
210
  .catch(function (err) {
package/lib/learn.js CHANGED
@@ -1,15 +1,52 @@
1
1
  const Promise = require('bluebird');
2
2
  const Shadow = require('./driver/shadow');
3
3
 
4
+ /**
5
+ * Objects unknown to the schema were not created by a v2 migration, but e.g.
6
+ * by a v1 migration or by hand. The errors point to the ways of handling them:
7
+ * adopting them into the schema first, or dropping them irreversibly.
8
+ */
9
+ const IRREVERSIBLE = ', or pass { irreversible: true } to drop it without being able to revert it';
10
+
11
+ const unknown = (what, adopt, drop) =>
12
+ new Error(
13
+ `${what} is unknown to the schema of db-migrate, it was not created by a ` +
14
+ `v2 migration. Declare it first with db.adopt.${adopt}` +
15
+ (drop ? IRREVERSIBLE : '') +
16
+ '.'
17
+ );
18
+
19
+ const unknownTable = (t, drop) =>
20
+ unknown(`The table "${t}"`, `createTable("${t}", columns)`, drop);
21
+
22
+ const unknownColumn = (t, c, drop) =>
23
+ unknown(`The column "${c}" of "${t}"`, `addColumn("${t}", "${c}", spec)`, drop);
24
+
25
+ const unknownIndex = (t, i) =>
26
+ unknown(`The index "${i}" of "${t}"`, `addIndex("${t}", "${i}", columns)`);
27
+
28
+ const unknownForeignKey = (t, k, drop) =>
29
+ unknown(
30
+ `The foreign key "${k}" of "${t}"`,
31
+ `addForeignKey("${t}", referencedTable, "${k}", mapping)`,
32
+ drop
33
+ );
34
+
35
+ /**
36
+ * A step dropping an object unknown to the schema, asked for explicitly.
37
+ * It can not be reverted, a rollback refuses to start because of it.
38
+ */
39
+ const irreversible = (action, args) => ({ t: 3, a: action, c: args });
40
+
4
41
  class STD {
5
42
  constructor ({ schema, modSchema: mod, unlearn }, driver) {
6
- this.checkColumn = function (t, c) {
43
+ this.checkColumn = function (t, c, drop) {
7
44
  if (!this.schema[t]) {
8
- throw new Error(`There is no ${t} table in schema!`);
45
+ throw unknownTable(t, drop);
9
46
  }
10
47
 
11
48
  if (!this.schema[t][c] && !this.schema[t].columns?.[c]) {
12
- throw new Error(`There is no ${c} column in schema!`);
49
+ throw unknownColumn(t, c, drop);
13
50
  }
14
51
  };
15
52
  this.unlearn = unlearn;
@@ -32,10 +69,19 @@ class STD {
32
69
  this.modF = mod.f;
33
70
  }
34
71
 
35
- dropTable (t) {
72
+ dropTable (t, o = {}) {
36
73
  let alter = {};
37
74
  alter = { c: {}, i: {}, f: {} };
38
75
 
76
+ if (!this.schema[t] && !this.unlearn) {
77
+ if (!o || o.irreversible !== true) {
78
+ throw unknownTable(t, true);
79
+ }
80
+
81
+ this.modC.push(irreversible('dropTable', [t]));
82
+ return Promise.resolve(alter);
83
+ }
84
+
39
85
  if (this.schema[t]) {
40
86
  alter.c[t] = this.schema[t];
41
87
  this.modS[t] = this.schema[t];
@@ -125,7 +171,19 @@ class STD {
125
171
 
126
172
  removeColumn (t, c, o = {}) {
127
173
  const alter = {};
128
- this.checkColumn(t, c);
174
+
175
+ if (
176
+ o &&
177
+ o.irreversible === true &&
178
+ !this.unlearn &&
179
+ (!this.schema[t] ||
180
+ (!this.schema[t][c] && !this.schema[t].columns?.[c]))
181
+ ) {
182
+ this.modC.push(irreversible('removeColumn', [t, c]));
183
+ return Promise.resolve(alter);
184
+ }
185
+
186
+ this.checkColumn(t, c, true);
129
187
  const hasColumns = this.schema[t].columns !== undefined;
130
188
  const columns = hasColumns ? this.schema[t].columns : this.schema[t];
131
189
 
@@ -230,7 +288,7 @@ class STD {
230
288
 
231
289
  addColumn (t, c, s) {
232
290
  if (!this.schema[t]) {
233
- throw new Error(`There is no ${t} table in schema!`);
291
+ throw unknownTable(t);
234
292
  }
235
293
  this.schema[t] = this.schema[t] || {};
236
294
  const hasColumns = this.schema[t].columns !== undefined;
@@ -292,11 +350,11 @@ class STD {
292
350
  }
293
351
 
294
352
  if (!this.schema[t]) {
295
- throw new Error(`There is no ${t} table in schema!`);
353
+ throw unknownTable(t);
296
354
  }
297
355
 
298
356
  if (!this.indizies[t] || !this.indizies[t][i]) {
299
- throw new Error(`There is no index ${i} in ${t} table!`);
357
+ throw unknownIndex(t, i);
300
358
  }
301
359
 
302
360
  alter.i[t] = {};
@@ -312,11 +370,11 @@ class STD {
312
370
 
313
371
  addForeignKey (t, rt, k, m, r) {
314
372
  if (!this.schema[t]) {
315
- throw new Error(`There is no ${t} table in schema!`);
373
+ throw unknownTable(t);
316
374
  }
317
375
 
318
376
  if (!this.schema[rt]) {
319
- throw new Error(`There is no ${rt} table in schema!`);
377
+ throw unknownTable(rt);
320
378
  }
321
379
 
322
380
  if (!this.foreign[t]) this.foreign[t] = {};
@@ -335,13 +393,19 @@ class STD {
335
393
  removeForeignKey (t, k, o) {
336
394
  let alter = {};
337
395
  alter = { c: {}, i: {}, f: {} };
396
+ const known = this.schema[t] && this.foreign[t] && this.foreign[t][k];
397
+
398
+ if (!known && o && o.irreversible === true && !this.unlearn) {
399
+ this.modC.push(irreversible('removeForeignKey', [t, k]));
400
+ return Promise.resolve(alter);
401
+ }
338
402
 
339
403
  if (!this.schema[t]) {
340
- throw new Error(`There is no ${t} table in schema!`);
404
+ throw unknownTable(t, true);
341
405
  }
342
406
 
343
407
  if (!this.foreign[t] || !this.foreign[t][k]) {
344
- throw new Error(`There is no foreign key ${k} in ${t} table!`);
408
+ throw unknownForeignKey(t, k, true);
345
409
  }
346
410
 
347
411
  alter.f[t] = {};
@@ -377,6 +441,9 @@ module.exports = {
377
441
  // learns the schema and records the reverse operations
378
442
  learns: true,
379
443
 
444
+ unknownTable,
445
+ unknownColumn,
446
+
380
447
  getInterface: (context, file, driver, internals) => {
381
448
  if (context.learnable) {
382
449
  const _std = new STD(internals, context);
@@ -28,6 +28,9 @@ const noAction = prop => {
28
28
  };
29
29
 
30
30
  module.exports = {
31
+ // adds the default column to tables created on the database
32
+ executes: true,
33
+
31
34
  getInterface: (context, file, driver, internals) => {
32
35
  if (context.conventions) {
33
36
  const st = new AddConventions(internals, file, context);
@@ -1,4 +1,7 @@
1
1
  module.exports = {
2
+ // executes the instructions on the database
3
+ executes: true,
4
+
2
5
  getInterface (context, file, driver) {
3
6
  return driver;
4
7
  }
@@ -100,6 +100,10 @@ async function processEntry (
100
100
  case 1:
101
101
  await f[action](driver, args, internals);
102
102
  break;
103
+ case 2:
104
+ // an adopted object, only forgotten again
105
+ await driver.learnOnly.apply(driver, [action].concat(args));
106
+ break;
103
107
 
104
108
  default:
105
109
  throw new Error(`Invalid state record, of type ${type}`);
@@ -110,8 +114,28 @@ async function processEntry (
110
114
  await State.update(pdriver, file, internals.modSchema, internals);
111
115
  }
112
116
 
117
+ /**
118
+ * Steps run with { irreversible: true } can not be reverted, the migration
119
+ * is not reverted at all then, instead of only partially.
120
+ */
121
+ const irreversibleSteps = internals =>
122
+ internals.modSchema.s
123
+ .filter(entry => entry.t === 3)
124
+ .map(entry => `step ${entry.n} ${entry.a}(${entry.c.map(x => JSON.stringify(x)).join(', ')})`);
125
+
113
126
  module.exports = async (context, file, driver, internals, pdriver) => {
114
127
  const mod = internals.modSchema;
128
+ const irreversible = irreversibleSteps(internals);
129
+
130
+ if (irreversible.length > 0) {
131
+ const err = new Error(
132
+ `Migration "${file.name}" can not be reverted, it ran ` +
133
+ `${irreversible.join(', ')} with { irreversible: true }.`
134
+ );
135
+ // nothing was changed
136
+ err.irreversible = true;
137
+ throw err;
138
+ }
115
139
 
116
140
  const chain = new Chain(context, file, driver, internals, pdriver);
117
141
  internals.unlearn = true;
package/package.json CHANGED
@@ -31,7 +31,7 @@
31
31
  "programmable",
32
32
  "api"
33
33
  ],
34
- "version": "1.0.0",
34
+ "version": "1.1.0",
35
35
  "bugs": {
36
36
  "url": "https://github.com/db-migrate/node-db-migrate/issues"
37
37
  },