rake-db 2.0.0 → 2.0.2

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.
@@ -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 "${config.migrationsTable}" ( version TEXT NOT NULL )`,
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,2 +1,6 @@
1
1
  export * from './commands/createOrDrop';
2
+ export * from './commands/generate';
3
+ export * from './commands/migrateOrRollback';
2
4
  export { change } from './migration/change';
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 "${tableName}"` + `\n ${state.alterTable.join(',\n ')}`,
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 "${tableName}" IS ${quote(value)}`,
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
- async (table) =>
54
- [
55
- table,
56
- await getPrimaryKeysOfTable(migration, table).then((items) =>
57
- items.map((item) => ({
58
- ...item,
59
- joinedName: joinWords(singular(table), item.name),
60
- })),
61
- ),
62
- ] as const,
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 "${tableName}"${dropMode ? ` ${dropMode}` : ''}`,
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 "${tableName}" (${lines.join(',')}\n)`,
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 "${tableName}" IS ${quote(options.comment)}`,
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
- constructor(tx: TransactionAdapter, public up: boolean) {
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 "${table}" RENAME TO "${newName}"`);
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[] },
@@ -1,5 +1,4 @@
1
1
  import {
2
- Adapter,
3
2
  ColumnType,
4
3
  ForeignKeyModel,
5
4
  ForeignKeyOptions,
@@ -10,7 +9,7 @@ import {
10
9
  toArray,
11
10
  } from 'pqb';
12
11
  import { ColumnComment, ColumnIndex, Migration } from './migration';
13
- import { joinColumns, joinWords } from '../common';
12
+ import { joinColumns, joinWords, quoteTable } from '../common';
14
13
 
15
14
  export const columnToSql = (
16
15
  key: string,
@@ -121,7 +120,9 @@ export const referencesToSql = (
121
120
  columns: string[],
122
121
  foreignKey: Pick<ForeignKeyOptions, 'match' | 'onDelete' | 'onUpdate'>,
123
122
  ) => {
124
- const sql: string[] = [`REFERENCES "${table}"(${joinColumns(columns)})`];
123
+ const sql: string[] = [
124
+ `REFERENCES ${quoteTable(table)}(${joinColumns(columns)})`,
125
+ ];
125
126
 
126
127
  if (foreignKey.match) {
127
128
  sql.push(`MATCH ${foreignKey.match.toUpperCase()}`);
@@ -176,7 +177,7 @@ export const migrateIndex = (
176
177
  sql.push('UNIQUE');
177
178
  }
178
179
 
179
- sql.push(`INDEX "${indexName}" ON "${state.tableName}"`);
180
+ sql.push(`INDEX "${indexName}" ON ${quoteTable(state.tableName)}`);
180
181
 
181
182
  if (options.using) {
182
183
  sql.push(`USING ${options.using}`);
@@ -243,7 +244,9 @@ export const migrateComments = async (
243
244
  ) => {
244
245
  for (const { column, comment } of comments) {
245
246
  await state.migration.query(
246
- `COMMENT ON COLUMN "${state.tableName}"."${column}" IS ${quote(comment)}`,
247
+ `COMMENT ON COLUMN ${quoteTable(state.tableName)}."${column}" IS ${quote(
248
+ comment,
249
+ )}`,
247
250
  );
248
251
  }
249
252
  };
@@ -258,11 +261,12 @@ export const primaryKeyToSql = (
258
261
  };
259
262
 
260
263
  export const getPrimaryKeysOfTable = async (
261
- db: Adapter,
264
+ db: Migration,
262
265
  tableName: string,
263
266
  ): Promise<{ name: string; type: string }[]> => {
264
- const { rows } = await db.query<{ name: string; type: string }>({
265
- text: `SELECT
267
+ const { rows } = await db.query<{ name: string; type: string }>(
268
+ {
269
+ text: `SELECT
266
270
  pg_attribute.attname AS name,
267
271
  format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS type
268
272
  FROM pg_index, pg_class, pg_attribute, pg_namespace
@@ -274,8 +278,11 @@ WHERE
274
278
  pg_attribute.attrelid = pg_class.oid AND
275
279
  pg_attribute.attnum = any(pg_index.indkey) AND
276
280
  indisprimary`,
277
- values: [tableName],
278
- });
281
+ values: [tableName],
282
+ },
283
+ db.types,
284
+ undefined,
285
+ );
279
286
 
280
287
  return rows;
281
288
  };
package/src/rakeDb.ts CHANGED
@@ -18,9 +18,9 @@ export const rakeDb = async (
18
18
  } else if (command === 'drop') {
19
19
  await dropDb(options);
20
20
  } else if (command === 'migrate') {
21
- await migrate(options, config);
21
+ await migrate(options, config, args.slice(1));
22
22
  } else if (command === 'rollback') {
23
- await rollback(options, config);
23
+ await rollback(options, config, args.slice(1));
24
24
  } else if (command === 'g' || command === 'generate') {
25
25
  await generate(config, args.slice(1));
26
26
  } else {
package/src/test-utils.ts CHANGED
@@ -6,7 +6,9 @@ let db: Migration | undefined;
6
6
  export const getDb = () => {
7
7
  if (db) return db;
8
8
 
9
- db = new Migration({} as unknown as TransactionAdapter, true);
9
+ db = new Migration({} as unknown as TransactionAdapter, true, {
10
+ log: false,
11
+ });
10
12
  db.query = queryMock;
11
13
  return db;
12
14
  };
package/tsconfig.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "extends": "../../tsconfig.json",
3
- "include": ["./src", "jest-setup.ts"],
3
+ "include": ["./src", "jest-setup.ts", "db.ts", "./migrations"],
4
4
  "compilerOptions": {
5
5
  "outDir": "./dist",
6
6
  "noEmit": false,
@@ -1,5 +0,0 @@
1
- import { change } from '../src';
2
-
3
- change(async () => {
4
- console.log('second');
5
- });