@vida-global/core 2.2.0 → 2.2.1
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.
|
@@ -72,6 +72,8 @@ Wire up the migration CLI by adding scripts to `package.json`:
|
|
|
72
72
|
|
|
73
73
|
Generate a migration with `npm run db:create_migration createUsers`. Pass `-- --databaseId metrics` to scope the migration to a non-default database. Run `npm run db:migrate` to apply migrations or `npm run db:rollback` to undo the most recent one.
|
|
74
74
|
|
|
75
|
+
Migration files are named `<timestamp>_<description>.js`, with the database id appended before the extension when one is given — for example `1754500000000_createUsers.js` or `1754500000000_createUsers.metrics.js`. The leading timestamp is the version recorded in `migration_versions` and the order migrations run in, so files sort chronologically by name. Files that don't match this pattern are ignored.
|
|
76
|
+
|
|
75
77
|
```js
|
|
76
78
|
module.exports = {
|
|
77
79
|
up: async function() {
|
|
@@ -102,7 +104,7 @@ createTable(tableName, details, { timestamps, paranoid })
|
|
|
102
104
|
dropTable(tableName)
|
|
103
105
|
addColumn(tableName, columnName, columnDetails)
|
|
104
106
|
removeColumn(tableName, columnName)
|
|
105
|
-
addIndex(tableName, fields, { concurrently, unique, name, where })
|
|
107
|
+
addIndex(tableName, fields, { concurrently, unique, name, using, where })
|
|
106
108
|
removeIndex(tableName, indexNameOrAttributes, concurrently=false)
|
|
107
109
|
renameColumn(tableName, oldName, newName)
|
|
108
110
|
changeColumn(tableName, columnName, dataTypeOrOptions)
|
|
@@ -111,9 +113,30 @@ changeColumn(tableName, columnName, dataTypeOrOptions)
|
|
|
111
113
|
**Conventions:**
|
|
112
114
|
|
|
113
115
|
- `createTable` adds an auto-increment `id` primary key and `created_at` / `updated_at` columns by default. Pass `{ timestamps: false }` to skip the timestamps.
|
|
114
|
-
- Pass `{ paranoid: true }` to add a nullable `deleted_at` column
|
|
116
|
+
- Pass `{ paranoid: true }` to `createTable` to add a nullable `deleted_at` column for soft deletes.
|
|
117
|
+
- Models over that table soft delete without any extra configuration: `initializationOptions` turns on Sequelize's `paranoid` for any table whose schema has `deleted_at` alongside the timestamp columns, so `destroy()` sets `deleted_at` and ordinary queries skip the row. A table created with `{ paranoid: true, timestamps: false }` has the column but does not qualify.
|
|
118
|
+
- Unique indexes on a paranoid table should be partial — `where: { deleted_at: null }` — or a soft-deleted row will still block re-creating the same one.
|
|
119
|
+
- An index created without a `name` gets `idx_<table>_<columns>`, so `addIndex('users', ['team_id', 'email'])` creates `idx_users_team_id_email`. `removeIndex` applies the same default, which means an index added by its columns can be dropped by its columns. Postgres truncates identifiers at 63 characters, so name long multi-column indexes explicitly.
|
|
115
120
|
- Column names are automatically converted to snake_case, so `teamId` in the details object becomes `team_id` in the database.
|
|
116
|
-
- Each migration runs inside a transaction.
|
|
121
|
+
- Each migration runs inside a transaction, so a migration that throws part way through leaves the database untouched.
|
|
122
|
+
|
|
123
|
+
Postgres refuses a few statements inside a transaction block — `CREATE INDEX CONCURRENTLY` and `DROP INDEX CONCURRENTLY` most of all. A migration that needs one of those opts out of the transaction entirely:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
module.exports = {
|
|
127
|
+
transactional: false,
|
|
128
|
+
|
|
129
|
+
up: async function() {
|
|
130
|
+
await this.addIndex('users', ['team_id'], { concurrently: true });
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
down: async function() {
|
|
134
|
+
await this.removeIndex('users', ['team_id'], true);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
An opted-out migration is **not** rolled back if it fails, so keep it to the single statement that needs it and put everything else in its own migration.
|
|
117
140
|
|
|
118
141
|
|
|
119
142
|
## Models
|
|
@@ -101,7 +101,7 @@ function generateMigrationFile(description, databaseId) {
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
function fileNameForMigration(description, databaseId) {
|
|
104
|
-
const name = `${
|
|
104
|
+
const name = `${new Date().getTime()}_${description}`;
|
|
105
105
|
const dbSuffix = databaseId ? `.${databaseId}` : '';
|
|
106
106
|
return `${name}${dbSuffix}.js`;
|
|
107
107
|
}
|
|
@@ -113,7 +113,7 @@ function fileNameForMigration(description, databaseId) {
|
|
|
113
113
|
function getMigrations() {
|
|
114
114
|
const files = fs.readdirSync(utils.migrationsDirectory);
|
|
115
115
|
const migrations = files.map(f => {
|
|
116
|
-
const m = f.match(
|
|
116
|
+
const m = f.match(/^(?<version>\d+)_[^.]+(\.(?<databaseId>\w+))?\.js$/);
|
|
117
117
|
if (!m) return;
|
|
118
118
|
return {fileName: f, databaseId: m.groups.databaseId, version: m.groups.version};
|
|
119
119
|
});
|
|
@@ -8,6 +8,7 @@ class Migrator {
|
|
|
8
8
|
#connection;
|
|
9
9
|
#fileName;
|
|
10
10
|
#migrationModule;
|
|
11
|
+
#transaction = null;
|
|
11
12
|
|
|
12
13
|
DataTypes = Sequelize.DataTypes;
|
|
13
14
|
Operators = Op;
|
|
@@ -22,19 +23,32 @@ class Migrator {
|
|
|
22
23
|
async run() {
|
|
23
24
|
if (!this.migrationModule.up) throw new Error('Missing migration `up`');
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
await this.#connection.queryInterface.transaction(async () => {
|
|
27
|
-
await migrator.migrationModule.up.call(migrator);
|
|
28
|
-
});
|
|
26
|
+
await this.perform(() => this.migrationModule.up.call(this));
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
async rollback() {
|
|
33
31
|
if (!this.migrationModule.down) return;
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
await this.perform(() => this.migrationModule.down.call(this));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async perform(callback) {
|
|
38
|
+
if (!this.transactional) return await callback();
|
|
39
|
+
|
|
40
|
+
await this.performInTransaction(callback);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async performInTransaction(callback) {
|
|
45
|
+
await this.#connection.queryInterface.transaction(async transaction => {
|
|
46
|
+
this.#transaction = transaction;
|
|
47
|
+
try {
|
|
48
|
+
await callback();
|
|
49
|
+
} finally {
|
|
50
|
+
this.#transaction = null;
|
|
51
|
+
}
|
|
38
52
|
});
|
|
39
53
|
}
|
|
40
54
|
|
|
@@ -58,6 +72,14 @@ class Migrator {
|
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
|
|
75
|
+
// Migrations opt out with `transactional: false`. Needed for statements Postgres refuses to
|
|
76
|
+
// run inside a transaction block, most notably CREATE/DROP INDEX CONCURRENTLY. An opted-out
|
|
77
|
+
// migration is not rolled back when it fails.
|
|
78
|
+
get transactional() {
|
|
79
|
+
return this.migrationModule.transactional !== false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
61
83
|
importMigrationModule() {
|
|
62
84
|
const filePath = `${utils.migrationsDirectory}/${this.#fileName}`;
|
|
63
85
|
return require(filePath);
|
|
@@ -77,7 +99,7 @@ class Migrator {
|
|
|
77
99
|
}
|
|
78
100
|
|
|
79
101
|
details = this.normalizeTableDetails(details);
|
|
80
|
-
await this.#sequelizeQueryInterface.createTable(tableName, details);
|
|
102
|
+
await this.#sequelizeQueryInterface.createTable(tableName, details, this.queryOptions);
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
|
|
@@ -116,8 +138,6 @@ class Migrator {
|
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
|
|
119
|
-
// Records use soft deletes when the table has this column: initializationOptions turns paranoid
|
|
120
|
-
// on for any model whose schema includes it.
|
|
121
141
|
get deletedAtColumn() {
|
|
122
142
|
return {
|
|
123
143
|
deleted_at: {
|
|
@@ -128,25 +148,25 @@ class Migrator {
|
|
|
128
148
|
|
|
129
149
|
|
|
130
150
|
async dropTable(tableName) {
|
|
131
|
-
await this.#sequelizeQueryInterface.dropTable(tableName);
|
|
151
|
+
await this.#sequelizeQueryInterface.dropTable(tableName, this.queryOptions);
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
|
|
135
155
|
async addColumn(tableName, columnName, columnDetails) {
|
|
136
|
-
await this.#sequelizeQueryInterface.addColumn(tableName, columnName, columnDetails);
|
|
156
|
+
await this.#sequelizeQueryInterface.addColumn(tableName, columnName, columnDetails, this.queryOptions);
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
|
|
140
160
|
async removeColumn(tableName, columnName) {
|
|
141
|
-
await this.#sequelizeQueryInterface.removeColumn(tableName, columnName);
|
|
161
|
+
await this.#sequelizeQueryInterface.removeColumn(tableName, columnName, this.queryOptions);
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
|
|
145
|
-
async addIndex(tableName, fields, { concurrently, unique, name, using, where }) {
|
|
146
|
-
const options = {fields: fields};
|
|
165
|
+
async addIndex(tableName, fields, { concurrently, unique, name, using, where }={}) {
|
|
166
|
+
const options = {fields: fields, ...this.queryOptions};
|
|
167
|
+
options.name = name || this.defaultIndexName(tableName, fields);
|
|
147
168
|
if (concurrently) options.concurrently = true;
|
|
148
169
|
if (unique) options.unique = true;
|
|
149
|
-
if (name) options.name = name;
|
|
150
170
|
if (using) options.using = using;
|
|
151
171
|
if (where) options.where = where;
|
|
152
172
|
|
|
@@ -155,20 +175,53 @@ class Migrator {
|
|
|
155
175
|
|
|
156
176
|
|
|
157
177
|
async removeIndex(tableName, indexNameOrAttributes, concurrently=false) {
|
|
158
|
-
const options
|
|
178
|
+
const options = this.queryOptions;
|
|
159
179
|
if (concurrently) options.concurrently = true;
|
|
160
|
-
|
|
180
|
+
|
|
181
|
+
const indexName = this.indexNameFor(tableName, indexNameOrAttributes);
|
|
182
|
+
await this.#sequelizeQueryInterface.removeIndex(tableName, indexName, options);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
/***********************************************************************************************
|
|
187
|
+
* INDEX NAMING
|
|
188
|
+
***********************************************************************************************/
|
|
189
|
+
// Indexes created without an explicit name get `idx_<table>_<columns>`. removeIndex applies the
|
|
190
|
+
// same default so an index can be dropped by its columns, the way it was added.
|
|
191
|
+
indexNameFor(tableName, indexNameOrAttributes) {
|
|
192
|
+
if (typeof indexNameOrAttributes === 'string') return indexNameOrAttributes;
|
|
193
|
+
return this.defaultIndexName(tableName, indexNameOrAttributes);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
defaultIndexName(tableName, fields) {
|
|
198
|
+
const columnNames = fields.map(field => this.indexColumnName(field));
|
|
199
|
+
return `idx_${tableName}_${columnNames.join('_')}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
indexColumnName(field) {
|
|
204
|
+
if (typeof field === 'string') return field;
|
|
205
|
+
return field.name;
|
|
161
206
|
}
|
|
162
207
|
|
|
163
208
|
|
|
164
209
|
async renameColumn(tableName, oldName, newName) {
|
|
165
210
|
newName = underscore(newName);
|
|
166
|
-
await this.#sequelizeQueryInterface.renameColumn(tableName, oldName, newName);
|
|
211
|
+
await this.#sequelizeQueryInterface.renameColumn(tableName, oldName, newName, this.queryOptions);
|
|
167
212
|
}
|
|
168
213
|
|
|
169
214
|
|
|
170
215
|
async changeColumn(tableName, columnName, dataTypeOrOptions) {
|
|
171
|
-
await this.#sequelizeQueryInterface.changeColumn(tableName, columnName, dataTypeOrOptions);
|
|
216
|
+
await this.#sequelizeQueryInterface.changeColumn(tableName, columnName, dataTypeOrOptions, this.queryOptions);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
/***********************************************************************************************
|
|
221
|
+
* QUERY OPTIONS
|
|
222
|
+
***********************************************************************************************/
|
|
223
|
+
get queryOptions() {
|
|
224
|
+
return { transaction: this.#transaction };
|
|
172
225
|
}
|
|
173
226
|
|
|
174
227
|
|
package/package.json
CHANGED
|
@@ -45,6 +45,97 @@ describe('Migrator', () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
describe('Migrator transaction threading', () => {
|
|
49
|
+
it ('passes the open transaction to operations run inside `up`', async () => {
|
|
50
|
+
const tableName = Helpers.randomString();
|
|
51
|
+
|
|
52
|
+
await Helpers.runInMigration(async function() {
|
|
53
|
+
await this.createTable(tableName, {});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(Helpers.mockQueryInterface.createTable).toHaveBeenCalledWith(
|
|
57
|
+
tableName,
|
|
58
|
+
expect.any(Object),
|
|
59
|
+
Helpers.inTransaction,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it ('passes the open transaction to operations run inside `down`', async () => {
|
|
64
|
+
const tableName = Helpers.randomString();
|
|
65
|
+
Helpers.migrationModule.down.mockImplementationOnce(async function() {
|
|
66
|
+
await this.dropTable(tableName);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await Helpers.migrator.rollback();
|
|
70
|
+
|
|
71
|
+
expect(Helpers.mockQueryInterface.dropTable).toHaveBeenCalledWith(tableName, Helpers.inTransaction);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it ('clears the transaction once the migration finishes', async () => {
|
|
75
|
+
const tableName = Helpers.randomString();
|
|
76
|
+
|
|
77
|
+
await Helpers.runInMigration(async function() {});
|
|
78
|
+
await Helpers.migrator.dropTable(tableName);
|
|
79
|
+
|
|
80
|
+
expect(Helpers.mockQueryInterface.dropTable).toHaveBeenCalledWith(tableName, Helpers.noTransaction);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it ('clears the transaction when the migration throws', async () => {
|
|
84
|
+
const tableName = Helpers.randomString();
|
|
85
|
+
|
|
86
|
+
await expect(Helpers.runInMigration(async function() {
|
|
87
|
+
throw new Error('migration blew up');
|
|
88
|
+
})).rejects.toThrow('migration blew up');
|
|
89
|
+
await Helpers.migrator.dropTable(tableName);
|
|
90
|
+
|
|
91
|
+
expect(Helpers.mockQueryInterface.dropTable).toHaveBeenCalledWith(tableName, Helpers.noTransaction);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
describe('Migrator opting out of the transaction', () => {
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
delete Helpers.migrationModule.transactional;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it ('runs `up` outside a transaction when the migration sets transactional to false', async () => {
|
|
102
|
+
const tableName = Helpers.randomString();
|
|
103
|
+
Helpers.migrationModule.transactional = false;
|
|
104
|
+
|
|
105
|
+
await Helpers.runInMigration(async function() {
|
|
106
|
+
await this.addIndex(tableName, ['name'], { concurrently: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(Helpers.transactionSpy).not.toHaveBeenCalled();
|
|
110
|
+
expect(Helpers.mockQueryInterface.addIndex).toHaveBeenCalledWith(tableName, {
|
|
111
|
+
fields: ['name'],
|
|
112
|
+
name: `idx_${tableName}_name`,
|
|
113
|
+
concurrently: true,
|
|
114
|
+
...Helpers.noTransaction,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it ('runs `down` outside a transaction when the migration sets transactional to false', async () => {
|
|
119
|
+
const tableName = Helpers.randomString();
|
|
120
|
+
Helpers.migrationModule.transactional = false;
|
|
121
|
+
Helpers.migrationModule.down.mockImplementationOnce(async function() {
|
|
122
|
+
await this.dropTable(tableName);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await Helpers.migrator.rollback();
|
|
126
|
+
|
|
127
|
+
expect(Helpers.transactionSpy).not.toHaveBeenCalled();
|
|
128
|
+
expect(Helpers.mockQueryInterface.dropTable).toHaveBeenCalledWith(tableName, Helpers.noTransaction);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it ('still uses a transaction when transactional is unset', async () => {
|
|
132
|
+
await Helpers.migrator.run();
|
|
133
|
+
|
|
134
|
+
expect(Helpers.transactionSpy).toHaveBeenCalledTimes(1);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
|
|
48
139
|
describe('Migrator#createTable', () => {
|
|
49
140
|
const defaultColumns = {
|
|
50
141
|
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: 'INTEGER' },
|
|
@@ -58,10 +149,10 @@ describe('Migrator', () => {
|
|
|
58
149
|
|
|
59
150
|
await Helpers.migrator.createTable(tableName, details);
|
|
60
151
|
|
|
61
|
-
expect(Helpers.mockQueryInterface.createTable).toHaveBeenCalledWith(tableName, details);
|
|
152
|
+
expect(Helpers.mockQueryInterface.createTable).toHaveBeenCalledWith(tableName, details, Helpers.noTransaction);
|
|
62
153
|
});
|
|
63
154
|
|
|
64
|
-
it ('adds a deleted_at column
|
|
155
|
+
it ('adds a nullable deleted_at column when paranoid is set', async () => {
|
|
65
156
|
const tableName = Helpers.randomString();
|
|
66
157
|
const details = { ...defaultColumns };
|
|
67
158
|
|
|
@@ -70,16 +161,27 @@ describe('Migrator', () => {
|
|
|
70
161
|
expect(Helpers.mockQueryInterface.createTable).toHaveBeenCalledWith(tableName, {
|
|
71
162
|
...defaultColumns,
|
|
72
163
|
deleted_at: { allowNull: true, type: 'DATE' },
|
|
73
|
-
});
|
|
164
|
+
}, Helpers.noTransaction);
|
|
74
165
|
});
|
|
75
166
|
|
|
76
|
-
it ('
|
|
167
|
+
it ('omits deleted_at when paranoid is not set', async () => {
|
|
77
168
|
const tableName = Helpers.randomString();
|
|
78
|
-
const details = { ...defaultColumns };
|
|
79
169
|
|
|
80
|
-
await Helpers.migrator.createTable(tableName,
|
|
170
|
+
await Helpers.migrator.createTable(tableName, { ...defaultColumns });
|
|
81
171
|
|
|
82
|
-
|
|
172
|
+
const [, columns] = Helpers.mockQueryInterface.createTable.mock.calls[0];
|
|
173
|
+
expect(columns).not.toHaveProperty('deleted_at');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it ('adds deleted_at alongside timestamps: false', async () => {
|
|
177
|
+
const tableName = Helpers.randomString();
|
|
178
|
+
|
|
179
|
+
await Helpers.migrator.createTable(tableName, {}, { paranoid: true, timestamps: false });
|
|
180
|
+
|
|
181
|
+
const [, columns] = Helpers.mockQueryInterface.createTable.mock.calls[0];
|
|
182
|
+
expect(columns).toHaveProperty('deleted_at');
|
|
183
|
+
expect(columns).not.toHaveProperty('created_at');
|
|
184
|
+
expect(columns).not.toHaveProperty('updated_at');
|
|
83
185
|
});
|
|
84
186
|
|
|
85
187
|
it ('snake_cases created table columns', async () => {
|
|
@@ -91,7 +193,7 @@ describe('Migrator', () => {
|
|
|
91
193
|
expect(Helpers.mockQueryInterface.createTable).toHaveBeenCalledWith(tableName, {
|
|
92
194
|
...defaultColumns,
|
|
93
195
|
my_column: 1,
|
|
94
|
-
});
|
|
196
|
+
}, Helpers.noTransaction);
|
|
95
197
|
});
|
|
96
198
|
});
|
|
97
199
|
|
|
@@ -116,7 +218,7 @@ describe('Migrator', () => {
|
|
|
116
218
|
|
|
117
219
|
await Helpers.migrator.renameColumn(tableName, oldName, 'myColumn');
|
|
118
220
|
|
|
119
|
-
expect(Helpers.mockQueryInterface.renameColumn).toHaveBeenCalledWith(tableName, oldName, 'my_column');
|
|
221
|
+
expect(Helpers.mockQueryInterface.renameColumn).toHaveBeenCalledWith(tableName, oldName, 'my_column', Helpers.noTransaction);
|
|
120
222
|
});
|
|
121
223
|
});
|
|
122
224
|
|
|
@@ -129,12 +231,12 @@ describe('Migrator', () => {
|
|
|
129
231
|
])('builds addIndex options for %s', async (_label, options) => {
|
|
130
232
|
const tableName = Helpers.randomString();
|
|
131
233
|
const fields = [Helpers.randomString(), Helpers.randomString()];
|
|
132
|
-
const expectedOptions = { fields };
|
|
234
|
+
const expectedOptions = { fields, ...Helpers.noTransaction };
|
|
133
235
|
|
|
134
236
|
if (options.concurrently) expectedOptions.concurrently = true;
|
|
135
237
|
if (options.unique) expectedOptions.unique = true;
|
|
136
238
|
if (options.where) expectedOptions.where = options.where;
|
|
137
|
-
|
|
239
|
+
expectedOptions.name = options.name || `idx_${tableName}_${fields.join('_')}`;
|
|
138
240
|
if (options.using) expectedOptions.using = options.using;
|
|
139
241
|
|
|
140
242
|
await Helpers.migrator.addIndex(tableName, fields, options);
|
|
@@ -147,8 +249,8 @@ describe('Migrator', () => {
|
|
|
147
249
|
|
|
148
250
|
describe('Migrator#removeIndex', () => {
|
|
149
251
|
it.each([
|
|
150
|
-
['default options', false, {}],
|
|
151
|
-
['concurrently', true, { concurrently: true }],
|
|
252
|
+
['default options', false, { transaction: null }],
|
|
253
|
+
['concurrently', true, { concurrently: true, transaction: null }],
|
|
152
254
|
])('passes removeIndex options for %s', async (_label, concurrently, expectedOptions) => {
|
|
153
255
|
const tableName = Helpers.randomString();
|
|
154
256
|
const fields = [Helpers.randomString(), Helpers.randomString()];
|
|
@@ -158,9 +260,54 @@ describe('Migrator', () => {
|
|
|
158
260
|
expect(Helpers.mockQueryInterface.removeIndex).toHaveBeenCalledTimes(1);
|
|
159
261
|
expect(Helpers.mockQueryInterface.removeIndex).toHaveBeenCalledWith(
|
|
160
262
|
tableName,
|
|
161
|
-
fields
|
|
263
|
+
`idx_${tableName}_${fields.join('_')}`,
|
|
162
264
|
expectedOptions,
|
|
163
265
|
);
|
|
164
266
|
});
|
|
267
|
+
|
|
268
|
+
it ('leaves an explicit index name alone', async () => {
|
|
269
|
+
const tableName = Helpers.randomString();
|
|
270
|
+
const indexName = Helpers.randomString();
|
|
271
|
+
|
|
272
|
+
await Helpers.migrator.removeIndex(tableName, indexName);
|
|
273
|
+
|
|
274
|
+
expect(Helpers.mockQueryInterface.removeIndex).toHaveBeenCalledWith(
|
|
275
|
+
tableName,
|
|
276
|
+
indexName,
|
|
277
|
+
Helpers.noTransaction,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
describe('Migrator#defaultIndexName', () => {
|
|
284
|
+
it ('builds idx_<table>_<columns> from a single column', () => {
|
|
285
|
+
expect(Helpers.migrator.defaultIndexName('users', ['team_id'])).toBe('idx_users_team_id');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it ('joins multiple columns with underscores', () => {
|
|
289
|
+
expect(Helpers.migrator.defaultIndexName('users', ['team_id', 'email'])).toBe('idx_users_team_id_email');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it ('reads the column name off object-style fields', () => {
|
|
293
|
+
const fields = ['team_id', { name: 'email', order: 'DESC' }];
|
|
294
|
+
|
|
295
|
+
expect(Helpers.migrator.defaultIndexName('users', fields)).toBe('idx_users_team_id_email');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
describe('Migrator#addIndex default naming', () => {
|
|
301
|
+
it ('names an index when no options are passed at all', async () => {
|
|
302
|
+
const tableName = Helpers.randomString();
|
|
303
|
+
|
|
304
|
+
await Helpers.migrator.addIndex(tableName, ['team_id']);
|
|
305
|
+
|
|
306
|
+
expect(Helpers.mockQueryInterface.addIndex).toHaveBeenCalledWith(tableName, {
|
|
307
|
+
fields: ['team_id'],
|
|
308
|
+
name: `idx_${tableName}_team_id`,
|
|
309
|
+
...Helpers.noTransaction,
|
|
310
|
+
});
|
|
311
|
+
});
|
|
165
312
|
});
|
|
166
313
|
});
|
|
@@ -13,6 +13,9 @@ const connectionConfig = { database: ' ', host: ' ', password: ' ', username:
|
|
|
13
13
|
const migrationModule = { up: jest.fn(), down: jest.fn() };
|
|
14
14
|
const randomNumber = TestHelpers.Faker.Math.randomNumber
|
|
15
15
|
const randomString = TestHelpers.Faker.Text.randomString;
|
|
16
|
+
const transaction = { id: randomString() };
|
|
17
|
+
const noTransaction = { transaction: null };
|
|
18
|
+
const inTransaction = { transaction };
|
|
16
19
|
const mockQueryInterface = {
|
|
17
20
|
createTable: jest.fn(),
|
|
18
21
|
dropTable: jest.fn(),
|
|
@@ -52,7 +55,7 @@ jest.spyOn(migrator, 'migrationModule', 'get').mockImplementation(() => migratio
|
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
const transactionSpy = jest.spyOn(QueryInterface.prototype, 'transaction');
|
|
55
|
-
transactionSpy.mockImplementation(callback => callback());
|
|
58
|
+
transactionSpy.mockImplementation(callback => callback(transaction));
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
/***************************************************************************************************
|
|
@@ -61,18 +64,29 @@ transactionSpy.mockImplementation(callback => callback());
|
|
|
61
64
|
async function expectQueryInterfaceCall(methodName, args) {
|
|
62
65
|
await migrator[methodName](...args);
|
|
63
66
|
expect(mockQueryInterface[methodName]).toHaveBeenCalledTimes(1);
|
|
64
|
-
expect(mockQueryInterface[methodName]).toHaveBeenCalledWith(...args);
|
|
67
|
+
expect(mockQueryInterface[methodName]).toHaveBeenCalledWith(...args, noTransaction);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// Runs `operation` from inside a migration's `up` so it sees the open transaction.
|
|
72
|
+
async function runInMigration(operation) {
|
|
73
|
+
migrationModule.up.mockImplementationOnce(operation);
|
|
74
|
+
await migrator.run();
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
|
|
68
78
|
module.exports = {
|
|
69
79
|
expectQueryInterfaceCall,
|
|
70
80
|
...Fixtures,
|
|
81
|
+
inTransaction,
|
|
71
82
|
migrationModule,
|
|
72
83
|
migrator,
|
|
73
84
|
mockQueryInterface,
|
|
85
|
+
noTransaction,
|
|
74
86
|
randomColumnDetails,
|
|
75
87
|
randomNumber,
|
|
76
88
|
randomString,
|
|
89
|
+
runInMigration,
|
|
90
|
+
transaction,
|
|
77
91
|
transactionSpy,
|
|
78
92
|
};
|