knex-migrator 4.0.5

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/lib/locking.js ADDED
@@ -0,0 +1,113 @@
1
+ const moment = require('moment');
2
+ const debug = require('debug')('knex-migrator:locking');
3
+ const errors = require('./errors');
4
+ const database = require('./database');
5
+
6
+ /**
7
+ * @description Private helper to lock the migrations_lock table.
8
+ * @TODO Locks in Sqlite won't work, because Sqlite doesn't offer read locks (https://github.com/TryGhost/knex-migrator/issues/87)
9
+ * @returns {*}
10
+ */
11
+ module.exports.lock = function (connection) {
12
+ debug('Lock.');
13
+
14
+ return database.createTransaction(connection, function (transacting) {
15
+ return transacting('migrations_lock')
16
+ .where({
17
+ lock_key: 'km01'
18
+ })
19
+ .forUpdate()
20
+ .then(function (data) {
21
+ if (!data || !data.length || data[0].locked) {
22
+ throw new errors.MigrationsAreLockedError({
23
+ message: 'Migrations are running at the moment. Please wait that the lock get`s released.',
24
+ context: 'Either the release was never released because of a e.g. died process or a parallel process is migrating at the moment.',
25
+ help: 'If your database looks okay, you can manually release the lock by running `UPDATE migrations_lock set locked=0 where lock_key=\'km01\';`.'
26
+ });
27
+ }
28
+
29
+ return (transacting || connection)('migrations_lock')
30
+ .where({
31
+ lock_key: 'km01'
32
+ })
33
+ .update({
34
+ locked: 1,
35
+ acquired_at: moment().format('YYYY-MM-DD HH:mm:ss')
36
+ });
37
+ })
38
+ .catch(function (err) {
39
+ if (errors.utils.isIgnitionError(err)) {
40
+ throw err;
41
+ }
42
+
43
+ throw new errors.LockError({
44
+ message: 'Error while acquire the migration lock.',
45
+ err: err
46
+ });
47
+ });
48
+ });
49
+ };
50
+
51
+ /**
52
+ * @description Private helper to determine whether the database is locked or not.
53
+ * @returns {Bluebird<boolean>}
54
+ */
55
+ module.exports.isLocked = function (connection) {
56
+ return connection('migrations_lock')
57
+ .where({
58
+ lock_key: 'km01'
59
+ })
60
+ .then(function (data) {
61
+ if (!data || !data.length || data[0].locked) {
62
+ throw new errors.MigrationsAreLockedError({
63
+ message: 'Migration lock was never released or currently a migration is running.',
64
+ help: 'If you are sure no migration is running, check your data and if your database is in a broken state, you could run `knex-migrator rollback`.'
65
+ });
66
+ }
67
+
68
+ return false;
69
+ });
70
+ };
71
+
72
+ /**
73
+ * @description Private helper to unlock the database table "migrations_lock".
74
+ * @TODO Locks in Sqlite won't work, because Sqlite doesn't offer read locks (https://github.com/TryGhost/knex-migrator/issues/87)
75
+ * @returns {*}
76
+ */
77
+ module.exports.unlock = function (connection) {
78
+ debug('Unlock.');
79
+
80
+ return database.createTransaction(connection, function (transacting) {
81
+ return transacting('migrations_lock')
82
+ .where({
83
+ lock_key: 'km01'
84
+ })
85
+ .forUpdate()
86
+ .then(function (data) {
87
+ if (!data || !data.length || !data[0].locked) {
88
+ throw new errors.MigrationsAreLockedError({
89
+ message: 'Migration lock was already released?.'
90
+ });
91
+ }
92
+
93
+ return transacting('migrations_lock')
94
+ .where({
95
+ lock_key: 'km01'
96
+ })
97
+ .update({
98
+ locked: 0,
99
+ released_at: moment().format('YYYY-MM-DD HH:mm:ss')
100
+ });
101
+ })
102
+ .catch(function (err) {
103
+ if (errors.utils.isIgnitionError(err)) {
104
+ throw err;
105
+ }
106
+
107
+ throw new errors.UnlockError({
108
+ message: 'Error while releasing the migration lock.',
109
+ err: err
110
+ });
111
+ });
112
+ });
113
+ };
package/lib/utils.js ADDED
@@ -0,0 +1,213 @@
1
+ const path = require('path'),
2
+ _ = require('lodash'),
3
+ fs = require('fs'),
4
+ compareVer = require('compare-ver'),
5
+ Promise = require('bluebird'),
6
+ resolve = Promise.promisify(require('resolve')),
7
+ debug = require('debug')('knex-migrator:utils'),
8
+ errors = require('./errors');
9
+
10
+ /**
11
+ * @description This helper function offers two ways of loading the knex-migrator configuration.
12
+ *
13
+ * 1. via JS object
14
+ * 2. via file location
15
+ *
16
+ * The expected format is:
17
+ *
18
+ * {
19
+ * database: Object,
20
+ * migrationPath: String,
21
+ * currentVersion: String
22
+ * }
23
+ *
24
+ * @param {Object} options
25
+ * @returns {*}
26
+ */
27
+ module.exports.loadConfig = function loadConfig(options) {
28
+ if (options.knexMigratorConfig) {
29
+ return options.knexMigratorConfig;
30
+ }
31
+
32
+ const knexMigratorFilePath = options.knexMigratorFilePath || process.cwd();
33
+
34
+ try {
35
+ return require(path.join(path.resolve(knexMigratorFilePath), '/MigratorConfig.js'));
36
+ } catch (err) {
37
+ if (err.code === 'MODULE_NOT_FOUND') {
38
+ throw new errors.KnexMigrateError({
39
+ message: 'Please provide a file named MigratorConfig.js in your project root.',
40
+ help: 'Read through the README.md to see which values are expected.'
41
+ });
42
+ }
43
+
44
+ throw new errors.KnexMigrateError({err: err});
45
+ }
46
+ };
47
+
48
+ /**
49
+ * @description List all migration files from disk based on a path.
50
+ *
51
+ * @param absolutePath
52
+ * @returns {Array}
53
+ */
54
+ exports.listFiles = function listFiles(absolutePath) {
55
+ let files = [];
56
+
57
+ try {
58
+ files = fs.readdirSync(absolutePath);
59
+ } catch (err) {
60
+ throw new errors.KnexMigrateError({
61
+ code: 'MIGRATION_PATH',
62
+ message: 'MigrationPath is wrong: ' + absolutePath
63
+ });
64
+ }
65
+
66
+ files = files.filter(function (file) {
67
+ // CASE: ignore dot files
68
+ return !file.match(/^\./);
69
+ });
70
+
71
+ debug(files);
72
+ return files;
73
+ };
74
+
75
+ /**
76
+ * @description Reads all migration files from disk based on a path.
77
+ * It returns an Array of migration files including it's up/down hooks, config and the name.
78
+ *
79
+ * @param absolutePath
80
+ * @returns {Array}
81
+ */
82
+ exports.readTasks = function readTasks(absolutePath) {
83
+ let tasks = [];
84
+
85
+ const files = exports.listFiles(absolutePath);
86
+
87
+ _.each(files, function (file) {
88
+ let executeFn = require(path.join(absolutePath, file));
89
+
90
+ try {
91
+ tasks.push({
92
+ up: executeFn.up,
93
+ down: executeFn.down,
94
+ config: executeFn.config,
95
+ name: file
96
+ });
97
+ } catch (err) {
98
+ debug(err.message);
99
+
100
+ throw new errors.MigrationScript({
101
+ message: err.message,
102
+ help: 'Cannot load Migrationscript.',
103
+ context: file
104
+ });
105
+ }
106
+ });
107
+
108
+ debug(tasks);
109
+ return tasks;
110
+ };
111
+
112
+ /**
113
+ * @description Reads all version folders from disk in correct order.
114
+ *
115
+ * @param absolutePath
116
+ * @returns {*}
117
+ */
118
+ exports.readVersionFolders = function readFolders(absolutePath) {
119
+ let folders = [],
120
+ toReturn = [];
121
+
122
+ try {
123
+ folders = fs.readdirSync(absolutePath);
124
+ } catch (err) {
125
+ throw new errors.KnexMigrateError({
126
+ message: 'MigrationPath is wrong: ' + absolutePath,
127
+ code: 'READ_FOLDERS'
128
+ });
129
+ }
130
+
131
+ if (!folders.length) {
132
+ return folders;
133
+ }
134
+
135
+ folders.forEach((folderToAdd) => {
136
+ let index = null;
137
+
138
+ // CASE: ignore dot files
139
+ if (folderToAdd.match(/^\./)) {
140
+ debug('Ignore Dotfile: ' + folderToAdd);
141
+ return;
142
+ }
143
+
144
+ toReturn.forEach((existingElement, _index) => {
145
+ if (index !== null) {
146
+ return;
147
+ }
148
+
149
+ // CASE: folder to add is smaller, push before this element
150
+ if (compareVer.gt(folderToAdd, existingElement) === -1) {
151
+ index = _index;
152
+ }
153
+ });
154
+
155
+ if (index === null) {
156
+ if (!toReturn.length) {
157
+ index = 0;
158
+ } else {
159
+ index = toReturn.length;
160
+ }
161
+ }
162
+
163
+ toReturn.splice(index, 0, folderToAdd);
164
+ });
165
+
166
+ debug(toReturn);
167
+ return toReturn;
168
+ };
169
+
170
+ /**
171
+ * @description Auto detect local installation to avoid version incompatible behaviour
172
+ */
173
+ exports.getKnexMigrator = function getKnexMigrator(options) {
174
+ options = options || {};
175
+
176
+ return resolve('knex-migrator', {basedir: options.path})
177
+ .then(function (localCLIPath) {
178
+ return require(localCLIPath);
179
+ })
180
+ .catch(function () {
181
+ return require('./');
182
+ });
183
+ };
184
+
185
+ /**
186
+ * @description A helper function to figure out if a version is greater than another version.
187
+ *
188
+ * Valid versions are:
189
+ * - 1
190
+ * - 1.1
191
+ * - 1.1.0
192
+ *
193
+ * It's up to you which pattern you would like to use.
194
+ *
195
+ * @param options
196
+ * @returns {boolean}
197
+ */
198
+ exports.isGreaterThanVersion = function isGreaterThanVersion(options) {
199
+ let greaterVersion = options.greaterVersion;
200
+ let smallerVersion = options.smallerVersion;
201
+
202
+ // CASE: are they semver like strings?
203
+ if (new RegExp(/\./g).test(greaterVersion) && new RegExp(/\./g).test(smallerVersion)) {
204
+ // -1 less than, 0 equal, 1 greater than
205
+ return compareVer.gt(greaterVersion, smallerVersion) === 1;
206
+ }
207
+
208
+ // CASE: must be numbers / number like strings
209
+ greaterVersion = Number(greaterVersion.toString());
210
+ smallerVersion = Number(smallerVersion.toString());
211
+
212
+ return greaterVersion > smallerVersion;
213
+ };
package/logging.js ADDED
@@ -0,0 +1,8 @@
1
+ var logging = require('ghost-ignition').logging;
2
+
3
+ module.exports = logging({
4
+ env: process.env.NODE_ENV,
5
+ mode: 'long',
6
+ level: 'info',
7
+ transports: ['stdout', 'stderr']
8
+ });
@@ -0,0 +1,58 @@
1
+ const debug = require('debug')('knex-migrator:lock-table');
2
+
3
+ /**
4
+ * Checks if primary key index exists in a table over the given columns.
5
+ */
6
+ function hasPrimaryKeySQLite(tableName, knex) {
7
+ const client = knex.client.config.client;
8
+
9
+ if (client !== 'sqlite3') {
10
+ throw new Error('Must use hasPrimaryKeySQLite on an SQLite3 database');
11
+ }
12
+
13
+ return knex.raw(`PRAGMA index_list('${tableName}');`)
14
+ .then((rawConstraints) => {
15
+ const tablePrimaryKey = rawConstraints.find(c => c.origin === 'pk');
16
+ return tablePrimaryKey;
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Adds an primary key index to a table over the given columns.
22
+ */
23
+ function addPrimaryKey(tableName, columns, knex) {
24
+ const isSQLite = knex.client.config.client === 'sqlite3';
25
+ if (isSQLite) {
26
+ return hasPrimaryKeySQLite(tableName, knex)
27
+ .then((primaryKeyExists) => {
28
+ if (primaryKeyExists) {
29
+ debug(`Primary key constraint for: ${columns} already exists for table: ${tableName}`);
30
+ return;
31
+ }
32
+
33
+ return knex.schema.table(tableName, function (table) {
34
+ table.primary(columns);
35
+ });
36
+ });
37
+ }
38
+
39
+ return knex.schema.table(tableName, function (table) {
40
+ table.primary(columns);
41
+ }).catch((err) => {
42
+ if (err.code === 'ER_MULTIPLE_PRI_KEY') {
43
+ debug(`Primary key constraint for: ${columns} already exists for table: ${tableName}`);
44
+ return;
45
+ }
46
+ throw err;
47
+ });
48
+ }
49
+
50
+ /**
51
+ * @description Private helper to create add a primary key to the migration lock table. The helper is called as part of `runDatabaseUpgrades`.
52
+ * @returns {*}
53
+ */
54
+ module.exports.up = function (connection) {
55
+ debug('Add primary key to the lock table.');
56
+
57
+ return addPrimaryKey('migrations_lock', 'lock_key', connection);
58
+ };
@@ -0,0 +1,22 @@
1
+ const debug = require('debug')('knex-migrator:field-length');
2
+
3
+ /**
4
+ * @description Private helper to migrate the migrations table. It will add missing constraints to existing fields.
5
+ * @returns {*}
6
+ */
7
+ module.exports.up = function (connection) {
8
+ debug('Ensure Field Length.');
9
+
10
+ return connection.schema.hasTable('migrations')
11
+ .then(function (exists) {
12
+ if (exists) {
13
+ return connection.schema.alterTable('migrations', function (table) {
14
+ table.string('name', 120).nullable(false).alter();
15
+ table.string('version', 70).nullable(false).alter();
16
+ }).catch(function () {
17
+ // ignore for now, it's not a urgent, required change
18
+ return Promise.resolve();
19
+ });
20
+ }
21
+ });
22
+ };
@@ -0,0 +1,17 @@
1
+ const lockTable = require('./lock-table');
2
+ const fieldLength = require('./field-length');
3
+ const useIndex = require('./use-index');
4
+ const addPKToLockTable = require('./add-primary-key-to-lock-table');
5
+
6
+ /**
7
+ * @description Helper to run database migrations for the database tables, which knex-migrator is using.
8
+ *
9
+ * You can use this helper to execute more database migrations. It get's called as soon as you hit any knex-migrator command.
10
+ */
11
+ module.exports.run = function (connection) {
12
+ return Promise.resolve()
13
+ .then(() => lockTable.up(connection))
14
+ .then(() => fieldLength.up(connection))
15
+ .then(() => useIndex.up(connection))
16
+ .then(() => addPKToLockTable.up(connection));
17
+ };
@@ -0,0 +1,41 @@
1
+ const debug = require('debug')('knex-migrator:lock-table');
2
+ const errors = require('../lib/errors');
3
+
4
+ /**
5
+ * @description Private helper to create the migration lock table. The helper is called as part of `runDatabaseUpgrades`.
6
+ * @returns {*}
7
+ */
8
+ module.exports.up = function (connection) {
9
+ debug('Ensure Lock Table.');
10
+
11
+ return connection.schema.hasTable('migrations_lock')
12
+ .then(function (table) {
13
+ if (table) {
14
+ return;
15
+ }
16
+
17
+ return connection.schema.createTable('migrations_lock', function (table) {
18
+ table.string('lock_key', 191).nullable(false).primary();
19
+ table.boolean('locked').default(0);
20
+ table.dateTime('acquired_at').nullable();
21
+ table.dateTime('released_at').nullable();
22
+ }).then(function () {
23
+ return connection('migrations_lock')
24
+ .insert({
25
+ lock_key: 'km01',
26
+ locked: 0
27
+ });
28
+ }).catch(function (err) {
29
+ // CASE: sqlite db is locked (e.g. concurrent migrations are running)
30
+ if (err.errno === 5) {
31
+ throw new errors.MigrationsAreLockedError({
32
+ message: 'Migrations are running at the moment. Please wait that the lock get`s released.',
33
+ context: 'Either the release was never released because of a e.g. died process or a parallel process is migrating at the moment.',
34
+ help: 'If you know what you are doing, you can manually release the lock by running `UPDATE migrations_lock set locked=0 where lock_key=\'km01\';`.'
35
+ });
36
+ }
37
+
38
+ throw err;
39
+ });
40
+ });
41
+ };
@@ -0,0 +1,24 @@
1
+ const debug = require('debug')('knex-migrator:use-index');
2
+
3
+ /**
4
+ * @description Private helper to migrate the migrations table. It will add missing indexes to existing fields.
5
+ * @returns {*}
6
+ */
7
+ module.exports.up = function (connection) {
8
+ debug('Ensure Unique Index.');
9
+
10
+ return connection.schema.hasTable('migrations')
11
+ .then(function (exists) {
12
+ if (exists) {
13
+ return connection.schema.alterTable('migrations', function (table) {
14
+ table.unique(['name', 'version']);
15
+ }).catch(function () {
16
+ // @NOTE: ignore for now, it's not a urgent, required change
17
+ // e.g. index exists already (1061,1)
18
+ // e.g. can't index because of already existing duplicates
19
+ return Promise.resolve();
20
+ });
21
+ }
22
+ });
23
+ };
24
+
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "knex-migrator",
3
+ "version": "4.0.5",
4
+ "description": "Database migrations with knex.",
5
+ "keywords": [
6
+ "ghost",
7
+ "migration",
8
+ "knex",
9
+ "knex-migrations",
10
+ "knex migration",
11
+ "knex migrations",
12
+ "bookshelf migration",
13
+ "bookshelf"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git@github.com:TryGhost/knex-migrator.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/TryGhost//knex-migrator/issues"
21
+ },
22
+ "author": {
23
+ "name": "Ghost Foundation",
24
+ "email": "hello@ghost.org",
25
+ "web": "https://ghost.org"
26
+ },
27
+ "main": "lib",
28
+ "license": "MIT",
29
+ "scripts": {
30
+ "lint": "eslint --ext .js --cache lib/** test/**",
31
+ "test": "LEVEL=fatal _mocha --require test/utils.js --report --exit lcovonly -- test/**/*_spec.js",
32
+ "test:unit": "yarn lint && LEVEL=fatal _mocha --require test/utils.js --report lcovonly -- test/unit/*_spec.js",
33
+ "posttest": "yarn lint",
34
+ "coverage": "nyc --reporter=lcov _mocha --require test/utils.js -- test/*_spec.js",
35
+ "preship": "yarn test",
36
+ "ship": "STATUS=$(git status --porcelain); echo $STATUS; if [ -z \"$STATUS\" ]; then yarn publish && git push ${GHOST_UPSTREAM:-upstream} master --follow-tags; fi"
37
+ },
38
+ "bin": {
39
+ "knex-migrator": "./bin/knex-migrator",
40
+ "knex-migrator-init": "./bin/knex-migrator-init",
41
+ "knex-migrator-health": "./bin/knex-migrator-health",
42
+ "knex-migrator-migrate": "./bin/knex-migrator-migrate",
43
+ "knex-migrator-reset": "./bin/knex-migrator-reset",
44
+ "knex-migrator-rollback": "./bin/knex-migrator-rollback"
45
+ },
46
+ "engines": {
47
+ "node": "^12.10.0 || ^14.14.0"
48
+ },
49
+ "dependencies": {
50
+ "bluebird": "3.7.2",
51
+ "commander": "5.1.0",
52
+ "compare-ver": "2.0.2",
53
+ "debug": "4.3.2",
54
+ "ghost-ignition": "4.6.3",
55
+ "knex": "0.21.19",
56
+ "lodash": "4.17.21",
57
+ "moment": "2.24.0",
58
+ "nconf": "0.11.3",
59
+ "resolve": "1.20.0"
60
+ },
61
+ "files": [
62
+ "bin",
63
+ "lib",
64
+ "migrations",
65
+ "logging.js"
66
+ ],
67
+ "devDependencies": {
68
+ "eslint": "6.8.0",
69
+ "eslint-plugin-ghost": "0.2.0",
70
+ "mocha": "7.2.0",
71
+ "nyc": "15.1.0",
72
+ "rimraf": "3.0.2",
73
+ "should": "13.2.3",
74
+ "sinon": "9.2.4"
75
+ },
76
+ "optionalDependencies": {
77
+ "mysql": "2.18.1",
78
+ "sqlite3": "5.0.2"
79
+ }
80
+ }