neonctl 1.13.0 → 1.15.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/api.js +9 -1
- package/auth.js +3 -3
- package/commands/auth.js +2 -1
- package/commands/branches.js +75 -22
- package/commands/branches.test.js +77 -16
- package/commands/connection_string.js +58 -16
- package/commands/connection_string.test.js +20 -12
- package/commands/databases.js +39 -11
- package/commands/databases.test.js +5 -5
- package/commands/operations.js +5 -4
- package/commands/operations.test.js +1 -1
- package/commands/projects.js +38 -15
- package/commands/projects.test.js +2 -2
- package/commands/roles.js +17 -11
- package/commands/roles.test.js +4 -4
- package/env.js +3 -0
- package/index.js +14 -2
- package/log.js +6 -0
- package/package.json +1 -1
- package/test_utils.js +7 -4
- package/utils/enrichers.js +48 -0
- package/utils/formats.js +5 -0
- package/enrichers.js +0 -19
- /package/{utils.js → utils/middlewares.js} +0 -0
package/api.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { createApiClient } from '@neondatabase/api-client';
|
|
2
2
|
import { isAxiosError } from 'axios';
|
|
3
3
|
import { log } from './log.js';
|
|
4
|
-
|
|
4
|
+
import pkg from './pkg.js';
|
|
5
|
+
export const getApiClient = ({ apiKey, apiHost }) => createApiClient({
|
|
6
|
+
apiKey,
|
|
7
|
+
baseURL: apiHost,
|
|
8
|
+
timeout: 10000,
|
|
9
|
+
headers: {
|
|
10
|
+
'User-Agent': `neonctl v${pkg.version}`,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
5
13
|
const RETRY_COUNT = 5;
|
|
6
14
|
const RETRY_DELAY = 3000;
|
|
7
15
|
export const retryOnLock = async (fn) => {
|
package/auth.js
CHANGED
|
@@ -23,7 +23,7 @@ custom.setHttpOptionsDefaults({
|
|
|
23
23
|
timeout: SERVER_TIMEOUT,
|
|
24
24
|
});
|
|
25
25
|
export const refreshToken = async ({ oauthHost, clientId }, tokenSet) => {
|
|
26
|
-
log.
|
|
26
|
+
log.debug('Discovering oauth server');
|
|
27
27
|
const issuer = await Issuer.discover(oauthHost);
|
|
28
28
|
const neonOAuthClient = new issuer.Client({
|
|
29
29
|
token_endpoint_auth_method: 'none',
|
|
@@ -33,7 +33,7 @@ export const refreshToken = async ({ oauthHost, clientId }, tokenSet) => {
|
|
|
33
33
|
return await neonOAuthClient.refresh(tokenSet);
|
|
34
34
|
};
|
|
35
35
|
export const auth = async ({ oauthHost, clientId }) => {
|
|
36
|
-
log.
|
|
36
|
+
log.debug('Discovering oauth server');
|
|
37
37
|
const issuer = await Issuer.discover(oauthHost);
|
|
38
38
|
//
|
|
39
39
|
// Start HTTP server and wait till /callback is hit
|
|
@@ -64,7 +64,7 @@ export const auth = async ({ oauthHost, clientId }) => {
|
|
|
64
64
|
response.end();
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
log.
|
|
67
|
+
log.debug(`Callback received: ${request.url}`);
|
|
68
68
|
const params = neonOAuthClient.callbackParams(request);
|
|
69
69
|
const tokenSet = await neonOAuthClient.callback(REDIRECT_URI(listen_port), params, {
|
|
70
70
|
code_verifier: codeVerifier,
|
package/commands/auth.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getApiClient } from '../api.js';
|
|
|
7
7
|
import { isCi } from '../env.js';
|
|
8
8
|
const CREDENTIALS_FILE = 'credentials.json';
|
|
9
9
|
export const command = 'auth';
|
|
10
|
+
export const aliases = ['login'];
|
|
10
11
|
export const describe = 'Authenticate';
|
|
11
12
|
export const builder = (yargs) => yargs;
|
|
12
13
|
export const handler = async (args) => {
|
|
@@ -52,7 +53,7 @@ export const ensureAuth = async (props) => {
|
|
|
52
53
|
const tokenSetContents = await JSON.parse(readFileSync(credentialsPath, 'utf8'));
|
|
53
54
|
const tokenSet = new TokenSet(tokenSetContents);
|
|
54
55
|
if (tokenSet.expired()) {
|
|
55
|
-
log.
|
|
56
|
+
log.debug('using refresh token to update access token');
|
|
56
57
|
const refreshedTokenSet = await refreshToken({
|
|
57
58
|
oauthHost: props.oauthHost,
|
|
58
59
|
clientId: props.clientId,
|
package/commands/branches.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { EndpointType } from '@neondatabase/api-client';
|
|
1
2
|
import { writer } from '../writer.js';
|
|
2
|
-
import { branchCreateRequest
|
|
3
|
-
import { commandFailHandler } from '../utils.js';
|
|
3
|
+
import { branchCreateRequest } from '../parameters.gen.js';
|
|
4
|
+
import { commandFailHandler } from '../utils/middlewares.js';
|
|
4
5
|
import { retryOnLock } from '../api.js';
|
|
5
|
-
import { branchIdFromProps } from '../enrichers.js';
|
|
6
|
+
import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
7
|
+
import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp, } from '../utils/formats.js';
|
|
6
8
|
const BRANCH_FIELDS = ['id', 'name', 'created_at', 'updated_at'];
|
|
7
9
|
export const command = 'branches';
|
|
8
10
|
export const describe = 'Manage branches';
|
|
@@ -12,39 +14,88 @@ export const builder = (argv) => argv
|
|
|
12
14
|
.fail(commandFailHandler)
|
|
13
15
|
.usage('usage: $0 branches <sub-command> [options]')
|
|
14
16
|
.options({
|
|
15
|
-
'project
|
|
17
|
+
'project-id': {
|
|
16
18
|
describe: 'Project ID',
|
|
17
19
|
type: 'string',
|
|
18
|
-
demandOption: true,
|
|
19
20
|
},
|
|
20
21
|
})
|
|
22
|
+
.middleware(fillSingleProject)
|
|
21
23
|
.command('list', 'List branches', (yargs) => yargs, async (args) => await list(args))
|
|
22
24
|
.command('create', 'Create a branch', (yargs) => yargs.options({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.command('update <id|name>', 'Update a branch', (yargs) => yargs.options({
|
|
27
|
-
'branch.name': {
|
|
28
|
-
describe: 'Branch name',
|
|
25
|
+
name: branchCreateRequest['branch.name'],
|
|
26
|
+
parent: {
|
|
27
|
+
describe: 'Parent branch name or id or timestamp or LSN. Defaults to the primary branch',
|
|
29
28
|
type: 'string',
|
|
30
|
-
demandOption: true,
|
|
31
29
|
},
|
|
32
|
-
|
|
30
|
+
endpoint: {
|
|
31
|
+
describe: 'Create a branch with or without an endpoint. By default branch is created with a read-write endpoint. To create a branch without endpoint use --no-endpoint',
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
default: true,
|
|
34
|
+
},
|
|
35
|
+
readonly: {
|
|
36
|
+
describe: 'Create a read-only branch',
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
implies: 'endpoint',
|
|
39
|
+
},
|
|
40
|
+
}), async (args) => await create(args))
|
|
41
|
+
.command('rename <id|name> <new-name>', 'Rename a branch', (yargs) => yargs, async (args) => await rename(args))
|
|
33
42
|
.command('delete <id|name>', 'Delete a branch', (yargs) => yargs, async (args) => await deleteBranch(args))
|
|
34
43
|
.command('get <id|name>', 'Get a branch', (yargs) => yargs, async (args) => await get(args));
|
|
35
44
|
export const handler = (args) => {
|
|
36
45
|
return args;
|
|
37
46
|
};
|
|
38
47
|
const list = async (props) => {
|
|
39
|
-
const { data } = await props.apiClient.listProjectBranches(props.
|
|
48
|
+
const { data } = await props.apiClient.listProjectBranches(props.projectId);
|
|
40
49
|
writer(props).end(data.branches, {
|
|
41
50
|
fields: BRANCH_FIELDS,
|
|
42
51
|
});
|
|
43
52
|
};
|
|
44
53
|
const create = async (props) => {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
const parentProps = await (() => {
|
|
55
|
+
if (!props.parent) {
|
|
56
|
+
return props.apiClient
|
|
57
|
+
.listProjectBranches(props.projectId)
|
|
58
|
+
.then(({ data }) => {
|
|
59
|
+
const branch = data.branches.find((b) => b.primary);
|
|
60
|
+
if (!branch) {
|
|
61
|
+
throw new Error('No primary branch found');
|
|
62
|
+
}
|
|
63
|
+
return { parent_id: branch.id };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (looksLikeLSN(props.parent)) {
|
|
67
|
+
return { parent_lsn: props.parent };
|
|
68
|
+
}
|
|
69
|
+
if (looksLikeTimestamp(props.parent)) {
|
|
70
|
+
return { parent_timestamp: props.parent };
|
|
71
|
+
}
|
|
72
|
+
if (looksLikeBranchId(props.parent)) {
|
|
73
|
+
return { parent_id: props.parent };
|
|
74
|
+
}
|
|
75
|
+
return props.apiClient
|
|
76
|
+
.listProjectBranches(props.projectId)
|
|
77
|
+
.then(({ data }) => {
|
|
78
|
+
const branch = data.branches.find((b) => b.name === props.parent);
|
|
79
|
+
if (!branch) {
|
|
80
|
+
throw new Error(`Branch ${props.parent} not found`);
|
|
81
|
+
}
|
|
82
|
+
return { parent_id: branch.id };
|
|
83
|
+
});
|
|
84
|
+
})();
|
|
85
|
+
const { data } = await retryOnLock(() => props.apiClient.createProjectBranch(props.projectId, {
|
|
86
|
+
branch: {
|
|
87
|
+
name: props.name,
|
|
88
|
+
...parentProps,
|
|
89
|
+
},
|
|
90
|
+
endpoints: props.endpoint
|
|
91
|
+
? [
|
|
92
|
+
{
|
|
93
|
+
type: props.readonly
|
|
94
|
+
? EndpointType.ReadOnly
|
|
95
|
+
: EndpointType.ReadWrite,
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
: [],
|
|
48
99
|
}));
|
|
49
100
|
const out = writer(props);
|
|
50
101
|
out.write(data.branch, {
|
|
@@ -65,10 +116,12 @@ const create = async (props) => {
|
|
|
65
116
|
}
|
|
66
117
|
out.end();
|
|
67
118
|
};
|
|
68
|
-
const
|
|
119
|
+
const rename = async (props) => {
|
|
69
120
|
const branchId = await branchIdFromProps(props);
|
|
70
|
-
const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.
|
|
71
|
-
branch:
|
|
121
|
+
const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.projectId, branchId, {
|
|
122
|
+
branch: {
|
|
123
|
+
name: props.newName,
|
|
124
|
+
},
|
|
72
125
|
}));
|
|
73
126
|
writer(props).end(data.branch, {
|
|
74
127
|
fields: BRANCH_FIELDS,
|
|
@@ -76,14 +129,14 @@ const update = async (props) => {
|
|
|
76
129
|
};
|
|
77
130
|
const deleteBranch = async (props) => {
|
|
78
131
|
const branchId = await branchIdFromProps(props);
|
|
79
|
-
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranch(props.
|
|
132
|
+
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranch(props.projectId, branchId));
|
|
80
133
|
writer(props).end(data.branch, {
|
|
81
134
|
fields: BRANCH_FIELDS,
|
|
82
135
|
});
|
|
83
136
|
};
|
|
84
137
|
const get = async (props) => {
|
|
85
138
|
const branchId = await branchIdFromProps(props);
|
|
86
|
-
const { data } = await props.apiClient.getProjectBranch(props.
|
|
139
|
+
const { data } = await props.apiClient.getProjectBranch(props.projectId, branchId);
|
|
87
140
|
writer(props).end(data.branch, {
|
|
88
141
|
fields: BRANCH_FIELDS,
|
|
89
142
|
});
|
|
@@ -3,22 +3,35 @@ import { testCliCommand } from '../test_utils.js';
|
|
|
3
3
|
describe('branches', () => {
|
|
4
4
|
testCliCommand({
|
|
5
5
|
name: 'list',
|
|
6
|
-
args: ['branches', 'list', '--project
|
|
6
|
+
args: ['branches', 'list', '--project-id', 'test'],
|
|
7
7
|
expected: {
|
|
8
8
|
snapshot: true,
|
|
9
9
|
},
|
|
10
10
|
});
|
|
11
11
|
testCliCommand({
|
|
12
|
-
name: 'create with endpoint',
|
|
12
|
+
name: 'create by default with r/w endpoint',
|
|
13
13
|
args: [
|
|
14
14
|
'branches',
|
|
15
15
|
'create',
|
|
16
|
-
'--project
|
|
16
|
+
'--project-id',
|
|
17
17
|
'test',
|
|
18
|
-
'--
|
|
18
|
+
'--name',
|
|
19
19
|
'test_branch',
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
],
|
|
21
|
+
expected: {
|
|
22
|
+
snapshot: true,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
testCliCommand({
|
|
26
|
+
name: 'create with readonly endpoint',
|
|
27
|
+
args: [
|
|
28
|
+
'branches',
|
|
29
|
+
'create',
|
|
30
|
+
'--project-id',
|
|
31
|
+
'test',
|
|
32
|
+
'--name',
|
|
33
|
+
'test_branch',
|
|
34
|
+
'--readonly',
|
|
22
35
|
],
|
|
23
36
|
expected: {
|
|
24
37
|
snapshot: true,
|
|
@@ -29,10 +42,59 @@ describe('branches', () => {
|
|
|
29
42
|
args: [
|
|
30
43
|
'branches',
|
|
31
44
|
'create',
|
|
32
|
-
'--project
|
|
45
|
+
'--project-id',
|
|
33
46
|
'test',
|
|
34
|
-
'--
|
|
47
|
+
'--name',
|
|
35
48
|
'test_branch',
|
|
49
|
+
'--no-endpoint',
|
|
50
|
+
],
|
|
51
|
+
expected: {
|
|
52
|
+
snapshot: true,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
testCliCommand({
|
|
56
|
+
name: 'create with parent by name',
|
|
57
|
+
args: [
|
|
58
|
+
'branches',
|
|
59
|
+
'create',
|
|
60
|
+
'--project-id',
|
|
61
|
+
'test',
|
|
62
|
+
'--name',
|
|
63
|
+
'test_branch_with_parent_name',
|
|
64
|
+
'--parent',
|
|
65
|
+
'main',
|
|
66
|
+
],
|
|
67
|
+
expected: {
|
|
68
|
+
snapshot: true,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
testCliCommand({
|
|
72
|
+
name: 'create with parent by lsn',
|
|
73
|
+
args: [
|
|
74
|
+
'branches',
|
|
75
|
+
'create',
|
|
76
|
+
'--project-id',
|
|
77
|
+
'test',
|
|
78
|
+
'--name',
|
|
79
|
+
'test_branch_with_parent_lsn',
|
|
80
|
+
'--parent',
|
|
81
|
+
'0/123ABC',
|
|
82
|
+
],
|
|
83
|
+
expected: {
|
|
84
|
+
snapshot: true,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
testCliCommand({
|
|
88
|
+
name: 'create with parent by timestamp',
|
|
89
|
+
args: [
|
|
90
|
+
'branches',
|
|
91
|
+
'create',
|
|
92
|
+
'--project-id',
|
|
93
|
+
'test',
|
|
94
|
+
'--name',
|
|
95
|
+
'test_branch_with_parent_timestamp',
|
|
96
|
+
'--parent',
|
|
97
|
+
'2021-01-01T00:00:00.000Z',
|
|
36
98
|
],
|
|
37
99
|
expected: {
|
|
38
100
|
snapshot: true,
|
|
@@ -44,7 +106,7 @@ describe('branches', () => {
|
|
|
44
106
|
'branches',
|
|
45
107
|
'delete',
|
|
46
108
|
'br-sunny-branch-123456',
|
|
47
|
-
'--project
|
|
109
|
+
'--project-id',
|
|
48
110
|
'test',
|
|
49
111
|
],
|
|
50
112
|
expected: {
|
|
@@ -52,15 +114,14 @@ describe('branches', () => {
|
|
|
52
114
|
},
|
|
53
115
|
});
|
|
54
116
|
testCliCommand({
|
|
55
|
-
name: '
|
|
117
|
+
name: 'rename',
|
|
56
118
|
args: [
|
|
57
119
|
'branches',
|
|
58
|
-
'
|
|
120
|
+
'rename',
|
|
59
121
|
'test_branch',
|
|
60
|
-
'--project.id',
|
|
61
|
-
'test',
|
|
62
|
-
'--branch.name',
|
|
63
122
|
'new_test_branch',
|
|
123
|
+
'--project-id',
|
|
124
|
+
'test',
|
|
64
125
|
],
|
|
65
126
|
expected: {
|
|
66
127
|
snapshot: true,
|
|
@@ -68,14 +129,14 @@ describe('branches', () => {
|
|
|
68
129
|
});
|
|
69
130
|
testCliCommand({
|
|
70
131
|
name: 'get by id',
|
|
71
|
-
args: ['branches', 'get', 'br-sunny-branch-123456', '--project
|
|
132
|
+
args: ['branches', 'get', 'br-sunny-branch-123456', '--project-id', 'test'],
|
|
72
133
|
expected: {
|
|
73
134
|
snapshot: true,
|
|
74
135
|
},
|
|
75
136
|
});
|
|
76
137
|
testCliCommand({
|
|
77
138
|
name: 'get by name',
|
|
78
|
-
args: ['branches', 'get', 'test_branch', '--project
|
|
139
|
+
args: ['branches', 'get', 'test_branch', '--project-id', 'test'],
|
|
79
140
|
expected: {
|
|
80
141
|
snapshot: true,
|
|
81
142
|
},
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import { EndpointType } from '@neondatabase/api-client';
|
|
2
|
-
import { branchIdFromProps } from '../enrichers.js';
|
|
3
|
-
export const command = 'connection-string
|
|
2
|
+
import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
3
|
+
export const command = 'connection-string [branch]';
|
|
4
4
|
export const aliases = ['cs'];
|
|
5
5
|
export const describe = 'Get connection string';
|
|
6
6
|
export const builder = (argv) => {
|
|
7
|
-
return argv
|
|
8
|
-
|
|
7
|
+
return argv
|
|
8
|
+
.usage('usage: $0 connection-string [branch] [options]')
|
|
9
|
+
.positional('branch', {
|
|
10
|
+
describe: 'Branch name or id. If ommited will use the primary branch',
|
|
11
|
+
type: 'string',
|
|
12
|
+
})
|
|
13
|
+
.options({
|
|
14
|
+
'project-id': {
|
|
9
15
|
type: 'string',
|
|
10
16
|
describe: 'Project ID',
|
|
11
|
-
demandOption: true,
|
|
12
17
|
},
|
|
13
|
-
'role
|
|
18
|
+
'role-name': {
|
|
14
19
|
type: 'string',
|
|
15
20
|
describe: 'Role name',
|
|
16
|
-
demandOption: true,
|
|
17
21
|
},
|
|
18
|
-
'database
|
|
22
|
+
'database-name': {
|
|
19
23
|
type: 'string',
|
|
20
24
|
describe: 'Database name',
|
|
21
|
-
demandOption: true,
|
|
22
25
|
},
|
|
23
26
|
pooled: {
|
|
24
27
|
type: 'boolean',
|
|
@@ -30,22 +33,61 @@ export const builder = (argv) => {
|
|
|
30
33
|
describe: 'Use connection string for Prisma setup',
|
|
31
34
|
default: false,
|
|
32
35
|
},
|
|
33
|
-
|
|
36
|
+
'endpoint-type': {
|
|
37
|
+
type: 'string',
|
|
38
|
+
choices: Object.values(EndpointType),
|
|
39
|
+
describe: 'Endpoint type',
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
.middleware(fillSingleProject);
|
|
34
43
|
};
|
|
35
44
|
export const handler = async (props) => {
|
|
45
|
+
const projectId = props.projectId;
|
|
36
46
|
const branchId = await branchIdFromProps(props);
|
|
37
|
-
const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(
|
|
38
|
-
const
|
|
47
|
+
const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(projectId, branchId);
|
|
48
|
+
const matchEndpointType = props.endpointType ?? EndpointType.ReadWrite;
|
|
49
|
+
let endpoint = endpoints.find((e) => e.type === matchEndpointType);
|
|
50
|
+
if (!endpoint && props.endpointType == null) {
|
|
51
|
+
endpoint = endpoints[0];
|
|
52
|
+
}
|
|
39
53
|
if (!endpoint) {
|
|
40
|
-
throw new Error(`No endpoint found for the branch: ${branchId}`);
|
|
54
|
+
throw new Error(`No ${props.endpointType ?? ''} endpoint found for the branch: ${branchId}`);
|
|
41
55
|
}
|
|
42
|
-
const
|
|
56
|
+
const role = props.roleName ||
|
|
57
|
+
(await props.apiClient
|
|
58
|
+
.listProjectBranchRoles(projectId, branchId)
|
|
59
|
+
.then(({ data }) => {
|
|
60
|
+
if (data.roles.length === 0) {
|
|
61
|
+
throw new Error(`No roles found for the branch: ${branchId}`);
|
|
62
|
+
}
|
|
63
|
+
if (data.roles.length === 1) {
|
|
64
|
+
return data.roles[0].name;
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Multiple roles found for the branch, please provide one with the --role.name option: ${data.roles
|
|
67
|
+
.map((r) => r.name)
|
|
68
|
+
.join(', ')}`);
|
|
69
|
+
}));
|
|
70
|
+
const database = props.databaseName ||
|
|
71
|
+
(await props.apiClient
|
|
72
|
+
.listProjectBranchDatabases(projectId, branchId)
|
|
73
|
+
.then(({ data }) => {
|
|
74
|
+
if (data.databases.length === 0) {
|
|
75
|
+
throw new Error(`No databases found for the branch: ${branchId}`);
|
|
76
|
+
}
|
|
77
|
+
if (data.databases.length === 1) {
|
|
78
|
+
return data.databases[0].name;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Multiple databases found for the branch, please provide one with the --database.name option: ${data.databases
|
|
81
|
+
.map((d) => d.name)
|
|
82
|
+
.join(', ')}`);
|
|
83
|
+
}));
|
|
84
|
+
const { data: password } = await props.apiClient.getProjectBranchRolePassword(props.projectId, endpoint.branch_id, role);
|
|
43
85
|
const host = props.pooled
|
|
44
86
|
? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`)
|
|
45
87
|
: endpoint.host;
|
|
46
88
|
const connectionString = new URL(`postgres://${host}`);
|
|
47
|
-
connectionString.pathname =
|
|
48
|
-
connectionString.username =
|
|
89
|
+
connectionString.pathname = database;
|
|
90
|
+
connectionString.username = role;
|
|
49
91
|
connectionString.password = password.password;
|
|
50
92
|
if (props.prisma) {
|
|
51
93
|
connectionString.searchParams.set('connect_timeout', '30');
|
|
@@ -6,11 +6,11 @@ describe('connection_string', () => {
|
|
|
6
6
|
args: [
|
|
7
7
|
'connection-string',
|
|
8
8
|
'test_branch',
|
|
9
|
-
'--project
|
|
9
|
+
'--project-id',
|
|
10
10
|
'test',
|
|
11
|
-
'--database
|
|
11
|
+
'--database-name',
|
|
12
12
|
'test_db',
|
|
13
|
-
'--role
|
|
13
|
+
'--role-name',
|
|
14
14
|
'test_role',
|
|
15
15
|
],
|
|
16
16
|
expected: {
|
|
@@ -22,11 +22,11 @@ describe('connection_string', () => {
|
|
|
22
22
|
args: [
|
|
23
23
|
'connection-string',
|
|
24
24
|
'test_branch',
|
|
25
|
-
'--project
|
|
25
|
+
'--project-id',
|
|
26
26
|
'test',
|
|
27
|
-
'--database
|
|
27
|
+
'--database-name',
|
|
28
28
|
'test_db',
|
|
29
|
-
'--role
|
|
29
|
+
'--role-name',
|
|
30
30
|
'test_role',
|
|
31
31
|
'--pooled',
|
|
32
32
|
],
|
|
@@ -39,11 +39,11 @@ describe('connection_string', () => {
|
|
|
39
39
|
args: [
|
|
40
40
|
'connection-string',
|
|
41
41
|
'test_branch',
|
|
42
|
-
'--project
|
|
42
|
+
'--project-id',
|
|
43
43
|
'test',
|
|
44
|
-
'--database
|
|
44
|
+
'--database-name',
|
|
45
45
|
'test_db',
|
|
46
|
-
'--role
|
|
46
|
+
'--role-name',
|
|
47
47
|
'test_role',
|
|
48
48
|
'--prisma',
|
|
49
49
|
],
|
|
@@ -56,11 +56,11 @@ describe('connection_string', () => {
|
|
|
56
56
|
args: [
|
|
57
57
|
'connection-string',
|
|
58
58
|
'test_branch',
|
|
59
|
-
'--project
|
|
59
|
+
'--project-id',
|
|
60
60
|
'test',
|
|
61
|
-
'--database
|
|
61
|
+
'--database-name',
|
|
62
62
|
'test_db',
|
|
63
|
-
'--role
|
|
63
|
+
'--role-name',
|
|
64
64
|
'test_role',
|
|
65
65
|
'--prisma',
|
|
66
66
|
'--pooled',
|
|
@@ -69,4 +69,12 @@ describe('connection_string', () => {
|
|
|
69
69
|
snapshot: true,
|
|
70
70
|
},
|
|
71
71
|
});
|
|
72
|
+
testCliCommand({
|
|
73
|
+
name: 'connection_string without any args should pass',
|
|
74
|
+
args: ['connection-string'],
|
|
75
|
+
mockDir: 'single_project',
|
|
76
|
+
expected: {
|
|
77
|
+
snapshot: true,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
72
80
|
});
|
package/commands/databases.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { retryOnLock } from '../api.js';
|
|
2
|
-
import { branchIdFromProps } from '../enrichers.js';
|
|
3
|
-
import {
|
|
4
|
-
import { commandFailHandler } from '../utils.js';
|
|
2
|
+
import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
3
|
+
import { commandFailHandler } from '../utils/middlewares.js';
|
|
5
4
|
import { writer } from '../writer.js';
|
|
6
5
|
const DATABASE_FIELDS = ['name', 'owner_name', 'created_at'];
|
|
7
6
|
export const command = 'databases';
|
|
@@ -12,34 +11,63 @@ export const builder = (argv) => argv
|
|
|
12
11
|
.fail(commandFailHandler)
|
|
13
12
|
.usage('usage: $0 databases <sub-command> [options]')
|
|
14
13
|
.options({
|
|
15
|
-
'project
|
|
14
|
+
'project-id': {
|
|
16
15
|
describe: 'Project ID',
|
|
17
16
|
type: 'string',
|
|
18
|
-
demandOption: true,
|
|
19
17
|
},
|
|
20
18
|
branch: {
|
|
21
19
|
describe: 'Branch ID or name',
|
|
22
20
|
type: 'string',
|
|
23
|
-
demandOption: true,
|
|
24
21
|
},
|
|
25
22
|
})
|
|
23
|
+
.middleware(fillSingleProject)
|
|
26
24
|
.command('list', 'List databases', (yargs) => yargs, async (args) => await list(args))
|
|
27
|
-
.command('create', 'Create a database', (yargs) => yargs.options(
|
|
25
|
+
.command('create', 'Create a database', (yargs) => yargs.options({
|
|
26
|
+
name: {
|
|
27
|
+
describe: 'Database name',
|
|
28
|
+
type: 'string',
|
|
29
|
+
demandOption: true,
|
|
30
|
+
},
|
|
31
|
+
'owner-name': {
|
|
32
|
+
describe: 'Owner name',
|
|
33
|
+
type: 'string',
|
|
34
|
+
},
|
|
35
|
+
}), async (args) => await create(args))
|
|
28
36
|
.command('delete <database>', 'Delete a database', (yargs) => yargs, async (args) => await deleteDb(args));
|
|
29
37
|
export const handler = (args) => {
|
|
30
38
|
return args;
|
|
31
39
|
};
|
|
32
40
|
export const list = async (props) => {
|
|
33
41
|
const branchId = await branchIdFromProps(props);
|
|
34
|
-
const { data } = await props.apiClient.listProjectBranchDatabases(props.
|
|
42
|
+
const { data } = await props.apiClient.listProjectBranchDatabases(props.projectId, branchId);
|
|
35
43
|
writer(props).end(data.databases, {
|
|
36
44
|
fields: DATABASE_FIELDS,
|
|
37
45
|
});
|
|
38
46
|
};
|
|
39
47
|
export const create = async (props) => {
|
|
40
48
|
const branchId = await branchIdFromProps(props);
|
|
41
|
-
const
|
|
42
|
-
|
|
49
|
+
const owner = props.ownerName ??
|
|
50
|
+
(await props.apiClient
|
|
51
|
+
.listProjectBranchRoles(props.projectId, branchId)
|
|
52
|
+
.then(({ data }) => {
|
|
53
|
+
if (data.roles.length === 0) {
|
|
54
|
+
throw new Error(`No roles found in branch ${branchId}`);
|
|
55
|
+
}
|
|
56
|
+
if (data.roles.length > 1) {
|
|
57
|
+
throw new Error(`More than one role found in branch ${branchId}. Please specify the owner name. Roles: ${data.roles
|
|
58
|
+
.map((r) => r.name)
|
|
59
|
+
.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
return data.roles[0].name;
|
|
62
|
+
}));
|
|
63
|
+
if (!owner) {
|
|
64
|
+
throw new Error('No owner found');
|
|
65
|
+
}
|
|
66
|
+
const { data } = await retryOnLock(() => props.apiClient.createProjectBranchDatabase(props.projectId, branchId, {
|
|
67
|
+
database: {
|
|
68
|
+
name: props.name,
|
|
69
|
+
owner_name: owner,
|
|
70
|
+
},
|
|
43
71
|
}));
|
|
44
72
|
writer(props).end(data.database, {
|
|
45
73
|
fields: DATABASE_FIELDS,
|
|
@@ -47,7 +75,7 @@ export const create = async (props) => {
|
|
|
47
75
|
};
|
|
48
76
|
export const deleteDb = async (props) => {
|
|
49
77
|
const branchId = await branchIdFromProps(props);
|
|
50
|
-
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchDatabase(props.
|
|
78
|
+
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchDatabase(props.projectId, branchId, props.database));
|
|
51
79
|
writer(props).end(data.database, {
|
|
52
80
|
fields: DATABASE_FIELDS,
|
|
53
81
|
});
|
|
@@ -6,7 +6,7 @@ describe('databases', () => {
|
|
|
6
6
|
args: [
|
|
7
7
|
'databases',
|
|
8
8
|
'list',
|
|
9
|
-
'--project
|
|
9
|
+
'--project-id',
|
|
10
10
|
'test',
|
|
11
11
|
'--branch',
|
|
12
12
|
'test_branch',
|
|
@@ -20,13 +20,13 @@ describe('databases', () => {
|
|
|
20
20
|
args: [
|
|
21
21
|
'databases',
|
|
22
22
|
'create',
|
|
23
|
-
'--project
|
|
23
|
+
'--project-id',
|
|
24
24
|
'test',
|
|
25
25
|
'--branch',
|
|
26
26
|
'test_branch',
|
|
27
|
-
'--
|
|
27
|
+
'--name',
|
|
28
28
|
'test_db',
|
|
29
|
-
'--
|
|
29
|
+
'--owner-name',
|
|
30
30
|
'test_owner',
|
|
31
31
|
],
|
|
32
32
|
expected: {
|
|
@@ -39,7 +39,7 @@ describe('databases', () => {
|
|
|
39
39
|
'databases',
|
|
40
40
|
'delete',
|
|
41
41
|
'test_db',
|
|
42
|
-
'--project
|
|
42
|
+
'--project-id',
|
|
43
43
|
'test',
|
|
44
44
|
'--branch',
|
|
45
45
|
'test_branch',
|
package/commands/operations.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fillSingleProject } from '../utils/enrichers.js';
|
|
2
|
+
import { commandFailHandler } from '../utils/middlewares.js';
|
|
2
3
|
import { writer } from '../writer.js';
|
|
3
4
|
const OPERATIONS_FIELDS = ['id', 'action', 'status', 'created_at'];
|
|
4
5
|
export const command = 'operations';
|
|
@@ -9,19 +10,19 @@ export const builder = (argv) => argv
|
|
|
9
10
|
.fail(commandFailHandler)
|
|
10
11
|
.usage('usage: $0 operations <sub-command> [options]')
|
|
11
12
|
.options({
|
|
12
|
-
'project
|
|
13
|
+
'project-id': {
|
|
13
14
|
describe: 'Project ID',
|
|
14
15
|
type: 'string',
|
|
15
|
-
demandOption: true,
|
|
16
16
|
},
|
|
17
17
|
})
|
|
18
|
+
.middleware(fillSingleProject)
|
|
18
19
|
.command('list', 'List operations', (yargs) => yargs, async (args) => await list(args));
|
|
19
20
|
export const handler = (args) => {
|
|
20
21
|
return args;
|
|
21
22
|
};
|
|
22
23
|
export const list = async (props) => {
|
|
23
24
|
const { data } = await props.apiClient.listProjectOperations({
|
|
24
|
-
projectId: props.
|
|
25
|
+
projectId: props.projectId,
|
|
25
26
|
limit: props.limit,
|
|
26
27
|
});
|
|
27
28
|
writer(props).end(data.operations, {
|
package/commands/projects.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { projectCreateRequest } from '../parameters.gen.js';
|
|
2
|
-
import { commandFailHandler } from '../utils.js';
|
|
2
|
+
import { commandFailHandler } from '../utils/middlewares.js';
|
|
3
3
|
import { writer } from '../writer.js';
|
|
4
4
|
const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'];
|
|
5
|
+
const REGIONS = [
|
|
6
|
+
'aws-us-west-2',
|
|
7
|
+
'aws-ap-southeast-1',
|
|
8
|
+
'aws-eu-central-1',
|
|
9
|
+
'aws-us-east-2',
|
|
10
|
+
'aws-us-east-1',
|
|
11
|
+
];
|
|
5
12
|
export const command = 'projects';
|
|
6
13
|
export const describe = 'Manage projects';
|
|
7
14
|
export const aliases = ['project'];
|
|
@@ -13,10 +20,24 @@ export const builder = (argv) => {
|
|
|
13
20
|
.command('list', 'List projects', (yargs) => yargs, async (args) => {
|
|
14
21
|
await list(args);
|
|
15
22
|
})
|
|
16
|
-
.command('create', 'Create a project', (yargs) => yargs.options(
|
|
23
|
+
.command('create', 'Create a project', (yargs) => yargs.options({
|
|
24
|
+
name: {
|
|
25
|
+
describe: projectCreateRequest['project.name'].description,
|
|
26
|
+
type: 'string',
|
|
27
|
+
},
|
|
28
|
+
'region-id': {
|
|
29
|
+
describe: `The region ID. Possible values: ${REGIONS.join(', ')}`,
|
|
30
|
+
type: 'string',
|
|
31
|
+
},
|
|
32
|
+
}), async (args) => {
|
|
17
33
|
await create(args);
|
|
18
34
|
})
|
|
19
|
-
.command('update <id>', 'Update a project', (yargs) => yargs.options(
|
|
35
|
+
.command('update <id>', 'Update a project', (yargs) => yargs.options({
|
|
36
|
+
name: {
|
|
37
|
+
describe: projectCreateRequest['project.name'].description,
|
|
38
|
+
type: 'string',
|
|
39
|
+
},
|
|
40
|
+
}), async (args) => {
|
|
20
41
|
await update(args);
|
|
21
42
|
})
|
|
22
43
|
.command('delete <id>', 'Delete a project', (yargs) => yargs, async (args) => {
|
|
@@ -34,20 +55,20 @@ const list = async (props) => {
|
|
|
34
55
|
writer(props).end(data.projects, { fields: PROJECT_FIELDS });
|
|
35
56
|
};
|
|
36
57
|
const create = async (props) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (answers.name) {
|
|
44
|
-
props.project = answers;
|
|
45
|
-
}
|
|
58
|
+
const project = {};
|
|
59
|
+
if (props.name) {
|
|
60
|
+
project.name = props.name;
|
|
61
|
+
}
|
|
62
|
+
if (props.regionId) {
|
|
63
|
+
project.region_id = props.regionId;
|
|
46
64
|
}
|
|
47
65
|
const { data } = await props.apiClient.createProject({
|
|
48
|
-
project
|
|
66
|
+
project,
|
|
49
67
|
});
|
|
50
|
-
writer(props)
|
|
68
|
+
const out = writer(props);
|
|
69
|
+
out.write(data.project, { fields: PROJECT_FIELDS });
|
|
70
|
+
out.write(data.connection_uris, { fields: ['connection_uri'] });
|
|
71
|
+
out.end();
|
|
51
72
|
};
|
|
52
73
|
const deleteProject = async (props) => {
|
|
53
74
|
const { data } = await props.apiClient.deleteProject(props.id);
|
|
@@ -57,7 +78,9 @@ const deleteProject = async (props) => {
|
|
|
57
78
|
};
|
|
58
79
|
const update = async (props) => {
|
|
59
80
|
const { data } = await props.apiClient.updateProject(props.id, {
|
|
60
|
-
project:
|
|
81
|
+
project: {
|
|
82
|
+
name: props.name,
|
|
83
|
+
},
|
|
61
84
|
});
|
|
62
85
|
writer(props).end(data.project, { fields: PROJECT_FIELDS });
|
|
63
86
|
};
|
|
@@ -10,7 +10,7 @@ describe('projects', () => {
|
|
|
10
10
|
});
|
|
11
11
|
testCliCommand({
|
|
12
12
|
name: 'create',
|
|
13
|
-
args: ['projects', 'create', '--
|
|
13
|
+
args: ['projects', 'create', '--name', 'test_project'],
|
|
14
14
|
expected: {
|
|
15
15
|
snapshot: true,
|
|
16
16
|
},
|
|
@@ -24,7 +24,7 @@ describe('projects', () => {
|
|
|
24
24
|
});
|
|
25
25
|
testCliCommand({
|
|
26
26
|
name: 'update',
|
|
27
|
-
args: ['projects', 'update', 'test', '--
|
|
27
|
+
args: ['projects', 'update', 'test', '--name', 'test_project'],
|
|
28
28
|
expected: {
|
|
29
29
|
snapshot: true,
|
|
30
30
|
},
|
package/commands/roles.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { retryOnLock } from '../api.js';
|
|
2
|
-
import { branchIdFromProps } from '../enrichers.js';
|
|
3
|
-
import {
|
|
4
|
-
import { commandFailHandler } from '../utils.js';
|
|
2
|
+
import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
3
|
+
import { commandFailHandler } from '../utils/middlewares.js';
|
|
5
4
|
import { writer } from '../writer.js';
|
|
6
5
|
const ROLES_FIELDS = ['name', 'created_at'];
|
|
7
6
|
export const command = 'roles';
|
|
@@ -12,34 +11,41 @@ export const builder = (argv) => argv
|
|
|
12
11
|
.fail(commandFailHandler)
|
|
13
12
|
.usage('usage: $0 roles <sub-command> [options]')
|
|
14
13
|
.options({
|
|
15
|
-
'project
|
|
14
|
+
'project-id': {
|
|
16
15
|
describe: 'Project ID',
|
|
17
16
|
type: 'string',
|
|
18
|
-
demandOption: true,
|
|
19
17
|
},
|
|
20
18
|
branch: {
|
|
21
19
|
describe: 'Branch ID or name',
|
|
22
20
|
type: 'string',
|
|
23
|
-
demandOption: true,
|
|
24
21
|
},
|
|
25
22
|
})
|
|
23
|
+
.middleware(fillSingleProject)
|
|
26
24
|
.command('list', 'List roles', (yargs) => yargs, async (args) => await list(args))
|
|
27
|
-
.command('create', 'Create a role', (yargs) => yargs.options(
|
|
25
|
+
.command('create', 'Create a role', (yargs) => yargs.options({
|
|
26
|
+
name: {
|
|
27
|
+
describe: 'Role name',
|
|
28
|
+
type: 'string',
|
|
29
|
+
demandOption: true,
|
|
30
|
+
},
|
|
31
|
+
}), async (args) => await create(args))
|
|
28
32
|
.command('delete <role>', 'Delete a role', (yargs) => yargs, async (args) => await deleteRole(args));
|
|
29
33
|
export const handler = (args) => {
|
|
30
34
|
return args;
|
|
31
35
|
};
|
|
32
36
|
export const list = async (props) => {
|
|
33
37
|
const branchId = await branchIdFromProps(props);
|
|
34
|
-
const { data } = await props.apiClient.listProjectBranchRoles(props.
|
|
38
|
+
const { data } = await props.apiClient.listProjectBranchRoles(props.projectId, branchId);
|
|
35
39
|
writer(props).end(data.roles, {
|
|
36
40
|
fields: ROLES_FIELDS,
|
|
37
41
|
});
|
|
38
42
|
};
|
|
39
43
|
export const create = async (props) => {
|
|
40
44
|
const branchId = await branchIdFromProps(props);
|
|
41
|
-
const { data } = await retryOnLock(() => props.apiClient.createProjectBranchRole(props.
|
|
42
|
-
role:
|
|
45
|
+
const { data } = await retryOnLock(() => props.apiClient.createProjectBranchRole(props.projectId, branchId, {
|
|
46
|
+
role: {
|
|
47
|
+
name: props.name,
|
|
48
|
+
},
|
|
43
49
|
}));
|
|
44
50
|
writer(props).end(data.role, {
|
|
45
51
|
fields: ROLES_FIELDS,
|
|
@@ -47,7 +53,7 @@ export const create = async (props) => {
|
|
|
47
53
|
};
|
|
48
54
|
export const deleteRole = async (props) => {
|
|
49
55
|
const branchId = await branchIdFromProps(props);
|
|
50
|
-
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchRole(props.
|
|
56
|
+
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchRole(props.projectId, branchId, props.role));
|
|
51
57
|
writer(props).end(data.role, {
|
|
52
58
|
fields: ROLES_FIELDS,
|
|
53
59
|
});
|
package/commands/roles.test.js
CHANGED
|
@@ -3,7 +3,7 @@ import { testCliCommand } from '../test_utils.js';
|
|
|
3
3
|
describe('roles', () => {
|
|
4
4
|
testCliCommand({
|
|
5
5
|
name: 'list',
|
|
6
|
-
args: ['roles', 'list', '--project
|
|
6
|
+
args: ['roles', 'list', '--project-id', 'test', '--branch', 'test_branch'],
|
|
7
7
|
expected: {
|
|
8
8
|
snapshot: true,
|
|
9
9
|
},
|
|
@@ -13,11 +13,11 @@ describe('roles', () => {
|
|
|
13
13
|
args: [
|
|
14
14
|
'roles',
|
|
15
15
|
'create',
|
|
16
|
-
'--project
|
|
16
|
+
'--project-id',
|
|
17
17
|
'test',
|
|
18
18
|
'--branch',
|
|
19
19
|
'test_branch',
|
|
20
|
-
'--
|
|
20
|
+
'--name',
|
|
21
21
|
'test_role',
|
|
22
22
|
],
|
|
23
23
|
expected: {
|
|
@@ -30,7 +30,7 @@ describe('roles', () => {
|
|
|
30
30
|
'roles',
|
|
31
31
|
'delete',
|
|
32
32
|
'test_role',
|
|
33
|
-
'--project
|
|
33
|
+
'--project-id',
|
|
34
34
|
'test',
|
|
35
35
|
'--branch',
|
|
36
36
|
'test_branch',
|
package/env.js
CHANGED
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { ensureAuth } from './commands/auth.js';
|
|
|
16
16
|
import { defaultDir, ensureConfigDir } from './config.js';
|
|
17
17
|
import { log } from './log.js';
|
|
18
18
|
import { defaultClientID } from './auth.js';
|
|
19
|
-
import { fillInArgs } from './utils.js';
|
|
19
|
+
import { fillInArgs } from './utils/middlewares.js';
|
|
20
20
|
import pkg from './pkg.js';
|
|
21
21
|
import commands from './commands/index.js';
|
|
22
22
|
import { analyticsMiddleware } from './analytics.js';
|
|
@@ -29,6 +29,7 @@ builder = builder
|
|
|
29
29
|
.help()
|
|
30
30
|
.option('output', {
|
|
31
31
|
alias: 'o',
|
|
32
|
+
group: 'Global options:',
|
|
32
33
|
describe: 'Set output format',
|
|
33
34
|
type: 'string',
|
|
34
35
|
choices: ['json', 'yaml', 'table'],
|
|
@@ -42,6 +43,7 @@ builder = builder
|
|
|
42
43
|
// Setup config directory
|
|
43
44
|
.option('config-dir', {
|
|
44
45
|
describe: 'Path to config directory',
|
|
46
|
+
group: 'Global options:',
|
|
45
47
|
type: 'string',
|
|
46
48
|
default: defaultDir,
|
|
47
49
|
})
|
|
@@ -60,6 +62,7 @@ builder = builder
|
|
|
60
62
|
})
|
|
61
63
|
.option('api-key', {
|
|
62
64
|
describe: 'API key',
|
|
65
|
+
group: 'Global options:',
|
|
63
66
|
type: 'string',
|
|
64
67
|
default: process.env.NEON_API_KEY ?? '',
|
|
65
68
|
})
|
|
@@ -74,14 +77,22 @@ builder = builder
|
|
|
74
77
|
.strictCommands()
|
|
75
78
|
.option('analytics', {
|
|
76
79
|
describe: 'Manage analytics. Example: --no-analytics, --analytics false',
|
|
80
|
+
group: 'Global options:',
|
|
77
81
|
type: 'boolean',
|
|
78
82
|
default: !isCi(),
|
|
79
83
|
})
|
|
80
84
|
.middleware(analyticsMiddleware)
|
|
85
|
+
.group('version', 'Global options:')
|
|
86
|
+
.alias('version', 'v')
|
|
87
|
+
.group('help', 'Global options:')
|
|
88
|
+
.alias('help', 'h')
|
|
81
89
|
.completion()
|
|
82
90
|
.fail(async (msg, err) => {
|
|
83
91
|
if (isAxiosError(err)) {
|
|
84
|
-
if (err.
|
|
92
|
+
if (err.code === 'ECONNABORTED') {
|
|
93
|
+
log.error('Request timed out');
|
|
94
|
+
}
|
|
95
|
+
else if (err.response?.status === 401) {
|
|
85
96
|
log.error('Authentication failed, please run `neonctl auth`');
|
|
86
97
|
}
|
|
87
98
|
else {
|
|
@@ -91,6 +102,7 @@ builder = builder
|
|
|
91
102
|
else {
|
|
92
103
|
log.error(msg || err?.message);
|
|
93
104
|
}
|
|
105
|
+
err?.stack && log.debug('Stack: %s', err.stack);
|
|
94
106
|
process.exit(1);
|
|
95
107
|
});
|
|
96
108
|
(async () => {
|
package/log.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { format } from 'node:util';
|
|
2
|
+
import { isDebug } from './env.js';
|
|
2
3
|
export const log = {
|
|
4
|
+
debug: (...args) => {
|
|
5
|
+
if (isDebug()) {
|
|
6
|
+
process.stderr.write(`DEBUG: ${format(...args)}\n`);
|
|
7
|
+
}
|
|
8
|
+
},
|
|
3
9
|
info: (...args) => {
|
|
4
10
|
process.stderr.write(`INFO: ${format(...args)}\n`);
|
|
5
11
|
},
|
package/package.json
CHANGED
package/test_utils.js
CHANGED
|
@@ -4,21 +4,21 @@ import emocks from 'emocks';
|
|
|
4
4
|
import express from 'express';
|
|
5
5
|
import { fork } from 'node:child_process';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
const runMockServer = async () => new Promise((resolve) => {
|
|
7
|
+
const runMockServer = async (mockDir) => new Promise((resolve) => {
|
|
8
8
|
const app = express();
|
|
9
9
|
app.use(express.json());
|
|
10
|
-
app.use('/', emocks(join(process.cwd(), 'mocks')));
|
|
10
|
+
app.use('/', emocks(join(process.cwd(), 'mocks', mockDir)));
|
|
11
11
|
const server = app.listen(0);
|
|
12
12
|
server.on('listening', () => {
|
|
13
13
|
console.log(`Mock server listening at ${server.address().port}`);
|
|
14
14
|
});
|
|
15
15
|
resolve(server);
|
|
16
16
|
});
|
|
17
|
-
export const testCliCommand = ({ args, name, expected, }) => {
|
|
17
|
+
export const testCliCommand = ({ args, name, expected, mockDir = 'main', }) => {
|
|
18
18
|
let server;
|
|
19
19
|
describe(name, () => {
|
|
20
20
|
beforeAll(async () => {
|
|
21
|
-
server = await runMockServer();
|
|
21
|
+
server = await runMockServer(mockDir);
|
|
22
22
|
});
|
|
23
23
|
afterAll(async () => {
|
|
24
24
|
return new Promise((resolve) => {
|
|
@@ -53,6 +53,9 @@ export const testCliCommand = ({ args, name, expected, }) => {
|
|
|
53
53
|
});
|
|
54
54
|
cp.on('close', (code) => {
|
|
55
55
|
try {
|
|
56
|
+
if (code !== 0 && error) {
|
|
57
|
+
console.error(error);
|
|
58
|
+
}
|
|
56
59
|
expect(code).toBe(0);
|
|
57
60
|
if (code === 0 && expected) {
|
|
58
61
|
if (expected.snapshot) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { looksLikeBranchId } from './formats.js';
|
|
2
|
+
export const branchIdResolve = async ({ branch, apiClient, projectId, }) => {
|
|
3
|
+
if (looksLikeBranchId(branch)) {
|
|
4
|
+
return branch;
|
|
5
|
+
}
|
|
6
|
+
const { data } = await apiClient.listProjectBranches(projectId);
|
|
7
|
+
const branchData = data.branches.find((b) => b.name === branch);
|
|
8
|
+
if (!branchData) {
|
|
9
|
+
throw new Error(`Branch ${branch} not found.\nAvailable branches: ${data.branches
|
|
10
|
+
.map((b) => b.name)
|
|
11
|
+
.join(', ')}`);
|
|
12
|
+
}
|
|
13
|
+
return branchData.id;
|
|
14
|
+
};
|
|
15
|
+
export const branchIdFromProps = async (props) => {
|
|
16
|
+
const branch = 'branch' in props && typeof props.branch === 'string'
|
|
17
|
+
? props.branch
|
|
18
|
+
: props.id;
|
|
19
|
+
if (branch) {
|
|
20
|
+
return await branchIdResolve({
|
|
21
|
+
branch,
|
|
22
|
+
apiClient: props.apiClient,
|
|
23
|
+
projectId: props.projectId,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const { data } = await props.apiClient.listProjectBranches(props.projectId);
|
|
27
|
+
const primaryBranch = data.branches.find((b) => b.primary);
|
|
28
|
+
if (primaryBranch) {
|
|
29
|
+
return primaryBranch.id;
|
|
30
|
+
}
|
|
31
|
+
throw new Error('No primary branch found');
|
|
32
|
+
};
|
|
33
|
+
export const fillSingleProject = async (props) => {
|
|
34
|
+
if (props.projectId) {
|
|
35
|
+
return props;
|
|
36
|
+
}
|
|
37
|
+
const { data } = await props.apiClient.listProjects({});
|
|
38
|
+
if (data.projects.length === 0) {
|
|
39
|
+
throw new Error('No projects found');
|
|
40
|
+
}
|
|
41
|
+
if (data.projects.length > 1) {
|
|
42
|
+
throw new Error(`Multiple projects found, please provide one with the --project.id option`);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
...props,
|
|
46
|
+
projectId: data.projects[0].id,
|
|
47
|
+
};
|
|
48
|
+
};
|
package/utils/formats.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const HAIKU_REGEX = /^[a-z]+-[a-z]+-\d{6}$/;
|
|
2
|
+
export const looksLikeBranchId = (branch) => branch.startsWith('br-') && HAIKU_REGEX.test(branch.substring(3));
|
|
3
|
+
const LSN_REGEX = /^[a-fA-F0-9]{1,8}\/[a-fA-F0-9]{1,8}$/;
|
|
4
|
+
export const looksLikeLSN = (lsn) => LSN_REGEX.test(lsn);
|
|
5
|
+
export const looksLikeTimestamp = (timestamp) => !isNaN(Date.parse(timestamp));
|
package/enrichers.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
const HAIKU_REGEX = /^[a-z]+-[a-z]+-\d{6}$/;
|
|
2
|
-
export const branchIdResolve = async ({ branch, apiClient, projectId, }) => {
|
|
3
|
-
if (branch.startsWith('br-') && HAIKU_REGEX.test(branch.substring(3))) {
|
|
4
|
-
return branch;
|
|
5
|
-
}
|
|
6
|
-
const { data } = await apiClient.listProjectBranches(projectId);
|
|
7
|
-
const branchData = data.branches.find((b) => b.name === branch);
|
|
8
|
-
if (!branchData) {
|
|
9
|
-
throw new Error(`Branch ${branch} not found`);
|
|
10
|
-
}
|
|
11
|
-
return branchData.id;
|
|
12
|
-
};
|
|
13
|
-
export const branchIdFromProps = async (props) => branchIdResolve({
|
|
14
|
-
branch: 'branch' in props && typeof props.branch === 'string'
|
|
15
|
-
? props.branch
|
|
16
|
-
: props.id,
|
|
17
|
-
apiClient: props.apiClient,
|
|
18
|
-
projectId: props.project.id,
|
|
19
|
-
});
|
|
File without changes
|