neonctl 1.30.0 → 1.31.1
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 +1 -1
- package/commands/branches.js +10 -7
- package/commands/branches.test.js +14 -0
- package/commands/connection_string.js +1 -1
- package/commands/index.js +2 -0
- package/commands/orgs.js +23 -0
- package/commands/projects.js +0 -1
- package/commands/schema_diff.js +5 -5
- package/commands/set_context.js +0 -7
- package/commands/set_context.test.js +45 -5
- package/context.js +6 -3
- package/package.json +2 -2
- package/parameters.gen.js +3 -3
- package/utils/enrichers.js +4 -4
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ The Neon CLI supports autocompletion, which you can configure in a few easy step
|
|
|
70
70
|
| [projects](https://neon.tech/docs/reference/cli-projects) | `list`, `create`, `update`, `delete`, `get` | Manage projects |
|
|
71
71
|
| [ip-allow](https://neon.tech/docs/reference/cli-ip-allow) | `list`, `add`, `remove`, `reset` | Manage IP Allow |
|
|
72
72
|
| [me](https://neon.tech/docs/reference/cli-me) | | Show current user |
|
|
73
|
-
| [branches](https://neon.tech/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-
|
|
73
|
+
| [branches](https://neon.tech/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-default`, `delete`, `get` | Manage branches |
|
|
74
74
|
| [databases](https://neon.tech/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases |
|
|
75
75
|
| [roles](https://neon.tech/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
|
|
76
76
|
| [operations](https://neon.tech/docs/reference/cli-operations) | `list` | Manage operations |
|
package/commands/branches.js
CHANGED
|
@@ -12,6 +12,7 @@ const BRANCH_FIELDS = [
|
|
|
12
12
|
'id',
|
|
13
13
|
'name',
|
|
14
14
|
'primary',
|
|
15
|
+
'default',
|
|
15
16
|
'created_at',
|
|
16
17
|
'updated_at',
|
|
17
18
|
];
|
|
@@ -19,6 +20,7 @@ const BRANCH_FIELDS_RESET = [
|
|
|
19
20
|
'id',
|
|
20
21
|
'name',
|
|
21
22
|
'primary',
|
|
23
|
+
'default',
|
|
22
24
|
'created_at',
|
|
23
25
|
'last_reset_at',
|
|
24
26
|
];
|
|
@@ -38,7 +40,7 @@ export const builder = (argv) => argv
|
|
|
38
40
|
.command('create', 'Create a branch', (yargs) => yargs.options({
|
|
39
41
|
name: branchCreateRequest['branch.name'],
|
|
40
42
|
parent: {
|
|
41
|
-
describe: 'Parent branch name or id or timestamp or LSN. Defaults to the
|
|
43
|
+
describe: 'Parent branch name or id or timestamp or LSN. Defaults to the default branch',
|
|
42
44
|
type: 'string',
|
|
43
45
|
},
|
|
44
46
|
compute: {
|
|
@@ -108,7 +110,8 @@ export const builder = (argv) => argv
|
|
|
108
110
|
],
|
|
109
111
|
]), async (args) => await restore(args))
|
|
110
112
|
.command('rename <id|name> <new-name>', 'Rename a branch', (yargs) => yargs, async (args) => await rename(args))
|
|
111
|
-
.command('set-primary <id|name>', 'Set a branch as primary', (yargs) => yargs, async (args) => await
|
|
113
|
+
.command('set-primary <id|name>', 'DEPRECATED: Use set-default. Set a branch as primary', (yargs) => yargs, async (args) => await setDefault(args))
|
|
114
|
+
.command('set-default <id|name>', 'Set a branch as default', (yargs) => yargs, async (args) => await setDefault(args))
|
|
112
115
|
.command('add-compute <id|name>', 'Add a compute to a branch', (yargs) => yargs.options({
|
|
113
116
|
type: {
|
|
114
117
|
type: 'string',
|
|
@@ -153,7 +156,7 @@ export const builder = (argv) => argv
|
|
|
153
156
|
],
|
|
154
157
|
[
|
|
155
158
|
'$0 branches schema-diff',
|
|
156
|
-
"If a branch is specified in 'set-context', compares this branch to its parent. Otherwise, compares the
|
|
159
|
+
"If a branch is specified in 'set-context', compares this branch to its parent. Otherwise, compares the default branch to its parent.",
|
|
157
160
|
],
|
|
158
161
|
]);
|
|
159
162
|
},
|
|
@@ -174,9 +177,9 @@ const create = async (props) => {
|
|
|
174
177
|
return props.apiClient
|
|
175
178
|
.listProjectBranches(props.projectId)
|
|
176
179
|
.then(({ data }) => {
|
|
177
|
-
const branch = data.branches.find((b) => b.
|
|
180
|
+
const branch = data.branches.find((b) => b.default);
|
|
178
181
|
if (!branch) {
|
|
179
|
-
throw new Error('No
|
|
182
|
+
throw new Error('No default branch found');
|
|
180
183
|
}
|
|
181
184
|
return { parent_id: branch.id };
|
|
182
185
|
});
|
|
@@ -252,9 +255,9 @@ const rename = async (props) => {
|
|
|
252
255
|
fields: BRANCH_FIELDS,
|
|
253
256
|
});
|
|
254
257
|
};
|
|
255
|
-
const
|
|
258
|
+
const setDefault = async (props) => {
|
|
256
259
|
const branchId = await branchIdFromProps(props);
|
|
257
|
-
const { data } = await retryOnLock(() => props.apiClient.
|
|
260
|
+
const { data } = await retryOnLock(() => props.apiClient.setDefaultProjectBranch(props.projectId, branchId));
|
|
258
261
|
writer(props).end(data.branch, {
|
|
259
262
|
fields: BRANCH_FIELDS,
|
|
260
263
|
});
|
|
@@ -195,6 +195,20 @@ describe('branches', () => {
|
|
|
195
195
|
snapshot: true,
|
|
196
196
|
},
|
|
197
197
|
});
|
|
198
|
+
/* set default */
|
|
199
|
+
testCliCommand({
|
|
200
|
+
name: 'set default by id',
|
|
201
|
+
args: [
|
|
202
|
+
'branches',
|
|
203
|
+
'set-default',
|
|
204
|
+
'br-sunny-branch-123456',
|
|
205
|
+
'--project-id',
|
|
206
|
+
'test',
|
|
207
|
+
],
|
|
208
|
+
expected: {
|
|
209
|
+
snapshot: true,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
198
212
|
/* get */
|
|
199
213
|
testCliCommand({
|
|
200
214
|
name: 'get by id',
|
|
@@ -14,7 +14,7 @@ export const builder = (argv) => {
|
|
|
14
14
|
.example('$0 cs main@2024-01-01T00:00:00Z', 'Get connection string for the main branch at a specific point in time')
|
|
15
15
|
.example('$0 cs main@0/234235', 'Get connection string for the main branch at a specific LSN')
|
|
16
16
|
.positional('branch', {
|
|
17
|
-
describe: `Branch name or id. Defaults to the
|
|
17
|
+
describe: `Branch name or id. Defaults to the default branch if omitted. Can be written in the point-in-time format: "branch@timestamp" or "branch@lsn"`,
|
|
18
18
|
type: 'string',
|
|
19
19
|
})
|
|
20
20
|
.options({
|
package/commands/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as auth from './auth.js';
|
|
|
2
2
|
import * as projects from './projects.js';
|
|
3
3
|
import * as ipAllow from './ip_allow.js';
|
|
4
4
|
import * as users from './user.js';
|
|
5
|
+
import * as orgs from './orgs.js';
|
|
5
6
|
import * as branches from './branches.js';
|
|
6
7
|
import * as databases from './databases.js';
|
|
7
8
|
import * as roles from './roles.js';
|
|
@@ -11,6 +12,7 @@ import * as setContext from './set_context.js';
|
|
|
11
12
|
export default [
|
|
12
13
|
auth,
|
|
13
14
|
users,
|
|
15
|
+
orgs,
|
|
14
16
|
projects,
|
|
15
17
|
ipAllow,
|
|
16
18
|
branches,
|
package/commands/orgs.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { writer } from '../writer.js';
|
|
2
|
+
const ORG_FIELDS = ['id', 'name'];
|
|
3
|
+
export const command = 'orgs';
|
|
4
|
+
export const describe = 'Manage organizations';
|
|
5
|
+
export const aliases = ['org'];
|
|
6
|
+
export const builder = (argv) => {
|
|
7
|
+
return argv.usage('$0 orgs <sub-command> [options]').command('list', 'List organizations', (yargs) => yargs, async (args) => {
|
|
8
|
+
// @ts-expect-error: TODO - Assert `args` is `CommonProps`
|
|
9
|
+
await list(args);
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
export const handler = (args) => {
|
|
13
|
+
return args;
|
|
14
|
+
};
|
|
15
|
+
const list = async (props) => {
|
|
16
|
+
const out = writer(props);
|
|
17
|
+
const { data: { organizations }, } = await props.apiClient.getCurrentUserOrganizations();
|
|
18
|
+
out.write(organizations, {
|
|
19
|
+
fields: ORG_FIELDS,
|
|
20
|
+
title: 'Organizations',
|
|
21
|
+
});
|
|
22
|
+
out.end();
|
|
23
|
+
};
|
package/commands/projects.js
CHANGED
package/commands/schema_diff.js
CHANGED
|
@@ -118,7 +118,7 @@ const generateHeader = (pointInTime) => {
|
|
|
118
118
|
/*
|
|
119
119
|
The command has two positional optional arguments - [base-branch] and [compare-source]
|
|
120
120
|
If only one argument is specified, we should consider it as `compare-source`
|
|
121
|
-
and `base-branch` will be either read from context or the
|
|
121
|
+
and `base-branch` will be either read from context or the default branch of project.
|
|
122
122
|
If no branches are specified, compare the context branch with its parent
|
|
123
123
|
*/
|
|
124
124
|
export const parseSchemaDiffParams = async (props) => {
|
|
@@ -138,11 +138,11 @@ export const parseSchemaDiffParams = async (props) => {
|
|
|
138
138
|
}
|
|
139
139
|
else {
|
|
140
140
|
const { data } = await props.apiClient.listProjectBranches(props.projectId);
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
throw new Error('No branch specified. Include a base branch or add a set-context branch to continue. Your
|
|
141
|
+
const defaultBranch = data.branches.find((b) => b.default);
|
|
142
|
+
if (defaultBranch?.parent_id == undefined) {
|
|
143
|
+
throw new Error('No branch specified. Include a base branch or add a set-context branch to continue. Your default branch has no parent, so no comparison is possible.');
|
|
144
144
|
}
|
|
145
|
-
log.info(`No branches specified. Comparing
|
|
145
|
+
log.info(`No branches specified. Comparing default branch with its parent`);
|
|
146
146
|
props.compareSource = '^parent';
|
|
147
147
|
}
|
|
148
148
|
}
|
package/commands/set_context.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { updateContextFile } from '../context.js';
|
|
2
|
-
import { branchIdFromProps } from '../utils/enrichers.js';
|
|
3
2
|
export const command = 'set-context';
|
|
4
3
|
export const describe = 'Set the current context';
|
|
5
4
|
export const builder = (argv) => argv.usage('$0 set-context [options]').options({
|
|
@@ -7,16 +6,10 @@ export const builder = (argv) => argv.usage('$0 set-context [options]').options(
|
|
|
7
6
|
describe: 'Project ID',
|
|
8
7
|
type: 'string',
|
|
9
8
|
},
|
|
10
|
-
branch: {
|
|
11
|
-
describe: 'Branch ID or name',
|
|
12
|
-
type: 'string',
|
|
13
|
-
},
|
|
14
9
|
});
|
|
15
10
|
export const handler = async (props) => {
|
|
16
|
-
const branchId = await branchIdFromProps(props);
|
|
17
11
|
const context = {
|
|
18
12
|
projectId: props.projectId,
|
|
19
|
-
branchId,
|
|
20
13
|
};
|
|
21
14
|
updateContextFile(props.contextFile, context);
|
|
22
15
|
};
|
|
@@ -28,24 +28,64 @@ describe('set_context', () => {
|
|
|
28
28
|
});
|
|
29
29
|
const overrideContextFile = join(tmpdir(), `neon_override_ctx_${Date.now()}`);
|
|
30
30
|
testCliCommand({
|
|
31
|
-
name: 'get
|
|
31
|
+
name: 'get project id overrides context set project',
|
|
32
32
|
before: async () => {
|
|
33
33
|
writeFileSync(overrideContextFile, JSON.stringify({
|
|
34
|
-
projectId: '
|
|
35
|
-
branchId: 'br-cloudy-branch-12345678',
|
|
34
|
+
projectId: 'new-project id',
|
|
36
35
|
}));
|
|
37
36
|
},
|
|
38
37
|
after: async () => {
|
|
39
38
|
rmSync(overrideContextFile);
|
|
40
39
|
},
|
|
41
40
|
args: [
|
|
42
|
-
'
|
|
41
|
+
'project',
|
|
43
42
|
'get',
|
|
44
|
-
'
|
|
43
|
+
'project-id-123',
|
|
44
|
+
'--context-file',
|
|
45
|
+
overrideContextFile,
|
|
46
|
+
],
|
|
47
|
+
expected: {
|
|
48
|
+
snapshot: true,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
testCliCommand({
|
|
52
|
+
name: 'set the branchId and projectId is from context',
|
|
53
|
+
before: async () => {
|
|
54
|
+
writeFileSync(overrideContextFile, JSON.stringify({
|
|
55
|
+
projectId: 'test',
|
|
56
|
+
branchId: 'test_branch',
|
|
57
|
+
}));
|
|
58
|
+
},
|
|
59
|
+
after: async () => {
|
|
60
|
+
rmSync(overrideContextFile);
|
|
61
|
+
},
|
|
62
|
+
args: ['databases', 'list', '--context-file', overrideContextFile],
|
|
63
|
+
expected: {
|
|
64
|
+
snapshot: true,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
testCliCommand({
|
|
68
|
+
name: 'should not set branchId from context for non-context projectId',
|
|
69
|
+
before: async () => {
|
|
70
|
+
writeFileSync(overrideContextFile, JSON.stringify({
|
|
71
|
+
projectId: 'project-id-123',
|
|
72
|
+
branchId: 'test_branch',
|
|
73
|
+
}));
|
|
74
|
+
},
|
|
75
|
+
after: async () => {
|
|
76
|
+
rmSync(overrideContextFile);
|
|
77
|
+
},
|
|
78
|
+
args: [
|
|
79
|
+
'databases',
|
|
80
|
+
'list',
|
|
81
|
+
'--project-id',
|
|
82
|
+
'test',
|
|
45
83
|
'--context-file',
|
|
46
84
|
overrideContextFile,
|
|
47
85
|
],
|
|
48
86
|
expected: {
|
|
87
|
+
code: 1,
|
|
88
|
+
stderr: 'ERROR: Not Found',
|
|
49
89
|
snapshot: true,
|
|
50
90
|
},
|
|
51
91
|
});
|
package/context.js
CHANGED
|
@@ -36,12 +36,15 @@ export const enrichFromContext = (args) => {
|
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
const context = readContextFile(args.contextFile);
|
|
39
|
-
if (!args.branch && !args.id && !args.name) {
|
|
40
|
-
args.branch = context.branchId;
|
|
41
|
-
}
|
|
42
39
|
if (!args.projectId) {
|
|
43
40
|
args.projectId = context.projectId;
|
|
44
41
|
}
|
|
42
|
+
if (!args.branch &&
|
|
43
|
+
!args.id &&
|
|
44
|
+
!args.name &&
|
|
45
|
+
context.projectId === args.projectId) {
|
|
46
|
+
args.branch = context.branchId;
|
|
47
|
+
}
|
|
45
48
|
};
|
|
46
49
|
export const updateContextFile = (file, context) => {
|
|
47
50
|
writeFileSync(file, JSON.stringify(context, null, 2));
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git+ssh://git@github.com/neondatabase/neonctl.git"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.31.1",
|
|
9
9
|
"description": "CLI tool for NeonDB Cloud management",
|
|
10
10
|
"main": "index.js",
|
|
11
11
|
"author": "NeonDB",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"typescript": "^4.7.4"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@neondatabase/api-client": "1.
|
|
57
|
+
"@neondatabase/api-client": "1.9.0",
|
|
58
58
|
"@segment/analytics-node": "^1.0.0-beta.26",
|
|
59
59
|
"axios": "^1.4.0",
|
|
60
60
|
"axios-debug-log": "^1.0.0",
|
package/parameters.gen.js
CHANGED
|
@@ -37,7 +37,7 @@ export const projectCreateRequest = {
|
|
|
37
37
|
},
|
|
38
38
|
'project.settings.allowed_ips.primary_branch_only': {
|
|
39
39
|
type: "boolean",
|
|
40
|
-
description: "If true, the list will be applied only to the
|
|
40
|
+
description: "If true, the list will be applied only to the default branch.",
|
|
41
41
|
demandOption: false,
|
|
42
42
|
},
|
|
43
43
|
'project.settings.enable_logical_replication': {
|
|
@@ -140,7 +140,7 @@ export const projectUpdateRequest = {
|
|
|
140
140
|
},
|
|
141
141
|
'project.settings.allowed_ips.primary_branch_only': {
|
|
142
142
|
type: "boolean",
|
|
143
|
-
description: "If true, the list will be applied only to the
|
|
143
|
+
description: "If true, the list will be applied only to the default branch.",
|
|
144
144
|
demandOption: false,
|
|
145
145
|
},
|
|
146
146
|
'project.settings.enable_logical_replication': {
|
|
@@ -172,7 +172,7 @@ export const branchCreateRequest = {
|
|
|
172
172
|
},
|
|
173
173
|
'branch.parent_id': {
|
|
174
174
|
type: "string",
|
|
175
|
-
description: "The `branch_id` of the parent branch. If omitted or empty, the branch will be created from the project's
|
|
175
|
+
description: "The `branch_id` of the parent branch. If omitted or empty, the branch will be created from the project's default branch.\n",
|
|
176
176
|
demandOption: false,
|
|
177
177
|
},
|
|
178
178
|
'branch.name': {
|
package/utils/enrichers.js
CHANGED
|
@@ -25,11 +25,11 @@ export const branchIdFromProps = async (props) => {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
const { data } = await props.apiClient.listProjectBranches(props.projectId);
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
return
|
|
28
|
+
const defaultBranch = data.branches.find((b) => b.default);
|
|
29
|
+
if (defaultBranch) {
|
|
30
|
+
return defaultBranch.id;
|
|
31
31
|
}
|
|
32
|
-
throw new Error('No
|
|
32
|
+
throw new Error('No default branch found');
|
|
33
33
|
};
|
|
34
34
|
export const fillSingleProject = async (props) => {
|
|
35
35
|
if (props.projectId) {
|