knex-migrator 5.4.1 → 6.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
@@ -1,185 +1,166 @@
1
1
  # knex-migrator
2
2
 
3
- A database migration tool for [knex.js](https://github.com/tgriesser/knex), which supports MySQL and SQlite3.
3
+ `knex-migrator` is a database migration library and CLI built on [Knex](https://knexjs.org/) for projects that need ordered init scripts, versioned migrations, health checks, locking, and rollback support.
4
4
 
5
- ## Features
5
+ It is maintained by Ghost and is used by [Ghost](https://github.com/TryGhost/Ghost) to run schema setup and migrations in production.
6
6
 
7
- - [x] JS API
8
- - [x] CLI Tool
9
- - [x] Differentiation between database initialization and migration (Support for a database schema, [like we use in Ghost](https://github.com/TryGhost/Ghost/blob/1.16.2/core/server/data/schema/schema.js))
10
- - [x] Support for database creation
11
- - [x] Hooks
12
- - [x] Rollback to latest version
13
- - [x] Auto-Rollback on error
14
- - [x] Database health check
15
- - [x] Supports transactions
16
- - [x] Full atomic, support for separate DML/DDL scripts (no autocommit)
17
- - [x] Migration lock
18
- - [x] Full debug & pretty log support
19
- - [x] Custom migration folder structure
20
- - [x] Stable (Used in [Ghost](https://github.com/TryGhost/Ghost) for many years in thousands of blogs in production mode)
7
+ ## Features
21
8
 
22
- # Install
9
+ - JavaScript API and CLI commands
10
+ - Separate database initialization and migration flows
11
+ - Database creation for MySQL-compatible targets
12
+ - Migration hooks for `init` and `migrate`
13
+ - Health checks against the current schema version
14
+ - Rollback to a previous migration version
15
+ - Automatic rollback when a migration fails
16
+ - Migration locking to avoid parallel runs
17
+ - Transaction support, including separate DML and DDL scripts
18
+ - Custom migration folder structure
19
+ - Debug logging with `DEBUG=knex-migrator:*`
23
20
 
24
- `npm install knex-migrator --save`
21
+ ## Install
25
22
 
26
- or
23
+ ```bash
24
+ pnpm add knex-migrator
25
+ ```
27
26
 
28
- `yarn add knex-migrator`
27
+ `npm install knex-migrator --save` also works for npm-based projects. For global CLI usage:
29
28
 
30
- Add me to your globals:
31
- - `npm install --global knex-migrator`
29
+ ```bash
30
+ npm install --global knex-migrator
31
+ ```
32
32
 
33
- # Usage
33
+ ## Requirements
34
34
 
35
- ## Pre-word
35
+ - Node.js `^22.13.1 || ^24.14.1`
36
+ - A compatible Knex installation. `knex-migrator` first tries to load `knex` from the project path passed with `--mgpath` or `knexMigratorFilePath`, then falls back to its bundled Knex `2.4.2`.
37
+ - A database client supported by this package: `mysql`, `mysql2`, `sqlite3`, or `better-sqlite3`
36
38
 
37
- - Replicas are unsupported, because Knex.js [doesn't support them](https://github.com/tgriesser/knex/issues/2253).
38
- - Sqlite does **not** support read locks by default. Read [here](https://github.com/TryGhost/knex-migrator/issues/87) why.
39
- - [Comparison](https://github.com/TryGhost/knex-migrator/issues/119) with other available migration tools.
40
- - Don't mix DDL/DML statements in a migration script. In MySQL DDL statements use implicit commits.
41
- - It's highly recommended to write both the `up` and the `down` function to ensure a full rollback.
42
- - If your process dies while migrations are running, knex-migrator won't be able to release the migration lock.
43
- To release to lock you can run `knex-migrator rollback`. **But** it's recommended to check your database first to see in which state it is.
44
- You can check the tables `migrations` and `migrations_lock`. The rollback will rollback any migrations which were executed based on your current version.
39
+ The `mysql` client name is accepted for backwards compatibility and is mapped to `mysql2`.
45
40
 
46
- ## Configure knex-migrator
41
+ ## Configuration
47
42
 
48
- The tool requires a config file in your project root.
49
- Please add a file named `MigratorConfig.js`. Knex-migrator will load the config file.
43
+ Add `MigratorConfig.js` to the project root that will run migrations. CLI commands load this file from the current working directory by default, or from the directory passed with `--mgpath`.
50
44
 
45
+ `MigratorConfig.cjs` and `MigratorConfig.mjs` are also supported and are resolved in that order after `MigratorConfig.js`. ESM configs (`.mjs`, or `.js`/`.cjs` in a `"type": "module"` package) are loaded synchronously, so they must export the config as the `default` export and must not use top-level `await`:
51
46
 
47
+ ```js
48
+ // MigratorConfig.mjs
49
+ export default {
50
+ database: { /* ... */ },
51
+ migrationPath: '/path/to/project/migrations',
52
+ currentVersion: '2.0'
53
+ };
52
54
  ```
55
+
56
+ ```js
53
57
  module.exports = {
54
58
  database: {
55
- client: String (Required) ['mysql', 'mysql2', 'sqlite3']
59
+ client: 'sqlite3',
56
60
  connection: {
57
- host: String, (Required) [e.g. '127.0.0.1']
58
- user: String, (Required)
59
- password: String, (Required)
60
- charset: String, (Optional) [Default: 'utf8mb4']
61
- database: String (Required)
61
+ filename: '/path/to/database.sqlite'
62
62
  }
63
63
  },
64
- migrationPath: String, (Required) [e.g. '/var/www/project/migrations']
65
- currentVersion: String, (Required) [e.g. '2.0']
66
- subfolder: String (Optional) [Default: 'versions']
67
- }
64
+ migrationPath: '/path/to/project/migrations',
65
+ currentVersion: '2.0',
66
+ subfolder: 'versions'
67
+ };
68
68
  ```
69
69
 
70
- Please take a look at [this real example](https://github.com/TryGhost/Ghost/blob/2.19.3/MigratorConfig.js).
71
-
72
- ## Folder Structure
70
+ For MySQL:
73
71
 
72
+ ```js
73
+ module.exports = {
74
+ database: {
75
+ client: 'mysql2',
76
+ connection: {
77
+ host: '127.0.0.1',
78
+ user: 'root',
79
+ password: 'root',
80
+ database: 'example',
81
+ charset: 'utf8mb4'
82
+ }
83
+ },
84
+ migrationPath: '/path/to/project/migrations',
85
+ currentVersion: '2.0'
86
+ };
74
87
  ```
75
- project/
76
- migrations/
77
- hooks/
78
- init/
79
- index.js
80
- before.js
81
- shutdown.js
82
- migrate/
83
- index.js
84
- after.js
85
- shutdown.js
86
- init/
87
- 1-add-tables.js
88
- versions/
89
- 1.0/
90
- 1-add-events-table.js
91
- 2-normalise-settings.js
92
- 2.0/
93
- 1-add-timestamps-columns.js
94
- 2.1/
95
- 1-remove-empty-strings.js
96
- 2-add-webhooks-table.js
97
- 3-add-permissions.js
98
- ```
99
-
100
- Please take a look at [this real example](https://github.com/TryGhost/Ghost/tree/2.19.3/core/server/data/migrations).
101
-
102
- ## Hooks
103
-
104
- Knex-migrator offers a couple of hooks, which makes it possible to hook into the migration process. You can create a hook per type: 'init' or 'migrate'. The folder name must be `hooks` and is not configurable. Please create an index.js file to export your functions, see [example](https://github.com/TryGhost/Ghost/blob/2.19.3/core/server/data/migrations/hooks/init/index.js).
105
88
 
106
- |hook|description|
107
- |---|---|
108
- |before|is called before anything happens|
109
- |beforeEach| is called before each migration script|
110
- |after|is called after everything happened|
111
- |afterEach|is called after each migration script|
112
- |shutdown|is called before the migrator shuts down|
89
+ `subfolder` is optional and defaults to `versions`.
113
90
 
91
+ ## Migration Structure
114
92
 
115
- ## Migration Files
93
+ The default migration layout separates one-time init scripts from versioned migrations.
116
94
 
117
- ### Config
118
- You can configure each migration script.
119
-
120
- ```
121
- module.exports.config = {
122
- transaction: Boolean
123
- }
124
- ```
125
-
126
-
127
- ### Examples
95
+ ```text
96
+ project/
97
+ migrations/
98
+ hooks/
99
+ init/
100
+ index.js
101
+ migrate/
102
+ index.js
103
+ init/
104
+ 1-add-tables.js
105
+ versions/
106
+ 1.0/
107
+ 1-add-events-table.js
108
+ 2-normalise-settings.js
109
+ 2.0/
110
+ 1-add-timestamps-columns.js
128
111
  ```
129
112
 
130
- module.exports.up = function(options) {
131
- const connection = options.connection;
132
-
133
- ...
113
+ Version folders are sorted in migration order. Each migration file can export `up`, `down`, and an optional `config` object.
134
114
 
135
- return Promise.resolve();
115
+ ```js
116
+ module.exports.config = {
117
+ transaction: true
136
118
  };
137
119
 
138
- module.exports.down = function(options) {
139
- const connection = options.connection;
140
-
141
- ...
142
-
143
- return Promise.resolve();
144
- }
145
- ```
146
-
147
- ```
120
+ module.exports.up = async function up(options) {
121
+ const connection = options.transacting || options.connection;
148
122
 
149
- module.exports.config = {
150
- transaction: true
123
+ await connection.schema.createTable('events', function (table) {
124
+ table.increments('id').primary();
125
+ table.string('name').notNullable();
126
+ });
151
127
  };
152
128
 
153
- module.exports.up = function(options) {
154
- const connection = options.transacting;
155
-
156
- ...
129
+ module.exports.down = async function down(options) {
130
+ const connection = options.transacting || options.connection;
157
131
 
158
- return Promise.resolve();
132
+ await connection.schema.dropTable('events');
159
133
  };
134
+ ```
160
135
 
161
- module.exports.down = function(options) {
162
- const connection = options.transacting;
136
+ Write both `up` and `down` whenever possible so failed migrations and manual rollbacks can return the database to a known state. Avoid mixing DDL and DML in one transactional migration for MySQL because DDL statements use implicit commits.
163
137
 
164
- ...
138
+ ## Hooks
165
139
 
166
- return Promise.resolve();
167
- }
168
- ```
140
+ Hooks live under `migrations/hooks/init` or `migrations/hooks/migrate`. Export any of these functions from the hook folder's `index.js`:
169
141
 
170
- ## CLI
142
+ | Hook | When it runs |
143
+ | --- | --- |
144
+ | `before` | Before the command starts running scripts |
145
+ | `beforeEach` | Before each migration script |
146
+ | `afterEach` | After each migration script |
147
+ | `after` | After all scripts finish |
148
+ | `shutdown` | Before the migrator disconnects |
171
149
 
172
- ### Commands
150
+ Hook functions receive the current Knex connection where applicable. `shutdown` receives `{executedFromShell}`.
173
151
 
174
- #### knex-migrator help
152
+ ## CLI
175
153
 
154
+ ```bash
155
+ knex-migrator --help
176
156
  ```
177
- $ knex-migrator help
157
+
158
+ ```text
178
159
  Usage: knex-migrator [options] [command]
179
160
 
180
161
  Options:
181
162
  -v, --version output the version number
182
- -h, --help output usage information
163
+ -h, --help display help for command
183
164
 
184
165
  Commands:
185
166
  init|i [config] init db
@@ -187,159 +168,114 @@ Commands:
187
168
  reset|r reset db
188
169
  health|h health of db
189
170
  rollback|ro rollbacks your db
190
- help [cmd] display help for [cmd]
171
+ help [command] display help for command
191
172
  ```
192
173
 
193
- #### knex-migrator health
194
-
195
- - Returns the database health/state
196
- - Based on your current version and your migration scripts
197
-
198
- #### knex-migrator init
199
-
200
- - Initializes your database based on your init scripts
201
- - Creates the database if it was not created yet
202
-
203
- ##### Options
174
+ Common commands:
204
175
 
205
176
  ```bash
206
- # Skips a specific migration script
207
- --skip
208
-
209
- # Runs only a specific migration script
210
- --only
211
-
212
- # Path to MigratorConfig.js
213
- --mgpath
177
+ knex-migrator init --mgpath /path/to/project
178
+ knex-migrator migrate --mgpath /path/to/project
179
+ knex-migrator migrate --mgpath /path/to/project --v 2.0 --force
180
+ knex-migrator migrate --mgpath /path/to/project --init
181
+ knex-migrator rollback --mgpath /path/to/project --force --v 1.0
182
+ knex-migrator health --mgpath /path/to/project
183
+ knex-migrator reset --mgpath /path/to/project --force
214
184
  ```
215
185
 
216
- #### knex-migrator migrate
217
-
218
- - Migrates your database to latest version
219
- - Automatic rollback if an error occurs
186
+ `migrate --only <file>` can run one file within the target version when combined with `--v`. `init` supports `--only` and `--skip` for init scripts.
220
187
 
221
- ##### Options
222
-
223
- ```bash
224
- # The version you would like to migrate to
225
- --v
226
-
227
- # Combo Feature to check whether the database was already initialized
228
- --init
229
-
230
- # Force the execution no matter which current version you are on
231
- --force
232
-
233
- # Path to MigratorConfig.js
234
- --mgpath
235
- ```
188
+ If a process exits while migrations are running, the migration lock may remain in place. Inspect the `migrations` and `migrations_lock` tables before forcing rollback or reset.
236
189
 
237
- #### knex-migrator rollback
238
-
239
- - Rolls back your database
240
- - By default, you can only rollback if the database is locked
241
-
242
- ##### Options
243
-
244
- ```bash
245
- # Ignores the migration lock
246
- --force
247
-
248
- # Version you would like to rollback to
249
- --v
250
- ```
251
-
252
- #### knex-migrator reset
253
-
254
- - Resets your database
255
- - Removes the database
256
-
257
- ##### Options
258
-
259
- ```bash
260
- # Ignores the migration lock
261
- --force
262
- ```
263
-
264
- ### Advanced
265
-
266
- `DEBUG=knex-migrator:* knex-migrator migrate`
267
-
268
-
269
- ## JS API
270
-
271
- ### Instantiation
190
+ ## JavaScript API
272
191
 
273
192
  ```js
274
193
  const KnexMigrator = require('knex-migrator');
275
194
 
276
- # Option 1: Pass path to MigratorConfig.js
277
- const knexMigrator = new KnexMigrator({
195
+ const migrator = new KnexMigrator({
278
196
  knexMigratorFilePath: process.cwd()
279
197
  });
280
-
281
- # Option 2: Pass object with config
282
- const knexMigrator = new KnexMigrator({
283
- knexMigratorConfig: { ... }
284
- });
285
-
286
198
  ```
287
199
 
288
- ### Commands
200
+ You can also pass config directly:
289
201
 
290
202
  ```js
291
- # Health
292
- knexMigrator.isDatabaseOK
293
-
294
- # Initialise database
295
- knexMigrator.init
296
-
297
- # Migrate database
298
- knexMigrator.migrate
203
+ const migrator = new KnexMigrator({
204
+ knexMigratorConfig: {
205
+ database: {
206
+ client: 'sqlite3',
207
+ connection: {
208
+ filename: '/path/to/database.sqlite'
209
+ }
210
+ },
211
+ migrationPath: '/path/to/project/migrations',
212
+ currentVersion: '2.0'
213
+ }
214
+ });
215
+ ```
299
216
 
300
- # Rollback database
301
- knexMigrator.rollback
217
+ Available methods:
302
218
 
303
- # Reset database
304
- knexMigrator.reset
305
- ```
219
+ - `init(options)`
220
+ - `migrate(options)`
221
+ - `rollback(options)`
222
+ - `reset(options)`
223
+ - `isDatabaseOK()`
306
224
 
307
- ### Examples
225
+ Example:
308
226
 
309
227
  ```js
310
- knexMigrator.isDatabaseOK()
311
- .then(function() {
312
- // database is OK
313
- // initialization & migrations are not missing
314
- })
315
- .catch(function(err) {
316
- if (err.code === 'DB_NOT_INITIALISED') {
317
- return knexMigrator.init();
318
- }
319
-
320
- if (err.code === 'DB_NEEDS_MIGRATION') {
321
- return knexMigrator.migrate();
322
- }
323
- });
228
+ migrator.isDatabaseOK()
229
+ .then(function () {
230
+ // Database is initialized and migrated.
231
+ })
232
+ .catch(function (err) {
233
+ if (err.code === 'DB_NOT_INITIALISED') {
234
+ return migrator.init();
235
+ }
236
+
237
+ if (err.code === 'DB_NEEDS_MIGRATION') {
238
+ return migrator.migrate();
239
+ }
324
240
 
241
+ throw err;
242
+ });
325
243
  ```
326
244
 
327
- # Test
245
+ ## Development
328
246
 
329
- - `yarn lint` run just eslint
330
- - `yarn test` run eslint && then tests
331
- - `NODE_ENV=testing-mysql yarn test` to test with MySQL
247
+ This repo uses pnpm and Corepack.
332
248
 
333
- # Publish
249
+ ```bash
250
+ corepack enable
251
+ pnpm install --frozen-lockfile
252
+ pnpm lint
253
+ pnpm test
254
+ pnpm coverage
255
+ ```
334
256
 
335
- - `yarn ship`
257
+ Useful test variants:
336
258
 
337
- # Copyright & License
259
+ ```bash
260
+ NODE_ENV=testing pnpm test
261
+ NODE_ENV=testing-better-sqlite3 pnpm test
262
+ NODE_ENV=testing-mysql pnpm test
263
+ ```
338
264
 
339
- Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE).
265
+ CI runs linting, coverage on Node `22.13.1` and `24.14.1` across sqlite3, better-sqlite3, and MySQL 8, plus a Ghost consumer smoke test. The smoke test checks out Ghost, links this package into Ghost core, then runs Ghost init, health, rollback, migrate, and health checks through the CLI.
340
266
 
267
+ To run the Ghost smoke locally, place a Ghost checkout next to this repo or set `GHOST_CORE_PATH`:
341
268
 
269
+ ```bash
270
+ GHOST_CORE_PATH=/path/to/Ghost pnpm smoke:ghost
271
+ ```
342
272
 
273
+ ## Publish
343
274
 
275
+ ```bash
276
+ pnpm ship
277
+ ```
344
278
 
279
+ ## License
345
280
 
281
+ Copyright (c) 2013-2026 Ghost Foundation. Released under the [MIT license](LICENSE).
package/bin/knex-migrator CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- var program = require('commander'),
4
- pkg = require('../package.json');
3
+ const program = require('commander').program;
4
+ const pkg = require('../package.json');
5
5
 
6
6
  program
7
7
  .version(pkg.version, '-v, --version')
@@ -1,30 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- var program = require('commander');
4
- var utils = require('../lib/utils');
5
-
6
- var logging = require('@tryghost/logging');
7
- var knexMigrator;
8
-
9
- utils.getKnexMigrator({path: process.cwd()})
10
- .then(function (KnexMigrator) {
11
- program
12
- .option('--mgpath <path>')
13
- .parse(process.argv);
14
-
15
- try {
16
- knexMigrator = new KnexMigrator({knexMigratorFilePath: program.mgpath, executedFromShell: true});
17
- } catch (err) {
18
- logging.error(err);
19
- process.exit(1);
20
- }
3
+ const program = require('commander').program;
4
+ const utils = require('../lib/utils');
5
+
6
+ const logging = require('@tryghost/logging');
7
+
8
+ async function main() {
9
+ const KnexMigrator = await utils.getKnexMigrator({path: process.cwd()});
10
+
11
+ program
12
+ .option('--mgpath <path>')
13
+ .parse(process.argv);
14
+
15
+ const options = program.opts();
16
+ let knexMigrator;
17
+
18
+ try {
19
+ knexMigrator = new KnexMigrator({
20
+ knexMigratorFilePath: options.mgpath,
21
+ executedFromShell: true,
22
+ });
23
+ } catch (err) {
24
+ logging.error(err);
25
+ process.exit(1);
26
+ }
27
+
28
+ await knexMigrator.isDatabaseOK();
29
+ logging.info('Woohoo, Database is healthy');
30
+ }
21
31
 
22
- return knexMigrator.isDatabaseOK()
23
- .then(function () {
24
- logging.info('Woohoo, Database is healthy');
25
- });
26
- })
27
- .catch(function (err) {
32
+ main().catch(function (err) {
28
33
  logging.error(err.message);
29
34
 
30
35
  if (err.help) {
@@ -1,34 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- var program = require('commander');
4
- var utils = require('../lib/utils');
5
-
6
- var logging = require('@tryghost/logging');
7
- var knexMigrator;
8
-
9
- utils.getKnexMigrator({path: process.cwd()})
10
- .then(function (KnexMigrator) {
11
- program
12
- .option('--skip <item>')
13
- .option('--only <item>')
14
- .option('--mgpath <path>')
15
- .parse(process.argv);
16
-
17
- try {
18
- knexMigrator = new KnexMigrator({knexMigratorFilePath: program.mgpath, executedFromShell: true});
19
- } catch (err) {
20
- logging.error(err);
21
- process.exit(1);
22
- }
23
-
24
- return knexMigrator.init({
25
- skip: program.skip,
26
- only: program.only
27
- }).then(function () {
28
- logging.info('Finished database init!');
3
+ const program = require('commander').program;
4
+ const utils = require('../lib/utils');
5
+
6
+ const logging = require('@tryghost/logging');
7
+
8
+ async function main() {
9
+ const KnexMigrator = await utils.getKnexMigrator({path: process.cwd()});
10
+
11
+ program
12
+ .option('--skip <item>')
13
+ .option('--only <item>')
14
+ .option('--mgpath <path>')
15
+ .parse(process.argv);
16
+
17
+ const options = program.opts();
18
+ let knexMigrator;
19
+
20
+ try {
21
+ knexMigrator = new KnexMigrator({
22
+ knexMigratorFilePath: options.mgpath,
23
+ executedFromShell: true,
29
24
  });
30
- })
31
- .catch(function (err) {
25
+ } catch (err) {
26
+ logging.error(err);
27
+ process.exit(1);
28
+ }
29
+
30
+ await knexMigrator.init({
31
+ skip: options.skip,
32
+ only: options.only,
33
+ });
34
+ logging.info('Finished database init!');
35
+ }
36
+
37
+ main().catch(function (err) {
32
38
  logging.error(err);
33
39
  process.exit(1);
34
40
  });