db-migrate 1.2.0 → 1.3.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.
@@ -12,84 +12,8 @@ const StateDone = require('../../methods/v2/statedone');
12
12
  const TranslateState = require('../../methods/v2/translatestate');
13
13
  const AddConventions = require('../../methods/v2/conventions');
14
14
  const util = require('util');
15
-
16
- /**
17
- * How to recover from an interrupted run of a migration, set per migration
18
- * by _meta.recovery:
19
- *
20
- * skip: skip the steps already executed and continue with the rest
21
- * rollback: revert the steps already executed and run the migration again
22
- */
23
- const RECOVERY_MODES = ['skip', 'rollback'];
24
- const RECOVERY_DEFAULT = 'skip';
25
-
26
- const recoveryMode = (file, interrupted) => {
27
- const name = file.name;
28
- const mode = file.get()._meta.recovery || RECOVERY_DEFAULT;
29
-
30
- if (RECOVERY_MODES.indexOf(mode) === -1) {
31
- throw new Error(
32
- `Invalid recovery mode "${mode}" in migration "${name}", use one of ` +
33
- RECOVERY_MODES.join(', ')
34
- );
35
- }
36
-
37
- if (interrupted.rollback) {
38
- if (mode !== 'rollback') {
39
- log.warn(
40
- `[recovery] ${name}: the previous run was interrupted while rolling ` +
41
- 'back, continuing the rollback'
42
- );
43
- }
44
-
45
- return 'rollback';
46
- }
47
-
48
- if (mode === 'skip' && interrupted.changed) {
49
- throw new Error(
50
- `Migration "${name}" was interrupted at step ${interrupted.step} and ` +
51
- 'changed since, so the executed steps can not be skipped safely. ' +
52
- "Set _meta.recovery to 'rollback' to revert them, or repair the " +
53
- 'state manually.'
54
- );
55
- }
56
-
57
- return mode;
58
- };
59
-
60
- /**
61
- * Revert the steps executed by an interrupted run. A step started but not
62
- * done did not change the database, so its reverse operation is dropped.
63
- */
64
- /**
65
- * Keep the reverse operations of the steps executed on the database. The
66
- * failed step only changed the database, if its main statement went through
67
- * before failing, signaled by the driver (e.g. a foreign key of createTable
68
- * failed after the table was created).
69
- */
70
- const executedSteps = (internals, failed, signaled) => {
71
- const done = internals.migrationDone;
72
-
73
- internals.modSchema.s = internals.modSchema.s.filter(
74
- entry =>
75
- entry.n <= done || (signaled && failed > done && entry.n === failed)
76
- );
77
- };
78
-
79
- /**
80
- * Name the instruction a migration failed at, or after if the instruction
81
- * itself finished and the migration code failed afterwards.
82
- */
83
- const describeFailure = (err, chain, internals) => {
84
- if (!err || typeof err !== 'object' || err.instruction || chain.op === 0) {
85
- return;
86
- }
87
-
88
- const after = internals.migrationDone >= chain.op;
89
- err.instruction = `${after ? 'after' : 'at'} step ${chain.op} ${
90
- chain.current
91
- }`;
92
- };
15
+ const { recoveryMode, executedSteps, describeFailure } = require('../../recovery');
16
+ const Dml = require('../../dml');
93
17
 
94
18
  const recoverByRollback = async (context, driver, file, interrupted) => {
95
19
  const internals = context.internals;
@@ -125,6 +49,11 @@ const execUnit = {
125
49
  },
126
50
 
127
51
  up: async function (context, driver, file) {
52
+ Dml.checkType(file);
53
+ if (Dml.isDml(file)) {
54
+ return Dml.up(context, driver, file);
55
+ }
56
+
128
57
  const _file = file.get();
129
58
  const chain = new Chain(context._driver, file, driver, context.internals, context._pdriver);
130
59
  if (!_file._meta.noDefaultColumn) {
@@ -205,6 +134,12 @@ const execUnit = {
205
134
  },
206
135
 
207
136
  fix: async function (context, driver, file) {
137
+ Dml.checkType(file);
138
+ if (Dml.isDml(file)) {
139
+ log.info(`[fix] skipping "${file.name}", dml migrations change no schema`);
140
+ return;
141
+ }
142
+
208
143
  const _file = file.get();
209
144
  const chain = new Chain(context._driver, file, driver, context.internals, context._pdriver);
210
145
  if (!_file._meta.noDefaultColumn) {
@@ -247,6 +182,11 @@ const execUnit = {
247
182
  },
248
183
 
249
184
  down: async function (context, driver, file, { abort } = {}) {
185
+ Dml.checkType(file);
186
+ if (Dml.isDml(file)) {
187
+ return Dml.down(context, driver, file, { abort });
188
+ }
189
+
250
190
  // if we get the abort signal this means we are in a rollback routine
251
191
  // which means in turn we want to avoid reloading the state
252
192
  if (abort !== true) {
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const log = require('db-migrate-shared').log;
4
+
5
+ /**
6
+ * How to recover from an interrupted run of a migration, set per migration
7
+ * by _meta.recovery:
8
+ *
9
+ * skip: skip the steps already executed and continue with the rest
10
+ * rollback: revert the steps already executed and run the migration again
11
+ */
12
+ const RECOVERY_MODES = ['skip', 'rollback'];
13
+ const RECOVERY_DEFAULT = 'skip';
14
+
15
+ const recoveryMode = (file, interrupted) => {
16
+ const name = file.name;
17
+ const mode = file.get()._meta.recovery || RECOVERY_DEFAULT;
18
+
19
+ if (RECOVERY_MODES.indexOf(mode) === -1) {
20
+ throw new Error(
21
+ `Invalid recovery mode "${mode}" in migration "${name}", use one of ` +
22
+ RECOVERY_MODES.join(', ')
23
+ );
24
+ }
25
+
26
+ if (interrupted.rollback) {
27
+ if (mode !== 'rollback') {
28
+ log.warn(
29
+ `[recovery] ${name}: the previous run was interrupted while rolling ` +
30
+ 'back, continuing the rollback'
31
+ );
32
+ }
33
+
34
+ return 'rollback';
35
+ }
36
+
37
+ if (mode === 'skip' && interrupted.changed) {
38
+ throw new Error(
39
+ `Migration "${name}" was interrupted at step ${interrupted.step} and ` +
40
+ 'changed since, so the executed steps can not be skipped safely. ' +
41
+ "Set _meta.recovery to 'rollback' to revert them, or repair the " +
42
+ 'state manually.'
43
+ );
44
+ }
45
+
46
+ return mode;
47
+ };
48
+
49
+ /**
50
+ * Revert the steps executed by an interrupted run. A step started but not
51
+ * done did not change the database, so its reverse operation is dropped.
52
+ */
53
+ /**
54
+ * Keep the reverse operations of the steps executed on the database. The
55
+ * failed step only changed the database, if its main statement went through
56
+ * before failing, signaled by the driver (e.g. a foreign key of createTable
57
+ * failed after the table was created).
58
+ */
59
+ const executedSteps = (internals, failed, signaled) => {
60
+ const done = internals.migrationDone;
61
+
62
+ internals.modSchema.s = internals.modSchema.s.filter(
63
+ entry =>
64
+ entry.n <= done || (signaled && failed > done && entry.n === failed)
65
+ );
66
+ };
67
+
68
+ /**
69
+ * Name the instruction a migration failed at, or after if the instruction
70
+ * itself finished and the migration code failed afterwards.
71
+ */
72
+ const describeFailure = (err, chain, internals) => {
73
+ if (!err || typeof err !== 'object' || err.instruction || chain.op === 0) {
74
+ return;
75
+ }
76
+
77
+ const after = internals.migrationDone >= chain.op;
78
+ err.instruction = `${after ? 'after' : 'at'} step ${chain.op} ${
79
+ chain.current
80
+ }`;
81
+ };
82
+
83
+ module.exports = { RECOVERY_MODES, recoveryMode, executedSteps, describeFailure };
package/lib/seed.js ADDED
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const Promise = require('bluebird');
6
+ const log = require('db-migrate-shared').log;
7
+ const { FLAG, insertRows } = require('./dml');
8
+
9
+ /**
10
+ * Static seeds insert data for development and tests, seeds/<name>.js:
11
+ *
12
+ * exports.seed = async db => {
13
+ * await db.insert('pets', [{ name: 'Rex' }, { name: 'Tom' }]);
14
+ * };
15
+ *
16
+ * The rows are flagged with seed:<name> in their __dbmigrate__flag column.
17
+ * Seeding again removes the rows of the previous run first, so seeds can be
18
+ * changed and run any time. The tables each seed inserted into are kept in
19
+ * the state, under SEEDS, in the order the seeds ran.
20
+ */
21
+
22
+ const SEEDS = '__dbmigrate_seeds__';
23
+
24
+ const flag = name => `seed:${name}`;
25
+
26
+ const loadState = async (pdriver, internals) => {
27
+ const row = await pdriver._getKV(internals.migrationState, SEEDS);
28
+ return { exists: !!row, seeds: row ? JSON.parse(row.value) : {} };
29
+ };
30
+
31
+ const saveState = (pdriver, internals, state) => {
32
+ if (internals.dryRun) {
33
+ return Promise.resolve();
34
+ }
35
+
36
+ const value = JSON.stringify(state.seeds);
37
+
38
+ if (state.exists) {
39
+ return pdriver._updateKV(internals.migrationState, SEEDS, value);
40
+ }
41
+
42
+ state.exists = true;
43
+ return pdriver._insertKV(internals.migrationState, SEEDS, value);
44
+ };
45
+
46
+ const columns = (internals, table) => {
47
+ const t = internals.schema.c[table];
48
+ return t ? t.columns || t : null;
49
+ };
50
+
51
+ class SeedDb {
52
+ constructor (driver, internals, name, touch) {
53
+ this.driver = driver;
54
+ this.internals = internals;
55
+ this.name = name;
56
+ this.touch = touch;
57
+ }
58
+
59
+ async insert (table, ...args) {
60
+ const { rows } = insertRows(args);
61
+ const known = columns(this.internals, table);
62
+
63
+ if (!known || !known[FLAG]) {
64
+ throw new Error(
65
+ `Seed "${this.name}" can not insert into "${table}", seeds insert ` +
66
+ `into tables created by v2 migrations only, by their ${FLAG} ` +
67
+ 'column, so the rows can be removed again.'
68
+ );
69
+ }
70
+
71
+ if (!rows.length) {
72
+ return;
73
+ }
74
+
75
+ await this.touch(table);
76
+ return this.driver.insert(
77
+ table,
78
+ rows.map(row => Object.assign({}, row, { [FLAG]: flag(this.name) }))
79
+ );
80
+ }
81
+
82
+ all (...args) {
83
+ return this.driver.all(...args);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Removes the rows of a seed, the tables in reverse order, for rows
89
+ * referencing rows of the same seed. Tables dropped since are skipped.
90
+ */
91
+ const remove = async (driver, internals, state, name) => {
92
+ const tables = (state.seeds[name] || []).slice().reverse();
93
+
94
+ await Promise.each(tables, table => {
95
+ if (!columns(internals, table)) {
96
+ return;
97
+ }
98
+
99
+ return driver.runSql(
100
+ `DELETE FROM ${driver.escapeDDL(table)} WHERE ${driver.escapeDDL(FLAG)} = ?`,
101
+ [flag(name)]
102
+ );
103
+ });
104
+
105
+ delete state.seeds[name];
106
+ };
107
+
108
+ const files = dir => {
109
+ if (!fs.existsSync(dir)) {
110
+ return [];
111
+ }
112
+
113
+ return fs
114
+ .readdirSync(dir)
115
+ .filter(file => /\.js$/.test(file))
116
+ .sort()
117
+ .map(file => ({
118
+ name: file.replace(/\.js$/, ''),
119
+ path: path.join(dir, file)
120
+ }));
121
+ };
122
+
123
+ const select = (list, name, what) => {
124
+ if (!name) {
125
+ return list;
126
+ }
127
+
128
+ const found = list.filter(seed => seed.name === name);
129
+ if (!found.length) {
130
+ throw new Error(`There is no ${what} "${name}"`);
131
+ }
132
+
133
+ return found;
134
+ };
135
+
136
+ /**
137
+ * Seeds all seeds of the directory, or the one named. The rows of the
138
+ * seeds run before are removed first, of all seeds the last run first,
139
+ * including seeds whose files were removed since.
140
+ */
141
+ const run = async (walker, name) => {
142
+ const internals = walker.internals;
143
+ const driver = walker._driver;
144
+ const pdriver = walker._pdriver;
145
+ const dir = path.resolve(internals.argv['seeds-dir']);
146
+ const seeds = select(files(dir), name, 'seed');
147
+ const state = await loadState(pdriver, internals);
148
+
149
+ const previous = name ? [name] : Object.keys(state.seeds).reverse();
150
+ await Promise.each(previous, seed => remove(driver, internals, state, seed));
151
+ await saveState(pdriver, internals, state);
152
+
153
+ await Promise.each(seeds, async seed => {
154
+ log.info(`[seed] ${seed.name}`);
155
+ delete require.cache[require.resolve(seed.path)];
156
+ const mod = require(seed.path);
157
+
158
+ if (typeof mod.seed !== 'function') {
159
+ throw new Error(`The seed "${seed.name}" exports no seed function`);
160
+ }
161
+
162
+ state.seeds[seed.name] = [];
163
+ const touch = async table => {
164
+ const tables = state.seeds[seed.name];
165
+ if (tables.indexOf(table) === -1) {
166
+ // recorded before inserting, so a failing seed is removed as well
167
+ tables.push(table);
168
+ await saveState(pdriver, internals, state);
169
+ }
170
+ };
171
+
172
+ await mod.seed(new SeedDb(driver, internals, seed.name, touch), {
173
+ options: internals.safeOptions,
174
+ dbm: internals.safeOptions.dbmigrate
175
+ });
176
+ await saveState(pdriver, internals, state);
177
+ });
178
+ };
179
+
180
+ /**
181
+ * Removes the rows of all seeds, or of the one named.
182
+ */
183
+ const undo = async (walker, name) => {
184
+ const internals = walker.internals;
185
+ const state = await loadState(walker._pdriver, internals);
186
+ const seeds = name
187
+ ? select(Object.keys(state.seeds).map(n => ({ name: n })), name, 'seeded seed')
188
+ : Object.keys(state.seeds)
189
+ .reverse()
190
+ .map(n => ({ name: n }));
191
+
192
+ await Promise.each(seeds, seed => {
193
+ log.info(`[seed] removing ${seed.name}`);
194
+ return remove(walker._driver, internals, state, seed.name);
195
+ });
196
+ await saveState(walker._pdriver, internals, state);
197
+ };
198
+
199
+ module.exports = { run, undo, SEEDS };
package/package.json CHANGED
@@ -31,7 +31,7 @@
31
31
  "programmable",
32
32
  "api"
33
33
  ],
34
- "version": "1.2.0",
34
+ "version": "1.3.0",
35
35
  "bugs": {
36
36
  "url": "https://github.com/db-migrate/node-db-migrate/issues"
37
37
  },
@@ -57,9 +57,9 @@
57
57
  "@hapi/code": "9.x",
58
58
  "@hapi/eslint-plugin": "^6.0.0",
59
59
  "@hapi/lab": "25.x",
60
- "db-migrate-base": "^2.4.1",
61
- "db-migrate-mysql": "^3.1.1",
62
- "db-migrate-sqlite3": "^1.1.1",
60
+ "db-migrate-base": "^2.5.0",
61
+ "db-migrate-mysql": "^3.2.0",
62
+ "db-migrate-sqlite3": "^1.2.0",
63
63
  "eslint": "^7.32.0",
64
64
  "eslint-config-standard": "^14.1.1",
65
65
  "eslint-plugin-import": "^2.26.0",