neonctl 1.3.0 → 1.4.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/README.md +126 -0
- package/commands/auth.js +4 -0
- package/commands/branches.js +40 -41
- package/commands/branches.test.js +85 -0
- package/commands/databases.js +54 -0
- package/commands/databases.test.js +52 -0
- package/commands/endpoints.js +86 -0
- package/commands/endpoints.test.js +85 -0
- package/commands/index.js +3 -1
- package/commands/projects.js +26 -18
- package/commands/projects.test.js +46 -0
- package/commands/users.js +3 -2
- package/config.js +2 -1
- package/env.js +3 -0
- package/index.js +12 -1
- package/package.json +10 -3
- package/parameters.gen.js +123 -0
- package/test_utils.js +75 -0
- package/utils.js +9 -0
- package/writer.js +59 -50
- package/writer.test.js +86 -0
package/commands/projects.js
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
import yargs from 'yargs';
|
|
2
|
-
import { hideBin } from 'yargs/helpers';
|
|
3
1
|
import { projectCreateRequest } from '../parameters.gen.js';
|
|
4
|
-
import {
|
|
2
|
+
import { commandFailHandler } from '../utils.js';
|
|
3
|
+
import { writer } from '../writer.js';
|
|
5
4
|
const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'];
|
|
6
5
|
export const command = 'projects [command]';
|
|
7
6
|
export const describe = 'Manage projects';
|
|
8
7
|
export const builder = (argv) => {
|
|
9
8
|
return argv
|
|
10
9
|
.demandCommand(1, '')
|
|
11
|
-
.fail(
|
|
12
|
-
const y = yargs(hideBin(process.argv));
|
|
13
|
-
if (y.argv._.length === 1) {
|
|
14
|
-
yyargs.showHelp();
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
})
|
|
10
|
+
.fail(commandFailHandler)
|
|
18
11
|
.usage('usage: $0 projects <cmd> [args]')
|
|
19
12
|
.command('list', 'List projects', (yargs) => yargs, async (args) => {
|
|
20
13
|
await list(args);
|
|
@@ -39,15 +32,23 @@ export const builder = (argv) => {
|
|
|
39
32
|
},
|
|
40
33
|
}), async (args) => {
|
|
41
34
|
await deleteProject(args);
|
|
35
|
+
})
|
|
36
|
+
.command('get', 'Get a project', (yargs) => yargs.options({
|
|
37
|
+
'project.id': {
|
|
38
|
+
describe: 'Project ID',
|
|
39
|
+
type: 'string',
|
|
40
|
+
demandOption: true,
|
|
41
|
+
},
|
|
42
|
+
}), async (args) => {
|
|
43
|
+
await get(args);
|
|
42
44
|
});
|
|
43
45
|
};
|
|
44
46
|
export const handler = (args) => {
|
|
45
47
|
return args;
|
|
46
48
|
};
|
|
47
49
|
const list = async (props) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
50
|
+
const { data } = await props.apiClient.listProjects({});
|
|
51
|
+
writer(props).end(data.projects, { fields: PROJECT_FIELDS });
|
|
51
52
|
};
|
|
52
53
|
const create = async (props) => {
|
|
53
54
|
if (props.project == null) {
|
|
@@ -60,17 +61,24 @@ const create = async (props) => {
|
|
|
60
61
|
props.project = answers;
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
+
const { data } = await props.apiClient.createProject({
|
|
64
65
|
project: props.project,
|
|
65
|
-
})
|
|
66
|
+
});
|
|
67
|
+
writer(props).end(data.project, { fields: PROJECT_FIELDS });
|
|
66
68
|
};
|
|
67
69
|
const deleteProject = async (props) => {
|
|
68
|
-
|
|
70
|
+
const { data } = await props.apiClient.deleteProject(props.project.id);
|
|
71
|
+
writer(props).end(data.project, {
|
|
69
72
|
fields: PROJECT_FIELDS,
|
|
70
73
|
});
|
|
71
74
|
};
|
|
72
75
|
const update = async (props) => {
|
|
73
|
-
|
|
76
|
+
const { data } = await props.apiClient.updateProject(props.project.id, {
|
|
74
77
|
project: props.project,
|
|
75
|
-
})
|
|
78
|
+
});
|
|
79
|
+
writer(props).end(data.project, { fields: PROJECT_FIELDS });
|
|
80
|
+
};
|
|
81
|
+
const get = async (props) => {
|
|
82
|
+
const { data } = await props.apiClient.getProject(props.project.id);
|
|
83
|
+
writer(props).end(data.project, { fields: PROJECT_FIELDS });
|
|
76
84
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe } from '@jest/globals';
|
|
2
|
+
import { testCliCommand } from '../test_utils.js';
|
|
3
|
+
describe('projects', () => {
|
|
4
|
+
testCliCommand({
|
|
5
|
+
name: 'list',
|
|
6
|
+
args: ['projects', 'list'],
|
|
7
|
+
expected: {
|
|
8
|
+
snapshot: true,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
testCliCommand({
|
|
12
|
+
name: 'create',
|
|
13
|
+
args: ['projects', 'create', '--project.name', 'test_project'],
|
|
14
|
+
expected: {
|
|
15
|
+
snapshot: true,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
testCliCommand({
|
|
19
|
+
name: 'delete',
|
|
20
|
+
args: ['projects', 'delete', '--project.id', 'test'],
|
|
21
|
+
expected: {
|
|
22
|
+
snapshot: true,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
testCliCommand({
|
|
26
|
+
name: 'update',
|
|
27
|
+
args: [
|
|
28
|
+
'projects',
|
|
29
|
+
'update',
|
|
30
|
+
'--project.id',
|
|
31
|
+
'test',
|
|
32
|
+
'--project.name',
|
|
33
|
+
'test_project',
|
|
34
|
+
],
|
|
35
|
+
expected: {
|
|
36
|
+
snapshot: true,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
testCliCommand({
|
|
40
|
+
name: 'get',
|
|
41
|
+
args: ['projects', 'get', '--project.id', 'test'],
|
|
42
|
+
expected: {
|
|
43
|
+
snapshot: true,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
});
|
package/commands/users.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writer } from '../writer.js';
|
|
2
2
|
export const command = 'me';
|
|
3
3
|
export const describe = 'Show current user';
|
|
4
4
|
export const builder = (yargs) => yargs;
|
|
@@ -6,7 +6,8 @@ export const handler = async (args) => {
|
|
|
6
6
|
await me(args);
|
|
7
7
|
};
|
|
8
8
|
const me = async (props) => {
|
|
9
|
-
|
|
9
|
+
const { data } = await props.apiClient.getCurrentUserInfo();
|
|
10
|
+
writer(props).end(data, {
|
|
10
11
|
fields: ['login', 'email', 'name', 'projects_limit'],
|
|
11
12
|
});
|
|
12
13
|
};
|
package/config.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { isCi } from './env.js';
|
|
4
5
|
export const defaultDir = join(homedir(), process.env.XDG_CONFIG_HOME || '.config', 'neonctl');
|
|
5
6
|
export const ensureConfigDir = async ({ 'config-dir': configDir, }) => {
|
|
6
|
-
if (!existsSync(configDir)) {
|
|
7
|
+
if (!existsSync(configDir) && !isCi()) {
|
|
7
8
|
mkdirSync(configDir);
|
|
8
9
|
}
|
|
9
10
|
};
|
package/env.js
ADDED
package/index.js
CHANGED
|
@@ -2,7 +2,18 @@ import yargs from 'yargs';
|
|
|
2
2
|
import { hideBin } from 'yargs/helpers';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
|
-
import 'axios-debug-log';
|
|
5
|
+
import axiosDebug from 'axios-debug-log';
|
|
6
|
+
axiosDebug({
|
|
7
|
+
request(debug, config) {
|
|
8
|
+
debug(`${config.method?.toUpperCase()} ${config.url}`);
|
|
9
|
+
},
|
|
10
|
+
response(debug, response) {
|
|
11
|
+
debug(`${response.status} ${response.statusText}`);
|
|
12
|
+
},
|
|
13
|
+
error(debug, error) {
|
|
14
|
+
debug(error);
|
|
15
|
+
},
|
|
16
|
+
});
|
|
6
17
|
import { ensureAuth } from './commands/auth.js';
|
|
7
18
|
import { defaultDir, ensureConfigDir } from './config.js';
|
|
8
19
|
import { log } from './log.js';
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git@github.com:neondatabase/neonctl.git"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.4.0",
|
|
9
9
|
"description": "CLI tool for NeonDB Cloud management",
|
|
10
10
|
"main": "index.js",
|
|
11
11
|
"author": "NeonDB",
|
|
@@ -19,19 +19,25 @@
|
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@apidevtools/swagger-parser": "^10.1.0",
|
|
22
|
+
"@jest/globals": "^29.5.0",
|
|
22
23
|
"@semantic-release/exec": "^6.0.3",
|
|
23
24
|
"@semantic-release/git": "^10.0.1",
|
|
24
25
|
"@types/cli-table": "^0.3.0",
|
|
26
|
+
"@types/express": "^4.17.17",
|
|
25
27
|
"@types/inquirer": "^9.0.3",
|
|
26
28
|
"@types/node": "^18.7.13",
|
|
27
29
|
"@types/yargs": "^17.0.24",
|
|
28
30
|
"@typescript-eslint/eslint-plugin": "^5.34.0",
|
|
29
31
|
"@typescript-eslint/parser": "^5.34.0",
|
|
32
|
+
"emocks": "^3.0.1",
|
|
30
33
|
"eslint": "^8.22.0",
|
|
34
|
+
"express": "^4.18.2",
|
|
31
35
|
"husky": "^8.0.1",
|
|
36
|
+
"jest": "^29.5.0",
|
|
32
37
|
"lint-staged": "^13.0.3",
|
|
33
38
|
"prettier": "^2.7.1",
|
|
34
39
|
"semantic-release": "^21.0.2",
|
|
40
|
+
"ts-jest": "^29.1.0",
|
|
35
41
|
"ts-node": "^10.9.1",
|
|
36
42
|
"typescript": "^4.7.4"
|
|
37
43
|
},
|
|
@@ -54,10 +60,11 @@
|
|
|
54
60
|
"scripts": {
|
|
55
61
|
"watch": "tsc --watch",
|
|
56
62
|
"lint": "tsc --noEmit && eslint src --ext .ts",
|
|
57
|
-
"build": "npm run generateParams && npm run clean && tsc && cp src/*.html
|
|
63
|
+
"build": "npm run generateParams && npm run clean && tsc && cp src/*.html package.json README.md ./dist",
|
|
58
64
|
"clean": "rm -rf dist",
|
|
59
65
|
"generateParams": "ts-node --esm generateOptionsFromSpec.ts",
|
|
60
|
-
"start": "node src/index.js"
|
|
66
|
+
"start": "node src/index.js",
|
|
67
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest"
|
|
61
68
|
},
|
|
62
69
|
"lint-staged": {
|
|
63
70
|
"*.ts": [
|
package/parameters.gen.js
CHANGED
|
@@ -3,98 +3,221 @@ export const projectCreateRequest = {
|
|
|
3
3
|
'project.settings.quota.active_time_seconds': {
|
|
4
4
|
type: "number",
|
|
5
5
|
description: "The total amount of wall-clock time allowed to be spent by project's compute endpoints.\n",
|
|
6
|
+
demandOption: false,
|
|
6
7
|
},
|
|
7
8
|
'project.settings.quota.compute_time_seconds': {
|
|
8
9
|
type: "number",
|
|
9
10
|
description: "The total amount of CPU seconds allowed to be spent by project's compute endpoints.\n",
|
|
11
|
+
demandOption: false,
|
|
10
12
|
},
|
|
11
13
|
'project.settings.quota.written_data_bytes': {
|
|
12
14
|
type: "number",
|
|
13
15
|
description: "Total amount of data written to all project's branches.\n",
|
|
16
|
+
demandOption: false,
|
|
14
17
|
},
|
|
15
18
|
'project.settings.quota.data_transfer_bytes': {
|
|
16
19
|
type: "number",
|
|
17
20
|
description: "Total amount of data transferred from all project's branches using proxy.\n",
|
|
21
|
+
demandOption: false,
|
|
18
22
|
},
|
|
19
23
|
'project.settings.quota.logical_size_bytes': {
|
|
20
24
|
type: "number",
|
|
21
25
|
description: "Limit on the logical size of every project's branch.\n",
|
|
26
|
+
demandOption: false,
|
|
22
27
|
},
|
|
23
28
|
'project.name': {
|
|
24
29
|
type: "string",
|
|
25
30
|
description: "The project name",
|
|
31
|
+
demandOption: false,
|
|
26
32
|
},
|
|
27
33
|
'project.branch.name': {
|
|
28
34
|
type: "string",
|
|
29
35
|
description: "The branch name. If not specified, the default branch name will be used.\n",
|
|
36
|
+
demandOption: false,
|
|
30
37
|
},
|
|
31
38
|
'project.branch.role_name': {
|
|
32
39
|
type: "string",
|
|
33
40
|
description: "The role name. If not specified, the default role name will be used.\n",
|
|
41
|
+
demandOption: false,
|
|
34
42
|
},
|
|
35
43
|
'project.branch.database_name': {
|
|
36
44
|
type: "string",
|
|
37
45
|
description: "The database name. If not specified, the default database name will be used.\n",
|
|
46
|
+
demandOption: false,
|
|
38
47
|
},
|
|
39
48
|
'project.provisioner': {
|
|
40
49
|
type: "string",
|
|
41
50
|
description: "The Neon compute provisioner.\n",
|
|
51
|
+
demandOption: false,
|
|
42
52
|
choices: ["k8s-pod", "k8s-neonvm", "docker"],
|
|
43
53
|
},
|
|
44
54
|
'project.region_id': {
|
|
45
55
|
type: "string",
|
|
46
56
|
description: "The region identifier. See [the documentation](https://neon.tech/docs/introduction/regions) for the list of supported regions.\n",
|
|
57
|
+
demandOption: false,
|
|
47
58
|
},
|
|
48
59
|
'project.pg_version': {
|
|
49
60
|
type: "number",
|
|
50
61
|
description: "The major PostgreSQL version number. Currently supported version are `14` and `15`.",
|
|
62
|
+
demandOption: false,
|
|
51
63
|
},
|
|
52
64
|
'project.store_passwords': {
|
|
53
65
|
type: "boolean",
|
|
54
66
|
description: "Whether or not passwords are stored for roles in the Neon project. Storing passwords facilitates access to Neon features that require authorization.\n",
|
|
67
|
+
demandOption: false,
|
|
55
68
|
},
|
|
56
69
|
'project.history_retention_seconds': {
|
|
57
70
|
type: "number",
|
|
58
71
|
description: "The number of seconds to retain PITR backup history for this project. Defaults to 7 days\n",
|
|
72
|
+
demandOption: false,
|
|
59
73
|
},
|
|
60
74
|
};
|
|
61
75
|
export const branchCreateRequest = {
|
|
62
76
|
'branch.parent_id': {
|
|
63
77
|
type: "string",
|
|
64
78
|
description: "The `branch_id` of the parent branch\n",
|
|
79
|
+
demandOption: false,
|
|
65
80
|
},
|
|
66
81
|
'branch.name': {
|
|
67
82
|
type: "string",
|
|
68
83
|
description: "The branch name\n",
|
|
84
|
+
demandOption: false,
|
|
69
85
|
},
|
|
70
86
|
'branch.parent_lsn': {
|
|
71
87
|
type: "string",
|
|
72
88
|
description: "A Log Sequence Number (LSN) on the parent branch. The branch will be created with data from this LSN.\n",
|
|
89
|
+
demandOption: false,
|
|
73
90
|
},
|
|
74
91
|
'branch.parent_timestamp': {
|
|
75
92
|
type: "string",
|
|
76
93
|
description: "A timestamp identifying a point in time on the parent branch. The branch will be created with data starting from this point in time.\n",
|
|
94
|
+
demandOption: false,
|
|
77
95
|
},
|
|
78
96
|
};
|
|
79
97
|
export const branchCreateRequestEndpointOptions = {
|
|
80
98
|
'type': {
|
|
81
99
|
type: "string",
|
|
82
100
|
description: "The compute endpoint type. Either `read_write` or `read_only`.\nThe `read_only` compute endpoint type is not yet supported.\n",
|
|
101
|
+
demandOption: true,
|
|
83
102
|
choices: ["read_only", "read_write"],
|
|
84
103
|
},
|
|
85
104
|
'provisioner': {
|
|
86
105
|
type: "string",
|
|
87
106
|
description: "The Neon compute provisioner.\n",
|
|
107
|
+
demandOption: false,
|
|
88
108
|
choices: ["k8s-pod", "k8s-neonvm", "docker"],
|
|
89
109
|
},
|
|
90
110
|
'suspend_timeout_seconds': {
|
|
91
111
|
type: "number",
|
|
92
112
|
description: "Duration of inactivity in seconds after which endpoint will be\nautomatically suspended. Value `0` means use global default,\n`-1` means never suspend. Maximum value is 1 week in seconds.\n",
|
|
113
|
+
demandOption: false,
|
|
93
114
|
},
|
|
94
115
|
};
|
|
95
116
|
export const branchUpdateRequest = {
|
|
96
117
|
'branch.name': {
|
|
97
118
|
type: "string",
|
|
98
119
|
description: undefined,
|
|
120
|
+
demandOption: false,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
export const endpointCreateRequest = {
|
|
124
|
+
'endpoint.branch_id': {
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "The ID of the branch the compute endpoint will be associated with\n",
|
|
127
|
+
demandOption: true,
|
|
128
|
+
},
|
|
129
|
+
'endpoint.region_id': {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "The region where the compute endpoint will be created. Only the project's `region_id` is permitted.\n",
|
|
132
|
+
demandOption: false,
|
|
133
|
+
},
|
|
134
|
+
'endpoint.type': {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "The compute endpoint type. Either `read_write` or `read_only`.\nThe `read_only` compute endpoint type is not yet supported.\n",
|
|
137
|
+
demandOption: true,
|
|
138
|
+
choices: ["read_only", "read_write"],
|
|
139
|
+
},
|
|
140
|
+
'endpoint.provisioner': {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "The Neon compute provisioner.\n",
|
|
143
|
+
demandOption: false,
|
|
144
|
+
choices: ["k8s-pod", "k8s-neonvm", "docker"],
|
|
145
|
+
},
|
|
146
|
+
'endpoint.pooler_enabled': {
|
|
147
|
+
type: "boolean",
|
|
148
|
+
description: "Whether to enable connection pooling for the compute endpoint\n",
|
|
149
|
+
demandOption: false,
|
|
150
|
+
},
|
|
151
|
+
'endpoint.pooler_mode': {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "The connection pooler mode. Neon supports PgBouncer in `transaction` mode only.\n",
|
|
154
|
+
demandOption: false,
|
|
155
|
+
choices: ["transaction"],
|
|
156
|
+
},
|
|
157
|
+
'endpoint.disabled': {
|
|
158
|
+
type: "boolean",
|
|
159
|
+
description: "Whether to restrict connections to the compute endpoint\n",
|
|
160
|
+
demandOption: false,
|
|
161
|
+
},
|
|
162
|
+
'endpoint.passwordless_access': {
|
|
163
|
+
type: "boolean",
|
|
164
|
+
description: "NOT YET IMPLEMENTED. Whether to permit passwordless access to the compute endpoint.\n",
|
|
165
|
+
demandOption: false,
|
|
166
|
+
},
|
|
167
|
+
'endpoint.suspend_timeout_seconds': {
|
|
168
|
+
type: "number",
|
|
169
|
+
description: "Duration of inactivity in seconds after which endpoint will be\nautomatically suspended. Value `0` means use global default,\n`-1` means never suspend. Maximum value is 1 week in seconds.\n",
|
|
170
|
+
demandOption: false,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
export const endpointUpdateRequest = {
|
|
174
|
+
'endpoint.branch_id': {
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "The destination branch ID. The destination branch must not have an exsiting read-write endpoint.\n",
|
|
177
|
+
demandOption: false,
|
|
178
|
+
},
|
|
179
|
+
'endpoint.provisioner': {
|
|
180
|
+
type: "string",
|
|
181
|
+
description: "The Neon compute provisioner.\n",
|
|
182
|
+
demandOption: false,
|
|
183
|
+
choices: ["k8s-pod", "k8s-neonvm", "docker"],
|
|
184
|
+
},
|
|
185
|
+
'endpoint.pooler_enabled': {
|
|
186
|
+
type: "boolean",
|
|
187
|
+
description: "Whether to enable connection pooling for the compute endpoint\n",
|
|
188
|
+
demandOption: false,
|
|
189
|
+
},
|
|
190
|
+
'endpoint.pooler_mode': {
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "The connection pooler mode. Neon supports PgBouncer in `transaction` mode only.\n",
|
|
193
|
+
demandOption: false,
|
|
194
|
+
choices: ["transaction"],
|
|
195
|
+
},
|
|
196
|
+
'endpoint.disabled': {
|
|
197
|
+
type: "boolean",
|
|
198
|
+
description: "Whether to restrict connections to the compute endpoint\n",
|
|
199
|
+
demandOption: false,
|
|
200
|
+
},
|
|
201
|
+
'endpoint.passwordless_access': {
|
|
202
|
+
type: "boolean",
|
|
203
|
+
description: "NOT YET IMPLEMENTED. Whether to permit passwordless access to the compute endpoint.\n",
|
|
204
|
+
demandOption: false,
|
|
205
|
+
},
|
|
206
|
+
'endpoint.suspend_timeout_seconds': {
|
|
207
|
+
type: "number",
|
|
208
|
+
description: "Duration of inactivity in seconds after which endpoint will be\nautomatically suspended. Value `0` means use global default,\n`-1` means never suspend. Maximum value is 1 week in seconds.\n",
|
|
209
|
+
demandOption: false,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
export const databaseCreateRequest = {
|
|
213
|
+
'database.name': {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "The name of the datbase\n",
|
|
216
|
+
demandOption: true,
|
|
217
|
+
},
|
|
218
|
+
'database.owner_name': {
|
|
219
|
+
type: "string",
|
|
220
|
+
description: "The name of the role that owns the database\n",
|
|
221
|
+
demandOption: true,
|
|
99
222
|
},
|
|
100
223
|
};
|
package/test_utils.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { test, expect, describe, beforeAll, afterAll } from '@jest/globals';
|
|
3
|
+
import emocks from 'emocks';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import { fork } from 'node:child_process';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
const runMockServer = async () => new Promise((resolve) => {
|
|
8
|
+
const app = express();
|
|
9
|
+
app.use(express.json());
|
|
10
|
+
app.use('/', emocks(join(process.cwd(), 'mocks')));
|
|
11
|
+
const server = app.listen(0);
|
|
12
|
+
server.on('listening', () => {
|
|
13
|
+
console.log(`Mock server listening at ${server.address().port}`);
|
|
14
|
+
});
|
|
15
|
+
resolve(server);
|
|
16
|
+
});
|
|
17
|
+
export const testCliCommand = ({ args, name, expected, }) => {
|
|
18
|
+
let server;
|
|
19
|
+
describe(name, () => {
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
server = await runMockServer();
|
|
22
|
+
});
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
server.close(() => {
|
|
26
|
+
resolve();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
test('test', async () => {
|
|
31
|
+
let output = '';
|
|
32
|
+
let error = '';
|
|
33
|
+
const cp = fork(join(process.cwd(), './dist/index.js'), [
|
|
34
|
+
'--api-host',
|
|
35
|
+
`http://localhost:${server.address().port}`,
|
|
36
|
+
'--api-key',
|
|
37
|
+
'test-key',
|
|
38
|
+
...args,
|
|
39
|
+
], {
|
|
40
|
+
stdio: 'pipe',
|
|
41
|
+
});
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
cp.stdout?.on('data', (data) => {
|
|
44
|
+
output += data.toString();
|
|
45
|
+
});
|
|
46
|
+
cp.stderr?.on('data', (data) => {
|
|
47
|
+
error += data.toString();
|
|
48
|
+
});
|
|
49
|
+
cp.on('error', (err) => {
|
|
50
|
+
throw err;
|
|
51
|
+
});
|
|
52
|
+
cp.on('close', (code) => {
|
|
53
|
+
if (error) {
|
|
54
|
+
console.log(error);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
expect(code).toBe(0);
|
|
58
|
+
if (code === 0 && expected) {
|
|
59
|
+
if ('snapshot' in expected) {
|
|
60
|
+
expect(output).toMatchSnapshot();
|
|
61
|
+
}
|
|
62
|
+
else if ('output' in expected) {
|
|
63
|
+
expect(output).toEqual(expected.output);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
resolve();
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
reject(err);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
};
|
package/utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
1
3
|
/**
|
|
2
4
|
* This middleware is needed to fill in the args for nested objects,
|
|
3
5
|
* so that required arguments would work
|
|
@@ -18,3 +20,10 @@ export const fillInArgs = (args, currentArgs = args, acc = []) => {
|
|
|
18
20
|
}
|
|
19
21
|
});
|
|
20
22
|
};
|
|
23
|
+
export const commandFailHandler = async (_msg, _err, yyargs) => {
|
|
24
|
+
const argv = yargs(hideBin(process.argv));
|
|
25
|
+
if (argv.argv._.length === 1) {
|
|
26
|
+
yyargs.showHelp();
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
};
|
package/writer.js
CHANGED
|
@@ -1,62 +1,71 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
import Table from 'cli-table';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
|
|
5
|
-
process.stdout.on('error', function (err) {
|
|
6
|
-
if (err.code == 'EPIPE') {
|
|
7
|
-
process.exit(0);
|
|
8
|
-
}
|
|
9
|
-
});
|
|
4
|
+
import { isCi } from './env.js';
|
|
10
5
|
/**
|
|
11
6
|
*
|
|
12
7
|
* Parses the output format, takes data and writes the output to stdout.
|
|
13
8
|
*
|
|
14
9
|
* @example
|
|
15
|
-
* // to output single data
|
|
16
10
|
* const { data } = await props.apiClient.listProjectBranches(props.project.id);
|
|
17
|
-
*
|
|
11
|
+
* // to output single data
|
|
12
|
+
* writer(props).end(data, { fields: ['id', 'name', 'created_at'] })
|
|
18
13
|
* // to output multiple data
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* data,
|
|
22
|
-
*
|
|
23
|
-
* },
|
|
24
|
-
* endpoints: {
|
|
25
|
-
* data,
|
|
26
|
-
* config: { fields: ['id', 'created_at'] }
|
|
27
|
-
* }
|
|
28
|
-
* })
|
|
14
|
+
* writer(props)
|
|
15
|
+
* .write(data, { fields: ['id', 'name', 'created_at'], title: 'branches' })
|
|
16
|
+
* .write(data, { fields: ['id', 'created_at'], title: 'endpoints' })
|
|
17
|
+
* .end()
|
|
29
18
|
*/
|
|
30
|
-
export const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
19
|
+
export const writer = (props) => {
|
|
20
|
+
const out = props.out ?? process.stdout;
|
|
21
|
+
const chunks = [];
|
|
22
|
+
return {
|
|
23
|
+
write(data, config) {
|
|
24
|
+
chunks.push({ data, config });
|
|
25
|
+
return this;
|
|
26
|
+
},
|
|
27
|
+
end: (...args) => {
|
|
28
|
+
if (args.length === 2) {
|
|
29
|
+
chunks.push({ data: args[0], config: args[1] });
|
|
30
|
+
}
|
|
31
|
+
if (props.output == 'yaml') {
|
|
32
|
+
out.write(YAML.stringify(chunks.length === 1
|
|
33
|
+
? chunks[0].data
|
|
34
|
+
: Object.fromEntries(chunks.map(({ config, data }, idx) => [
|
|
35
|
+
config.title ?? idx,
|
|
36
|
+
data,
|
|
37
|
+
])), null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (props.output == 'json') {
|
|
41
|
+
out.write(JSON.stringify(chunks.length === 1
|
|
42
|
+
? chunks[0].data
|
|
43
|
+
: Object.fromEntries(chunks.map(({ config, data }, idx) => [
|
|
44
|
+
config.title ?? idx,
|
|
45
|
+
data,
|
|
46
|
+
])), null, 2));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
chunks.forEach(({ data, config }) => {
|
|
50
|
+
const arrayData = Array.isArray(data) ? data : [data];
|
|
51
|
+
const table = new Table({
|
|
52
|
+
style: {
|
|
53
|
+
head: ['green'],
|
|
54
|
+
},
|
|
55
|
+
head: config.fields.map((field) => field
|
|
56
|
+
.split('_')
|
|
57
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
58
|
+
.join(' ')),
|
|
59
|
+
});
|
|
60
|
+
arrayData.forEach((item) => {
|
|
61
|
+
table.push(config.fields.map((field) => item[field] ?? ''));
|
|
62
|
+
});
|
|
63
|
+
if (config.title) {
|
|
64
|
+
out.write((isCi() ? config.title : chalk.bold(config.title)) + '\n');
|
|
65
|
+
}
|
|
66
|
+
out.write(table.toString());
|
|
67
|
+
out.write('\n');
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
|
62
71
|
};
|