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.
@@ -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: 'Branch name or id. If ommited will use the primary branch',
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
- const host = props.pooled
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
  });
@@ -82,22 +82,38 @@ export const handler = (args) => {
82
82
  return args;
83
83
  };
84
84
  const list = async (props) => {
85
- const result = [];
86
- let cursor;
87
- let end = false;
88
- while (!end) {
89
- const { data } = await props.apiClient.listProjects({
90
- limit: PROJECTS_LIST_LIMIT,
91
- cursor,
92
- });
93
- result.push(...data.projects);
94
- cursor = data.pagination?.cursor;
95
- log.debug('Got %d projects, with cursor: %s', data.projects.length, cursor);
96
- if (data.projects.length < PROJECTS_LIST_LIMIT) {
97
- end = true;
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
- writer(props).end(result, { fields: PROJECT_FIELDS });
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:neondatabase/neonctl.git"
5
+ "url": "git+ssh://git@github.com/neondatabase/neonctl.git"
6
6
  },
7
7
  "type": "module",
8
- "version": "1.28.0",
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": "^21.0.2",
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 src/index.js",
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"
@@ -6,12 +6,12 @@ export class PointInTimeParseError extends Error {
6
6
  this.name = 'PointInTimeParseError';
7
7
  }
8
8
  }
9
- export const parsePointInTime = async ({ pointInTime, targetBranchId, projectId, api, }) => {
10
- const splitIndex = pointInTime.lastIndexOf('@');
11
- const sourceBranch = splitIndex === -1 ? pointInTime : pointInTime.slice(0, splitIndex);
12
- const exactPIT = splitIndex === -1 ? null : pointInTime.slice(splitIndex + 1);
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
- branchId: '',
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
- if (sourceBranch === '^self') {
25
- return {
26
- ...result,
27
- branchId: targetBranchId,
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 (sourceBranch === '^parent') {
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
- return { ...result, branchId: parentId };
38
+ branchId = parentId;
37
39
  }
38
- const branchId = await branchIdResolve({
39
- branch: sourceBranch,
40
- projectId,
41
- apiClient: api,
42
- });
43
- return { ...result, branchId };
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
  };