rake-db 1.3.2 → 2.0.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/.env +3 -0
- package/.env.local +1 -0
- package/README.md +1 -545
- package/db.ts +16 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.esm.js +190 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +201 -0
- package/dist/index.js.map +1 -0
- package/jest-setup.ts +3 -0
- package/migrations/20221009210157_first.ts +8 -0
- package/migrations/20221009210200_second.ts +5 -0
- package/package.json +55 -41
- package/rollup.config.js +3 -0
- package/src/commands/createOrDrop.test.ts +145 -0
- package/src/commands/createOrDrop.ts +107 -0
- package/src/commands/generate.test.ts +133 -0
- package/src/commands/generate.ts +85 -0
- package/src/commands/migrateOrRollback.test.ts +118 -0
- package/src/commands/migrateOrRollback.ts +108 -0
- package/src/common.test.ts +281 -0
- package/src/common.ts +224 -0
- package/src/index.ts +2 -0
- package/src/migration/change.ts +20 -0
- package/src/migration/changeTable.test.ts +417 -0
- package/src/migration/changeTable.ts +375 -0
- package/src/migration/createTable.test.ts +269 -0
- package/src/migration/createTable.ts +169 -0
- package/src/migration/migration.test.ts +341 -0
- package/src/migration/migration.ts +296 -0
- package/src/migration/migrationUtils.ts +281 -0
- package/src/rakeDb.ts +29 -0
- package/src/test-utils.ts +45 -0
- package/tsconfig.json +12 -0
- package/dist/lib/createAndDrop.d.ts +0 -2
- package/dist/lib/createAndDrop.js +0 -63
- package/dist/lib/defaults.d.ts +0 -2
- package/dist/lib/defaults.js +0 -5
- package/dist/lib/errorCodes.d.ts +0 -4
- package/dist/lib/errorCodes.js +0 -7
- package/dist/lib/generate.d.ts +0 -1
- package/dist/lib/generate.js +0 -99
- package/dist/lib/help.d.ts +0 -2
- package/dist/lib/help.js +0 -24
- package/dist/lib/init.d.ts +0 -2
- package/dist/lib/init.js +0 -276
- package/dist/lib/migrate.d.ts +0 -4
- package/dist/lib/migrate.js +0 -189
- package/dist/lib/migration.d.ts +0 -37
- package/dist/lib/migration.js +0 -159
- package/dist/lib/schema/changeTable.d.ts +0 -23
- package/dist/lib/schema/changeTable.js +0 -109
- package/dist/lib/schema/column.d.ts +0 -31
- package/dist/lib/schema/column.js +0 -201
- package/dist/lib/schema/createTable.d.ts +0 -10
- package/dist/lib/schema/createTable.js +0 -53
- package/dist/lib/schema/foreignKey.d.ts +0 -11
- package/dist/lib/schema/foreignKey.js +0 -53
- package/dist/lib/schema/index.d.ts +0 -3
- package/dist/lib/schema/index.js +0 -54
- package/dist/lib/schema/primaryKey.d.ts +0 -9
- package/dist/lib/schema/primaryKey.js +0 -24
- package/dist/lib/schema/table.d.ts +0 -43
- package/dist/lib/schema/table.js +0 -110
- package/dist/lib/schema/timestamps.d.ts +0 -3
- package/dist/lib/schema/timestamps.js +0 -9
- package/dist/lib/utils.d.ts +0 -26
- package/dist/lib/utils.js +0 -114
- package/dist/rake-db.d.ts +0 -2
- package/dist/rake-db.js +0 -34
- package/dist/types.d.ts +0 -94
- package/dist/types.js +0 -40
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createDb, dropDb } from './createOrDrop';
|
|
2
|
+
import { Adapter } from 'pqb';
|
|
3
|
+
import {
|
|
4
|
+
createSchemaMigrations,
|
|
5
|
+
migrationConfigDefaults,
|
|
6
|
+
setAdminCredentialsToOptions,
|
|
7
|
+
} from '../common';
|
|
8
|
+
|
|
9
|
+
jest.mock('../common', () => ({
|
|
10
|
+
...jest.requireActual('../common'),
|
|
11
|
+
setAdminCredentialsToOptions: jest.fn((options: Record<string, unknown>) => ({
|
|
12
|
+
...options,
|
|
13
|
+
user: 'admin-user',
|
|
14
|
+
password: 'admin-password',
|
|
15
|
+
})),
|
|
16
|
+
createSchemaMigrations: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const options = { database: 'dbname', user: 'user', password: 'password' };
|
|
20
|
+
const queryMock = jest.fn();
|
|
21
|
+
Adapter.prototype.query = queryMock;
|
|
22
|
+
|
|
23
|
+
const logMock = jest.fn();
|
|
24
|
+
console.log = logMock;
|
|
25
|
+
|
|
26
|
+
describe('createOrDrop', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('createDb', () => {
|
|
32
|
+
it('should create database when user is an admin', async () => {
|
|
33
|
+
queryMock.mockResolvedValueOnce(undefined);
|
|
34
|
+
|
|
35
|
+
await createDb(options, migrationConfigDefaults);
|
|
36
|
+
|
|
37
|
+
expect(queryMock.mock.calls).toEqual([
|
|
38
|
+
[`CREATE DATABASE "dbname" OWNER "user"`],
|
|
39
|
+
]);
|
|
40
|
+
expect(logMock.mock.calls).toEqual([
|
|
41
|
+
[`Database dbname successfully created`],
|
|
42
|
+
]);
|
|
43
|
+
expect(createSchemaMigrations).toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should create databases for each provided option', async () => {
|
|
47
|
+
queryMock.mockResolvedValue(undefined);
|
|
48
|
+
|
|
49
|
+
await createDb(
|
|
50
|
+
[options, { ...options, database: 'dbname-test' }],
|
|
51
|
+
migrationConfigDefaults,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(queryMock.mock.calls).toEqual([
|
|
55
|
+
[`CREATE DATABASE "dbname" OWNER "user"`],
|
|
56
|
+
[`CREATE DATABASE "dbname-test" OWNER "user"`],
|
|
57
|
+
]);
|
|
58
|
+
expect(logMock.mock.calls).toEqual([
|
|
59
|
+
[`Database dbname successfully created`],
|
|
60
|
+
[`Database dbname-test successfully created`],
|
|
61
|
+
]);
|
|
62
|
+
expect(createSchemaMigrations).toHaveBeenCalledTimes(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should inform if database already exists', async () => {
|
|
66
|
+
queryMock.mockRejectedValueOnce({ code: '42P04' });
|
|
67
|
+
|
|
68
|
+
await createDb(options, migrationConfigDefaults);
|
|
69
|
+
|
|
70
|
+
expect(queryMock.mock.calls).toEqual([
|
|
71
|
+
[`CREATE DATABASE "dbname" OWNER "user"`],
|
|
72
|
+
]);
|
|
73
|
+
expect(logMock.mock.calls).toEqual([[`Database dbname already exists`]]);
|
|
74
|
+
expect(createSchemaMigrations).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should ask and use admin credentials when cannot connect', async () => {
|
|
78
|
+
queryMock.mockRejectedValueOnce({ code: '42501' });
|
|
79
|
+
|
|
80
|
+
await createDb(options, migrationConfigDefaults);
|
|
81
|
+
|
|
82
|
+
expect(setAdminCredentialsToOptions).toHaveBeenCalled();
|
|
83
|
+
expect(queryMock.mock.calls).toEqual([
|
|
84
|
+
[`CREATE DATABASE "dbname" OWNER "user"`],
|
|
85
|
+
[`CREATE DATABASE "dbname" OWNER "user"`],
|
|
86
|
+
]);
|
|
87
|
+
expect(logMock.mock.calls).toEqual([
|
|
88
|
+
[`Database dbname successfully created`],
|
|
89
|
+
]);
|
|
90
|
+
expect(createSchemaMigrations).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('dropDb', () => {
|
|
95
|
+
it('should drop database when user is an admin', async () => {
|
|
96
|
+
queryMock.mockResolvedValueOnce(undefined);
|
|
97
|
+
|
|
98
|
+
await dropDb(options);
|
|
99
|
+
|
|
100
|
+
expect(queryMock.mock.calls).toEqual([[`DROP DATABASE "dbname"`]]);
|
|
101
|
+
expect(logMock.mock.calls).toEqual([
|
|
102
|
+
[`Database dbname was successfully dropped`],
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should drop databases for each provided option', async () => {
|
|
107
|
+
queryMock.mockResolvedValue(undefined);
|
|
108
|
+
|
|
109
|
+
await dropDb([options, { ...options, database: 'dbname-test' }]);
|
|
110
|
+
|
|
111
|
+
expect(queryMock.mock.calls).toEqual([
|
|
112
|
+
[`DROP DATABASE "dbname"`],
|
|
113
|
+
[`DROP DATABASE "dbname-test"`],
|
|
114
|
+
]);
|
|
115
|
+
expect(logMock.mock.calls).toEqual([
|
|
116
|
+
[`Database dbname was successfully dropped`],
|
|
117
|
+
[`Database dbname-test was successfully dropped`],
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should inform if database does not exist', async () => {
|
|
122
|
+
queryMock.mockRejectedValueOnce({ code: '3D000' });
|
|
123
|
+
|
|
124
|
+
await dropDb(options);
|
|
125
|
+
|
|
126
|
+
expect(queryMock.mock.calls).toEqual([[`DROP DATABASE "dbname"`]]);
|
|
127
|
+
expect(logMock.mock.calls).toEqual([[`Database dbname does not exist`]]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should ask and use admin credentials when cannot connect', async () => {
|
|
131
|
+
queryMock.mockRejectedValueOnce({ code: '42501' });
|
|
132
|
+
|
|
133
|
+
await dropDb(options);
|
|
134
|
+
|
|
135
|
+
expect(setAdminCredentialsToOptions).toHaveBeenCalled();
|
|
136
|
+
expect(queryMock.mock.calls).toEqual([
|
|
137
|
+
[`DROP DATABASE "dbname"`],
|
|
138
|
+
[`DROP DATABASE "dbname"`],
|
|
139
|
+
]);
|
|
140
|
+
expect(logMock.mock.calls).toEqual([
|
|
141
|
+
[`Database dbname was successfully dropped`],
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Adapter, AdapterOptions, MaybeArray, toArray } from 'pqb';
|
|
2
|
+
import {
|
|
3
|
+
getDatabaseAndUserFromOptions,
|
|
4
|
+
setAdminCredentialsToOptions,
|
|
5
|
+
setAdapterOptions,
|
|
6
|
+
createSchemaMigrations,
|
|
7
|
+
MigrationConfig,
|
|
8
|
+
migrationConfigDefaults,
|
|
9
|
+
} from '../common';
|
|
10
|
+
|
|
11
|
+
const execute = async (
|
|
12
|
+
options: AdapterOptions,
|
|
13
|
+
sql: string,
|
|
14
|
+
): Promise<'ok' | 'already' | 'forbidden' | { error: unknown }> => {
|
|
15
|
+
const db = new Adapter(options);
|
|
16
|
+
try {
|
|
17
|
+
await db.query(sql);
|
|
18
|
+
return 'ok';
|
|
19
|
+
} catch (error) {
|
|
20
|
+
const err = error as Record<string, unknown>;
|
|
21
|
+
if (err.code === '42P04' || err.code === '3D000') {
|
|
22
|
+
return 'already';
|
|
23
|
+
} else if (err.code === '42501') {
|
|
24
|
+
return 'forbidden';
|
|
25
|
+
} else {
|
|
26
|
+
return { error };
|
|
27
|
+
}
|
|
28
|
+
} finally {
|
|
29
|
+
await db.destroy();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const createOrDrop = async (
|
|
34
|
+
options: AdapterOptions,
|
|
35
|
+
adminOptions: AdapterOptions,
|
|
36
|
+
config: MigrationConfig,
|
|
37
|
+
args: {
|
|
38
|
+
sql(params: { database: string; user: string }): string;
|
|
39
|
+
successMessage(params: { database: string }): string;
|
|
40
|
+
alreadyMessage(params: { database: string }): string;
|
|
41
|
+
createVersionsTable?: boolean;
|
|
42
|
+
},
|
|
43
|
+
) => {
|
|
44
|
+
const params = getDatabaseAndUserFromOptions(options);
|
|
45
|
+
|
|
46
|
+
const result = await execute(
|
|
47
|
+
setAdapterOptions(adminOptions, { database: 'postgres' }),
|
|
48
|
+
args.sql(params),
|
|
49
|
+
);
|
|
50
|
+
if (result === 'ok') {
|
|
51
|
+
console.log(args.successMessage(params));
|
|
52
|
+
} else if (result === 'already') {
|
|
53
|
+
console.log(args.alreadyMessage(params));
|
|
54
|
+
} else if (result === 'forbidden') {
|
|
55
|
+
await createOrDrop(
|
|
56
|
+
options,
|
|
57
|
+
await setAdminCredentialsToOptions(options),
|
|
58
|
+
config,
|
|
59
|
+
args,
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
} else {
|
|
63
|
+
throw result.error;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!args.createVersionsTable) return;
|
|
67
|
+
|
|
68
|
+
const db = new Adapter(options);
|
|
69
|
+
await createSchemaMigrations(db, config);
|
|
70
|
+
await db.destroy();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const createDb = async (
|
|
74
|
+
arg: MaybeArray<AdapterOptions>,
|
|
75
|
+
config: MigrationConfig,
|
|
76
|
+
) => {
|
|
77
|
+
for (const options of toArray(arg)) {
|
|
78
|
+
await createOrDrop(options, options, config, {
|
|
79
|
+
sql({ database, user }) {
|
|
80
|
+
return `CREATE DATABASE "${database}" OWNER "${user}"`;
|
|
81
|
+
},
|
|
82
|
+
successMessage({ database }) {
|
|
83
|
+
return `Database ${database} successfully created`;
|
|
84
|
+
},
|
|
85
|
+
alreadyMessage({ database }) {
|
|
86
|
+
return `Database ${database} already exists`;
|
|
87
|
+
},
|
|
88
|
+
createVersionsTable: true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const dropDb = async (arg: MaybeArray<AdapterOptions>) => {
|
|
94
|
+
for (const options of toArray(arg)) {
|
|
95
|
+
await createOrDrop(options, options, migrationConfigDefaults, {
|
|
96
|
+
sql({ database }) {
|
|
97
|
+
return `DROP DATABASE "${database}"`;
|
|
98
|
+
},
|
|
99
|
+
successMessage({ database }) {
|
|
100
|
+
return `Database ${database} was successfully dropped`;
|
|
101
|
+
},
|
|
102
|
+
alreadyMessage({ database }) {
|
|
103
|
+
return `Database ${database} does not exist`;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { generate } from './generate';
|
|
2
|
+
import { migrationConfigDefaults } from '../common';
|
|
3
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
jest.mock('fs/promises', () => ({
|
|
7
|
+
mkdir: jest.fn(),
|
|
8
|
+
writeFile: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const logMock = jest.fn();
|
|
12
|
+
console.log = logMock;
|
|
13
|
+
|
|
14
|
+
const migrationsPath = migrationConfigDefaults.migrationsPath;
|
|
15
|
+
|
|
16
|
+
const testGenerate = async (args: string[], content: string) => {
|
|
17
|
+
const name = args[0];
|
|
18
|
+
await generate(migrationConfigDefaults, args);
|
|
19
|
+
|
|
20
|
+
expect(mkdir).toHaveBeenCalledWith(migrationsPath, { recursive: true });
|
|
21
|
+
|
|
22
|
+
const filePath = path.resolve(migrationsPath, `20000101000000_${name}.ts`);
|
|
23
|
+
expect(writeFile).toHaveBeenCalledWith(filePath, content);
|
|
24
|
+
|
|
25
|
+
expect(logMock.mock.calls).toEqual([[`Created ${filePath}`]]);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('generate', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.useFakeTimers().setSystemTime(new Date(2000, 0, 1, 0, 0, 0));
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should throw if migration name is not provided', async () => {
|
|
35
|
+
expect(generate(migrationConfigDefaults, [])).rejects.toThrow(
|
|
36
|
+
'Migration name is missing',
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should create a file for create table migration', async () => {
|
|
41
|
+
await testGenerate(
|
|
42
|
+
['createTable', 'id:integer.primaryKey', 'name:varchar(20).nullable'],
|
|
43
|
+
`import { change } from 'rake-db';
|
|
44
|
+
|
|
45
|
+
change(async (db) => {
|
|
46
|
+
db.createTable('table', (t) => ({
|
|
47
|
+
id: t.integer().primaryKey(),
|
|
48
|
+
name: t.varchar(20).nullable(),
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
`,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should create a file for change migration', async () => {
|
|
56
|
+
await testGenerate(
|
|
57
|
+
['changeTable'],
|
|
58
|
+
`import { change } from 'rake-db';
|
|
59
|
+
|
|
60
|
+
change(async (db) => {
|
|
61
|
+
db.changeTable('table', (t) => ({
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
`,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should create a file for add columns migration', async () => {
|
|
69
|
+
await testGenerate(
|
|
70
|
+
['addColumns'],
|
|
71
|
+
`import { change } from 'rake-db';
|
|
72
|
+
|
|
73
|
+
change(async (db) => {
|
|
74
|
+
db.changeTable(tableName, (t) => ({
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
`,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should create a file for add columns migration with table', async () => {
|
|
82
|
+
await testGenerate(
|
|
83
|
+
[
|
|
84
|
+
'addColumnsToTable',
|
|
85
|
+
'id:integer.primaryKey',
|
|
86
|
+
'name:varchar(20).nullable',
|
|
87
|
+
],
|
|
88
|
+
`import { change } from 'rake-db';
|
|
89
|
+
|
|
90
|
+
change(async (db) => {
|
|
91
|
+
db.changeTable('table', (t) => ({
|
|
92
|
+
id: t.add(t.integer().primaryKey()),
|
|
93
|
+
name: t.add(t.varchar(20).nullable()),
|
|
94
|
+
}));
|
|
95
|
+
});
|
|
96
|
+
`,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should create a file for remove columns migration with table', async () => {
|
|
101
|
+
await testGenerate(
|
|
102
|
+
[
|
|
103
|
+
'removeColumnsFromTable',
|
|
104
|
+
'id:integer.primaryKey',
|
|
105
|
+
'name:varchar(20).nullable',
|
|
106
|
+
],
|
|
107
|
+
`import { change } from 'rake-db';
|
|
108
|
+
|
|
109
|
+
change(async (db) => {
|
|
110
|
+
db.changeTable('table', (t) => ({
|
|
111
|
+
id: t.remove(t.integer().primaryKey()),
|
|
112
|
+
name: t.remove(t.varchar(20).nullable()),
|
|
113
|
+
}));
|
|
114
|
+
});
|
|
115
|
+
`,
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should create a file for drop table migration', async () => {
|
|
120
|
+
await testGenerate(
|
|
121
|
+
['dropTable', 'id:integer.primaryKey', 'name:varchar(20).nullable'],
|
|
122
|
+
`import { change } from 'rake-db';
|
|
123
|
+
|
|
124
|
+
change(async (db) => {
|
|
125
|
+
db.dropTable('table', (t) => ({
|
|
126
|
+
id: t.integer().primaryKey(),
|
|
127
|
+
name: t.varchar(20).nullable(),
|
|
128
|
+
}));
|
|
129
|
+
});
|
|
130
|
+
`,
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getFirstWordAndRest,
|
|
3
|
+
getTextAfterFrom,
|
|
4
|
+
getTextAfterTo,
|
|
5
|
+
MigrationConfig,
|
|
6
|
+
} from '../common';
|
|
7
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
export const generate = async (config: MigrationConfig, 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 });
|
|
15
|
+
|
|
16
|
+
const filePath = path.resolve(
|
|
17
|
+
config.migrationsPath,
|
|
18
|
+
`${makeFileTimeStamp()}_${name}.ts`,
|
|
19
|
+
);
|
|
20
|
+
await writeFile(filePath, makeContent(name, args.slice(1)));
|
|
21
|
+
console.log(`Created ${filePath}`);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const makeFileTimeStamp = () => {
|
|
25
|
+
const now = new Date();
|
|
26
|
+
return [
|
|
27
|
+
now.getUTCFullYear(),
|
|
28
|
+
now.getUTCMonth() + 1,
|
|
29
|
+
now.getUTCDate(),
|
|
30
|
+
now.getUTCHours(),
|
|
31
|
+
now.getUTCMinutes(),
|
|
32
|
+
now.getUTCSeconds(),
|
|
33
|
+
]
|
|
34
|
+
.map((value) => (value < 10 ? `0${value}` : value))
|
|
35
|
+
.join('');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const makeContent = (name: string, args: string[]): string => {
|
|
39
|
+
let content = `import { change } from 'rake-db';\n\nchange(async (db) => {`;
|
|
40
|
+
|
|
41
|
+
const [first, rest] = getFirstWordAndRest(name);
|
|
42
|
+
if (rest) {
|
|
43
|
+
if (first === 'create' || first === 'drop') {
|
|
44
|
+
content += `\n db.${
|
|
45
|
+
first === 'create' ? 'createTable' : 'dropTable'
|
|
46
|
+
}('${rest}', (t) => ({`;
|
|
47
|
+
content += makeColumnsContent(args);
|
|
48
|
+
content += '\n }));';
|
|
49
|
+
} else if (first === 'change') {
|
|
50
|
+
content += `\n db.changeTable('${rest}', (t) => ({`;
|
|
51
|
+
content += '\n }));';
|
|
52
|
+
} else if (first === 'add' || first === 'remove') {
|
|
53
|
+
const table =
|
|
54
|
+
first === 'add' ? getTextAfterTo(rest) : getTextAfterFrom(rest);
|
|
55
|
+
content += `\n db.changeTable(${
|
|
56
|
+
table ? `'${table}'` : 'tableName'
|
|
57
|
+
}, (t) => ({`;
|
|
58
|
+
content += makeColumnsContent(args, first);
|
|
59
|
+
content += '\n }));';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return content + '\n});\n';
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const makeColumnsContent = (args: string[], method?: string) => {
|
|
67
|
+
let content = '';
|
|
68
|
+
const prepend = method ? `t.${method}(` : '';
|
|
69
|
+
const append = method ? ')' : '';
|
|
70
|
+
|
|
71
|
+
for (const arg of args) {
|
|
72
|
+
const [name, def] = arg.split(':');
|
|
73
|
+
if (!def) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Column argument should be similar to name:type, name:type.method1.method2, name:type(arg).method(arg). Example: name:varchar(20).nullable. Received: ${arg}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const methods = def
|
|
80
|
+
.split('.')
|
|
81
|
+
.map((method) => (method.endsWith(')') ? `.${method}` : `.${method}()`));
|
|
82
|
+
content += `\n ${name}: ${prepend}t${methods.join('')}${append},`;
|
|
83
|
+
}
|
|
84
|
+
return content;
|
|
85
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { migrate, rollback } from './migrateOrRollback';
|
|
2
|
+
import { createSchemaMigrations, migrationConfigDefaults } from '../common';
|
|
3
|
+
import { getMigrationFiles } from '../common';
|
|
4
|
+
import { Adapter, TransactionAdapter } from 'pqb';
|
|
5
|
+
import { Migration } from '../migration/migration';
|
|
6
|
+
|
|
7
|
+
jest.mock('../common', () => ({
|
|
8
|
+
...jest.requireActual('../common'),
|
|
9
|
+
getMigrationFiles: jest.fn(),
|
|
10
|
+
createSchemaMigrations: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const options = { connectionString: 'postgres://user@localhost/dbname' };
|
|
14
|
+
|
|
15
|
+
const files = [
|
|
16
|
+
{ path: 'file1', version: '1' },
|
|
17
|
+
{ path: 'file2', version: '2' },
|
|
18
|
+
{ path: 'file3', version: '3' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const getMigratedVersionsArrayMock = jest.fn();
|
|
22
|
+
Adapter.prototype.arrays = getMigratedVersionsArrayMock;
|
|
23
|
+
|
|
24
|
+
const queryMock = jest.fn();
|
|
25
|
+
Adapter.prototype.query = queryMock;
|
|
26
|
+
|
|
27
|
+
Adapter.prototype.transaction = (cb) => {
|
|
28
|
+
return cb({} as unknown as TransactionAdapter);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const transactionQueryMock = jest.fn();
|
|
32
|
+
Migration.prototype.query = transactionQueryMock;
|
|
33
|
+
|
|
34
|
+
const requireTsMock = jest.fn();
|
|
35
|
+
const config = {
|
|
36
|
+
...migrationConfigDefaults,
|
|
37
|
+
requireTs: requireTsMock,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
describe('migrateOrRollback', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('migrate', () => {
|
|
46
|
+
it('should work properly', async () => {
|
|
47
|
+
(getMigrationFiles as jest.Mock).mockReturnValueOnce(files);
|
|
48
|
+
getMigratedVersionsArrayMock.mockResolvedValueOnce({ rows: [['1']] });
|
|
49
|
+
queryMock.mockReturnValueOnce(undefined);
|
|
50
|
+
requireTsMock.mockResolvedValue(undefined);
|
|
51
|
+
|
|
52
|
+
await migrate(options, config);
|
|
53
|
+
|
|
54
|
+
expect(getMigrationFiles).toBeCalledWith(config, true);
|
|
55
|
+
|
|
56
|
+
expect(requireTsMock).toBeCalledWith('file2');
|
|
57
|
+
expect(requireTsMock).toBeCalledWith('file3');
|
|
58
|
+
|
|
59
|
+
expect(transactionQueryMock).toBeCalledWith(
|
|
60
|
+
`INSERT INTO "schemaMigrations" VALUES ('2')`,
|
|
61
|
+
);
|
|
62
|
+
expect(transactionQueryMock).toBeCalledWith(
|
|
63
|
+
`INSERT INTO "schemaMigrations" VALUES ('3')`,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should create migrations table if it not exist', async () => {
|
|
68
|
+
(getMigrationFiles as jest.Mock).mockReturnValueOnce([]);
|
|
69
|
+
getMigratedVersionsArrayMock.mockRejectedValueOnce({ code: '42P01' });
|
|
70
|
+
(createSchemaMigrations as jest.Mock).mockResolvedValueOnce(undefined);
|
|
71
|
+
|
|
72
|
+
await migrate(options, config);
|
|
73
|
+
|
|
74
|
+
expect(getMigrationFiles).toBeCalledWith(config, true);
|
|
75
|
+
expect(createSchemaMigrations).toBeCalled();
|
|
76
|
+
expect(requireTsMock).not.toBeCalled();
|
|
77
|
+
expect(transactionQueryMock).not.toBeCalled();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('rollback', () => {
|
|
82
|
+
it('should work properly', async () => {
|
|
83
|
+
(getMigrationFiles as jest.Mock).mockReturnValueOnce(files.reverse());
|
|
84
|
+
getMigratedVersionsArrayMock.mockResolvedValueOnce({
|
|
85
|
+
rows: [['1'], ['2']],
|
|
86
|
+
});
|
|
87
|
+
queryMock.mockReturnValueOnce(undefined);
|
|
88
|
+
requireTsMock.mockResolvedValue(undefined);
|
|
89
|
+
|
|
90
|
+
await rollback(options, config);
|
|
91
|
+
|
|
92
|
+
expect(getMigrationFiles).toBeCalledWith(config, false);
|
|
93
|
+
|
|
94
|
+
expect(requireTsMock).toBeCalledWith('file2');
|
|
95
|
+
expect(requireTsMock).toBeCalledWith('file1');
|
|
96
|
+
|
|
97
|
+
expect(transactionQueryMock).toBeCalledWith(
|
|
98
|
+
`DELETE FROM "schemaMigrations" WHERE version = '2'`,
|
|
99
|
+
);
|
|
100
|
+
expect(transactionQueryMock).toBeCalledWith(
|
|
101
|
+
`DELETE FROM "schemaMigrations" WHERE version = '1'`,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should create migrations table if it not exist', async () => {
|
|
106
|
+
(getMigrationFiles as jest.Mock).mockReturnValueOnce([]);
|
|
107
|
+
getMigratedVersionsArrayMock.mockRejectedValueOnce({ code: '42P01' });
|
|
108
|
+
(createSchemaMigrations as jest.Mock).mockResolvedValueOnce(undefined);
|
|
109
|
+
|
|
110
|
+
await rollback(options, config);
|
|
111
|
+
|
|
112
|
+
expect(getMigrationFiles).toBeCalledWith(config, false);
|
|
113
|
+
expect(createSchemaMigrations).toBeCalled();
|
|
114
|
+
expect(requireTsMock).not.toBeCalled();
|
|
115
|
+
expect(transactionQueryMock).not.toBeCalled();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Adapter, AdapterOptions, MaybeArray, toArray } from 'pqb';
|
|
2
|
+
import {
|
|
3
|
+
createSchemaMigrations,
|
|
4
|
+
getMigrationFiles,
|
|
5
|
+
MigrationConfig,
|
|
6
|
+
MigrationFile,
|
|
7
|
+
} from '../common';
|
|
8
|
+
import {
|
|
9
|
+
getCurrentPromise,
|
|
10
|
+
setCurrentMigrationUp,
|
|
11
|
+
setCurrentMigration,
|
|
12
|
+
} from '../migration/change';
|
|
13
|
+
import { Migration } from '../migration/migration';
|
|
14
|
+
|
|
15
|
+
const migrateOrRollback = async (
|
|
16
|
+
options: MaybeArray<AdapterOptions>,
|
|
17
|
+
config: MigrationConfig,
|
|
18
|
+
up: boolean,
|
|
19
|
+
) => {
|
|
20
|
+
const files = await getMigrationFiles(config, up);
|
|
21
|
+
|
|
22
|
+
for (const opts of toArray(options)) {
|
|
23
|
+
const db = new Adapter(opts);
|
|
24
|
+
const migratedVersions = await getMigratedVersionsMap(db, config);
|
|
25
|
+
try {
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
if (
|
|
28
|
+
(up && migratedVersions[file.version]) ||
|
|
29
|
+
(!up && !migratedVersions[file.version])
|
|
30
|
+
) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await processMigration(db, up, file, config);
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
await db.destroy();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const processMigration = async (
|
|
43
|
+
db: Adapter,
|
|
44
|
+
up: boolean,
|
|
45
|
+
file: MigrationFile,
|
|
46
|
+
config: MigrationConfig,
|
|
47
|
+
) => {
|
|
48
|
+
await db.transaction(async (tx) => {
|
|
49
|
+
const db = new Migration(tx, up);
|
|
50
|
+
setCurrentMigration(db);
|
|
51
|
+
setCurrentMigrationUp(up);
|
|
52
|
+
config.requireTs(file.path);
|
|
53
|
+
await getCurrentPromise();
|
|
54
|
+
await (up ? saveMigratedVersion : removeMigratedVersion)(
|
|
55
|
+
db,
|
|
56
|
+
file.version,
|
|
57
|
+
config,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const saveMigratedVersion = async (
|
|
63
|
+
db: Adapter,
|
|
64
|
+
version: string,
|
|
65
|
+
config: MigrationConfig,
|
|
66
|
+
) => {
|
|
67
|
+
await db.query(
|
|
68
|
+
`INSERT INTO "${config.migrationsTable}" VALUES ('${version}')`,
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const removeMigratedVersion = async (
|
|
73
|
+
db: Adapter,
|
|
74
|
+
version: string,
|
|
75
|
+
config: MigrationConfig,
|
|
76
|
+
) => {
|
|
77
|
+
await db.query(
|
|
78
|
+
`DELETE FROM "${config.migrationsTable}" WHERE version = '${version}'`,
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const getMigratedVersionsMap = async (
|
|
83
|
+
db: Adapter,
|
|
84
|
+
config: MigrationConfig,
|
|
85
|
+
): Promise<Record<string, boolean>> => {
|
|
86
|
+
try {
|
|
87
|
+
const result = await db.arrays<[string]>(
|
|
88
|
+
`SELECT * FROM "${config.migrationsTable}"`,
|
|
89
|
+
);
|
|
90
|
+
return Object.fromEntries(result.rows.map((row) => [row[0], true]));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if ((err as Record<string, unknown>).code === '42P01') {
|
|
93
|
+
await createSchemaMigrations(db, config);
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const migrate = (
|
|
101
|
+
options: MaybeArray<AdapterOptions>,
|
|
102
|
+
config: MigrationConfig,
|
|
103
|
+
) => migrateOrRollback(options, config, true);
|
|
104
|
+
|
|
105
|
+
export const rollback = (
|
|
106
|
+
options: MaybeArray<AdapterOptions>,
|
|
107
|
+
config: MigrationConfig,
|
|
108
|
+
) => migrateOrRollback(options, config, false);
|