rake-db 2.0.1 → 2.0.3
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/.env +1 -3
- package/.env.local +1 -1
- package/dist/index.d.ts +26 -4
- package/dist/index.esm.js +224 -196
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +225 -194
- package/dist/index.js.map +1 -1
- package/migrations/20221017181504_createUser.ts +14 -0
- package/migrations/20221017200111_createProfile.ts +10 -0
- package/migrations/20221017200252_createChat.ts +9 -0
- package/migrations/20221017200326_createChatUser.ts +7 -0
- package/migrations/20221017200900_createMessage.ts +12 -0
- package/migrations/20221017201235_createGeoSchema.ts +5 -0
- package/migrations/{20221009210157_first.ts → 20221017210011_createCountry.ts} +2 -2
- package/migrations/20221017210133_createCity.ts +9 -0
- package/package.json +2 -1
- package/src/commands/generate.test.ts +6 -6
- package/src/commands/generate.ts +3 -3
- package/src/commands/migrateOrRollback.test.ts +20 -9
- package/src/commands/migrateOrRollback.ts +18 -6
- package/src/common.test.ts +13 -0
- package/src/common.ts +16 -3
- package/src/index.ts +2 -1
- package/src/migration/changeTable.ts +4 -2
- package/src/migration/createTable.ts +21 -16
- package/src/migration/migration.test.ts +93 -0
- package/src/migration/migration.ts +137 -2
- package/src/migration/migrationUtils.ts +17 -10
- package/src/rakeDb.ts +28 -3
- package/src/test-utils.ts +3 -1
- package/tsconfig.json +1 -1
- package/migrations/20221009210200_second.ts +0 -5
package/src/commands/generate.ts
CHANGED
|
@@ -41,18 +41,18 @@ const makeContent = (name: string, args: string[]): string => {
|
|
|
41
41
|
const [first, rest] = getFirstWordAndRest(name);
|
|
42
42
|
if (rest) {
|
|
43
43
|
if (first === 'create' || first === 'drop') {
|
|
44
|
-
content += `\n db.${
|
|
44
|
+
content += `\n await db.${
|
|
45
45
|
first === 'create' ? 'createTable' : 'dropTable'
|
|
46
46
|
}('${rest}', (t) => ({`;
|
|
47
47
|
content += makeColumnsContent(args);
|
|
48
48
|
content += '\n }));';
|
|
49
49
|
} else if (first === 'change') {
|
|
50
|
-
content += `\n db.changeTable('${rest}', (t) => ({`;
|
|
50
|
+
content += `\n await db.changeTable('${rest}', (t) => ({`;
|
|
51
51
|
content += '\n }));';
|
|
52
52
|
} else if (first === 'add' || first === 'remove') {
|
|
53
53
|
const table =
|
|
54
54
|
first === 'add' ? getTextAfterTo(rest) : getTextAfterFrom(rest);
|
|
55
|
-
content += `\n db.changeTable(${
|
|
55
|
+
content += `\n await db.changeTable(${
|
|
56
56
|
table ? `'${table}'` : 'tableName'
|
|
57
57
|
}, (t) => ({`;
|
|
58
58
|
content += makeColumnsContent(args, first);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { migrate, rollback } from './migrateOrRollback';
|
|
2
2
|
import { createSchemaMigrations, migrationConfigDefaults } from '../common';
|
|
3
3
|
import { getMigrationFiles } from '../common';
|
|
4
|
-
import { Adapter, TransactionAdapter } from 'pqb';
|
|
4
|
+
import { Adapter, noop, TransactionAdapter } from 'pqb';
|
|
5
5
|
import { Migration } from '../migration/migration';
|
|
6
6
|
|
|
7
7
|
jest.mock('../common', () => ({
|
|
@@ -35,6 +35,11 @@ const requireTsMock = jest.fn();
|
|
|
35
35
|
const config = {
|
|
36
36
|
...migrationConfigDefaults,
|
|
37
37
|
requireTs: requireTsMock,
|
|
38
|
+
log: false,
|
|
39
|
+
logger: {
|
|
40
|
+
log: jest.fn(),
|
|
41
|
+
error: noop,
|
|
42
|
+
},
|
|
38
43
|
};
|
|
39
44
|
|
|
40
45
|
describe('migrateOrRollback', () => {
|
|
@@ -49,7 +54,7 @@ describe('migrateOrRollback', () => {
|
|
|
49
54
|
queryMock.mockReturnValueOnce(undefined);
|
|
50
55
|
requireTsMock.mockResolvedValue(undefined);
|
|
51
56
|
|
|
52
|
-
await migrate(options, config);
|
|
57
|
+
await migrate(options, config, []);
|
|
53
58
|
|
|
54
59
|
expect(getMigrationFiles).toBeCalledWith(config, true);
|
|
55
60
|
|
|
@@ -62,6 +67,9 @@ describe('migrateOrRollback', () => {
|
|
|
62
67
|
expect(transactionQueryMock).toBeCalledWith(
|
|
63
68
|
`INSERT INTO "schemaMigrations" VALUES ('3')`,
|
|
64
69
|
);
|
|
70
|
+
|
|
71
|
+
expect(config.logger.log).toBeCalledWith('file2 migrated');
|
|
72
|
+
expect(config.logger.log).toBeCalledWith('file3 migrated');
|
|
65
73
|
});
|
|
66
74
|
|
|
67
75
|
it('should create migrations table if it not exist', async () => {
|
|
@@ -69,12 +77,13 @@ describe('migrateOrRollback', () => {
|
|
|
69
77
|
getMigratedVersionsArrayMock.mockRejectedValueOnce({ code: '42P01' });
|
|
70
78
|
(createSchemaMigrations as jest.Mock).mockResolvedValueOnce(undefined);
|
|
71
79
|
|
|
72
|
-
await migrate(options, config);
|
|
80
|
+
await migrate(options, config, []);
|
|
73
81
|
|
|
74
82
|
expect(getMigrationFiles).toBeCalledWith(config, true);
|
|
75
83
|
expect(createSchemaMigrations).toBeCalled();
|
|
76
84
|
expect(requireTsMock).not.toBeCalled();
|
|
77
85
|
expect(transactionQueryMock).not.toBeCalled();
|
|
86
|
+
expect(config.logger.log).not.toBeCalled();
|
|
78
87
|
});
|
|
79
88
|
});
|
|
80
89
|
|
|
@@ -87,19 +96,20 @@ describe('migrateOrRollback', () => {
|
|
|
87
96
|
queryMock.mockReturnValueOnce(undefined);
|
|
88
97
|
requireTsMock.mockResolvedValue(undefined);
|
|
89
98
|
|
|
90
|
-
await rollback(options, config);
|
|
99
|
+
await rollback(options, config, []);
|
|
91
100
|
|
|
92
101
|
expect(getMigrationFiles).toBeCalledWith(config, false);
|
|
93
102
|
|
|
103
|
+
expect(requireTsMock).toBeCalledTimes(1);
|
|
94
104
|
expect(requireTsMock).toBeCalledWith('file2');
|
|
95
|
-
expect(requireTsMock).toBeCalledWith('file1');
|
|
96
105
|
|
|
106
|
+
expect(transactionQueryMock).toBeCalledTimes(1);
|
|
97
107
|
expect(transactionQueryMock).toBeCalledWith(
|
|
98
108
|
`DELETE FROM "schemaMigrations" WHERE version = '2'`,
|
|
99
109
|
);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
);
|
|
110
|
+
|
|
111
|
+
expect(config.logger.log).toBeCalledTimes(1);
|
|
112
|
+
expect(config.logger.log).toBeCalledWith('file2 rolled back');
|
|
103
113
|
});
|
|
104
114
|
|
|
105
115
|
it('should create migrations table if it not exist', async () => {
|
|
@@ -107,12 +117,13 @@ describe('migrateOrRollback', () => {
|
|
|
107
117
|
getMigratedVersionsArrayMock.mockRejectedValueOnce({ code: '42P01' });
|
|
108
118
|
(createSchemaMigrations as jest.Mock).mockResolvedValueOnce(undefined);
|
|
109
119
|
|
|
110
|
-
await rollback(options, config);
|
|
120
|
+
await rollback(options, config, []);
|
|
111
121
|
|
|
112
122
|
expect(getMigrationFiles).toBeCalledWith(config, false);
|
|
113
123
|
expect(createSchemaMigrations).toBeCalled();
|
|
114
124
|
expect(requireTsMock).not.toBeCalled();
|
|
115
125
|
expect(transactionQueryMock).not.toBeCalled();
|
|
126
|
+
expect(config.logger.log).not.toBeCalled();
|
|
116
127
|
});
|
|
117
128
|
});
|
|
118
129
|
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getMigrationFiles,
|
|
5
5
|
MigrationConfig,
|
|
6
6
|
MigrationFile,
|
|
7
|
+
quoteTable,
|
|
7
8
|
} from '../common';
|
|
8
9
|
import {
|
|
9
10
|
getCurrentPromise,
|
|
@@ -15,10 +16,14 @@ import { Migration } from '../migration/migration';
|
|
|
15
16
|
const migrateOrRollback = async (
|
|
16
17
|
options: MaybeArray<AdapterOptions>,
|
|
17
18
|
config: MigrationConfig,
|
|
19
|
+
args: string[],
|
|
18
20
|
up: boolean,
|
|
19
21
|
) => {
|
|
20
22
|
const files = await getMigrationFiles(config, up);
|
|
21
23
|
|
|
24
|
+
const argCount = args[0] === 'all' ? Infinity : parseInt(args[0]);
|
|
25
|
+
let count = isNaN(argCount) ? (up ? Infinity : 1) : argCount;
|
|
26
|
+
|
|
22
27
|
for (const opts of toArray(options)) {
|
|
23
28
|
const db = new Adapter(opts);
|
|
24
29
|
const migratedVersions = await getMigratedVersionsMap(db, config);
|
|
@@ -31,7 +36,10 @@ const migrateOrRollback = async (
|
|
|
31
36
|
continue;
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
if (count-- <= 0) break;
|
|
40
|
+
|
|
34
41
|
await processMigration(db, up, file, config);
|
|
42
|
+
config.logger?.log(`${file.path} ${up ? 'migrated' : 'rolled back'}`);
|
|
35
43
|
}
|
|
36
44
|
} finally {
|
|
37
45
|
await db.destroy();
|
|
@@ -46,7 +54,7 @@ const processMigration = async (
|
|
|
46
54
|
config: MigrationConfig,
|
|
47
55
|
) => {
|
|
48
56
|
await db.transaction(async (tx) => {
|
|
49
|
-
const db = new Migration(tx, up);
|
|
57
|
+
const db = new Migration(tx, up, config);
|
|
50
58
|
setCurrentMigration(db);
|
|
51
59
|
setCurrentMigrationUp(up);
|
|
52
60
|
config.requireTs(file.path);
|
|
@@ -65,7 +73,7 @@ const saveMigratedVersion = async (
|
|
|
65
73
|
config: MigrationConfig,
|
|
66
74
|
) => {
|
|
67
75
|
await db.query(
|
|
68
|
-
`INSERT INTO
|
|
76
|
+
`INSERT INTO ${quoteTable(config.migrationsTable)} VALUES ('${version}')`,
|
|
69
77
|
);
|
|
70
78
|
};
|
|
71
79
|
|
|
@@ -75,7 +83,9 @@ const removeMigratedVersion = async (
|
|
|
75
83
|
config: MigrationConfig,
|
|
76
84
|
) => {
|
|
77
85
|
await db.query(
|
|
78
|
-
`DELETE FROM
|
|
86
|
+
`DELETE FROM ${quoteTable(
|
|
87
|
+
config.migrationsTable,
|
|
88
|
+
)} WHERE version = '${version}'`,
|
|
79
89
|
);
|
|
80
90
|
};
|
|
81
91
|
|
|
@@ -85,7 +95,7 @@ const getMigratedVersionsMap = async (
|
|
|
85
95
|
): Promise<Record<string, boolean>> => {
|
|
86
96
|
try {
|
|
87
97
|
const result = await db.arrays<[string]>(
|
|
88
|
-
`SELECT * FROM
|
|
98
|
+
`SELECT * FROM ${quoteTable(config.migrationsTable)}`,
|
|
89
99
|
);
|
|
90
100
|
return Object.fromEntries(result.rows.map((row) => [row[0], true]));
|
|
91
101
|
} catch (err) {
|
|
@@ -100,9 +110,11 @@ const getMigratedVersionsMap = async (
|
|
|
100
110
|
export const migrate = (
|
|
101
111
|
options: MaybeArray<AdapterOptions>,
|
|
102
112
|
config: MigrationConfig,
|
|
103
|
-
|
|
113
|
+
args: string[],
|
|
114
|
+
) => migrateOrRollback(options, config, args, true);
|
|
104
115
|
|
|
105
116
|
export const rollback = (
|
|
106
117
|
options: MaybeArray<AdapterOptions>,
|
|
107
118
|
config: MigrationConfig,
|
|
108
|
-
|
|
119
|
+
args: string[],
|
|
120
|
+
) => migrateOrRollback(options, config, args, false);
|
package/src/common.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
joinColumns,
|
|
9
9
|
joinWords,
|
|
10
10
|
migrationConfigDefaults,
|
|
11
|
+
quoteTable,
|
|
11
12
|
setAdapterOptions,
|
|
12
13
|
setAdminCredentialsToOptions,
|
|
13
14
|
sortAsc,
|
|
@@ -45,6 +46,8 @@ describe('common', () => {
|
|
|
45
46
|
migrationsPath: 'custom-path',
|
|
46
47
|
migrationsTable: 'schemaMigrations',
|
|
47
48
|
requireTs: expect.any(Function),
|
|
49
|
+
log: true,
|
|
50
|
+
logger: console,
|
|
48
51
|
});
|
|
49
52
|
});
|
|
50
53
|
});
|
|
@@ -278,4 +281,14 @@ describe('common', () => {
|
|
|
278
281
|
expect(joinColumns(['a', 'b', 'c'])).toBe('"a", "b", "c"');
|
|
279
282
|
});
|
|
280
283
|
});
|
|
284
|
+
|
|
285
|
+
describe('quoteTable', () => {
|
|
286
|
+
it('should quote a table', () => {
|
|
287
|
+
expect(quoteTable('table')).toBe('"table"');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should quote a table with schema', () => {
|
|
291
|
+
expect(quoteTable('schema.table')).toBe('"schema"."table"');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
281
294
|
});
|
package/src/common.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Adapter, AdapterOptions } from 'pqb';
|
|
1
|
+
import { Adapter, AdapterOptions, QueryLogOptions } from 'pqb';
|
|
2
2
|
import Enquirer from 'enquirer';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { readdir } from 'fs/promises';
|
|
@@ -7,7 +7,7 @@ export type MigrationConfig = {
|
|
|
7
7
|
migrationsPath: string;
|
|
8
8
|
migrationsTable: string;
|
|
9
9
|
requireTs(path: string): void;
|
|
10
|
-
};
|
|
10
|
+
} & QueryLogOptions;
|
|
11
11
|
|
|
12
12
|
const registered = false;
|
|
13
13
|
|
|
@@ -21,6 +21,8 @@ export const migrationConfigDefaults = {
|
|
|
21
21
|
}
|
|
22
22
|
require(path);
|
|
23
23
|
},
|
|
24
|
+
log: true,
|
|
25
|
+
logger: console,
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
export const getMigrationConfigWithDefaults = (
|
|
@@ -116,7 +118,9 @@ export const createSchemaMigrations = async (
|
|
|
116
118
|
) => {
|
|
117
119
|
try {
|
|
118
120
|
await db.query(
|
|
119
|
-
`CREATE TABLE
|
|
121
|
+
`CREATE TABLE ${quoteTable(
|
|
122
|
+
config.migrationsTable,
|
|
123
|
+
)} ( version TEXT NOT NULL )`,
|
|
120
124
|
);
|
|
121
125
|
console.log('Created versions table');
|
|
122
126
|
} catch (err) {
|
|
@@ -222,3 +226,12 @@ export const joinWords = (...words: string[]) => {
|
|
|
222
226
|
export const joinColumns = (columns: string[]) => {
|
|
223
227
|
return columns.map((column) => `"${column}"`).join(', ');
|
|
224
228
|
};
|
|
229
|
+
|
|
230
|
+
export const quoteTable = (table: string) => {
|
|
231
|
+
const index = table.indexOf('.');
|
|
232
|
+
if (index !== -1) {
|
|
233
|
+
return `"${table.slice(0, index)}"."${table.slice(index + 1)}"`;
|
|
234
|
+
} else {
|
|
235
|
+
return `"${table}"`;
|
|
236
|
+
}
|
|
237
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './commands/createOrDrop';
|
|
2
2
|
export * from './commands/generate';
|
|
3
|
-
export * from './commands/migrateOrRollback
|
|
3
|
+
export * from './commands/migrateOrRollback';
|
|
4
4
|
export { change } from './migration/change';
|
|
5
5
|
export * from './migration/migration';
|
|
6
|
+
export { rakeDb } from './rakeDb';
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
migrateIndexes,
|
|
30
30
|
primaryKeyToSql,
|
|
31
31
|
} from './migrationUtils';
|
|
32
|
+
import { quoteTable } from '../common';
|
|
32
33
|
|
|
33
34
|
const newChangeTableData = () => ({
|
|
34
35
|
add: [],
|
|
@@ -203,7 +204,8 @@ export const changeTable = async (
|
|
|
203
204
|
|
|
204
205
|
if (state.alterTable.length) {
|
|
205
206
|
await migration.query(
|
|
206
|
-
`ALTER TABLE
|
|
207
|
+
`ALTER TABLE ${quoteTable(tableName)}` +
|
|
208
|
+
`\n ${state.alterTable.join(',\n ')}`,
|
|
207
209
|
);
|
|
208
210
|
}
|
|
209
211
|
|
|
@@ -227,7 +229,7 @@ const changeActions = {
|
|
|
227
229
|
value = Array.isArray(comment) ? comment[0] : null;
|
|
228
230
|
}
|
|
229
231
|
return migration.query(
|
|
230
|
-
`COMMENT ON TABLE
|
|
232
|
+
`COMMENT ON TABLE ${quoteTable(tableName)} IS ${quote(value)}`,
|
|
231
233
|
);
|
|
232
234
|
},
|
|
233
235
|
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
migrateIndexes,
|
|
25
25
|
primaryKeyToSql,
|
|
26
26
|
} from './migrationUtils';
|
|
27
|
-
import { joinWords } from '../common';
|
|
27
|
+
import { joinWords, quoteTable } from '../common';
|
|
28
28
|
import { singular } from 'pluralize';
|
|
29
29
|
|
|
30
30
|
class UnknownColumn extends ColumnType {
|
|
@@ -49,18 +49,23 @@ export const createJoinTable = async (
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const tablesWithPrimaryKeys = await Promise.all(
|
|
52
|
-
tables.map(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
tables.map(async (table) => {
|
|
53
|
+
const primaryKeys = await getPrimaryKeysOfTable(migration, table).then(
|
|
54
|
+
(items) =>
|
|
55
|
+
items.map((item) => ({
|
|
56
|
+
...item,
|
|
57
|
+
joinedName: joinWords(singular(table), item.name),
|
|
58
|
+
})),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (!primaryKeys.length) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Primary key for table ${quoteTable(table)} is not defined`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return [table, primaryKeys] as const;
|
|
68
|
+
}),
|
|
64
69
|
);
|
|
65
70
|
|
|
66
71
|
return createTable(migration, up, tableName, options, (t) => {
|
|
@@ -114,7 +119,7 @@ export const createTable = async (
|
|
|
114
119
|
if (!up) {
|
|
115
120
|
const { dropMode } = options;
|
|
116
121
|
await migration.query(
|
|
117
|
-
`DROP TABLE
|
|
122
|
+
`DROP TABLE ${quoteTable(tableName)}${dropMode ? ` ${dropMode}` : ''}`,
|
|
118
123
|
);
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
@@ -152,7 +157,7 @@ export const createTable = async (
|
|
|
152
157
|
});
|
|
153
158
|
|
|
154
159
|
await migration.query({
|
|
155
|
-
text: `CREATE TABLE
|
|
160
|
+
text: `CREATE TABLE ${quoteTable(tableName)} (${lines.join(',')}\n)`,
|
|
156
161
|
values: state.values,
|
|
157
162
|
});
|
|
158
163
|
|
|
@@ -163,7 +168,7 @@ export const createTable = async (
|
|
|
163
168
|
|
|
164
169
|
if (options.comment) {
|
|
165
170
|
await migration.query(
|
|
166
|
-
`COMMENT ON TABLE
|
|
171
|
+
`COMMENT ON TABLE ${quoteTable(tableName)} IS ${quote(options.comment)}`,
|
|
167
172
|
);
|
|
168
173
|
}
|
|
169
174
|
};
|
|
@@ -306,6 +306,99 @@ describe('migration', () => {
|
|
|
306
306
|
? expectDropTable
|
|
307
307
|
: expectCreateTable)();
|
|
308
308
|
});
|
|
309
|
+
|
|
310
|
+
it('should throw error if table has no primary key', async () => {
|
|
311
|
+
if (action === 'dropJoinTable') {
|
|
312
|
+
db.up = false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
(getPrimaryKeysOfTable as jest.Mock)
|
|
316
|
+
.mockResolvedValueOnce([
|
|
317
|
+
{
|
|
318
|
+
name: 'id',
|
|
319
|
+
type: 'integer',
|
|
320
|
+
},
|
|
321
|
+
])
|
|
322
|
+
.mockResolvedValueOnce([]);
|
|
323
|
+
|
|
324
|
+
await expect(db[action](['posts', 'comments'])).rejects.toThrow(
|
|
325
|
+
'Primary key for table "comments" is not defined',
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
(['createSchema', 'dropSchema'] as const).forEach((action) => {
|
|
332
|
+
describe(action, () => {
|
|
333
|
+
it(`should ${
|
|
334
|
+
action === 'createSchema' ? 'add' : 'drop'
|
|
335
|
+
} a schema`, async () => {
|
|
336
|
+
const fn = () => {
|
|
337
|
+
return db[action]('schemaName');
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const expectCreateSchema = () => {
|
|
341
|
+
expectSql(`
|
|
342
|
+
CREATE SCHEMA "schemaName"
|
|
343
|
+
`);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const expectDropSchema = () => {
|
|
347
|
+
expectSql(`
|
|
348
|
+
DROP SCHEMA "schemaName"
|
|
349
|
+
`);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
await fn();
|
|
353
|
+
(action === 'createSchema' ? expectCreateSchema : expectDropSchema)();
|
|
354
|
+
|
|
355
|
+
db.up = false;
|
|
356
|
+
queryMock.mockClear();
|
|
357
|
+
await fn();
|
|
358
|
+
(action === 'createSchema' ? expectDropSchema : expectCreateSchema)();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
(['createExtension', 'dropExtension'] as const).forEach((action) => {
|
|
364
|
+
describe(action, () => {
|
|
365
|
+
it(`should ${
|
|
366
|
+
action === 'createExtension' ? 'add' : 'drop'
|
|
367
|
+
} an extension`, async () => {
|
|
368
|
+
const fn = () => {
|
|
369
|
+
return db[action]('extensionName', {
|
|
370
|
+
ifExists: true,
|
|
371
|
+
ifNotExists: true,
|
|
372
|
+
schema: 'schemaName',
|
|
373
|
+
version: '123',
|
|
374
|
+
cascade: true,
|
|
375
|
+
});
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const expectCreateExtension = () => {
|
|
379
|
+
expectSql(`
|
|
380
|
+
CREATE EXTENSION IF NOT EXISTS "extensionName" SCHEMA "schemaName" VERSION '123' CASCADE
|
|
381
|
+
`);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const expectDropExtension = () => {
|
|
385
|
+
expectSql(`
|
|
386
|
+
DROP EXTENSION IF EXISTS "extensionName" CASCADE
|
|
387
|
+
`);
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
await fn();
|
|
391
|
+
(action === 'createExtension'
|
|
392
|
+
? expectCreateExtension
|
|
393
|
+
: expectDropExtension)();
|
|
394
|
+
|
|
395
|
+
db.up = false;
|
|
396
|
+
queryMock.mockClear();
|
|
397
|
+
await fn();
|
|
398
|
+
(action === 'createExtension'
|
|
399
|
+
? expectDropExtension
|
|
400
|
+
: expectCreateExtension)();
|
|
401
|
+
});
|
|
309
402
|
});
|
|
310
403
|
});
|
|
311
404
|
|
|
@@ -5,11 +5,21 @@ import {
|
|
|
5
5
|
ForeignKeyOptions,
|
|
6
6
|
IndexColumnOptions,
|
|
7
7
|
IndexOptions,
|
|
8
|
+
logParamToLogObject,
|
|
8
9
|
MaybeArray,
|
|
10
|
+
QueryArraysResult,
|
|
11
|
+
QueryInput,
|
|
12
|
+
QueryLogObject,
|
|
13
|
+
QueryLogOptions,
|
|
14
|
+
QueryResult,
|
|
15
|
+
QueryResultRow,
|
|
16
|
+
Sql,
|
|
9
17
|
TransactionAdapter,
|
|
18
|
+
TypeParsers,
|
|
10
19
|
} from 'pqb';
|
|
11
20
|
import { createJoinTable, createTable } from './createTable';
|
|
12
21
|
import { changeTable, TableChangeData, TableChanger } from './changeTable';
|
|
22
|
+
import { quoteTable } from '../common';
|
|
13
23
|
|
|
14
24
|
export type DropMode = 'CASCADE' | 'RESTRICT';
|
|
15
25
|
|
|
@@ -31,9 +41,40 @@ export type JoinTableOptions = {
|
|
|
31
41
|
dropMode?: DropMode;
|
|
32
42
|
};
|
|
33
43
|
|
|
44
|
+
export type ExtensionOptions = {
|
|
45
|
+
schema?: string;
|
|
46
|
+
version?: string;
|
|
47
|
+
cascade?: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
34
50
|
export class Migration extends TransactionAdapter {
|
|
35
|
-
|
|
51
|
+
public log?: QueryLogObject;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
tx: TransactionAdapter,
|
|
55
|
+
public up: boolean,
|
|
56
|
+
options: QueryLogOptions,
|
|
57
|
+
) {
|
|
36
58
|
super(tx.pool, tx.client, tx.types);
|
|
59
|
+
this.log = logParamToLogObject(options.logger || console, options.log);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
async query<T extends QueryResultRow = any>(
|
|
64
|
+
query: QueryInput,
|
|
65
|
+
types: TypeParsers = this.types,
|
|
66
|
+
log = this.log,
|
|
67
|
+
): Promise<QueryResult<T>> {
|
|
68
|
+
return wrapWithLog(log, query, () => super.query(query, types));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
+
async arrays<R extends any[] = any[]>(
|
|
73
|
+
query: QueryInput,
|
|
74
|
+
types: TypeParsers = this.types,
|
|
75
|
+
log = this.log,
|
|
76
|
+
): Promise<QueryArraysResult<R>> {
|
|
77
|
+
return wrapWithLog(log, query, () => super.arrays(query, types));
|
|
37
78
|
}
|
|
38
79
|
|
|
39
80
|
createTable(
|
|
@@ -123,7 +164,7 @@ export class Migration extends TransactionAdapter {
|
|
|
123
164
|
|
|
124
165
|
async renameTable(from: string, to: string): Promise<void> {
|
|
125
166
|
const [table, newName] = this.up ? [from, to] : [to, from];
|
|
126
|
-
await this.query(`ALTER TABLE
|
|
167
|
+
await this.query(`ALTER TABLE ${quoteTable(table)} RENAME TO "${newName}"`);
|
|
127
168
|
}
|
|
128
169
|
|
|
129
170
|
addColumn(
|
|
@@ -216,6 +257,34 @@ export class Migration extends TransactionAdapter {
|
|
|
216
257
|
}));
|
|
217
258
|
}
|
|
218
259
|
|
|
260
|
+
createSchema(schemaName: string) {
|
|
261
|
+
return createSchema(this, this.up, schemaName);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
dropSchema(schemaName: string) {
|
|
265
|
+
return createSchema(this, !this.up, schemaName);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
createExtension(
|
|
269
|
+
name: string,
|
|
270
|
+
options: ExtensionOptions & { ifNotExists?: boolean } = {},
|
|
271
|
+
) {
|
|
272
|
+
return createExtension(this, this.up, name, {
|
|
273
|
+
...options,
|
|
274
|
+
checkExists: options.ifNotExists,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
dropExtension(
|
|
279
|
+
name: string,
|
|
280
|
+
options: { ifExists?: boolean; cascade?: boolean } = {},
|
|
281
|
+
) {
|
|
282
|
+
return createExtension(this, !this.up, name, {
|
|
283
|
+
...options,
|
|
284
|
+
checkExists: options.ifExists,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
219
288
|
async tableExists(tableName: string) {
|
|
220
289
|
return queryExists(this, {
|
|
221
290
|
text: `SELECT 1 FROM "information_schema"."tables" WHERE "table_name" = $1`,
|
|
@@ -238,6 +307,35 @@ export class Migration extends TransactionAdapter {
|
|
|
238
307
|
}
|
|
239
308
|
}
|
|
240
309
|
|
|
310
|
+
const wrapWithLog = async <Result>(
|
|
311
|
+
log: QueryLogObject | undefined,
|
|
312
|
+
query: QueryInput,
|
|
313
|
+
fn: () => Promise<Result>,
|
|
314
|
+
): Promise<Result> => {
|
|
315
|
+
if (!log) {
|
|
316
|
+
return fn();
|
|
317
|
+
} else {
|
|
318
|
+
const sql = (
|
|
319
|
+
typeof query === 'string'
|
|
320
|
+
? { text: query, values: [] }
|
|
321
|
+
: query.values
|
|
322
|
+
? query
|
|
323
|
+
: { ...query, values: [] }
|
|
324
|
+
) as Sql;
|
|
325
|
+
|
|
326
|
+
const logData = log.beforeQuery(sql);
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const result = await fn();
|
|
330
|
+
log.afterQuery(sql, logData);
|
|
331
|
+
return result;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
log.onError(err as Error, sql, logData);
|
|
334
|
+
throw err;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
241
339
|
const addColumn = (
|
|
242
340
|
migration: Migration,
|
|
243
341
|
up: boolean,
|
|
@@ -288,6 +386,43 @@ const addPrimaryKey = (
|
|
|
288
386
|
}));
|
|
289
387
|
};
|
|
290
388
|
|
|
389
|
+
const createSchema = (
|
|
390
|
+
migration: Migration,
|
|
391
|
+
up: boolean,
|
|
392
|
+
schemaName: string,
|
|
393
|
+
) => {
|
|
394
|
+
if (up) {
|
|
395
|
+
return migration.query(`CREATE SCHEMA "${schemaName}"`);
|
|
396
|
+
} else {
|
|
397
|
+
return migration.query(`DROP SCHEMA "${schemaName}"`);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const createExtension = (
|
|
402
|
+
migration: Migration,
|
|
403
|
+
up: boolean,
|
|
404
|
+
name: string,
|
|
405
|
+
options: ExtensionOptions & {
|
|
406
|
+
checkExists?: boolean;
|
|
407
|
+
},
|
|
408
|
+
) => {
|
|
409
|
+
if (!up) {
|
|
410
|
+
return migration.query(
|
|
411
|
+
`DROP EXTENSION${options.checkExists ? ' IF EXISTS' : ''} "${name}"${
|
|
412
|
+
options.cascade ? ' CASCADE' : ''
|
|
413
|
+
}`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return migration.query(
|
|
418
|
+
`CREATE EXTENSION${options.checkExists ? ' IF NOT EXISTS' : ''} "${name}"${
|
|
419
|
+
options.schema ? ` SCHEMA "${options.schema}"` : ''
|
|
420
|
+
}${options.version ? ` VERSION '${options.version}'` : ''}${
|
|
421
|
+
options.cascade ? ' CASCADE' : ''
|
|
422
|
+
}`,
|
|
423
|
+
);
|
|
424
|
+
};
|
|
425
|
+
|
|
291
426
|
const queryExists = (
|
|
292
427
|
db: Migration,
|
|
293
428
|
sql: { text: string; values: unknown[] },
|