rake-db 2.3.22 → 2.3.24

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/jest-setup.ts CHANGED
@@ -1,3 +1,3 @@
1
- jest.mock('pqb', () => require('../pqb/src'), {
1
+ jest.mock('pqb', () => require('../qb/pqb/src'), {
2
2
  virtual: true,
3
3
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rake-db",
3
- "version": "2.3.22",
3
+ "version": "2.3.24",
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": {
@@ -40,9 +40,9 @@
40
40
  "author": "Roman Kushyn",
41
41
  "license": "ISC",
42
42
  "dependencies": {
43
- "enquirer": "^2.3.6",
44
43
  "pluralize": "^8.0.0",
45
- "pqb": "0.9.12"
44
+ "pqb": "0.9.13",
45
+ "prompts": "^2.4.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@swc/core": "^1.2.210",
@@ -51,6 +51,7 @@
51
51
  "@types/jest": "^28.1.2",
52
52
  "@types/node": "^18.0.1",
53
53
  "@types/pluralize": "^0.0.29",
54
+ "@types/prompts": "^2.4.2",
54
55
  "dotenv": "^16.0.3",
55
56
  "jest": "^28.1.2",
56
57
  "rimraf": "^3.0.2",
@@ -65,6 +66,7 @@
65
66
  "scripts": {
66
67
  "db": "ts-node -r tsconfig-paths/register app/dbScript.ts",
67
68
  "test": "jest",
69
+ "test:ci": "jest --coverage --coverageReporters json-summary",
68
70
  "build": "rimraf ./dist/ && rollup -c --rollup.config"
69
71
  }
70
72
  }
@@ -28,6 +28,11 @@ Adapter.prototype.query = queryMock;
28
28
  const logMock = jest.fn();
29
29
  console.log = logMock;
30
30
 
31
+ const config = {
32
+ ...migrationConfigDefaults,
33
+ basePath: __dirname,
34
+ };
35
+
31
36
  describe('createOrDrop', () => {
32
37
  beforeEach(() => {
33
38
  jest.clearAllMocks();
@@ -37,7 +42,7 @@ describe('createOrDrop', () => {
37
42
  it('should create database when user is an admin', async () => {
38
43
  queryMock.mockResolvedValueOnce(undefined);
39
44
 
40
- await createDb(options, migrationConfigDefaults);
45
+ await createDb(options, config);
41
46
 
42
47
  expect(queryMock.mock.calls).toEqual([
43
48
  [`CREATE DATABASE "dbname" OWNER "user"`],
@@ -53,7 +58,7 @@ describe('createOrDrop', () => {
53
58
 
54
59
  await createDb(
55
60
  [options, { ...options, database: 'dbname-test' }],
56
- migrationConfigDefaults,
61
+ config,
57
62
  );
58
63
 
59
64
  expect(queryMock.mock.calls).toEqual([
@@ -70,7 +75,7 @@ describe('createOrDrop', () => {
70
75
  it('should inform if database already exists', async () => {
71
76
  queryMock.mockRejectedValueOnce({ code: '42P04' });
72
77
 
73
- await createDb(options, migrationConfigDefaults);
78
+ await createDb(options, config);
74
79
 
75
80
  expect(queryMock.mock.calls).toEqual([
76
81
  [`CREATE DATABASE "dbname" OWNER "user"`],
@@ -79,10 +84,27 @@ describe('createOrDrop', () => {
79
84
  expect(createSchemaMigrations).toHaveBeenCalled();
80
85
  });
81
86
 
87
+ it('should inform if ssl is required', async () => {
88
+ queryMock.mockRejectedValueOnce({
89
+ code: 'XX000',
90
+ message: 'sslmode=require',
91
+ });
92
+
93
+ await createDb(options, config);
94
+
95
+ expect(queryMock.mock.calls).toEqual([
96
+ [`CREATE DATABASE "dbname" OWNER "user"`],
97
+ ]);
98
+ expect(logMock.mock.calls).toEqual([
99
+ ['SSL is required: append ?ssl=true to the database url string'],
100
+ ]);
101
+ expect(createSchemaMigrations).not.toHaveBeenCalled();
102
+ });
103
+
82
104
  it('should ask and use admin credentials when cannot connect', async () => {
83
105
  queryMock.mockRejectedValueOnce({ code: '42501' });
84
106
 
85
- await createDb(options, migrationConfigDefaults);
107
+ await createDb(options, config);
86
108
 
87
109
  expect(setAdminCredentialsToOptions).toHaveBeenCalled();
88
110
  expect(queryMock.mock.calls).toEqual([
@@ -90,6 +112,9 @@ describe('createOrDrop', () => {
90
112
  [`CREATE DATABASE "dbname" OWNER "user"`],
91
113
  ]);
92
114
  expect(logMock.mock.calls).toEqual([
115
+ [
116
+ `Permission denied to create database.\nDon't use this command for database service providers, only for a local db.`,
117
+ ],
93
118
  [`Database dbname successfully created`],
94
119
  ]);
95
120
  expect(createSchemaMigrations).toHaveBeenCalled();
@@ -132,6 +157,23 @@ describe('createOrDrop', () => {
132
157
  expect(logMock.mock.calls).toEqual([[`Database dbname does not exist`]]);
133
158
  });
134
159
 
160
+ it('should inform if ssl is required', async () => {
161
+ queryMock.mockRejectedValueOnce({
162
+ code: 'XX000',
163
+ message: 'sslmode=require',
164
+ });
165
+
166
+ await createDb(options, config);
167
+
168
+ expect(queryMock.mock.calls).toEqual([
169
+ [`CREATE DATABASE "dbname" OWNER "user"`],
170
+ ]);
171
+ expect(logMock.mock.calls).toEqual([
172
+ ['SSL is required: append ?ssl=true to the database url string'],
173
+ ]);
174
+ expect(createSchemaMigrations).not.toHaveBeenCalled();
175
+ });
176
+
135
177
  it('should ask and use admin credentials when cannot connect', async () => {
136
178
  queryMock.mockRejectedValueOnce({ code: '42501' });
137
179
 
@@ -143,6 +185,9 @@ describe('createOrDrop', () => {
143
185
  [`DROP DATABASE "dbname"`],
144
186
  ]);
145
187
  expect(logMock.mock.calls).toEqual([
188
+ [
189
+ `Permission denied to drop database.\nDon't use this command for database service providers, only for a local db.`,
190
+ ],
146
191
  [`Database dbname was successfully dropped`],
147
192
  ]);
148
193
  });
@@ -152,7 +197,7 @@ describe('createOrDrop', () => {
152
197
  it('should drop and create database', async () => {
153
198
  queryMock.mockResolvedValue(undefined);
154
199
 
155
- await resetDb(options, migrationConfigDefaults);
200
+ await resetDb(options, config);
156
201
 
157
202
  expect(queryMock.mock.calls).toEqual([
158
203
  [`DROP DATABASE "dbname"`],
@@ -12,16 +12,31 @@ import { migrate } from './migrateOrRollback';
12
12
  const execute = async (
13
13
  options: AdapterOptions,
14
14
  sql: string,
15
- ): Promise<'ok' | 'already' | 'forbidden' | { error: unknown }> => {
15
+ ): Promise<
16
+ 'ok' | 'already' | 'forbidden' | 'ssl required' | { error: unknown }
17
+ > => {
16
18
  const db = new Adapter(options);
19
+
17
20
  try {
18
21
  await db.query(sql);
19
22
  return 'ok';
20
23
  } catch (error) {
21
24
  const err = error as Record<string, unknown>;
25
+
26
+ if (
27
+ typeof err.message === 'string' &&
28
+ err.message.includes('sslmode=require')
29
+ ) {
30
+ return 'ssl required';
31
+ }
32
+
22
33
  if (err.code === '42P04' || err.code === '3D000') {
23
34
  return 'already';
24
- } else if (err.code === '42501') {
35
+ } else if (
36
+ err.code === '42501' ||
37
+ (typeof err.message === 'string' &&
38
+ err.message.includes('password authentication failed'))
39
+ ) {
25
40
  return 'forbidden';
26
41
  } else {
27
42
  return { error };
@@ -39,7 +54,7 @@ const createOrDrop = async (
39
54
  sql(params: { database: string; user: string }): string;
40
55
  successMessage(params: { database: string }): string;
41
56
  alreadyMessage(params: { database: string }): string;
42
- createVersionsTable?: boolean;
57
+ create?: boolean;
43
58
  },
44
59
  ) => {
45
60
  const params = getDatabaseAndUserFromOptions(options);
@@ -52,19 +67,38 @@ const createOrDrop = async (
52
67
  console.log(args.successMessage(params));
53
68
  } else if (result === 'already') {
54
69
  console.log(args.alreadyMessage(params));
70
+ } else if (result === 'ssl required') {
71
+ console.log('SSL is required: append ?ssl=true to the database url string');
72
+ return;
55
73
  } else if (result === 'forbidden') {
56
- await createOrDrop(
74
+ let message = `Permission denied to ${
75
+ args.create ? 'create' : 'drop'
76
+ } database.`;
77
+
78
+ const host = adminOptions.databaseURL
79
+ ? new URL(adminOptions.databaseURL).hostname
80
+ : adminOptions.host;
81
+
82
+ const isLocal = host === 'localhost';
83
+ if (!isLocal) {
84
+ message += `\nDon't use this command for database service providers, only for a local db.`;
85
+ }
86
+
87
+ console.log(message);
88
+
89
+ const updatedOptions = await setAdminCredentialsToOptions(
57
90
  options,
58
- await setAdminCredentialsToOptions(options),
59
- config,
60
- args,
91
+ args.create,
61
92
  );
93
+ if (!updatedOptions) return;
94
+
95
+ await createOrDrop(options, updatedOptions, config, args);
62
96
  return;
63
97
  } else {
64
98
  throw result.error;
65
99
  }
66
100
 
67
- if (!args.createVersionsTable) return;
101
+ if (!args.create) return;
68
102
 
69
103
  const db = new Adapter(options);
70
104
  await createSchemaMigrations(db, config);
@@ -86,7 +120,7 @@ export const createDb = async (
86
120
  alreadyMessage({ database }) {
87
121
  return `Database ${database} already exists`;
88
122
  },
89
- createVersionsTable: true,
123
+ create: true,
90
124
  });
91
125
  }
92
126
  };
@@ -14,22 +14,13 @@ import {
14
14
  sortDesc,
15
15
  migrationConfigDefaults,
16
16
  } from './common';
17
- import Enquirer from 'enquirer';
17
+ import prompts from 'prompts';
18
18
  import { Adapter } from 'pqb';
19
19
  import { readdir } from 'fs/promises';
20
20
  import path from 'path';
21
+ import { asMock } from './test-utils';
21
22
 
22
- jest.mock('enquirer', () => {
23
- class Snippet {
24
- constructor(public params: Record<string, unknown>) {}
25
- run() {}
26
- }
27
- Snippet.prototype.run = jest.fn();
28
-
29
- return {
30
- Snippet,
31
- };
32
- });
23
+ jest.mock('prompts', () => jest.fn());
33
24
 
34
25
  jest.mock('fs/promises', () => ({
35
26
  readdir: jest.fn(),
@@ -41,6 +32,7 @@ describe('common', () => {
41
32
  describe('processRakeDbConfig', () => {
42
33
  it('should return config with defaults', () => {
43
34
  const result = processRakeDbConfig({
35
+ basePath: __dirname,
44
36
  migrationsPath: 'custom-path',
45
37
  });
46
38
 
@@ -125,11 +117,13 @@ describe('common', () => {
125
117
 
126
118
  describe('setAdminCredentialsToOptions', () => {
127
119
  beforeEach(() => {
128
- (Enquirer as any).Snippet.prototype.run.mockReturnValueOnce({
129
- values: {
130
- user: 'admin-user',
131
- password: 'admin-password',
132
- },
120
+ asMock(prompts).mockResolvedValueOnce({
121
+ confirm: true,
122
+ });
123
+
124
+ asMock(prompts).mockResolvedValueOnce({
125
+ user: 'admin-user',
126
+ password: 'admin-password',
133
127
  });
134
128
  });
135
129
 
package/src/common.ts CHANGED
@@ -7,10 +7,10 @@ import {
7
7
  QueryLogOptions,
8
8
  singleQuote,
9
9
  } from 'pqb';
10
- import Enquirer from 'enquirer';
11
10
  import path from 'path';
12
11
  import { readdir } from 'fs/promises';
13
12
  import { RakeDbAst } from './ast';
13
+ import prompts from 'prompts';
14
14
 
15
15
  type Db = DbResult<DefaultColumnTypes>;
16
16
 
@@ -152,40 +152,44 @@ export const setAdapterOptions = (
152
152
  }
153
153
  };
154
154
 
155
- const askAdminCredentials = async (): Promise<{
156
- user: string;
157
- password: string;
158
- }> => {
159
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
- const prompt = new (Enquirer as any).Snippet({
161
- message: `What are postgres admin login and password?`,
162
- fields: [
163
- {
164
- name: 'user',
165
- required: true,
166
- },
167
- {
168
- name: 'password',
169
- },
170
- ],
171
- values: {
172
- user: 'postgres',
173
- password: '',
155
+ export const setAdminCredentialsToOptions = async (
156
+ options: AdapterOptions,
157
+ create?: boolean,
158
+ ): Promise<AdapterOptions | undefined> => {
159
+ const confirm = await prompts([
160
+ {
161
+ message: `Would you like to share admin credentials to ${
162
+ create ? 'create' : 'drop'
163
+ } a database`,
164
+ type: 'confirm',
165
+ name: 'confirm',
166
+ initial: true,
174
167
  },
175
- template: 'Admin user: {{user}}\nAdmin password: {{password}}',
176
- });
168
+ ]);
177
169
 
178
- const { values } = await prompt.run();
179
- if (!values.password) values.password = '';
170
+ if (!confirm.confirm) {
171
+ return;
172
+ }
180
173
 
181
- return values;
182
- };
174
+ const values = await prompts([
175
+ {
176
+ message: 'Enter admin user:',
177
+ type: 'text',
178
+ name: 'user',
179
+ initial: 'postgres',
180
+ min: 1,
181
+ },
182
+ {
183
+ message: 'Enter admin password:',
184
+ type: 'password',
185
+ name: 'password',
186
+ },
187
+ ]);
183
188
 
184
- export const setAdminCredentialsToOptions = async (
185
- options: AdapterOptions,
186
- ): Promise<AdapterOptions> => {
187
- const values = await askAdminCredentials();
188
- return setAdapterOptions(options, values);
189
+ return setAdapterOptions(options, {
190
+ ...values,
191
+ password: values.password || undefined,
192
+ });
189
193
  };
190
194
 
191
195
  export const createSchemaMigrations = async (
package/tsconfig.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "noEmit": false,
7
7
  "baseUrl": ".",
8
8
  "paths": {
9
- "pqb": ["../pqb/src"]
9
+ "pqb": ["../qb/pqb/src"]
10
10
  }
11
11
  }
12
12
  }