neonctl 1.12.0 → 1.14.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/commands/branches.js +17 -19
- package/commands/branches.test.js +12 -14
- package/commands/connection_string.js +48 -15
- package/commands/connection_string.test.js +12 -8
- package/commands/databases.js +11 -14
- package/commands/databases.test.js +7 -8
- package/commands/index.js +0 -2
- package/commands/operations.js +2 -1
- package/commands/projects.js +6 -24
- package/commands/projects.test.js +3 -10
- package/commands/roles.js +11 -14
- package/commands/roles.test.js +6 -14
- package/enrichers.js +48 -0
- package/env.js +3 -0
- package/index.js +6 -1
- package/log.js +6 -0
- package/package.json +1 -1
- package/test_utils.js +9 -4
- package/commands/endpoints.js +0 -92
- package/commands/endpoints.test.js +0 -102
package/commands/branches.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { writer } from '../writer.js';
|
|
2
|
-
import { branchCreateRequest, branchCreateRequestEndpointOptions,
|
|
2
|
+
import { branchCreateRequest, branchCreateRequestEndpointOptions, } from '../parameters.gen.js';
|
|
3
3
|
import { commandFailHandler } from '../utils.js';
|
|
4
4
|
import { retryOnLock } from '../api.js';
|
|
5
|
+
import { branchIdFromProps, fillSingleProject } from '../enrichers.js';
|
|
5
6
|
const BRANCH_FIELDS = ['id', 'name', 'created_at', 'updated_at'];
|
|
6
7
|
export const command = 'branches';
|
|
7
8
|
export const describe = 'Manage branches';
|
|
@@ -14,29 +15,23 @@ export const builder = (argv) => argv
|
|
|
14
15
|
'project.id': {
|
|
15
16
|
describe: 'Project ID',
|
|
16
17
|
type: 'string',
|
|
17
|
-
demandOption: true,
|
|
18
18
|
},
|
|
19
19
|
})
|
|
20
|
+
.middleware(fillSingleProject)
|
|
20
21
|
.command('list', 'List branches', (yargs) => yargs, async (args) => await list(args))
|
|
21
22
|
.command('create', 'Create a branch', (yargs) => yargs.options({
|
|
22
23
|
...branchCreateRequest,
|
|
23
24
|
...Object.fromEntries(Object.entries(branchCreateRequestEndpointOptions).map(([key, value]) => [`endpoint.${key}`, { ...value, demandOption: false }])),
|
|
24
25
|
}), async (args) => await create(args))
|
|
25
|
-
.command('update', 'Update a branch', (yargs) => yargs.options(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
.command('update <id|name>', 'Update a branch', (yargs) => yargs.options({
|
|
27
|
+
'branch.name': {
|
|
28
|
+
describe: 'Branch name',
|
|
29
|
+
type: 'string',
|
|
30
|
+
demandOption: true,
|
|
31
|
+
},
|
|
29
32
|
}), async (args) => await update(args))
|
|
30
|
-
.command('delete', 'Delete a branch', (yargs) => yargs
|
|
31
|
-
|
|
32
|
-
type: 'string',
|
|
33
|
-
demandOption: true,
|
|
34
|
-
}), async (args) => await deleteBranch(args))
|
|
35
|
-
.command('get', 'Get a branch', (yargs) => yargs.option('branch.id', {
|
|
36
|
-
describe: 'Branch ID',
|
|
37
|
-
type: 'string',
|
|
38
|
-
demandOption: true,
|
|
39
|
-
}), async (args) => await get(args));
|
|
33
|
+
.command('delete <id|name>', 'Delete a branch', (yargs) => yargs, async (args) => await deleteBranch(args))
|
|
34
|
+
.command('get <id|name>', 'Get a branch', (yargs) => yargs, async (args) => await get(args));
|
|
40
35
|
export const handler = (args) => {
|
|
41
36
|
return args;
|
|
42
37
|
};
|
|
@@ -71,7 +66,8 @@ const create = async (props) => {
|
|
|
71
66
|
out.end();
|
|
72
67
|
};
|
|
73
68
|
const update = async (props) => {
|
|
74
|
-
const
|
|
69
|
+
const branchId = await branchIdFromProps(props);
|
|
70
|
+
const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.project.id, branchId, {
|
|
75
71
|
branch: props.branch,
|
|
76
72
|
}));
|
|
77
73
|
writer(props).end(data.branch, {
|
|
@@ -79,13 +75,15 @@ const update = async (props) => {
|
|
|
79
75
|
});
|
|
80
76
|
};
|
|
81
77
|
const deleteBranch = async (props) => {
|
|
82
|
-
const
|
|
78
|
+
const branchId = await branchIdFromProps(props);
|
|
79
|
+
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranch(props.project.id, branchId));
|
|
83
80
|
writer(props).end(data.branch, {
|
|
84
81
|
fields: BRANCH_FIELDS,
|
|
85
82
|
});
|
|
86
83
|
};
|
|
87
84
|
const get = async (props) => {
|
|
88
|
-
const
|
|
85
|
+
const branchId = await branchIdFromProps(props);
|
|
86
|
+
const { data } = await props.apiClient.getProjectBranch(props.project.id, branchId);
|
|
89
87
|
writer(props).end(data.branch, {
|
|
90
88
|
fields: BRANCH_FIELDS,
|
|
91
89
|
});
|
|
@@ -39,14 +39,13 @@ describe('branches', () => {
|
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
41
|
testCliCommand({
|
|
42
|
-
name: 'delete',
|
|
42
|
+
name: 'delete by id',
|
|
43
43
|
args: [
|
|
44
44
|
'branches',
|
|
45
45
|
'delete',
|
|
46
|
+
'br-sunny-branch-123456',
|
|
46
47
|
'--project.id',
|
|
47
48
|
'test',
|
|
48
|
-
'--branch.id',
|
|
49
|
-
'test_branch_id',
|
|
50
49
|
],
|
|
51
50
|
expected: {
|
|
52
51
|
snapshot: true,
|
|
@@ -57,10 +56,9 @@ describe('branches', () => {
|
|
|
57
56
|
args: [
|
|
58
57
|
'branches',
|
|
59
58
|
'update',
|
|
59
|
+
'test_branch',
|
|
60
60
|
'--project.id',
|
|
61
61
|
'test',
|
|
62
|
-
'--branch.id',
|
|
63
|
-
'test_branch_id',
|
|
64
62
|
'--branch.name',
|
|
65
63
|
'new_test_branch',
|
|
66
64
|
],
|
|
@@ -69,15 +67,15 @@ describe('branches', () => {
|
|
|
69
67
|
},
|
|
70
68
|
});
|
|
71
69
|
testCliCommand({
|
|
72
|
-
name: 'get',
|
|
73
|
-
args: [
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
],
|
|
70
|
+
name: 'get by id',
|
|
71
|
+
args: ['branches', 'get', 'br-sunny-branch-123456', '--project.id', 'test'],
|
|
72
|
+
expected: {
|
|
73
|
+
snapshot: true,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
testCliCommand({
|
|
77
|
+
name: 'get by name',
|
|
78
|
+
args: ['branches', 'get', 'test_branch', '--project.id', 'test'],
|
|
81
79
|
expected: {
|
|
82
80
|
snapshot: true,
|
|
83
81
|
},
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import { EndpointType } from '@neondatabase/api-client';
|
|
2
|
+
import { branchIdFromProps, fillSingleProject } from '../enrichers.js';
|
|
3
|
+
export const command = 'connection-string [branch]';
|
|
2
4
|
export const aliases = ['cs'];
|
|
3
5
|
export const describe = 'Get connection string';
|
|
4
6
|
export const builder = (argv) => {
|
|
5
|
-
return argv
|
|
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({
|
|
6
14
|
'project.id': {
|
|
7
15
|
type: 'string',
|
|
8
16
|
describe: 'Project ID',
|
|
9
|
-
demandOption: true,
|
|
10
|
-
},
|
|
11
|
-
'endpoint.id': {
|
|
12
|
-
type: 'string',
|
|
13
|
-
describe: 'Endpoint ID',
|
|
14
|
-
demandOption: true,
|
|
15
17
|
},
|
|
16
18
|
'role.name': {
|
|
17
19
|
type: 'string',
|
|
18
20
|
describe: 'Role name',
|
|
19
|
-
demandOption: true,
|
|
20
21
|
},
|
|
21
22
|
'database.name': {
|
|
22
23
|
type: 'string',
|
|
23
24
|
describe: 'Database name',
|
|
24
|
-
demandOption: true,
|
|
25
25
|
},
|
|
26
26
|
pooled: {
|
|
27
27
|
type: 'boolean',
|
|
@@ -33,17 +33,50 @@ export const builder = (argv) => {
|
|
|
33
33
|
describe: 'Use connection string for Prisma setup',
|
|
34
34
|
default: false,
|
|
35
35
|
},
|
|
36
|
-
})
|
|
36
|
+
})
|
|
37
|
+
.middleware(fillSingleProject);
|
|
37
38
|
};
|
|
38
39
|
export const handler = async (props) => {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
40
|
+
const projectId = props.project.id;
|
|
41
|
+
const branchId = await branchIdFromProps(props);
|
|
42
|
+
const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(projectId, branchId);
|
|
43
|
+
const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite);
|
|
44
|
+
if (!endpoint) {
|
|
45
|
+
throw new Error(`No endpoint found for the branch: ${branchId}`);
|
|
46
|
+
}
|
|
47
|
+
const role = props.role?.name ||
|
|
48
|
+
(await props.apiClient
|
|
49
|
+
.listProjectBranchRoles(projectId, branchId)
|
|
50
|
+
.then(({ data }) => {
|
|
51
|
+
if (data.roles.length === 0) {
|
|
52
|
+
throw new Error(`No roles found for the branch: ${branchId}`);
|
|
53
|
+
}
|
|
54
|
+
if (data.roles.length === 1) {
|
|
55
|
+
return data.roles[0].name;
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Multiple roles found for the branch, please provide one with the --role.name option: ${data.roles
|
|
58
|
+
.map((r) => r.name)
|
|
59
|
+
.join(', ')}`);
|
|
60
|
+
}));
|
|
61
|
+
const database = props.database?.name ||
|
|
62
|
+
(await props.apiClient
|
|
63
|
+
.listProjectBranchDatabases(projectId, branchId)
|
|
64
|
+
.then(({ data }) => {
|
|
65
|
+
if (data.databases.length === 0) {
|
|
66
|
+
throw new Error(`No databases found for the branch: ${branchId}`);
|
|
67
|
+
}
|
|
68
|
+
if (data.databases.length === 1) {
|
|
69
|
+
return data.databases[0].name;
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Multiple databases found for the branch, please provide one with the --database.name option: ${data.databases}`);
|
|
72
|
+
}));
|
|
73
|
+
const { data: password } = await props.apiClient.getProjectBranchRolePassword(props.project.id, endpoint.branch_id, role);
|
|
41
74
|
const host = props.pooled
|
|
42
75
|
? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`)
|
|
43
76
|
: endpoint.host;
|
|
44
77
|
const connectionString = new URL(`postgres://${host}`);
|
|
45
|
-
connectionString.pathname =
|
|
46
|
-
connectionString.username =
|
|
78
|
+
connectionString.pathname = database;
|
|
79
|
+
connectionString.username = role;
|
|
47
80
|
connectionString.password = password.password;
|
|
48
81
|
if (props.prisma) {
|
|
49
82
|
connectionString.searchParams.set('connect_timeout', '30');
|
|
@@ -5,10 +5,9 @@ describe('connection_string', () => {
|
|
|
5
5
|
name: 'connection_string',
|
|
6
6
|
args: [
|
|
7
7
|
'connection-string',
|
|
8
|
+
'test_branch',
|
|
8
9
|
'--project.id',
|
|
9
10
|
'test',
|
|
10
|
-
'--endpoint.id',
|
|
11
|
-
'test_endpoint_id',
|
|
12
11
|
'--database.name',
|
|
13
12
|
'test_db',
|
|
14
13
|
'--role.name',
|
|
@@ -22,10 +21,9 @@ describe('connection_string', () => {
|
|
|
22
21
|
name: 'connection_string pooled',
|
|
23
22
|
args: [
|
|
24
23
|
'connection-string',
|
|
24
|
+
'test_branch',
|
|
25
25
|
'--project.id',
|
|
26
26
|
'test',
|
|
27
|
-
'--endpoint.id',
|
|
28
|
-
'test_endpoint_id',
|
|
29
27
|
'--database.name',
|
|
30
28
|
'test_db',
|
|
31
29
|
'--role.name',
|
|
@@ -40,10 +38,9 @@ describe('connection_string', () => {
|
|
|
40
38
|
name: 'connection_string prisma',
|
|
41
39
|
args: [
|
|
42
40
|
'connection-string',
|
|
41
|
+
'test_branch',
|
|
43
42
|
'--project.id',
|
|
44
43
|
'test',
|
|
45
|
-
'--endpoint.id',
|
|
46
|
-
'test_endpoint_id',
|
|
47
44
|
'--database.name',
|
|
48
45
|
'test_db',
|
|
49
46
|
'--role.name',
|
|
@@ -58,10 +55,9 @@ describe('connection_string', () => {
|
|
|
58
55
|
name: 'connection_string prisma pooled',
|
|
59
56
|
args: [
|
|
60
57
|
'connection-string',
|
|
58
|
+
'test_branch',
|
|
61
59
|
'--project.id',
|
|
62
60
|
'test',
|
|
63
|
-
'--endpoint.id',
|
|
64
|
-
'test_endpoint_id',
|
|
65
61
|
'--database.name',
|
|
66
62
|
'test_db',
|
|
67
63
|
'--role.name',
|
|
@@ -73,4 +69,12 @@ describe('connection_string', () => {
|
|
|
73
69
|
snapshot: true,
|
|
74
70
|
},
|
|
75
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
|
+
});
|
|
76
80
|
});
|
package/commands/databases.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { retryOnLock } from '../api.js';
|
|
2
|
+
import { branchIdFromProps, fillSingleProject } from '../enrichers.js';
|
|
2
3
|
import { databaseCreateRequest } from '../parameters.gen.js';
|
|
3
4
|
import { commandFailHandler } from '../utils.js';
|
|
4
5
|
import { writer } from '../writer.js';
|
|
@@ -14,34 +15,29 @@ export const builder = (argv) => argv
|
|
|
14
15
|
'project.id': {
|
|
15
16
|
describe: 'Project ID',
|
|
16
17
|
type: 'string',
|
|
17
|
-
demandOption: true,
|
|
18
18
|
},
|
|
19
|
-
|
|
20
|
-
describe: 'Branch ID',
|
|
19
|
+
branch: {
|
|
20
|
+
describe: 'Branch ID or name',
|
|
21
21
|
type: 'string',
|
|
22
|
-
demandOption: true,
|
|
23
22
|
},
|
|
24
23
|
})
|
|
24
|
+
.middleware(fillSingleProject)
|
|
25
25
|
.command('list', 'List databases', (yargs) => yargs, async (args) => await list(args))
|
|
26
26
|
.command('create', 'Create a database', (yargs) => yargs.options(databaseCreateRequest), async (args) => await create(args))
|
|
27
|
-
.command('delete', 'Delete a database', (yargs) => yargs
|
|
28
|
-
'database.name': {
|
|
29
|
-
describe: 'Database name',
|
|
30
|
-
type: 'string',
|
|
31
|
-
demandOption: true,
|
|
32
|
-
},
|
|
33
|
-
}), async (args) => await deleteDb(args));
|
|
27
|
+
.command('delete <database>', 'Delete a database', (yargs) => yargs, async (args) => await deleteDb(args));
|
|
34
28
|
export const handler = (args) => {
|
|
35
29
|
return args;
|
|
36
30
|
};
|
|
37
31
|
export const list = async (props) => {
|
|
38
|
-
const
|
|
32
|
+
const branchId = await branchIdFromProps(props);
|
|
33
|
+
const { data } = await props.apiClient.listProjectBranchDatabases(props.project.id, branchId);
|
|
39
34
|
writer(props).end(data.databases, {
|
|
40
35
|
fields: DATABASE_FIELDS,
|
|
41
36
|
});
|
|
42
37
|
};
|
|
43
38
|
export const create = async (props) => {
|
|
44
|
-
const
|
|
39
|
+
const branchId = await branchIdFromProps(props);
|
|
40
|
+
const { data } = await retryOnLock(() => props.apiClient.createProjectBranchDatabase(props.project.id, branchId, {
|
|
45
41
|
database: props.database,
|
|
46
42
|
}));
|
|
47
43
|
writer(props).end(data.database, {
|
|
@@ -49,7 +45,8 @@ export const create = async (props) => {
|
|
|
49
45
|
});
|
|
50
46
|
};
|
|
51
47
|
export const deleteDb = async (props) => {
|
|
52
|
-
const
|
|
48
|
+
const branchId = await branchIdFromProps(props);
|
|
49
|
+
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchDatabase(props.project.id, branchId, props.database));
|
|
53
50
|
writer(props).end(data.database, {
|
|
54
51
|
fields: DATABASE_FIELDS,
|
|
55
52
|
});
|
|
@@ -8,8 +8,8 @@ describe('databases', () => {
|
|
|
8
8
|
'list',
|
|
9
9
|
'--project.id',
|
|
10
10
|
'test',
|
|
11
|
-
'--branch
|
|
12
|
-
'
|
|
11
|
+
'--branch',
|
|
12
|
+
'test_branch',
|
|
13
13
|
],
|
|
14
14
|
expected: {
|
|
15
15
|
snapshot: true,
|
|
@@ -22,8 +22,8 @@ describe('databases', () => {
|
|
|
22
22
|
'create',
|
|
23
23
|
'--project.id',
|
|
24
24
|
'test',
|
|
25
|
-
'--branch
|
|
26
|
-
'
|
|
25
|
+
'--branch',
|
|
26
|
+
'test_branch',
|
|
27
27
|
'--database.name',
|
|
28
28
|
'test_db',
|
|
29
29
|
'--database.owner_name',
|
|
@@ -38,12 +38,11 @@ describe('databases', () => {
|
|
|
38
38
|
args: [
|
|
39
39
|
'databases',
|
|
40
40
|
'delete',
|
|
41
|
+
'test_db',
|
|
41
42
|
'--project.id',
|
|
42
43
|
'test',
|
|
43
|
-
'--branch
|
|
44
|
-
'
|
|
45
|
-
'--database.name',
|
|
46
|
-
'test_db',
|
|
44
|
+
'--branch',
|
|
45
|
+
'test_branch',
|
|
47
46
|
],
|
|
48
47
|
expected: {
|
|
49
48
|
snapshot: true,
|
package/commands/index.js
CHANGED
|
@@ -2,7 +2,6 @@ import * as auth from './auth.js';
|
|
|
2
2
|
import * as projects from './projects.js';
|
|
3
3
|
import * as users from './user.js';
|
|
4
4
|
import * as branches from './branches.js';
|
|
5
|
-
import * as endpoints from './endpoints.js';
|
|
6
5
|
import * as databases from './databases.js';
|
|
7
6
|
import * as roles from './roles.js';
|
|
8
7
|
import * as operations from './operations.js';
|
|
@@ -12,7 +11,6 @@ export default [
|
|
|
12
11
|
users,
|
|
13
12
|
projects,
|
|
14
13
|
branches,
|
|
15
|
-
endpoints,
|
|
16
14
|
databases,
|
|
17
15
|
roles,
|
|
18
16
|
operations,
|
package/commands/operations.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fillSingleProject } from '../enrichers.js';
|
|
1
2
|
import { commandFailHandler } from '../utils.js';
|
|
2
3
|
import { writer } from '../writer.js';
|
|
3
4
|
const OPERATIONS_FIELDS = ['id', 'action', 'status', 'created_at'];
|
|
@@ -12,9 +13,9 @@ export const builder = (argv) => argv
|
|
|
12
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;
|
package/commands/projects.js
CHANGED
|
@@ -16,31 +16,13 @@ export const builder = (argv) => {
|
|
|
16
16
|
.command('create', 'Create a project', (yargs) => yargs.options(projectCreateRequest), async (args) => {
|
|
17
17
|
await create(args);
|
|
18
18
|
})
|
|
19
|
-
.command('update', 'Update a project', (yargs) => yargs
|
|
20
|
-
.option('project.id', {
|
|
21
|
-
describe: 'Project ID',
|
|
22
|
-
type: 'string',
|
|
23
|
-
demandOption: true,
|
|
24
|
-
})
|
|
25
|
-
.options(projectCreateRequest), async (args) => {
|
|
19
|
+
.command('update <id>', 'Update a project', (yargs) => yargs.options(projectCreateRequest), async (args) => {
|
|
26
20
|
await update(args);
|
|
27
21
|
})
|
|
28
|
-
.command('delete', 'Delete a project', (yargs) => yargs
|
|
29
|
-
'project.id': {
|
|
30
|
-
describe: 'Project ID',
|
|
31
|
-
type: 'string',
|
|
32
|
-
demandOption: true,
|
|
33
|
-
},
|
|
34
|
-
}), async (args) => {
|
|
22
|
+
.command('delete <id>', 'Delete a project', (yargs) => yargs, async (args) => {
|
|
35
23
|
await deleteProject(args);
|
|
36
24
|
})
|
|
37
|
-
.command('get', 'Get a project', (yargs) => yargs
|
|
38
|
-
'project.id': {
|
|
39
|
-
describe: 'Project ID',
|
|
40
|
-
type: 'string',
|
|
41
|
-
demandOption: true,
|
|
42
|
-
},
|
|
43
|
-
}), async (args) => {
|
|
25
|
+
.command('get <id>', 'Get a project', (yargs) => yargs, async (args) => {
|
|
44
26
|
await get(args);
|
|
45
27
|
});
|
|
46
28
|
};
|
|
@@ -68,18 +50,18 @@ const create = async (props) => {
|
|
|
68
50
|
writer(props).end(data.project, { fields: PROJECT_FIELDS });
|
|
69
51
|
};
|
|
70
52
|
const deleteProject = async (props) => {
|
|
71
|
-
const { data } = await props.apiClient.deleteProject(props.
|
|
53
|
+
const { data } = await props.apiClient.deleteProject(props.id);
|
|
72
54
|
writer(props).end(data.project, {
|
|
73
55
|
fields: PROJECT_FIELDS,
|
|
74
56
|
});
|
|
75
57
|
};
|
|
76
58
|
const update = async (props) => {
|
|
77
|
-
const { data } = await props.apiClient.updateProject(props.
|
|
59
|
+
const { data } = await props.apiClient.updateProject(props.id, {
|
|
78
60
|
project: props.project,
|
|
79
61
|
});
|
|
80
62
|
writer(props).end(data.project, { fields: PROJECT_FIELDS });
|
|
81
63
|
};
|
|
82
64
|
const get = async (props) => {
|
|
83
|
-
const { data } = await props.apiClient.getProject(props.
|
|
65
|
+
const { data } = await props.apiClient.getProject(props.id);
|
|
84
66
|
writer(props).end(data.project, { fields: PROJECT_FIELDS });
|
|
85
67
|
};
|
|
@@ -17,28 +17,21 @@ describe('projects', () => {
|
|
|
17
17
|
});
|
|
18
18
|
testCliCommand({
|
|
19
19
|
name: 'delete',
|
|
20
|
-
args: ['projects', 'delete', '
|
|
20
|
+
args: ['projects', 'delete', 'test'],
|
|
21
21
|
expected: {
|
|
22
22
|
snapshot: true,
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
testCliCommand({
|
|
26
26
|
name: 'update',
|
|
27
|
-
args: [
|
|
28
|
-
'projects',
|
|
29
|
-
'update',
|
|
30
|
-
'--project.id',
|
|
31
|
-
'test',
|
|
32
|
-
'--project.name',
|
|
33
|
-
'test_project',
|
|
34
|
-
],
|
|
27
|
+
args: ['projects', 'update', 'test', '--project.name', 'test_project'],
|
|
35
28
|
expected: {
|
|
36
29
|
snapshot: true,
|
|
37
30
|
},
|
|
38
31
|
});
|
|
39
32
|
testCliCommand({
|
|
40
33
|
name: 'get',
|
|
41
|
-
args: ['projects', 'get', '
|
|
34
|
+
args: ['projects', 'get', 'test'],
|
|
42
35
|
expected: {
|
|
43
36
|
snapshot: true,
|
|
44
37
|
},
|
package/commands/roles.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { retryOnLock } from '../api.js';
|
|
2
|
+
import { branchIdFromProps, fillSingleProject } from '../enrichers.js';
|
|
2
3
|
import { roleCreateRequest } from '../parameters.gen.js';
|
|
3
4
|
import { commandFailHandler } from '../utils.js';
|
|
4
5
|
import { writer } from '../writer.js';
|
|
@@ -14,34 +15,29 @@ export const builder = (argv) => argv
|
|
|
14
15
|
'project.id': {
|
|
15
16
|
describe: 'Project ID',
|
|
16
17
|
type: 'string',
|
|
17
|
-
demandOption: true,
|
|
18
18
|
},
|
|
19
|
-
|
|
20
|
-
describe: 'Branch ID',
|
|
19
|
+
branch: {
|
|
20
|
+
describe: 'Branch ID or name',
|
|
21
21
|
type: 'string',
|
|
22
|
-
demandOption: true,
|
|
23
22
|
},
|
|
24
23
|
})
|
|
24
|
+
.middleware(fillSingleProject)
|
|
25
25
|
.command('list', 'List roles', (yargs) => yargs, async (args) => await list(args))
|
|
26
26
|
.command('create', 'Create a role', (yargs) => yargs.options(roleCreateRequest), async (args) => await create(args))
|
|
27
|
-
.command('delete', 'Delete a role', (yargs) => yargs
|
|
28
|
-
'role.name': {
|
|
29
|
-
describe: 'Role name',
|
|
30
|
-
type: 'string',
|
|
31
|
-
demandOption: true,
|
|
32
|
-
},
|
|
33
|
-
}), async (args) => await deleteRole(args));
|
|
27
|
+
.command('delete <role>', 'Delete a role', (yargs) => yargs, async (args) => await deleteRole(args));
|
|
34
28
|
export const handler = (args) => {
|
|
35
29
|
return args;
|
|
36
30
|
};
|
|
37
31
|
export const list = async (props) => {
|
|
38
|
-
const
|
|
32
|
+
const branchId = await branchIdFromProps(props);
|
|
33
|
+
const { data } = await props.apiClient.listProjectBranchRoles(props.project.id, branchId);
|
|
39
34
|
writer(props).end(data.roles, {
|
|
40
35
|
fields: ROLES_FIELDS,
|
|
41
36
|
});
|
|
42
37
|
};
|
|
43
38
|
export const create = async (props) => {
|
|
44
|
-
const
|
|
39
|
+
const branchId = await branchIdFromProps(props);
|
|
40
|
+
const { data } = await retryOnLock(() => props.apiClient.createProjectBranchRole(props.project.id, branchId, {
|
|
45
41
|
role: props.role,
|
|
46
42
|
}));
|
|
47
43
|
writer(props).end(data.role, {
|
|
@@ -49,7 +45,8 @@ export const create = async (props) => {
|
|
|
49
45
|
});
|
|
50
46
|
};
|
|
51
47
|
export const deleteRole = async (props) => {
|
|
52
|
-
const
|
|
48
|
+
const branchId = await branchIdFromProps(props);
|
|
49
|
+
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchRole(props.project.id, branchId, props.role));
|
|
53
50
|
writer(props).end(data.role, {
|
|
54
51
|
fields: ROLES_FIELDS,
|
|
55
52
|
});
|
package/commands/roles.test.js
CHANGED
|
@@ -3,14 +3,7 @@ import { testCliCommand } from '../test_utils.js';
|
|
|
3
3
|
describe('roles', () => {
|
|
4
4
|
testCliCommand({
|
|
5
5
|
name: 'list',
|
|
6
|
-
args: [
|
|
7
|
-
'roles',
|
|
8
|
-
'list',
|
|
9
|
-
'--project.id',
|
|
10
|
-
'test',
|
|
11
|
-
'--branch.id',
|
|
12
|
-
'test_branch_id',
|
|
13
|
-
],
|
|
6
|
+
args: ['roles', 'list', '--project.id', 'test', '--branch', 'test_branch'],
|
|
14
7
|
expected: {
|
|
15
8
|
snapshot: true,
|
|
16
9
|
},
|
|
@@ -22,8 +15,8 @@ describe('roles', () => {
|
|
|
22
15
|
'create',
|
|
23
16
|
'--project.id',
|
|
24
17
|
'test',
|
|
25
|
-
'--branch
|
|
26
|
-
'
|
|
18
|
+
'--branch',
|
|
19
|
+
'test_branch',
|
|
27
20
|
'--role.name',
|
|
28
21
|
'test_role',
|
|
29
22
|
],
|
|
@@ -36,12 +29,11 @@ describe('roles', () => {
|
|
|
36
29
|
args: [
|
|
37
30
|
'roles',
|
|
38
31
|
'delete',
|
|
32
|
+
'test_role',
|
|
39
33
|
'--project.id',
|
|
40
34
|
'test',
|
|
41
|
-
'--branch
|
|
42
|
-
'
|
|
43
|
-
'--role.name',
|
|
44
|
-
'test_role',
|
|
35
|
+
'--branch',
|
|
36
|
+
'test_branch',
|
|
45
37
|
],
|
|
46
38
|
expected: {
|
|
47
39
|
snapshot: true,
|
package/enrichers.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
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.\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.project.id,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const { data } = await props.apiClient.listProjectBranches(props.project.id);
|
|
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.project) {
|
|
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
|
+
project: { id: data.projects[0].id },
|
|
47
|
+
};
|
|
48
|
+
};
|
package/env.js
CHANGED
package/index.js
CHANGED
|
@@ -78,9 +78,13 @@ builder = builder
|
|
|
78
78
|
default: !isCi(),
|
|
79
79
|
})
|
|
80
80
|
.middleware(analyticsMiddleware)
|
|
81
|
+
.completion()
|
|
81
82
|
.fail(async (msg, err) => {
|
|
82
83
|
if (isAxiosError(err)) {
|
|
83
|
-
if (err.
|
|
84
|
+
if (err.code === 'ECONNABORTED') {
|
|
85
|
+
log.error('Request timed out');
|
|
86
|
+
}
|
|
87
|
+
else if (err.response?.status === 401) {
|
|
84
88
|
log.error('Authentication failed, please run `neonctl auth`');
|
|
85
89
|
}
|
|
86
90
|
else {
|
|
@@ -90,6 +94,7 @@ builder = builder
|
|
|
90
94
|
else {
|
|
91
95
|
log.error(msg || err?.message);
|
|
92
96
|
}
|
|
97
|
+
err.stack && log.degug('Stack: %s', err.stack);
|
|
93
98
|
process.exit(1);
|
|
94
99
|
});
|
|
95
100
|
(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
|
+
degug: (...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) => {
|
|
@@ -35,6 +35,8 @@ export const testCliCommand = ({ args, name, expected, }) => {
|
|
|
35
35
|
`http://localhost:${server.address().port}`,
|
|
36
36
|
'--api-key',
|
|
37
37
|
'test-key',
|
|
38
|
+
'--output',
|
|
39
|
+
'yaml',
|
|
38
40
|
...args,
|
|
39
41
|
], {
|
|
40
42
|
stdio: 'pipe',
|
|
@@ -51,6 +53,9 @@ export const testCliCommand = ({ args, name, expected, }) => {
|
|
|
51
53
|
});
|
|
52
54
|
cp.on('close', (code) => {
|
|
53
55
|
try {
|
|
56
|
+
if (code !== 0 && error) {
|
|
57
|
+
console.error(error);
|
|
58
|
+
}
|
|
54
59
|
expect(code).toBe(0);
|
|
55
60
|
if (code === 0 && expected) {
|
|
56
61
|
if (expected.snapshot) {
|
package/commands/endpoints.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { retryOnLock } from '../api.js';
|
|
2
|
-
import { endpointCreateRequest, endpointUpdateRequest, } from '../parameters.gen.js';
|
|
3
|
-
import { commandFailHandler } from '../utils.js';
|
|
4
|
-
import { writer } from '../writer.js';
|
|
5
|
-
const ENDPOINT_FIELDS = [
|
|
6
|
-
'id',
|
|
7
|
-
'created_at',
|
|
8
|
-
'branch_id',
|
|
9
|
-
'type',
|
|
10
|
-
'current_state',
|
|
11
|
-
];
|
|
12
|
-
export const command = 'endpoints';
|
|
13
|
-
export const describe = 'Manage endpoints';
|
|
14
|
-
export const aliases = ['endpoint'];
|
|
15
|
-
export const builder = (argv) => argv
|
|
16
|
-
.demandCommand(1, '')
|
|
17
|
-
.fail(commandFailHandler)
|
|
18
|
-
.usage('usage: $0 endpoints <sub-command> [options]')
|
|
19
|
-
.options({
|
|
20
|
-
'project.id': {
|
|
21
|
-
describe: 'Project ID',
|
|
22
|
-
type: 'string',
|
|
23
|
-
demandOption: true,
|
|
24
|
-
},
|
|
25
|
-
})
|
|
26
|
-
.command('list', 'List endpoints', (yargs) => yargs.options({
|
|
27
|
-
'branch.id': {
|
|
28
|
-
describe: 'Branch ID',
|
|
29
|
-
type: 'string',
|
|
30
|
-
demandOption: false,
|
|
31
|
-
},
|
|
32
|
-
}), async (args) => await list(args))
|
|
33
|
-
.command('create', 'Create an endpoint', (yargs) => yargs.options(endpointCreateRequest), async (args) => await create(args))
|
|
34
|
-
.command('update', 'Update an endpoint', (yargs) => yargs.options({
|
|
35
|
-
'endpoint.id': {
|
|
36
|
-
describe: 'Endpoint ID',
|
|
37
|
-
type: 'string',
|
|
38
|
-
demandOption: true,
|
|
39
|
-
},
|
|
40
|
-
...endpointUpdateRequest,
|
|
41
|
-
}), async (args) => await update(args))
|
|
42
|
-
.command('delete', 'Delete an endpoint', (yargs) => yargs.options({
|
|
43
|
-
'endpoint.id': {
|
|
44
|
-
describe: 'Endpoint ID',
|
|
45
|
-
type: 'string',
|
|
46
|
-
demandOption: true,
|
|
47
|
-
},
|
|
48
|
-
}), async (args) => await deleteEndpoint(args))
|
|
49
|
-
.command('get', 'Get an endpoint', (yargs) => yargs.options({
|
|
50
|
-
'endpoint.id': {
|
|
51
|
-
describe: 'Endpoint ID',
|
|
52
|
-
type: 'string',
|
|
53
|
-
demandOption: true,
|
|
54
|
-
},
|
|
55
|
-
}), async (args) => await getEndpoint(args));
|
|
56
|
-
export const handler = async (args) => args;
|
|
57
|
-
const list = async (props) => {
|
|
58
|
-
const { data } = props.branch?.id
|
|
59
|
-
? await props.apiClient.listProjectBranchEndpoints(props.project.id, props.branch.id)
|
|
60
|
-
: await props.apiClient.listProjectEndpoints(props.project.id);
|
|
61
|
-
writer(props).end(data.endpoints, {
|
|
62
|
-
fields: ENDPOINT_FIELDS,
|
|
63
|
-
});
|
|
64
|
-
};
|
|
65
|
-
const create = async (props) => {
|
|
66
|
-
const { data } = await retryOnLock(() => props.apiClient.createProjectEndpoint(props.project.id, {
|
|
67
|
-
endpoint: props.endpoint,
|
|
68
|
-
}));
|
|
69
|
-
writer(props).end(data.endpoint, {
|
|
70
|
-
fields: ENDPOINT_FIELDS,
|
|
71
|
-
});
|
|
72
|
-
};
|
|
73
|
-
const update = async (props) => {
|
|
74
|
-
const { data } = await retryOnLock(() => props.apiClient.updateProjectEndpoint(props.project.id, props.endpoint.id, {
|
|
75
|
-
endpoint: props.endpoint,
|
|
76
|
-
}));
|
|
77
|
-
writer(props).end(data.endpoint, {
|
|
78
|
-
fields: ENDPOINT_FIELDS,
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
const deleteEndpoint = async (props) => {
|
|
82
|
-
const { data } = await retryOnLock(() => props.apiClient.deleteProjectEndpoint(props.project.id, props.endpoint.id));
|
|
83
|
-
writer(props).end(data.endpoint, {
|
|
84
|
-
fields: ENDPOINT_FIELDS,
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
const getEndpoint = async (props) => {
|
|
88
|
-
const { data } = await props.apiClient.getProjectEndpoint(props.project.id, props.endpoint.id);
|
|
89
|
-
writer(props).end(data.endpoint, {
|
|
90
|
-
fields: ENDPOINT_FIELDS,
|
|
91
|
-
});
|
|
92
|
-
};
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { describe, expect } from '@jest/globals';
|
|
2
|
-
import { testCliCommand } from '../test_utils.js';
|
|
3
|
-
describe('endpoints', () => {
|
|
4
|
-
testCliCommand({
|
|
5
|
-
name: 'list',
|
|
6
|
-
args: ['endpoints', 'list', '--project.id', 'test'],
|
|
7
|
-
expected: {
|
|
8
|
-
snapshot: true,
|
|
9
|
-
},
|
|
10
|
-
});
|
|
11
|
-
testCliCommand({
|
|
12
|
-
name: 'list with branch filter',
|
|
13
|
-
args: [
|
|
14
|
-
'endpoints',
|
|
15
|
-
'list',
|
|
16
|
-
'--project.id',
|
|
17
|
-
'test',
|
|
18
|
-
'--branch.id',
|
|
19
|
-
'test_branch_id',
|
|
20
|
-
],
|
|
21
|
-
expected: {
|
|
22
|
-
snapshot: true,
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
testCliCommand({
|
|
26
|
-
name: 'create',
|
|
27
|
-
args: [
|
|
28
|
-
'endpoints',
|
|
29
|
-
'create',
|
|
30
|
-
'--project.id',
|
|
31
|
-
'test',
|
|
32
|
-
'--endpoint.branch_id',
|
|
33
|
-
'test_branch_id',
|
|
34
|
-
'--endpoint.type',
|
|
35
|
-
'read_only',
|
|
36
|
-
],
|
|
37
|
-
expected: {
|
|
38
|
-
snapshot: true,
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
testCliCommand({
|
|
42
|
-
name: 'create with retry',
|
|
43
|
-
args: [
|
|
44
|
-
'endpoints',
|
|
45
|
-
'create',
|
|
46
|
-
'--project.id',
|
|
47
|
-
'test',
|
|
48
|
-
'--endpoint.branch_id',
|
|
49
|
-
'test_branch_with_retry',
|
|
50
|
-
'--endpoint.type',
|
|
51
|
-
'read_only',
|
|
52
|
-
],
|
|
53
|
-
expected: {
|
|
54
|
-
stderr: expect.stringContaining('Resource is locked'),
|
|
55
|
-
snapshot: true,
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
testCliCommand({
|
|
59
|
-
name: 'delete',
|
|
60
|
-
args: [
|
|
61
|
-
'endpoints',
|
|
62
|
-
'delete',
|
|
63
|
-
'--project.id',
|
|
64
|
-
'test',
|
|
65
|
-
'--endpoint.id',
|
|
66
|
-
'test_endpoint_id',
|
|
67
|
-
],
|
|
68
|
-
expected: {
|
|
69
|
-
snapshot: true,
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
testCliCommand({
|
|
73
|
-
name: 'update',
|
|
74
|
-
args: [
|
|
75
|
-
'endpoints',
|
|
76
|
-
'update',
|
|
77
|
-
'--project.id',
|
|
78
|
-
'test',
|
|
79
|
-
'--endpoint.id',
|
|
80
|
-
'test_endpoint_id',
|
|
81
|
-
'--endpoint.branch_id',
|
|
82
|
-
'test_branch_id',
|
|
83
|
-
],
|
|
84
|
-
expected: {
|
|
85
|
-
snapshot: true,
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
testCliCommand({
|
|
89
|
-
name: 'get',
|
|
90
|
-
args: [
|
|
91
|
-
'endpoints',
|
|
92
|
-
'get',
|
|
93
|
-
'--project.id',
|
|
94
|
-
'test',
|
|
95
|
-
'--endpoint.id',
|
|
96
|
-
'test_endpoint_id',
|
|
97
|
-
],
|
|
98
|
-
expected: {
|
|
99
|
-
snapshot: true,
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
});
|