neonctl 1.28.0 → 1.29.3
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/connection_string.js +21 -2
- package/commands/connection_string.test.js +32 -0
- package/commands/projects.js +31 -15
- package/package.json +6 -5
- package/utils/point_in_time.js +24 -18
|
@@ -2,6 +2,7 @@ import { EndpointType } from '@neondatabase/api-client';
|
|
|
2
2
|
import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
3
3
|
import { writer } from '../writer.js';
|
|
4
4
|
import { psql } from '../utils/psql.js';
|
|
5
|
+
import { parsePITBranch } from '../utils/point_in_time.js';
|
|
5
6
|
const SSL_MODES = ['require', 'verify-ca', 'verify-full', 'omit'];
|
|
6
7
|
export const command = 'connection-string [branch]';
|
|
7
8
|
export const aliases = ['cs'];
|
|
@@ -9,8 +10,11 @@ export const describe = 'Get connection string';
|
|
|
9
10
|
export const builder = (argv) => {
|
|
10
11
|
return argv
|
|
11
12
|
.usage('$0 connection-string [branch] [options]')
|
|
13
|
+
.example('$0 cs main', 'Get connection string for the main branch')
|
|
14
|
+
.example('$0 cs main@2024-01-01T00:00:00Z', 'Get connection string for the main branch at a specific point in time')
|
|
15
|
+
.example('$0 cs main@0/234235', 'Get connection string for the main branch at a specific LSN')
|
|
12
16
|
.positional('branch', {
|
|
13
|
-
describe:
|
|
17
|
+
describe: `Branch name or id. Defaults to the primary branch if omitted. Can be written in the point-in-time format: "branch@timestamp" or "branch@lsn"`,
|
|
14
18
|
type: 'string',
|
|
15
19
|
})
|
|
16
20
|
.options({
|
|
@@ -61,6 +65,12 @@ export const builder = (argv) => {
|
|
|
61
65
|
};
|
|
62
66
|
export const handler = async (props) => {
|
|
63
67
|
const projectId = props.projectId;
|
|
68
|
+
const parsedPIT = props.branch
|
|
69
|
+
? parsePITBranch(props.branch)
|
|
70
|
+
: { tag: 'head', branch: '' };
|
|
71
|
+
if (props.branch) {
|
|
72
|
+
props.branch = parsedPIT.branch;
|
|
73
|
+
}
|
|
64
74
|
const branchId = await branchIdFromProps(props);
|
|
65
75
|
const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(projectId, branchId);
|
|
66
76
|
const matchEndpointType = props.endpointType ?? EndpointType.ReadWrite;
|
|
@@ -100,9 +110,12 @@ export const handler = async (props) => {
|
|
|
100
110
|
.join(', ')}`);
|
|
101
111
|
}));
|
|
102
112
|
const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(props.projectId, endpoint.branch_id, role);
|
|
103
|
-
|
|
113
|
+
let host = props.pooled
|
|
104
114
|
? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`)
|
|
105
115
|
: endpoint.host;
|
|
116
|
+
if (parsedPIT.tag !== 'head') {
|
|
117
|
+
host = endpoint.host.replace(endpoint.id, endpoint.branch_id);
|
|
118
|
+
}
|
|
106
119
|
const connectionString = new URL(`postgres://${host}`);
|
|
107
120
|
connectionString.pathname = database;
|
|
108
121
|
connectionString.username = role;
|
|
@@ -117,6 +130,12 @@ export const handler = async (props) => {
|
|
|
117
130
|
if (props.ssl !== 'omit') {
|
|
118
131
|
connectionString.searchParams.set('sslmode', props.ssl);
|
|
119
132
|
}
|
|
133
|
+
if (parsedPIT.tag === 'lsn') {
|
|
134
|
+
connectionString.searchParams.set('options', `neon_lsn:${parsedPIT.lsn}`);
|
|
135
|
+
}
|
|
136
|
+
else if (parsedPIT.tag === 'timestamp') {
|
|
137
|
+
connectionString.searchParams.set('options', `neon_timestamp:${parsedPIT.timestamp}`);
|
|
138
|
+
}
|
|
120
139
|
if (props.psql) {
|
|
121
140
|
const psqlArgs = props['--'];
|
|
122
141
|
await psql(connectionString.toString(), psqlArgs);
|
|
@@ -201,4 +201,36 @@ describe('connection_string', () => {
|
|
|
201
201
|
snapshot: true,
|
|
202
202
|
},
|
|
203
203
|
});
|
|
204
|
+
testCliCommand({
|
|
205
|
+
name: 'connection_string with lsn',
|
|
206
|
+
args: [
|
|
207
|
+
'connection-string',
|
|
208
|
+
'test_branch@0/123456',
|
|
209
|
+
'--project-id',
|
|
210
|
+
'test',
|
|
211
|
+
'--database-name',
|
|
212
|
+
'test_db',
|
|
213
|
+
'--role-name',
|
|
214
|
+
'test_role',
|
|
215
|
+
],
|
|
216
|
+
expected: {
|
|
217
|
+
snapshot: true,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
testCliCommand({
|
|
221
|
+
name: 'connection_string with timestamp',
|
|
222
|
+
args: [
|
|
223
|
+
'connection-string',
|
|
224
|
+
'test_branch@2021-01-01T00:00:00Z',
|
|
225
|
+
'--project-id',
|
|
226
|
+
'test',
|
|
227
|
+
'--database-name',
|
|
228
|
+
'test_db',
|
|
229
|
+
'--role-name',
|
|
230
|
+
'test_role',
|
|
231
|
+
],
|
|
232
|
+
expected: {
|
|
233
|
+
snapshot: true,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
204
236
|
});
|
package/commands/projects.js
CHANGED
|
@@ -82,22 +82,38 @@ export const handler = (args) => {
|
|
|
82
82
|
return args;
|
|
83
83
|
};
|
|
84
84
|
const list = async (props) => {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
const getList = async (fn) => {
|
|
86
|
+
const result = [];
|
|
87
|
+
let cursor;
|
|
88
|
+
let end = false;
|
|
89
|
+
while (!end) {
|
|
90
|
+
const { data } = await fn({
|
|
91
|
+
limit: PROJECTS_LIST_LIMIT,
|
|
92
|
+
cursor,
|
|
93
|
+
});
|
|
94
|
+
result.push(...data.projects);
|
|
95
|
+
cursor = data.pagination?.cursor;
|
|
96
|
+
log.debug('Got %d projects, with cursor: %s', data.projects.length, cursor);
|
|
97
|
+
if (data.projects.length < PROJECTS_LIST_LIMIT) {
|
|
98
|
+
end = true;
|
|
99
|
+
}
|
|
98
100
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
return result;
|
|
102
|
+
};
|
|
103
|
+
const [ownedProjects, sharedProjects] = await Promise.all([
|
|
104
|
+
getList(props.apiClient.listProjects),
|
|
105
|
+
getList(props.apiClient.listSharedProjects),
|
|
106
|
+
]);
|
|
107
|
+
const out = writer(props);
|
|
108
|
+
out.write(ownedProjects, {
|
|
109
|
+
fields: PROJECT_FIELDS,
|
|
110
|
+
title: 'Projects',
|
|
111
|
+
});
|
|
112
|
+
out.write(sharedProjects, {
|
|
113
|
+
fields: PROJECT_FIELDS,
|
|
114
|
+
title: 'Shared with me',
|
|
115
|
+
});
|
|
116
|
+
out.end();
|
|
101
117
|
};
|
|
102
118
|
const create = async (props) => {
|
|
103
119
|
const project = {};
|
package/package.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"name": "neonctl",
|
|
3
3
|
"repository": {
|
|
4
4
|
"type": "git",
|
|
5
|
-
"url": "git@github.com
|
|
5
|
+
"url": "git+ssh://git@github.com/neondatabase/neonctl.git"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.29.3",
|
|
9
9
|
"description": "CLI tool for NeonDB Cloud management",
|
|
10
10
|
"main": "index.js",
|
|
11
11
|
"author": "NeonDB",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"pkg": "^5.8.1",
|
|
47
47
|
"prettier": "^3.1.0",
|
|
48
48
|
"rollup": "^3.26.2",
|
|
49
|
-
"semantic-release": "^
|
|
49
|
+
"semantic-release": "^23.0.8",
|
|
50
50
|
"strip-ansi": "^7.1.0",
|
|
51
51
|
"ts-jest": "^29.1.0",
|
|
52
52
|
"ts-node": "^10.9.1",
|
|
@@ -72,7 +72,8 @@
|
|
|
72
72
|
},
|
|
73
73
|
"pkg": {
|
|
74
74
|
"assets": [
|
|
75
|
-
"callback.html"
|
|
75
|
+
"callback.html",
|
|
76
|
+
"package.json"
|
|
76
77
|
],
|
|
77
78
|
"scripts": [
|
|
78
79
|
"bundle/*.js"
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
"build": "npm run generateParams && npm run clean && tsc && cp src/*.html package*.json README.md ./dist",
|
|
92
93
|
"clean": "rm -rf dist",
|
|
93
94
|
"generateParams": "node --loader ts-node/esm ./generateOptionsFromSpec.ts",
|
|
94
|
-
"start": "node
|
|
95
|
+
"start": "node dist/index.js",
|
|
95
96
|
"pretest": "npm run build",
|
|
96
97
|
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
97
98
|
"prepare": "test -d .git && husky install || true"
|
package/utils/point_in_time.js
CHANGED
|
@@ -6,12 +6,12 @@ export class PointInTimeParseError extends Error {
|
|
|
6
6
|
this.name = 'PointInTimeParseError';
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
-
export const
|
|
10
|
-
const splitIndex =
|
|
11
|
-
const sourceBranch = splitIndex === -1 ?
|
|
12
|
-
const exactPIT = splitIndex === -1 ? null :
|
|
9
|
+
export const parsePITBranch = (input) => {
|
|
10
|
+
const splitIndex = input.lastIndexOf('@');
|
|
11
|
+
const sourceBranch = splitIndex === -1 ? input : input.slice(0, splitIndex);
|
|
12
|
+
const exactPIT = splitIndex === -1 ? null : input.slice(splitIndex + 1);
|
|
13
13
|
const result = {
|
|
14
|
-
|
|
14
|
+
branch: sourceBranch,
|
|
15
15
|
...(exactPIT === null
|
|
16
16
|
? { tag: 'head' }
|
|
17
17
|
: looksLikeLSN(exactPIT)
|
|
@@ -21,24 +21,30 @@ export const parsePointInTime = async ({ pointInTime, targetBranchId, projectId,
|
|
|
21
21
|
if (result.tag === 'timestamp' && !looksLikeTimestamp(result.timestamp)) {
|
|
22
22
|
throw new PointInTimeParseError('Invalid source branch format');
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
export const parsePointInTime = async ({ pointInTime, targetBranchId, projectId, api, }) => {
|
|
27
|
+
const parsedPIT = parsePITBranch(pointInTime);
|
|
28
|
+
let branchId = '';
|
|
29
|
+
if (parsedPIT.branch === '^self') {
|
|
30
|
+
branchId = targetBranchId;
|
|
29
31
|
}
|
|
30
|
-
if (
|
|
32
|
+
else if (parsedPIT.branch === '^parent') {
|
|
31
33
|
const { data } = await api.getProjectBranch(projectId, targetBranchId);
|
|
32
34
|
const { parent_id: parentId } = data.branch;
|
|
33
35
|
if (parentId == null) {
|
|
34
36
|
throw new PointInTimeParseError('Branch has no parent');
|
|
35
37
|
}
|
|
36
|
-
|
|
38
|
+
branchId = parentId;
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
else {
|
|
41
|
+
branchId = await branchIdResolve({
|
|
42
|
+
branch: parsedPIT.branch,
|
|
43
|
+
projectId,
|
|
44
|
+
apiClient: api,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// @ts-expect-error extracting pit from parsedPIT
|
|
48
|
+
delete parsedPIT.branch;
|
|
49
|
+
return { ...parsedPIT, branchId };
|
|
44
50
|
};
|