neonctl 1.26.2 → 1.27.0-beta.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/commands/branches.js +77 -1
- package/commands/branches.test.js +73 -26
- package/commands/connection_string.js +8 -0
- package/commands/connection_string.test.js +18 -0
- package/commands/ip_allow.test.js +3 -6
- package/help.js +29 -2
- package/index.js +5 -2
- package/package.json +1 -1
- package/test_utils/mock_server.js +3 -1
- package/test_utils/test_cli_command.js +6 -3
- package/utils/point_in_time.js +44 -0
- package/utils/ui.js +3 -0
package/commands/branches.js
CHANGED
|
@@ -2,9 +2,11 @@ import { EndpointType } from '@neondatabase/api-client';
|
|
|
2
2
|
import { writer } from '../writer.js';
|
|
3
3
|
import { branchCreateRequest } from '../parameters.gen.js';
|
|
4
4
|
import { retryOnLock } from '../api.js';
|
|
5
|
-
import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
5
|
+
import { branchIdFromProps, branchIdResolve, fillSingleProject, } from '../utils/enrichers.js';
|
|
6
6
|
import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp, } from '../utils/formats.js';
|
|
7
7
|
import { psql } from '../utils/psql.js';
|
|
8
|
+
import { parsePointInTime } from '../utils/point_in_time.js';
|
|
9
|
+
import { log } from '../log.js';
|
|
8
10
|
const BRANCH_FIELDS = [
|
|
9
11
|
'id',
|
|
10
12
|
'name',
|
|
@@ -72,6 +74,38 @@ export const builder = (argv) => argv
|
|
|
72
74
|
describe: 'Name under which to preserve the old branch',
|
|
73
75
|
},
|
|
74
76
|
}), async (args) => await reset(args))
|
|
77
|
+
.command('restore <target-id|name> <source>[@(timestamp|lsn)]', 'Restores a branch to a specific point in time\n<source> can be: ^self, ^parent, or <source-branch-id|name>', (yargs) => yargs
|
|
78
|
+
// we want to show meaningful help for the command
|
|
79
|
+
// but it makes yargs to fail on parsing the command
|
|
80
|
+
// so we need to fill in the missing args manually
|
|
81
|
+
.middleware((args) => {
|
|
82
|
+
args.id = args.targetId;
|
|
83
|
+
args.pointInTime = args['source@(timestamp'];
|
|
84
|
+
})
|
|
85
|
+
.usage('$0 branches restore <target-id|name> <source>[@(timestamp|lsn)]')
|
|
86
|
+
.options({
|
|
87
|
+
'preserve-under-name': {
|
|
88
|
+
describe: 'Name under which to preserve the old branch',
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
.example([
|
|
92
|
+
[
|
|
93
|
+
'$0 branches restore main br-source-branch-123456',
|
|
94
|
+
'Restore main to the head of the branch with id br-source-branch-123456',
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
'$0 branches restore main source@2021-01-01T00:00:00Z',
|
|
98
|
+
'Restore main to the timestamp 2021-01-01T00:00:00Z of the source branch',
|
|
99
|
+
],
|
|
100
|
+
[
|
|
101
|
+
'$0 branches restore my-branch ^self@0/123456',
|
|
102
|
+
'Restore my-branch to the LSN 0/123456 of the branch itself',
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
'$0 branches restore my-branch ^parent',
|
|
106
|
+
'Restore my-branch to the head of the parent branch',
|
|
107
|
+
],
|
|
108
|
+
]), async (args) => await restore(args))
|
|
75
109
|
.command('rename <id|name> <new-name>', 'Rename a branch', (yargs) => yargs, async (args) => await rename(args))
|
|
76
110
|
.command('set-primary <id|name>', 'Set a branch as primary', (yargs) => yargs, async (args) => await setPrimary(args))
|
|
77
111
|
.command('add-compute <id|name>', 'Add a compute to a branch', (yargs) => yargs.options({
|
|
@@ -233,3 +267,45 @@ const reset = async (props) => {
|
|
|
233
267
|
fields: BRANCH_FIELDS_RESET,
|
|
234
268
|
});
|
|
235
269
|
};
|
|
270
|
+
const restore = async (props) => {
|
|
271
|
+
const targetBranchId = await branchIdResolve({
|
|
272
|
+
branch: props.id,
|
|
273
|
+
projectId: props.projectId,
|
|
274
|
+
apiClient: props.apiClient,
|
|
275
|
+
});
|
|
276
|
+
const pointInTime = await parsePointInTime({
|
|
277
|
+
pointInTime: props.pointInTime,
|
|
278
|
+
targetBranchId,
|
|
279
|
+
projectId: props.projectId,
|
|
280
|
+
api: props.apiClient,
|
|
281
|
+
});
|
|
282
|
+
log.info(`Restoring branch ${targetBranchId} to the branch ${pointInTime.branchId} ${(pointInTime.tag === 'lsn' && 'LSN ' + pointInTime.lsn) ||
|
|
283
|
+
(pointInTime.tag === 'timestamp' &&
|
|
284
|
+
'timestamp ' + pointInTime.timestamp) ||
|
|
285
|
+
'head'}`);
|
|
286
|
+
const { data } = await retryOnLock(() => props.apiClient.request({
|
|
287
|
+
method: 'POST',
|
|
288
|
+
path: `/projects/${props.projectId}/branches/${targetBranchId}/reset`,
|
|
289
|
+
body: {
|
|
290
|
+
source_branch_id: pointInTime.branchId,
|
|
291
|
+
preserve_under_name: props.preserveUnderName || undefined,
|
|
292
|
+
...(pointInTime.tag === 'lsn' && { source_lsn: pointInTime.lsn }),
|
|
293
|
+
...(pointInTime.tag === 'timestamp' && {
|
|
294
|
+
source_timestamp: pointInTime.timestamp,
|
|
295
|
+
}),
|
|
296
|
+
},
|
|
297
|
+
}));
|
|
298
|
+
const branch = data.branch;
|
|
299
|
+
const writeInst = writer(props).write(branch, {
|
|
300
|
+
title: 'Restored branch',
|
|
301
|
+
fields: ['id', 'name', 'last_reset_at'],
|
|
302
|
+
});
|
|
303
|
+
if (props.preserveUnderName && branch.parent_id) {
|
|
304
|
+
const { data } = await props.apiClient.getProjectBranch(props.projectId, branch.parent_id);
|
|
305
|
+
writeInst.write(data.branch, {
|
|
306
|
+
title: 'Backup branch',
|
|
307
|
+
fields: ['id', 'name'],
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
writeInst.end();
|
|
311
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe } from '@jest/globals';
|
|
2
2
|
import { testCliCommand } from '../test_utils/test_cli_command.js';
|
|
3
3
|
describe('branches', () => {
|
|
4
|
+
/* list */
|
|
4
5
|
testCliCommand({
|
|
5
6
|
name: 'list',
|
|
6
7
|
args: ['branches', 'list', '--project-id', 'test'],
|
|
@@ -8,6 +9,7 @@ describe('branches', () => {
|
|
|
8
9
|
snapshot: true,
|
|
9
10
|
},
|
|
10
11
|
});
|
|
12
|
+
/* create */
|
|
11
13
|
testCliCommand({
|
|
12
14
|
name: 'create by default with r/w endpoint',
|
|
13
15
|
args: [
|
|
@@ -150,6 +152,7 @@ describe('branches', () => {
|
|
|
150
152
|
snapshot: true,
|
|
151
153
|
},
|
|
152
154
|
});
|
|
155
|
+
/* delete */
|
|
153
156
|
testCliCommand({
|
|
154
157
|
name: 'delete by id',
|
|
155
158
|
args: [
|
|
@@ -163,19 +166,7 @@ describe('branches', () => {
|
|
|
163
166
|
snapshot: true,
|
|
164
167
|
},
|
|
165
168
|
});
|
|
166
|
-
|
|
167
|
-
name: 'delete by id',
|
|
168
|
-
args: [
|
|
169
|
-
'branches',
|
|
170
|
-
'delete',
|
|
171
|
-
'br-cloudy-branch-12345678',
|
|
172
|
-
'--project-id',
|
|
173
|
-
'test',
|
|
174
|
-
],
|
|
175
|
-
expected: {
|
|
176
|
-
snapshot: true,
|
|
177
|
-
},
|
|
178
|
-
});
|
|
169
|
+
/* rename */
|
|
179
170
|
testCliCommand({
|
|
180
171
|
name: 'rename',
|
|
181
172
|
args: [
|
|
@@ -190,6 +181,7 @@ describe('branches', () => {
|
|
|
190
181
|
snapshot: true,
|
|
191
182
|
},
|
|
192
183
|
});
|
|
184
|
+
/* set primary */
|
|
193
185
|
testCliCommand({
|
|
194
186
|
name: 'set primary by id',
|
|
195
187
|
args: [
|
|
@@ -203,19 +195,7 @@ describe('branches', () => {
|
|
|
203
195
|
snapshot: true,
|
|
204
196
|
},
|
|
205
197
|
});
|
|
206
|
-
|
|
207
|
-
name: 'set primary by id',
|
|
208
|
-
args: [
|
|
209
|
-
'branches',
|
|
210
|
-
'set-primary',
|
|
211
|
-
'br-cloudy-branch-12345678',
|
|
212
|
-
'--project-id',
|
|
213
|
-
'test',
|
|
214
|
-
],
|
|
215
|
-
expected: {
|
|
216
|
-
snapshot: true,
|
|
217
|
-
},
|
|
218
|
-
});
|
|
198
|
+
/* get */
|
|
219
199
|
testCliCommand({
|
|
220
200
|
name: 'get by id',
|
|
221
201
|
args: ['branches', 'get', 'br-sunny-branch-123456', '--project-id', 'test'],
|
|
@@ -250,6 +230,7 @@ describe('branches', () => {
|
|
|
250
230
|
snapshot: true,
|
|
251
231
|
},
|
|
252
232
|
});
|
|
233
|
+
/* add compute */
|
|
253
234
|
testCliCommand({
|
|
254
235
|
name: 'add compute',
|
|
255
236
|
args: ['branches', 'add-compute', 'test_branch', '--project-id', 'test'],
|
|
@@ -257,6 +238,7 @@ describe('branches', () => {
|
|
|
257
238
|
snapshot: true,
|
|
258
239
|
},
|
|
259
240
|
});
|
|
241
|
+
/* reset */
|
|
260
242
|
testCliCommand({
|
|
261
243
|
name: 'reset branch to parent',
|
|
262
244
|
args: [
|
|
@@ -271,4 +253,69 @@ describe('branches', () => {
|
|
|
271
253
|
snapshot: true,
|
|
272
254
|
},
|
|
273
255
|
});
|
|
256
|
+
/* restore */
|
|
257
|
+
testCliCommand({
|
|
258
|
+
name: 'restore branch to lsn',
|
|
259
|
+
args: [
|
|
260
|
+
'branches',
|
|
261
|
+
'restore',
|
|
262
|
+
'br-self-tolsn-123456',
|
|
263
|
+
'^self@0/123ABC',
|
|
264
|
+
'--project-id',
|
|
265
|
+
'test',
|
|
266
|
+
'--preserve-under-name',
|
|
267
|
+
'backup',
|
|
268
|
+
],
|
|
269
|
+
mockDir: 'restore',
|
|
270
|
+
expected: {
|
|
271
|
+
snapshot: true,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
testCliCommand({
|
|
275
|
+
name: 'restore to parent branch timestamp by name',
|
|
276
|
+
args: [
|
|
277
|
+
'branches',
|
|
278
|
+
'restore',
|
|
279
|
+
'parent-tots',
|
|
280
|
+
'^parent@2021-01-01T00:00:00.000Z',
|
|
281
|
+
'--project-id',
|
|
282
|
+
'test',
|
|
283
|
+
],
|
|
284
|
+
mockDir: 'restore',
|
|
285
|
+
expected: {
|
|
286
|
+
snapshot: true,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
testCliCommand({
|
|
290
|
+
name: 'restore to another branch head',
|
|
291
|
+
args: [
|
|
292
|
+
'branches',
|
|
293
|
+
'restore',
|
|
294
|
+
'br-another-branch-123456',
|
|
295
|
+
'br-any-branch-123456',
|
|
296
|
+
'--project-id',
|
|
297
|
+
'test',
|
|
298
|
+
],
|
|
299
|
+
mockDir: 'restore',
|
|
300
|
+
expected: {
|
|
301
|
+
snapshot: true,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
testCliCommand({
|
|
305
|
+
name: 'restore with unexisted branch outputs error',
|
|
306
|
+
args: [
|
|
307
|
+
'branches',
|
|
308
|
+
'restore',
|
|
309
|
+
'unexisting-branch',
|
|
310
|
+
'^parent',
|
|
311
|
+
'--project-id',
|
|
312
|
+
'test',
|
|
313
|
+
],
|
|
314
|
+
mockDir: 'restore',
|
|
315
|
+
expected: {
|
|
316
|
+
code: 1,
|
|
317
|
+
stderr: `ERROR: Branch unexisting-branch not found.
|
|
318
|
+
Available branches: self-tolsn-123456, any-branch, parent-tots, another-branch`,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
274
321
|
});
|
|
@@ -49,6 +49,11 @@ export const builder = (argv) => {
|
|
|
49
49
|
describe: 'Connect to a database via psql using connection string',
|
|
50
50
|
default: false,
|
|
51
51
|
},
|
|
52
|
+
ssl: {
|
|
53
|
+
type: 'boolean',
|
|
54
|
+
describe: 'Add sslmode=require to the connection string',
|
|
55
|
+
default: true,
|
|
56
|
+
},
|
|
52
57
|
})
|
|
53
58
|
.middleware(fillSingleProject);
|
|
54
59
|
};
|
|
@@ -107,6 +112,9 @@ export const handler = async (props) => {
|
|
|
107
112
|
connectionString.searchParams.set('pgbouncer', 'true');
|
|
108
113
|
}
|
|
109
114
|
}
|
|
115
|
+
if (props.ssl) {
|
|
116
|
+
connectionString.searchParams.set('sslmode', 'require');
|
|
117
|
+
}
|
|
110
118
|
if (props.psql) {
|
|
111
119
|
const psqlArgs = props['--'];
|
|
112
120
|
await psql(connectionString.toString(), psqlArgs);
|
|
@@ -165,4 +165,22 @@ describe('connection_string', () => {
|
|
|
165
165
|
snapshot: true,
|
|
166
166
|
},
|
|
167
167
|
});
|
|
168
|
+
testCliCommand({
|
|
169
|
+
name: 'connection_string without ssl',
|
|
170
|
+
args: [
|
|
171
|
+
'connection-string',
|
|
172
|
+
'test_branch',
|
|
173
|
+
'--project-id',
|
|
174
|
+
'test',
|
|
175
|
+
'--database-name',
|
|
176
|
+
'test_db',
|
|
177
|
+
'--role-name',
|
|
178
|
+
'test_role',
|
|
179
|
+
'--ssl',
|
|
180
|
+
'false',
|
|
181
|
+
],
|
|
182
|
+
expected: {
|
|
183
|
+
snapshot: true,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
168
186
|
});
|
|
@@ -21,8 +21,7 @@ describe('ip-allow', () => {
|
|
|
21
21
|
args: ['ip-allow', 'add', '--projectId', 'test'],
|
|
22
22
|
expected: {
|
|
23
23
|
stderr: `ERROR: Enter individual IP addresses, define ranges with a dash, or use CIDR notation for more flexibility.
|
|
24
|
-
Example: neonctl ip-allow add 192.168.1.1, 192.168.1.20-192.168.1.50, 192.168.1.0/24 --project-id <id
|
|
25
|
-
`,
|
|
24
|
+
Example: neonctl ip-allow add 192.168.1.1, 192.168.1.20-192.168.1.50, 192.168.1.0/24 --project-id <id>`,
|
|
26
25
|
},
|
|
27
26
|
});
|
|
28
27
|
testCliCommand({
|
|
@@ -44,8 +43,7 @@ describe('ip-allow', () => {
|
|
|
44
43
|
name: 'Remove IP allow - Error',
|
|
45
44
|
args: ['ip-allow', 'remove', '--project-id', 'test'],
|
|
46
45
|
expected: {
|
|
47
|
-
stderr: `ERROR: Remove individual IP addresses and ranges. Example: neonctl ip-allow remove 192.168.1.1 --project-id <id
|
|
48
|
-
`,
|
|
46
|
+
stderr: `ERROR: Remove individual IP addresses and ranges. Example: neonctl ip-allow remove 192.168.1.1 --project-id <id>`,
|
|
49
47
|
},
|
|
50
48
|
});
|
|
51
49
|
testCliCommand({
|
|
@@ -65,8 +63,7 @@ name: test_project
|
|
|
65
63
|
IP_addresses: []
|
|
66
64
|
primary_branch_only: false
|
|
67
65
|
`,
|
|
68
|
-
stderr: `INFO: The IP allowlist has been reset. All databases on project "test_project" are now exposed to the internet
|
|
69
|
-
`,
|
|
66
|
+
stderr: `INFO: The IP allowlist has been reset. All databases on project "test_project" are now exposed to the internet`,
|
|
70
67
|
},
|
|
71
68
|
});
|
|
72
69
|
testCliCommand({
|
package/help.js
CHANGED
|
@@ -22,6 +22,14 @@ const formatHelp = (help) => {
|
|
|
22
22
|
width: 0,
|
|
23
23
|
});
|
|
24
24
|
commandsBlock.forEach((line) => {
|
|
25
|
+
if (line.match(/^\s{3,}/)) {
|
|
26
|
+
ui.div({
|
|
27
|
+
text: '',
|
|
28
|
+
width: SPACE_WIDTH,
|
|
29
|
+
padding: [0, 0, 0, 0],
|
|
30
|
+
}, { text: line.trim(), padding: [0, 0, 0, 0] });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
25
33
|
const [command, description] = splitColumns(line);
|
|
26
34
|
// patch the previous command if it was multiline
|
|
27
35
|
if (!description && ui.rows.length > 1) {
|
|
@@ -65,7 +73,7 @@ const formatHelp = (help) => {
|
|
|
65
73
|
// example command to see: neonctl projects list
|
|
66
74
|
const descritpionBlock = consumeBlockIfMatches(lines, /^(?!.*options:)/i);
|
|
67
75
|
if (descritpionBlock.length > 0) {
|
|
68
|
-
result.push(descritpionBlock
|
|
76
|
+
result.push(...descritpionBlock);
|
|
69
77
|
result.push('');
|
|
70
78
|
}
|
|
71
79
|
while (true) {
|
|
@@ -109,11 +117,30 @@ const formatHelp = (help) => {
|
|
|
109
117
|
});
|
|
110
118
|
result.push('');
|
|
111
119
|
}
|
|
120
|
+
const exampleBlock = consumeBlockIfMatches(lines, /Examples:/);
|
|
121
|
+
if (exampleBlock.length > 0) {
|
|
122
|
+
result.push(exampleBlock.shift());
|
|
123
|
+
const ui = cliui({
|
|
124
|
+
width: 0,
|
|
125
|
+
});
|
|
126
|
+
for (const line of exampleBlock) {
|
|
127
|
+
const [command, description] = splitColumns(line);
|
|
128
|
+
ui.div({
|
|
129
|
+
text: chalk.bold(command),
|
|
130
|
+
padding: [0, 0, 0, 0],
|
|
131
|
+
});
|
|
132
|
+
ui.div({
|
|
133
|
+
text: chalk.reset(description),
|
|
134
|
+
padding: [0, 0, 0, 2],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
result.push(ui.toString());
|
|
138
|
+
}
|
|
112
139
|
return [...result, ...lines];
|
|
113
140
|
};
|
|
114
141
|
export const showHelp = async (argv) => {
|
|
115
142
|
// add wrap to ensure that there are no line breaks
|
|
116
|
-
const help = await argv.
|
|
143
|
+
const help = await argv.getHelp();
|
|
117
144
|
process.stderr.write(formatHelp(help).join('\n') + '\n');
|
|
118
145
|
process.exit(0);
|
|
119
146
|
};
|
package/index.js
CHANGED
|
@@ -129,6 +129,7 @@ builder = builder
|
|
|
129
129
|
.completion()
|
|
130
130
|
.scriptName(basename(process.argv[1]) === 'neon' ? 'neon' : 'neonctl')
|
|
131
131
|
.epilog('For more information, visit https://neon.tech/docs/reference/neon-cli')
|
|
132
|
+
.wrap(null)
|
|
132
133
|
.fail(async (msg, err) => {
|
|
133
134
|
if (process.argv.some((arg) => arg === '--help' || arg === '-h')) {
|
|
134
135
|
await showHelp(builder);
|
|
@@ -144,8 +145,10 @@ builder = builder
|
|
|
144
145
|
log.error('Authentication failed, please run `neonctl auth`');
|
|
145
146
|
}
|
|
146
147
|
else {
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
if (err.response?.data?.message) {
|
|
149
|
+
log.error(err.response?.data?.message);
|
|
150
|
+
}
|
|
151
|
+
log.debug('status: %d %s | path: %s', err.response?.status, err.response?.statusText, err.request?.path);
|
|
149
152
|
sendError(err, 'API_ERROR');
|
|
150
153
|
}
|
|
151
154
|
}
|
package/package.json
CHANGED
|
@@ -5,7 +5,9 @@ import { log } from '../log';
|
|
|
5
5
|
export const runMockServer = async (mockDir) => new Promise((resolve) => {
|
|
6
6
|
const app = express();
|
|
7
7
|
app.use(express.json());
|
|
8
|
-
app.use('/', emocks(join(process.cwd(), 'mocks', mockDir)
|
|
8
|
+
app.use('/', emocks(join(process.cwd(), 'mocks', mockDir), {
|
|
9
|
+
'404': (req, res) => res.status(404).send({ message: 'Not Found' }),
|
|
10
|
+
}));
|
|
9
11
|
const server = app.listen(0);
|
|
10
12
|
server.on('listening', () => {
|
|
11
13
|
resolve(server);
|
|
@@ -33,6 +33,7 @@ export const testCliCommand = ({ args, name, expected, before, after, mockDir =
|
|
|
33
33
|
'yaml',
|
|
34
34
|
'--api-key',
|
|
35
35
|
'test-key',
|
|
36
|
+
'--no-analytics',
|
|
36
37
|
...args,
|
|
37
38
|
], {
|
|
38
39
|
stdio: 'pipe',
|
|
@@ -53,8 +54,8 @@ export const testCliCommand = ({ args, name, expected, before, after, mockDir =
|
|
|
53
54
|
});
|
|
54
55
|
cp.on('close', (code) => {
|
|
55
56
|
try {
|
|
56
|
-
expect(code).toBe(0);
|
|
57
|
-
if (
|
|
57
|
+
expect(code).toBe(expected?.code ?? 0);
|
|
58
|
+
if (expected) {
|
|
58
59
|
if (expected.snapshot) {
|
|
59
60
|
expect(output).toMatchSnapshot();
|
|
60
61
|
}
|
|
@@ -62,7 +63,9 @@ export const testCliCommand = ({ args, name, expected, before, after, mockDir =
|
|
|
62
63
|
expect(strip(output)).toEqual(expected.stdout);
|
|
63
64
|
}
|
|
64
65
|
if (expected.stderr !== undefined) {
|
|
65
|
-
expect(strip(error)).toEqual(expected.stderr
|
|
66
|
+
expect(strip(error).replace(/\s+/g, ' ').trim()).toEqual(typeof expected.stderr === 'string'
|
|
67
|
+
? expected.stderr.toString().replace(/\s+/g, ' ')
|
|
68
|
+
: expected.stderr);
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
resolve();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { looksLikeLSN, looksLikeTimestamp } from './formats.js';
|
|
2
|
+
import { branchIdResolve } from './enrichers.js';
|
|
3
|
+
export class PointInTimeParseError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'PointInTimeParseError';
|
|
7
|
+
}
|
|
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);
|
|
13
|
+
const result = {
|
|
14
|
+
branchId: '',
|
|
15
|
+
...(exactPIT === null
|
|
16
|
+
? { tag: 'head' }
|
|
17
|
+
: looksLikeLSN(exactPIT)
|
|
18
|
+
? { tag: 'lsn', lsn: exactPIT }
|
|
19
|
+
: { tag: 'timestamp', timestamp: exactPIT }),
|
|
20
|
+
};
|
|
21
|
+
if (result.tag === 'timestamp' && !looksLikeTimestamp(result.timestamp)) {
|
|
22
|
+
throw new PointInTimeParseError('Invalid source branch format');
|
|
23
|
+
}
|
|
24
|
+
if (sourceBranch === '^self') {
|
|
25
|
+
return {
|
|
26
|
+
...result,
|
|
27
|
+
branchId: targetBranchId,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (sourceBranch === '^parent') {
|
|
31
|
+
const { data } = await api.getProjectBranch(projectId, targetBranchId);
|
|
32
|
+
const { parent_id: parentId } = data.branch;
|
|
33
|
+
if (parentId == null) {
|
|
34
|
+
throw new PointInTimeParseError('Branch has no parent');
|
|
35
|
+
}
|
|
36
|
+
return { ...result, branchId: parentId };
|
|
37
|
+
}
|
|
38
|
+
const branchId = await branchIdResolve({
|
|
39
|
+
branch: sourceBranch,
|
|
40
|
+
projectId,
|
|
41
|
+
apiClient: api,
|
|
42
|
+
});
|
|
43
|
+
return { ...result, branchId };
|
|
44
|
+
};
|
package/utils/ui.js
CHANGED
|
@@ -43,6 +43,9 @@ export const consumeBlockIfMatches = (lines, matcher) => {
|
|
|
43
43
|
export const splitColumns = (line) => {
|
|
44
44
|
const result = line.trim().split(/\s{2,}/);
|
|
45
45
|
result[1] = result[1] ?? '';
|
|
46
|
+
if (result.length > 2) {
|
|
47
|
+
result[1] = result.slice(1).join(' ');
|
|
48
|
+
}
|
|
46
49
|
return result;
|
|
47
50
|
};
|
|
48
51
|
export const drawPointer = (width) => {
|