rake-db 2.3.0 → 2.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rake-db",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "Migrations tool for Postgresql DB",
5
5
  "homepage": "https://orchid-orm.netlify.app/guide/migration-setup-and-overview.html",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "enquirer": "^2.3.6",
44
44
  "pluralize": "^8.0.0",
45
- "pqb": "0.9.0"
45
+ "pqb": "0.9.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@swc/core": "^1.2.210",
@@ -7,20 +7,28 @@ import {
7
7
  import { mkdir, writeFile } from 'fs/promises';
8
8
  import path from 'path';
9
9
 
10
- export const generate = async (config: RakeDbConfig, args: string[]) => {
11
- const name = args[0];
12
- if (!name) throw new Error('Migration name is missing');
13
-
14
- await mkdir(config.migrationsPath, { recursive: true });
10
+ export const writeMigrationFile = async (
11
+ config: RakeDbConfig,
12
+ name: string,
13
+ content: string,
14
+ ) => {
15
+ await mkdir(path.resolve(config.migrationsPath), { recursive: true });
15
16
 
16
17
  const filePath = path.resolve(
17
18
  config.migrationsPath,
18
19
  `${makeFileTimeStamp()}_${name}.ts`,
19
20
  );
20
- await writeFile(filePath, makeContent(name, args.slice(1)));
21
+ await writeFile(filePath, content);
21
22
  console.log(`Created ${filePath}`);
22
23
  };
23
24
 
25
+ export const generate = async (config: RakeDbConfig, args: string[]) => {
26
+ const name = args[0];
27
+ if (!name) throw new Error('Migration name is missing');
28
+
29
+ await writeMigrationFile(config, name, makeContent(name, args.slice(1)));
30
+ };
31
+
24
32
  const makeFileTimeStamp = () => {
25
33
  const now = new Date();
26
34
  return [
package/src/common.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  AdapterOptions,
4
4
  NoPrimaryKeyOption,
5
5
  QueryLogOptions,
6
+ singleQuote,
6
7
  } from 'pqb';
7
8
  import Enquirer from 'enquirer';
8
9
  import path from 'path';
@@ -25,7 +26,7 @@ export type AppCodeUpdater = (params: {
25
26
  }) => Promise<void>;
26
27
 
27
28
  export const migrationConfigDefaults = {
28
- migrationsPath: path.resolve(process.cwd(), 'src', 'migrations'),
29
+ migrationsPath: path.resolve('src', 'migrations'),
29
30
  migrationsTable: 'schemaMigrations',
30
31
  requireTs: require,
31
32
  log: true,
@@ -191,7 +192,7 @@ export const getMigrationFiles = async (
191
192
 
192
193
  let files: string[];
193
194
  try {
194
- files = await readdir(migrationsPath);
195
+ files = await readdir(path.resolve(migrationsPath));
195
196
  } catch (_) {
196
197
  return [];
197
198
  }
@@ -212,7 +213,7 @@ export const getMigrationFiles = async (
212
213
  }
213
214
 
214
215
  return {
215
- path: path.join(migrationsPath, file),
216
+ path: path.resolve(migrationsPath, file),
216
217
  version: timestampMatch[1],
217
218
  };
218
219
  });
@@ -253,3 +254,13 @@ export const getSchemaAndTableFromName = (
253
254
  ? [name.slice(0, index), name.slice(index + 1)]
254
255
  : [undefined, name];
255
256
  };
257
+
258
+ export const quoteSchemaTable = ({
259
+ schema,
260
+ name,
261
+ }: {
262
+ schema?: string;
263
+ name: string;
264
+ }) => {
265
+ return singleQuote(schema ? `${schema}.${name}` : name);
266
+ };
@@ -0,0 +1,150 @@
1
+ import { astToMigration } from './astToMigration';
2
+ import { columnTypes } from 'pqb';
3
+ import { RakeDbAst } from '../ast';
4
+
5
+ const template = (content: string) => `import { change } from 'rake-db';
6
+
7
+ change(async (db) => {
8
+ ${content}
9
+ });
10
+ `;
11
+
12
+ const tableAst: RakeDbAst.Table = {
13
+ type: 'table',
14
+ action: 'create',
15
+ schema: 'schema',
16
+ name: 'table',
17
+ shape: {},
18
+ noPrimaryKey: 'ignore',
19
+ indexes: [],
20
+ foreignKeys: [],
21
+ };
22
+
23
+ describe('astToMigration', () => {
24
+ beforeEach(jest.clearAllMocks);
25
+
26
+ it('should return undefined when ast is empty', () => {
27
+ const result = astToMigration([]);
28
+
29
+ expect(result).toBe(undefined);
30
+ });
31
+
32
+ it('should create schema', () => {
33
+ const result = astToMigration([
34
+ {
35
+ type: 'schema',
36
+ action: 'create',
37
+ name: 'schemaName',
38
+ },
39
+ ]);
40
+
41
+ expect(result).toBe(template(` await db.createSchema('schemaName');`));
42
+ });
43
+
44
+ describe('table', () => {
45
+ it('should create table', () => {
46
+ const result = astToMigration([
47
+ {
48
+ ...tableAst,
49
+ shape: {
50
+ id: columnTypes.serial().primaryKey(),
51
+ },
52
+ },
53
+ ]);
54
+
55
+ expect(result).toBe(
56
+ template(` await db.createTable('schema.table', (t) => ({
57
+ id: t.serial().primaryKey(),
58
+ }));`),
59
+ );
60
+ });
61
+
62
+ it('should add columns with indexes and foreignKeys', () => {
63
+ const result = astToMigration([
64
+ {
65
+ ...tableAst,
66
+ shape: {
67
+ someId: columnTypes
68
+ .integer()
69
+ .unique({ name: 'indexName' })
70
+ .foreignKey('otherTable', 'otherId', {
71
+ name: 'fkey',
72
+ match: 'FULL',
73
+ onUpdate: 'CASCADE',
74
+ onDelete: 'CASCADE',
75
+ }),
76
+ },
77
+ },
78
+ ]);
79
+
80
+ expect(result).toBe(`import { change } from 'rake-db';
81
+
82
+ change(async (db) => {
83
+ await db.createTable('schema.table', (t) => ({
84
+ someId: t.integer().foreignKey('otherTable', 'otherId', {
85
+ name: 'fkey',
86
+ match: 'FULL',
87
+ onUpdate: 'CASCADE',
88
+ onDelete: 'CASCADE',
89
+ }).unique({
90
+ name: 'indexName',
91
+ }),
92
+ }));
93
+ });
94
+ `);
95
+ });
96
+
97
+ it('should add composite primaryKeys, indexes, foreignKeys', () => {
98
+ const result = astToMigration([
99
+ {
100
+ ...tableAst,
101
+ shape: {
102
+ id: columnTypes.serial().primaryKey(),
103
+ },
104
+ primaryKey: { columns: ['id', 'name'], options: { name: 'pkey' } },
105
+ indexes: [
106
+ {
107
+ columns: [{ column: 'id' }, { column: 'name' }],
108
+ options: { name: 'index', unique: true },
109
+ },
110
+ ],
111
+ foreignKeys: [
112
+ {
113
+ columns: ['id', 'name'],
114
+ fnOrTable: 'otherTable',
115
+ foreignColumns: ['otherId', 'otherName'],
116
+ options: {
117
+ name: 'fkey',
118
+ match: 'FULL',
119
+ onUpdate: 'CASCADE',
120
+ onDelete: 'CASCADE',
121
+ },
122
+ },
123
+ ],
124
+ },
125
+ ]);
126
+
127
+ expect(result).toBe(
128
+ template(` await db.createTable('schema.table', (t) => ({
129
+ id: t.serial().primaryKey(),
130
+ ...t.primaryKey(['id', 'name'], { name: 'pkey' }),
131
+ ...t.index(['id', 'name'], {
132
+ name: 'index',
133
+ unique: true,
134
+ }),
135
+ ...t.foreignKey(
136
+ ['id', 'name'],
137
+ 'otherTable',
138
+ ['otherId', 'otherName'],
139
+ {
140
+ name: 'fkey',
141
+ match: 'FULL',
142
+ onUpdate: 'CASCADE',
143
+ onDelete: 'CASCADE',
144
+ },
145
+ ),
146
+ }));`),
147
+ );
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,93 @@
1
+ import { RakeDbAst } from '../ast';
2
+ import {
3
+ addCode,
4
+ Code,
5
+ codeToString,
6
+ ColumnType,
7
+ foreignKeyToCode,
8
+ indexToCode,
9
+ isRaw,
10
+ primaryKeyToCode,
11
+ quoteObjectKey,
12
+ singleQuote,
13
+ TimestampColumn,
14
+ } from 'pqb';
15
+ import { quoteSchemaTable } from '../common';
16
+
17
+ export const astToMigration = (ast: RakeDbAst[]): string | undefined => {
18
+ const code: Code[] = [];
19
+ for (const item of ast) {
20
+ if (item.type === 'schema' && item.action === 'create') {
21
+ code.push(createSchema(item));
22
+ } else if (item.type === 'table' && item.action === 'create') {
23
+ if (code.length) code.push([]);
24
+ code.push(...createTable(item));
25
+ }
26
+ }
27
+
28
+ if (!code.length) return;
29
+
30
+ return `import { change } from 'rake-db';
31
+
32
+ change(async (db) => {
33
+ ${codeToString(code, ' ', ' ')}
34
+ });
35
+ `;
36
+ };
37
+
38
+ const createSchema = (ast: RakeDbAst.Schema) => {
39
+ return `await db.createSchema(${singleQuote(ast.name)});`;
40
+ };
41
+
42
+ const createTable = (ast: RakeDbAst.Table) => {
43
+ const code: Code[] = [];
44
+ addCode(code, `await db.createTable(${quoteSchemaTable(ast)}, (t) => ({`);
45
+
46
+ const hasTimestamps =
47
+ isTimestamp(ast.shape.createdAt) && isTimestamp(ast.shape.updatedAt);
48
+
49
+ for (const key in ast.shape) {
50
+ if (hasTimestamps && (key === 'createdAt' || key === 'updatedAt')) continue;
51
+
52
+ const line: Code[] = [`${quoteObjectKey(key)}: `];
53
+ for (const part of ast.shape[key].toCode('t')) {
54
+ addCode(line, part);
55
+ }
56
+ addCode(line, ',');
57
+ code.push(line);
58
+ }
59
+
60
+ if (hasTimestamps) {
61
+ code.push(['...t.timestamps(),']);
62
+ }
63
+
64
+ if (ast.primaryKey) {
65
+ code.push([primaryKeyToCode(ast.primaryKey, 't')]);
66
+ }
67
+
68
+ for (const index of ast.indexes) {
69
+ code.push(indexToCode(index, 't'));
70
+ }
71
+
72
+ for (const foreignKey of ast.foreignKeys) {
73
+ code.push(foreignKeyToCode(foreignKey, 't'));
74
+ }
75
+
76
+ addCode(code, '}));');
77
+
78
+ return code;
79
+ };
80
+
81
+ const isTimestamp = (column?: ColumnType) => {
82
+ if (!column) return false;
83
+
84
+ const { default: def } = column.data;
85
+ return (
86
+ column instanceof TimestampColumn &&
87
+ !column.data.isNullable &&
88
+ def &&
89
+ typeof def === 'object' &&
90
+ isRaw(def) &&
91
+ def.__raw === 'now()'
92
+ );
93
+ };
@@ -0,0 +1,111 @@
1
+ import { DbStructure } from './dbStructure';
2
+ import { pullDbStructure } from './pull';
3
+ import { getMigrationConfigWithDefaults } from '../common';
4
+ import { writeMigrationFile } from '../commands/generate';
5
+
6
+ jest.mock('./dbStructure', () => {
7
+ const { DbStructure } = jest.requireActual('./dbStructure');
8
+ for (const key of Object.getOwnPropertyNames(DbStructure.prototype)) {
9
+ (DbStructure.prototype as unknown as Record<string, () => unknown[]>)[key] =
10
+ () => [];
11
+ }
12
+
13
+ return { DbStructure };
14
+ });
15
+
16
+ jest.mock('../commands/generate', () => ({
17
+ writeMigrationFile: jest.fn(),
18
+ }));
19
+
20
+ const db = DbStructure.prototype;
21
+
22
+ describe('pull', () => {
23
+ it('should get db structure, convert it to ast, generate migrations', async () => {
24
+ db.getSchemas = async () => ['schema1', 'schema2'];
25
+ db.getTables = async () => [
26
+ {
27
+ schemaName: 'schema',
28
+ name: 'table1',
29
+ },
30
+ {
31
+ schemaName: 'public',
32
+ name: 'table2',
33
+ },
34
+ ];
35
+ db.getPrimaryKeys = async () => [
36
+ {
37
+ schemaName: 'schema',
38
+ tableName: 'table1',
39
+ name: 'table1_pkey',
40
+ columnNames: ['id'],
41
+ },
42
+ ];
43
+ db.getColumns = async () => [
44
+ {
45
+ schemaName: 'schema',
46
+ tableName: 'table1',
47
+ name: 'id',
48
+ type: 'int4',
49
+ default: `nextval('table1_id_seq'::regclass)`,
50
+ isNullable: false,
51
+ },
52
+ {
53
+ schemaName: 'public',
54
+ tableName: 'table2',
55
+ name: 'text',
56
+ type: 'text',
57
+ isNullable: false,
58
+ },
59
+ {
60
+ schemaName: 'public',
61
+ tableName: 'table2',
62
+ name: 'createdAt',
63
+ type: 'timestamp',
64
+ dateTimePrecision: 6,
65
+ isNullable: false,
66
+ default: 'now()',
67
+ },
68
+ {
69
+ schemaName: 'public',
70
+ tableName: 'table2',
71
+ name: 'updatedAt',
72
+ type: 'timestamp',
73
+ dateTimePrecision: 6,
74
+ isNullable: false,
75
+ default: 'now()',
76
+ },
77
+ ];
78
+
79
+ const config = getMigrationConfigWithDefaults({
80
+ migrationsPath: 'migrations',
81
+ });
82
+
83
+ await pullDbStructure(
84
+ {
85
+ databaseURL: 'file:path',
86
+ },
87
+ config,
88
+ );
89
+
90
+ expect(writeMigrationFile).toBeCalledWith(
91
+ config,
92
+ 'pull',
93
+ `import { change } from 'rake-db';
94
+
95
+ change(async (db) => {
96
+ await db.createSchema('schema1');
97
+ await db.createSchema('schema2');
98
+
99
+ await db.createTable('schema.table1', (t) => ({
100
+ id: t.serial().primaryKey(),
101
+ }));
102
+
103
+ await db.createTable('table2', (t) => ({
104
+ text: t.text(),
105
+ ...t.timestamps(),
106
+ }));
107
+ });
108
+ `,
109
+ );
110
+ });
111
+ });
@@ -0,0 +1,22 @@
1
+ import { RakeDbConfig } from '../common';
2
+ import { Adapter, AdapterOptions } from 'pqb';
3
+ import { DbStructure } from './dbStructure';
4
+ import { structureToAst } from './structureToAst';
5
+ import { astToMigration } from './astToMigration';
6
+ import { writeMigrationFile } from '../commands/generate';
7
+
8
+ export const pullDbStructure = async (
9
+ options: AdapterOptions,
10
+ config: RakeDbConfig,
11
+ ) => {
12
+ const adapter = new Adapter(options);
13
+ const db = new DbStructure(adapter);
14
+ const ast = await structureToAst(db);
15
+
16
+ await adapter.close();
17
+
18
+ const result = astToMigration(ast);
19
+ if (!result) return;
20
+
21
+ await writeMigrationFile(config, 'pull', result);
22
+ };
@@ -1,8 +1,13 @@
1
1
  import { DbStructure } from './dbStructure';
2
2
  import {
3
3
  Adapter,
4
+ BigSerialColumn,
4
5
  DecimalColumn,
5
6
  IntegerColumn,
7
+ isRaw,
8
+ RawExpression,
9
+ SerialColumn,
10
+ SmallSerialColumn,
6
11
  TextColumn,
7
12
  TimestampColumn,
8
13
  VarCharColumn,
@@ -118,7 +123,9 @@ describe('structureToAst', () => {
118
123
  db.getTables = async () => [
119
124
  { schemaName: 'public', name: 'table', comment: 'comment' },
120
125
  ];
126
+
121
127
  const ast = await structureToAst(db);
128
+
122
129
  expect(ast).toEqual([
123
130
  {
124
131
  type: 'table',
@@ -136,7 +143,9 @@ describe('structureToAst', () => {
136
143
  it('should add table with schema', async () => {
137
144
  const db = new DbStructure(adapter);
138
145
  db.getTables = async () => [{ schemaName: 'custom', name: 'table' }];
146
+
139
147
  const ast = await structureToAst(db);
148
+
140
149
  expect(ast).toEqual([
141
150
  {
142
151
  type: 'table',
@@ -151,9 +160,20 @@ describe('structureToAst', () => {
151
160
  ]);
152
161
  });
153
162
 
163
+ it('should ignore schemaMigrations table', async () => {
164
+ const db = new DbStructure(adapter);
165
+ db.getTables = async () => [
166
+ { schemaName: 'public', name: 'schemaMigrations' },
167
+ ];
168
+
169
+ const ast = await structureToAst(db);
170
+
171
+ expect(ast).toEqual([]);
172
+ });
173
+
154
174
  it('should add columns', async () => {
155
175
  const db = new DbStructure(adapter);
156
- db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
176
+ db.getTables = async () => [table];
157
177
  db.getColumns = async () => columns;
158
178
 
159
179
  const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
@@ -164,9 +184,83 @@ describe('structureToAst', () => {
164
184
  expect(ast.shape.name).toBeInstanceOf(TextColumn);
165
185
  });
166
186
 
187
+ it('should wrap column default into raw', async () => {
188
+ const db = new DbStructure(adapter);
189
+ db.getTables = async () => [table];
190
+ db.getColumns = async () => [{ ...timestampColumn, default: 'now()' }];
191
+
192
+ const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
193
+
194
+ const { default: def } = ast.shape.timestamp.data;
195
+ expect(def && typeof def === 'object' && isRaw(def)).toBe(true);
196
+ expect((def as RawExpression).__raw).toBe('now()');
197
+ });
198
+
199
+ describe('serial column', () => {
200
+ it('should add serial column based on various default values', async () => {
201
+ const db = new DbStructure(adapter);
202
+ db.getTables = async () => [{ schemaName: 'schema', name: 'table' }];
203
+
204
+ const defaults = [
205
+ `nextval('table_id_seq'::regclass)`,
206
+ `nextval('"table_id_seq"'::regclass)`,
207
+ `nextval('schema.table_id_seq'::regclass)`,
208
+ `nextval('schema."table_id_seq"'::regclass)`,
209
+ `nextval('"schema".table_id_seq'::regclass)`,
210
+ `nextval('"schema"."table_id_seq"'::regclass)`,
211
+ ];
212
+
213
+ for (const def of defaults) {
214
+ db.getColumns = async () => [
215
+ {
216
+ ...intColumn,
217
+ name: 'id',
218
+ schemaName: 'schema',
219
+ tableName: 'table',
220
+ default: def,
221
+ },
222
+ ];
223
+
224
+ const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
225
+
226
+ expect(ast.shape.id).toBeInstanceOf(SerialColumn);
227
+ expect(ast.shape.id.data.default).toBe(undefined);
228
+ }
229
+ });
230
+
231
+ it('should support smallserial, serial, and bigserial', async () => {
232
+ const db = new DbStructure(adapter);
233
+ db.getTables = async () => [{ schemaName: 'schema', name: 'table' }];
234
+
235
+ const types = [
236
+ ['int2', SmallSerialColumn],
237
+ ['int4', SerialColumn],
238
+ ['int8', BigSerialColumn],
239
+ ] as const;
240
+
241
+ for (const [type, Column] of types) {
242
+ db.getColumns = async () => [
243
+ {
244
+ ...intColumn,
245
+ type,
246
+ name: 'id',
247
+ schemaName: 'schema',
248
+ tableName: 'table',
249
+ default: `nextval('table_id_seq'::regclass)`,
250
+ },
251
+ ];
252
+
253
+ const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
254
+
255
+ expect(ast.shape.id).toBeInstanceOf(Column);
256
+ expect(ast.shape.id.data.default).toBe(undefined);
257
+ }
258
+ });
259
+ });
260
+
167
261
  it('should set maxChars to char column', async () => {
168
262
  const db = new DbStructure(adapter);
169
- db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
263
+ db.getTables = async () => [table];
170
264
  db.getColumns = async () => [varCharColumn];
171
265
 
172
266
  const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
@@ -178,7 +272,7 @@ describe('structureToAst', () => {
178
272
 
179
273
  it('should set numericPrecision and numericScale to decimal column', async () => {
180
274
  const db = new DbStructure(adapter);
181
- db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
275
+ db.getTables = async () => [table];
182
276
  db.getColumns = async () => [decimalColumn];
183
277
 
184
278
  const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
@@ -191,7 +285,7 @@ describe('structureToAst', () => {
191
285
 
192
286
  it('should set dateTimePrecision to timestamp column', async () => {
193
287
  const db = new DbStructure(adapter);
194
- db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
288
+ db.getTables = async () => [table];
195
289
  db.getColumns = async () => [timestampColumn];
196
290
 
197
291
  const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
@@ -205,7 +299,7 @@ describe('structureToAst', () => {
205
299
 
206
300
  it('should set primaryKey to column', async () => {
207
301
  const db = new DbStructure(adapter);
208
- db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
302
+ db.getTables = async () => [table];
209
303
  db.getColumns = async () => columns;
210
304
  db.getPrimaryKeys = async () => [primaryKey];
211
305
 
@@ -218,7 +312,7 @@ describe('structureToAst', () => {
218
312
 
219
313
  it('should add composite primary key', async () => {
220
314
  const db = new DbStructure(adapter);
221
- db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
315
+ db.getTables = async () => [table];
222
316
  db.getColumns = async () => columns;
223
317
  db.getPrimaryKeys = async () => [
224
318
  { ...primaryKey, columnNames: ['id', 'name'] },
@@ -236,7 +330,7 @@ describe('structureToAst', () => {
236
330
 
237
331
  it('should ignore primary key name if it is standard', async () => {
238
332
  const db = new DbStructure(adapter);
239
- db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
333
+ db.getTables = async () => [table];
240
334
  db.getColumns = async () => columns;
241
335
  db.getPrimaryKeys = async () => [
242
336
  { ...primaryKey, columnNames: ['id', 'name'], name: 'table_pkey' },