neon 7.0.3 → 7.0.5
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/dist/commands/branches.js +139 -40
- package/dist/commands/connection_string.js +11 -3
- package/dist/commands/schema_diff.js +29 -10
- package/dist/human_table.js +5 -2
- package/dist/utils/enrichers.js +40 -7
- package/dist/utils/point_in_time.js +4 -4
- package/dist/writer.js +8 -5
- package/package.json +3 -3
|
@@ -6,11 +6,12 @@ import { branchCreateRequest } from "../parameters.gen.js";
|
|
|
6
6
|
import { writer } from "../writer.js";
|
|
7
7
|
import { EndpointType } from "../utils/api_enums.js";
|
|
8
8
|
import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp } from "../utils/formats.js";
|
|
9
|
-
import { branchIdFromProps,
|
|
9
|
+
import { branchIdFromProps, fillSingleProject, resolveBranchFromProps } from "../utils/enrichers.js";
|
|
10
10
|
import { getComputeUnits } from "../utils/compute_units.js";
|
|
11
11
|
import { psql } from "../utils/psql.js";
|
|
12
12
|
import { parsePointInTime } from "../utils/point_in_time.js";
|
|
13
13
|
import { parseSchemaDiffParams, schemaDiff } from "./schema_diff.js";
|
|
14
|
+
import chalk from "chalk";
|
|
14
15
|
//#region src/commands/branches.ts
|
|
15
16
|
var branches_exports = /* @__PURE__ */ __exportAll({
|
|
16
17
|
BRANCH_FIELDS: () => BRANCH_FIELDS,
|
|
@@ -27,14 +28,64 @@ const BRANCH_FIELDS = [
|
|
|
27
28
|
"expires_at",
|
|
28
29
|
"created_at"
|
|
29
30
|
];
|
|
30
|
-
const BRANCH_FIELDS_RESET = [
|
|
31
|
+
const BRANCH_FIELDS_RESET = [...BRANCH_FIELDS, "last_reset_at"];
|
|
32
|
+
const BRANCH_DETAIL_FIELDS = [
|
|
31
33
|
"name",
|
|
32
34
|
"id",
|
|
33
|
-
"
|
|
35
|
+
"parent_id",
|
|
34
36
|
"current_state",
|
|
37
|
+
"expires_at",
|
|
35
38
|
"created_at",
|
|
36
|
-
"
|
|
39
|
+
"created_by",
|
|
40
|
+
"last_reset_at",
|
|
41
|
+
"logical_size"
|
|
37
42
|
];
|
|
43
|
+
/**
|
|
44
|
+
* The branch `.neon` pins, by name (preferred) or id, when `.neon` links this same project;
|
|
45
|
+
* a `--project-id` pointing elsewhere makes its branch not current.
|
|
46
|
+
*/
|
|
47
|
+
const currentBranchOf = (props) => {
|
|
48
|
+
const context = readContextFile(props.contextFile);
|
|
49
|
+
return context.projectId === props.projectId ? contextBranch(context) : void 0;
|
|
50
|
+
};
|
|
51
|
+
const branchNameCell = (br, labels) => {
|
|
52
|
+
const tags = [];
|
|
53
|
+
if (br.default) tags.push(chalk.cyan("[default]"));
|
|
54
|
+
if (br.protected) tags.push(chalk.yellow("[protected]"));
|
|
55
|
+
if (labels.annotations?.[br.id]?.value.anonymized) tags.push(chalk.dim("[anon]"));
|
|
56
|
+
if (labels.current !== void 0 && (br.id === labels.current || br.name === labels.current)) tags.push(chalk.green("[current]"));
|
|
57
|
+
tags.push(br.name);
|
|
58
|
+
return tags.join(" ");
|
|
59
|
+
};
|
|
60
|
+
const stateCell = (state) => {
|
|
61
|
+
if (!state) return "";
|
|
62
|
+
if (state === "ready") return chalk.green(state);
|
|
63
|
+
if (state === "init" || state === "resetting") return chalk.yellow(state);
|
|
64
|
+
return chalk.dim(state);
|
|
65
|
+
};
|
|
66
|
+
const formatBytes = (bytes) => {
|
|
67
|
+
const units = [
|
|
68
|
+
"bytes",
|
|
69
|
+
"KiB",
|
|
70
|
+
"MiB",
|
|
71
|
+
"GiB",
|
|
72
|
+
"TiB"
|
|
73
|
+
];
|
|
74
|
+
let value = bytes;
|
|
75
|
+
let unit = 0;
|
|
76
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
77
|
+
value /= 1024;
|
|
78
|
+
unit++;
|
|
79
|
+
}
|
|
80
|
+
return unit === 0 ? `${bytes} bytes` : `${value.toFixed(1)} ${units[unit]}`;
|
|
81
|
+
};
|
|
82
|
+
const branchColumns = (labels) => ({
|
|
83
|
+
name: (br) => branchNameCell(br, labels),
|
|
84
|
+
current_state: (br) => stateCell(br.current_state),
|
|
85
|
+
expires_at: (br) => br.expires_at || "never",
|
|
86
|
+
created_by: (br) => br.created_by?.name ?? "",
|
|
87
|
+
logical_size: (br) => br.logical_size === void 0 ? "" : formatBytes(br.logical_size)
|
|
88
|
+
});
|
|
38
89
|
const command = "branches";
|
|
39
90
|
const describe = "Manage branches";
|
|
40
91
|
const aliases = ["branch"];
|
|
@@ -171,22 +222,12 @@ const handler = (args) => {
|
|
|
171
222
|
};
|
|
172
223
|
const list = async (props) => {
|
|
173
224
|
const { data: { branches, annotations } } = await props.apiClient.listProjectBranches({ projectId: props.projectId });
|
|
174
|
-
const currentBranch = contextBranch(readContextFile(props.contextFile));
|
|
175
225
|
writer(props).end(branches, {
|
|
176
226
|
fields: BRANCH_FIELDS,
|
|
177
|
-
renderColumns: {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const labels = [];
|
|
182
|
-
if (br.default) labels.push("[default]");
|
|
183
|
-
if (br.protected) labels.push("[protected]");
|
|
184
|
-
if (isAnon) labels.push("[anon]");
|
|
185
|
-
if (currentBranch !== void 0 && (br.id === currentBranch || br.name === currentBranch)) labels.push("[current]");
|
|
186
|
-
labels.push(br.name);
|
|
187
|
-
return labels.join(" ");
|
|
188
|
-
}
|
|
189
|
-
}
|
|
227
|
+
renderColumns: branchColumns({
|
|
228
|
+
annotations,
|
|
229
|
+
current: currentBranchOf(props)
|
|
230
|
+
})
|
|
190
231
|
});
|
|
191
232
|
};
|
|
192
233
|
const create = async (props) => {
|
|
@@ -237,16 +278,20 @@ const create = async (props) => {
|
|
|
237
278
|
out.write(data.branch, {
|
|
238
279
|
fields: BRANCH_FIELDS,
|
|
239
280
|
title: "branch",
|
|
240
|
-
|
|
281
|
+
humanTitle: "Branch",
|
|
282
|
+
emptyMessage: "No branches have been found.",
|
|
283
|
+
renderColumns: branchColumns({ current: currentBranchOf(props) })
|
|
241
284
|
});
|
|
242
285
|
if (writeEndpoints) out.write(endpoints, {
|
|
243
286
|
fields: ["id", "created_at"],
|
|
244
287
|
title: "endpoints",
|
|
288
|
+
humanTitle: "Compute",
|
|
245
289
|
emptyMessage: "No endpoints have been found."
|
|
246
290
|
});
|
|
247
291
|
if (writeUris) out.write(connectionUris, {
|
|
248
292
|
fields: ["connection_uri"],
|
|
249
293
|
title: "connection_uris",
|
|
294
|
+
humanTitle: "Connection string",
|
|
250
295
|
emptyMessage: "No connection uris have been found"
|
|
251
296
|
});
|
|
252
297
|
}
|
|
@@ -259,24 +304,54 @@ const create = async (props) => {
|
|
|
259
304
|
}
|
|
260
305
|
};
|
|
261
306
|
const rename = async (props) => {
|
|
262
|
-
const branchId = await
|
|
307
|
+
const { branchId, listing } = await resolveBranchFromProps(props);
|
|
263
308
|
const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.projectId, branchId, { branch: { name: props.newName } }));
|
|
264
|
-
writer(props).end(data.branch, {
|
|
309
|
+
writer(props).end(data.branch, {
|
|
310
|
+
fields: BRANCH_FIELDS,
|
|
311
|
+
renderColumns: branchColumns({
|
|
312
|
+
annotations: listing?.annotations,
|
|
313
|
+
current: currentBranchOf(props)
|
|
314
|
+
})
|
|
315
|
+
});
|
|
265
316
|
};
|
|
266
317
|
const setDefault = async (props) => {
|
|
267
|
-
const branchId = await
|
|
318
|
+
const { branchId, listing } = await resolveBranchFromProps(props);
|
|
268
319
|
const { data } = await retryOnLock(() => props.apiClient.setDefaultProjectBranch(props.projectId, branchId));
|
|
269
|
-
writer(props).end(data.branch, {
|
|
320
|
+
writer(props).end(data.branch, {
|
|
321
|
+
fields: BRANCH_FIELDS,
|
|
322
|
+
renderColumns: branchColumns({
|
|
323
|
+
annotations: listing?.annotations,
|
|
324
|
+
current: currentBranchOf(props)
|
|
325
|
+
})
|
|
326
|
+
});
|
|
270
327
|
};
|
|
271
328
|
const deleteBranch = async (props) => {
|
|
272
|
-
const branchId = await
|
|
329
|
+
const { branchId, listing } = await resolveBranchFromProps(props);
|
|
273
330
|
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranch(props.projectId, branchId));
|
|
274
|
-
if (data) writer(props).end(data.branch, {
|
|
331
|
+
if (data) writer(props).end(data.branch, {
|
|
332
|
+
fields: BRANCH_FIELDS,
|
|
333
|
+
renderColumns: branchColumns({
|
|
334
|
+
annotations: listing?.annotations,
|
|
335
|
+
current: currentBranchOf(props)
|
|
336
|
+
})
|
|
337
|
+
});
|
|
275
338
|
};
|
|
276
339
|
const get = async (props) => {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
340
|
+
const resolved = await resolveBranchFromProps(props);
|
|
341
|
+
let branch = resolved.branch;
|
|
342
|
+
let annotations = resolved.listing?.annotations;
|
|
343
|
+
if (!branch) {
|
|
344
|
+
const { data } = await props.apiClient.getProjectBranch(props.projectId, resolved.branchId);
|
|
345
|
+
branch = data.branch;
|
|
346
|
+
annotations = data.annotation ? { [data.branch.id]: data.annotation } : void 0;
|
|
347
|
+
}
|
|
348
|
+
writer(props).end(branch, {
|
|
349
|
+
fields: BRANCH_DETAIL_FIELDS,
|
|
350
|
+
renderColumns: branchColumns({
|
|
351
|
+
annotations,
|
|
352
|
+
current: currentBranchOf(props)
|
|
353
|
+
})
|
|
354
|
+
});
|
|
280
355
|
};
|
|
281
356
|
const addCompute = async (props) => {
|
|
282
357
|
const branchId = await branchIdFromProps(props);
|
|
@@ -291,34 +366,50 @@ const addCompute = async (props) => {
|
|
|
291
366
|
};
|
|
292
367
|
const reset = async (props) => {
|
|
293
368
|
if (!props.parent) throw new Error("Only resetting to parent is supported for now");
|
|
294
|
-
const branchId = await
|
|
295
|
-
const
|
|
369
|
+
const { branchId, branch, listing } = await resolveBranchFromProps(props);
|
|
370
|
+
const parent_id = branch?.parent_id ?? (await props.apiClient.getProjectBranch(props.projectId, branchId)).data.branch.parent_id;
|
|
296
371
|
if (!parent_id) throw new Error("Branch has no parent");
|
|
297
372
|
const { data } = await retryOnLock(() => props.apiClient.restoreProjectBranch(props.projectId, branchId, {
|
|
298
373
|
source_branch_id: parent_id,
|
|
299
374
|
preserve_under_name: props.preserveUnderName || void 0
|
|
300
375
|
}));
|
|
301
|
-
writer(props).end(data.branch, {
|
|
376
|
+
writer(props).end(data.branch, {
|
|
377
|
+
fields: BRANCH_FIELDS_RESET,
|
|
378
|
+
renderColumns: branchColumns({
|
|
379
|
+
annotations: listing?.annotations,
|
|
380
|
+
current: currentBranchOf(props)
|
|
381
|
+
})
|
|
382
|
+
});
|
|
302
383
|
};
|
|
303
384
|
const restore = async (props) => {
|
|
304
|
-
const targetBranchId = await
|
|
305
|
-
|
|
385
|
+
const { branchId: targetBranchId, branch: targetBranch, listing } = await resolveBranchFromProps({
|
|
386
|
+
apiClient: props.apiClient,
|
|
306
387
|
projectId: props.projectId,
|
|
307
|
-
|
|
388
|
+
id: props.id
|
|
308
389
|
});
|
|
309
390
|
const pointInTime = await parsePointInTime({
|
|
310
391
|
pointInTime: props.pointInTime,
|
|
311
392
|
targetBranchId,
|
|
312
393
|
projectId: props.projectId,
|
|
313
|
-
api: props.apiClient
|
|
394
|
+
api: props.apiClient,
|
|
395
|
+
branches: listing?.branches,
|
|
396
|
+
targetBranch
|
|
314
397
|
});
|
|
315
|
-
|
|
398
|
+
if (props.output === "table") {
|
|
399
|
+
const nameOf = (id) => listing?.branches.find((b) => b.id === id)?.name ?? id;
|
|
400
|
+
const point = pointInTime.tag === "lsn" ? `LSN ${pointInTime.lsn} of` : pointInTime.tag === "timestamp" ? `timestamp ${pointInTime.timestamp} of` : "the head of";
|
|
401
|
+
log.info(`Restoring ${nameOf(targetBranchId)} to ${point} ${nameOf(pointInTime.branchId)}`);
|
|
402
|
+
} else log.info(`Restoring branch ${targetBranchId} to the branch ${pointInTime.branchId} ${pointInTime.tag === "lsn" && "LSN " + pointInTime.lsn || pointInTime.tag === "timestamp" && "timestamp " + pointInTime.timestamp || "head"}`);
|
|
316
403
|
const { data } = await retryOnLock(() => props.apiClient.restoreProjectBranch(props.projectId, targetBranchId, {
|
|
317
404
|
source_branch_id: pointInTime.branchId,
|
|
318
405
|
preserve_under_name: props.preserveUnderName || void 0,
|
|
319
406
|
...pointInTime.tag === "lsn" && { source_lsn: pointInTime.lsn },
|
|
320
407
|
...pointInTime.tag === "timestamp" && { source_timestamp: pointInTime.timestamp }
|
|
321
408
|
}));
|
|
409
|
+
const labels = {
|
|
410
|
+
annotations: listing?.annotations,
|
|
411
|
+
current: currentBranchOf(props)
|
|
412
|
+
};
|
|
322
413
|
const writeInst = writer(props).write(data.branch, {
|
|
323
414
|
title: "Restored branch",
|
|
324
415
|
fields: [
|
|
@@ -326,7 +417,8 @@ const restore = async (props) => {
|
|
|
326
417
|
"name",
|
|
327
418
|
"last_reset_at"
|
|
328
419
|
],
|
|
329
|
-
emptyMessage: "No branches have been restored."
|
|
420
|
+
emptyMessage: "No branches have been restored.",
|
|
421
|
+
renderColumns: branchColumns(labels)
|
|
330
422
|
});
|
|
331
423
|
const parentId = data.branch.parent_id;
|
|
332
424
|
if (props.preserveUnderName && parentId) {
|
|
@@ -334,19 +426,26 @@ const restore = async (props) => {
|
|
|
334
426
|
writeInst.write(data.branch, {
|
|
335
427
|
title: "Backup branch",
|
|
336
428
|
fields: ["id", "name"],
|
|
337
|
-
emptyMessage: "Backup branch has not been found."
|
|
429
|
+
emptyMessage: "Backup branch has not been found.",
|
|
430
|
+
renderColumns: branchColumns(labels)
|
|
338
431
|
});
|
|
339
432
|
}
|
|
340
433
|
writeInst.end();
|
|
341
434
|
};
|
|
342
435
|
const setExpiration = async (props) => {
|
|
343
|
-
const branchId = await
|
|
436
|
+
const { branchId, listing } = await resolveBranchFromProps({
|
|
344
437
|
...props,
|
|
345
438
|
id: props.branchId
|
|
346
439
|
});
|
|
347
440
|
const expiresAt = typeof props.expiresAt === "string" ? new Date(props.expiresAt).toISOString() : null;
|
|
348
441
|
const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.projectId, branchId, { branch: { expires_at: expiresAt } }));
|
|
349
|
-
writer(props).end(data.branch, {
|
|
442
|
+
writer(props).end(data.branch, {
|
|
443
|
+
fields: BRANCH_FIELDS,
|
|
444
|
+
renderColumns: branchColumns({
|
|
445
|
+
annotations: listing?.annotations,
|
|
446
|
+
current: currentBranchOf(props)
|
|
447
|
+
})
|
|
448
|
+
});
|
|
350
449
|
};
|
|
351
450
|
//#endregion
|
|
352
451
|
export { BRANCH_FIELDS, aliases, builder, command, describe, handler, branches_exports as t };
|
|
@@ -85,17 +85,25 @@ const handler = async (props) => {
|
|
|
85
85
|
};
|
|
86
86
|
if (props.branch) props.branch = parsedPIT.branch;
|
|
87
87
|
const branchId = await branchIdFromProps(props);
|
|
88
|
-
const
|
|
88
|
+
const endpointsRequest = props.apiClient.listProjectBranchEndpoints(projectId, branchId);
|
|
89
|
+
const rolesRequest = props.roleName ? void 0 : props.apiClient.listProjectBranchRoles(projectId, branchId);
|
|
90
|
+
const databasesRequest = props.apiClient.listProjectBranchDatabases(projectId, branchId);
|
|
91
|
+
for (const request of [
|
|
92
|
+
endpointsRequest,
|
|
93
|
+
rolesRequest,
|
|
94
|
+
databasesRequest
|
|
95
|
+
]) request?.catch(() => void 0);
|
|
96
|
+
const { data: { endpoints } } = await endpointsRequest;
|
|
89
97
|
const matchEndpointType = props.endpointType ?? EndpointType.ReadWrite;
|
|
90
98
|
let endpoint = endpoints.find((e) => e.type === matchEndpointType);
|
|
91
99
|
if (!endpoint && props.endpointType == null) endpoint = endpoints[0];
|
|
92
100
|
if (!endpoint) throw new Error(`No ${props.endpointType ?? ""} endpoint found for the branch: ${branchId}`);
|
|
93
|
-
const role = props.roleName
|
|
101
|
+
const role = !rolesRequest ? props.roleName : await rolesRequest.then(({ data }) => {
|
|
94
102
|
if (data.roles.length === 0) throw new Error(`No roles found for the branch: ${branchId}`);
|
|
95
103
|
if (data.roles.length === 1) return data.roles[0].name;
|
|
96
104
|
throw new Error(`Multiple roles found for the branch, please provide one with the --role-name option: ${data.roles.map((r) => r.name).join(", ")}`);
|
|
97
105
|
});
|
|
98
|
-
const { data: { databases: branchDatabases } } = await
|
|
106
|
+
const { data: { databases: branchDatabases } } = await databasesRequest;
|
|
99
107
|
const database = props.databaseName || (() => {
|
|
100
108
|
if (branchDatabases.length === 0) throw new Error(`No databases found for the branch: ${branchId}`);
|
|
101
109
|
if (branchDatabases.length === 1) return branchDatabases[0].name;
|
|
@@ -2,7 +2,7 @@ import { log } from "../log.js";
|
|
|
2
2
|
import { isNeonApiError, messageFromBody } from "../api.js";
|
|
3
3
|
import { sendError } from "../analytics.js";
|
|
4
4
|
import { writer } from "../writer.js";
|
|
5
|
-
import {
|
|
5
|
+
import { listAllProjectBranchesWithAnnotations, resolveBranchFromProps } from "../utils/enrichers.js";
|
|
6
6
|
import { parsePointInTime } from "../utils/point_in_time.js";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { createPatch } from "diff";
|
|
@@ -15,12 +15,14 @@ const COLORS = {
|
|
|
15
15
|
};
|
|
16
16
|
const schemaDiff = async (props) => {
|
|
17
17
|
props.branch = props.baseBranch || props.branch;
|
|
18
|
-
const baseBranch = await
|
|
18
|
+
const { branchId: baseBranch, branch: baseBranchData, listing } = await resolveBranchFromProps(props, props.branchListing);
|
|
19
19
|
let pointInTime = await parsePointInTime({
|
|
20
20
|
pointInTime: props.compareSource,
|
|
21
21
|
targetBranchId: baseBranch,
|
|
22
22
|
projectId: props.projectId,
|
|
23
|
-
api: props.apiClient
|
|
23
|
+
api: props.apiClient,
|
|
24
|
+
branches: listing?.branches,
|
|
25
|
+
targetBranch: baseBranchData
|
|
24
26
|
});
|
|
25
27
|
const comparingWithParent = props.compareSource.startsWith("^parent");
|
|
26
28
|
let baseBranchPoint = {
|
|
@@ -32,21 +34,35 @@ const schemaDiff = async (props) => {
|
|
|
32
34
|
if (props.database) {
|
|
33
35
|
const database = baseDatabases.find((db) => db.name === props.database);
|
|
34
36
|
if (!database) throw new Error(`Database ${props.database} does not exist in base branch ${baseBranch}`);
|
|
35
|
-
const
|
|
36
|
-
writer(props).text(colorize(patch));
|
|
37
|
+
const diff = await createSchemaDiff(baseBranchPoint, pointInTime, database, props);
|
|
38
|
+
writer(props).text(colorize(diff.patch));
|
|
39
|
+
reportUnchanged(diff, [baseBranchPoint, pointInTime], database, props, listing);
|
|
37
40
|
return;
|
|
38
41
|
}
|
|
39
42
|
await Promise.all(baseDatabases.map(async (database) => {
|
|
40
|
-
const
|
|
41
|
-
writer(props).text(colorize(patch));
|
|
43
|
+
const diff = await createSchemaDiff(baseBranchPoint, pointInTime, database, props);
|
|
44
|
+
writer(props).text(colorize(diff.patch));
|
|
45
|
+
reportUnchanged(diff, [baseBranchPoint, pointInTime], database, props, listing);
|
|
42
46
|
}));
|
|
43
47
|
};
|
|
48
|
+
/** The patch alone is two header lines when nothing differs, which reads like missing output. */
|
|
49
|
+
const reportUnchanged = (diff, [base, compare], database, props, listing) => {
|
|
50
|
+
if (!diff.unchanged || props.output !== "table") return;
|
|
51
|
+
const label = (point) => {
|
|
52
|
+
const name = listing?.branches.find((b) => b.id === point.branchId)?.name ?? point.branchId;
|
|
53
|
+
return point.tag === "lsn" ? `${name} at ${point.lsn}` : point.tag === "timestamp" ? `${name} at ${point.timestamp}` : name;
|
|
54
|
+
};
|
|
55
|
+
log.info(`No schema differences for database ${database.name} between ${label(base)} and ${label(compare)}.`);
|
|
56
|
+
};
|
|
44
57
|
const fetchDatabases = async (branch, props) => {
|
|
45
58
|
return props.apiClient.listProjectBranchDatabases(props.projectId, branch).then((response) => response.data.databases);
|
|
46
59
|
};
|
|
47
60
|
const createSchemaDiff = async (baseBranch, pointInTime, database, props) => {
|
|
48
61
|
const [baseSchema, compareSchema] = await Promise.all([fetchSchema(baseBranch, database, props), fetchSchema(pointInTime, database, props)]);
|
|
49
|
-
return
|
|
62
|
+
return {
|
|
63
|
+
patch: createPatch(`Database: ${database.name}`, baseSchema, compareSchema, generateHeader(baseBranch), generateHeader(pointInTime)),
|
|
64
|
+
unchanged: baseSchema === compareSchema
|
|
65
|
+
};
|
|
50
66
|
};
|
|
51
67
|
const fetchSchema = async (pointInTime, database, props) => {
|
|
52
68
|
try {
|
|
@@ -68,6 +84,7 @@ const colorize = (patch) => {
|
|
|
68
84
|
return patch.replace(/^([^\n]+)\n([^\n]+)\n/m, "").replace(/^-.*/gm, colorizer("removed")).replace(/^\+.*/gm, colorizer("added")).replace(/^@@.+@@.*/gm, colorizer("section"));
|
|
69
85
|
};
|
|
70
86
|
const colorizer = (colorId) => {
|
|
87
|
+
if (process.env.NO_COLOR) return (line) => line;
|
|
71
88
|
const color = COLORS[colorId];
|
|
72
89
|
return (line) => color(line);
|
|
73
90
|
};
|
|
@@ -92,11 +109,13 @@ const parseSchemaDiffParams = async (props) => {
|
|
|
92
109
|
props.compareSource = props.baseBranch;
|
|
93
110
|
props.baseBranch = props.branch;
|
|
94
111
|
} else if (props.branch) {
|
|
95
|
-
|
|
112
|
+
props.branchListing = await listAllProjectBranchesWithAnnotations(props.apiClient, props.projectId);
|
|
113
|
+
if (props.branchListing.branches.find((b) => b.id === props.branch || b.name === props.branch)?.parent_id == void 0) throw new Error(`No branch specified. Your context branch (${props.branch}) has no parent, so no comparison is possible.`);
|
|
96
114
|
log.info(`No branches specified. Comparing your context branch '${props.branch}' with its parent`);
|
|
97
115
|
props.compareSource = "^parent";
|
|
98
116
|
} else {
|
|
99
|
-
|
|
117
|
+
props.branchListing = await listAllProjectBranchesWithAnnotations(props.apiClient, props.projectId);
|
|
118
|
+
if (props.branchListing.branches.find((b) => b.default)?.parent_id == void 0) 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.");
|
|
100
119
|
log.info(`No branches specified. Comparing default branch with its parent`);
|
|
101
120
|
props.compareSource = "^parent";
|
|
102
121
|
}
|
package/dist/human_table.js
CHANGED
|
@@ -22,8 +22,11 @@ function parseColumns(value) {
|
|
|
22
22
|
if (!Number.isFinite(n) || n <= 0) return;
|
|
23
23
|
return n;
|
|
24
24
|
}
|
|
25
|
+
function stripAnsi(s) {
|
|
26
|
+
return s.replace(ANSI_RE, "");
|
|
27
|
+
}
|
|
25
28
|
function displayWidth(s) {
|
|
26
|
-
const plain = s
|
|
29
|
+
const plain = stripAnsi(s);
|
|
27
30
|
let width = 0;
|
|
28
31
|
for (const char of plain) {
|
|
29
32
|
const code = char.codePointAt(0);
|
|
@@ -203,4 +206,4 @@ function charWidth(code) {
|
|
|
203
206
|
return 1;
|
|
204
207
|
}
|
|
205
208
|
//#endregion
|
|
206
|
-
export { displayWidth, formatHumanChunk, parseColumns, resolveOutputWidth };
|
|
209
|
+
export { displayWidth, formatHumanChunk, parseColumns, resolveOutputWidth, stripAnsi };
|
package/dist/utils/enrichers.js
CHANGED
|
@@ -7,8 +7,9 @@ const BRANCHES_LIST_LIMIT = 100;
|
|
|
7
7
|
* Branch list pages cap at 100. A name stored in `.neon` can live on a later
|
|
8
8
|
* page, so resolution has to walk `pagination.next` before treating it as missing.
|
|
9
9
|
*/
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const listAllProjectBranchesWithAnnotations = async (apiClient, projectId) => {
|
|
11
|
+
const branches = [];
|
|
12
|
+
const annotations = {};
|
|
12
13
|
let cursor;
|
|
13
14
|
while (true) {
|
|
14
15
|
const { data } = await apiClient.listProjectBranches({
|
|
@@ -16,16 +17,21 @@ const listAllProjectBranches = async (apiClient, projectId) => {
|
|
|
16
17
|
limit: BRANCHES_LIST_LIMIT,
|
|
17
18
|
cursor
|
|
18
19
|
});
|
|
19
|
-
|
|
20
|
+
branches.push(...data.branches);
|
|
21
|
+
Object.assign(annotations, data.annotations);
|
|
20
22
|
cursor = data.pagination?.next;
|
|
21
23
|
if (!cursor || data.branches.length === 0) break;
|
|
22
24
|
}
|
|
23
|
-
return
|
|
25
|
+
return {
|
|
26
|
+
branches,
|
|
27
|
+
annotations
|
|
28
|
+
};
|
|
24
29
|
};
|
|
25
|
-
const
|
|
30
|
+
const listAllProjectBranches = async (apiClient, projectId) => (await listAllProjectBranchesWithAnnotations(apiClient, projectId)).branches;
|
|
31
|
+
const branchIdResolve = async ({ branch, apiClient, projectId, branches: listed }) => {
|
|
26
32
|
branch = branch.toString();
|
|
27
33
|
if (looksLikeBranchId(branch)) return branch;
|
|
28
|
-
const branches = await listAllProjectBranches(apiClient, projectId);
|
|
34
|
+
const branches = listed ?? await listAllProjectBranches(apiClient, projectId);
|
|
29
35
|
const branchData = branches.find((b) => b.name === branch);
|
|
30
36
|
if (!branchData) throw new Error(`Branch ${branch} not found.\nAvailable branches: ${branches.map((b) => b.name).join(", ")}`);
|
|
31
37
|
return branchData.id;
|
|
@@ -45,6 +51,33 @@ const branchIdFromProps = async (props) => {
|
|
|
45
51
|
props.branchId = await getBranchIdFromProps(props);
|
|
46
52
|
return props.branchId;
|
|
47
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* {@link branchIdFromProps}, plus whatever the resolution already fetched: a name or the
|
|
56
|
+
* default branch is resolved from a listing, so the branch object and the listing come back
|
|
57
|
+
* with the id and callers need not fetch them again. An explicit `br-…` id is not listed.
|
|
58
|
+
*/
|
|
59
|
+
const resolveBranchFromProps = async (props, known) => {
|
|
60
|
+
const ref = typeof props.branch === "string" ? props.branch : props.id;
|
|
61
|
+
const explicit = ref ? String(ref) : void 0;
|
|
62
|
+
if (explicit !== void 0 && looksLikeBranchId(explicit)) {
|
|
63
|
+
props.branchId = explicit;
|
|
64
|
+
const branch = known?.branches.find((b) => b.id === explicit);
|
|
65
|
+
return branch ? {
|
|
66
|
+
branchId: explicit,
|
|
67
|
+
branch,
|
|
68
|
+
listing: known
|
|
69
|
+
} : { branchId: explicit };
|
|
70
|
+
}
|
|
71
|
+
const listing = known ?? await listAllProjectBranchesWithAnnotations(props.apiClient, props.projectId);
|
|
72
|
+
const branch = explicit === void 0 ? listing.branches.find((b) => b.default) : listing.branches.find((b) => b.name === explicit);
|
|
73
|
+
if (!branch) throw new Error(explicit === void 0 ? "No default branch found" : `Branch ${explicit} not found.\nAvailable branches: ${listing.branches.map((b) => b.name).join(", ")}`);
|
|
74
|
+
props.branchId = branch.id;
|
|
75
|
+
return {
|
|
76
|
+
branchId: branch.id,
|
|
77
|
+
branch,
|
|
78
|
+
listing
|
|
79
|
+
};
|
|
80
|
+
};
|
|
48
81
|
const resolveBranchRef = async (props) => {
|
|
49
82
|
const branch = typeof props.branch === "string" ? props.branch : props.id;
|
|
50
83
|
const branches = await listAllProjectBranches(props.apiClient, props.projectId);
|
|
@@ -130,4 +163,4 @@ const fillSingleOrg = async (props) => {
|
|
|
130
163
|
};
|
|
131
164
|
};
|
|
132
165
|
//#endregion
|
|
133
|
-
export { branchIdFromProps, branchIdResolve, fillSingleOrg, fillSingleProject, listAllProjectBranches, resolveBranchRef, resolveSingleDatabase };
|
|
166
|
+
export { branchIdFromProps, branchIdResolve, fillSingleOrg, fillSingleProject, listAllProjectBranches, listAllProjectBranchesWithAnnotations, resolveBranchFromProps, resolveBranchRef, resolveSingleDatabase };
|
|
@@ -28,19 +28,19 @@ const parsePITBranch = (input) => {
|
|
|
28
28
|
}
|
|
29
29
|
return result;
|
|
30
30
|
};
|
|
31
|
-
const parsePointInTime = async ({ pointInTime, targetBranchId, projectId, api }) => {
|
|
31
|
+
const parsePointInTime = async ({ pointInTime, targetBranchId, projectId, api, branches, targetBranch }) => {
|
|
32
32
|
const parsedPIT = parsePITBranch(pointInTime);
|
|
33
33
|
let branchId = "";
|
|
34
34
|
if (parsedPIT.branch === "^self") branchId = targetBranchId;
|
|
35
35
|
else if (parsedPIT.branch === "^parent") {
|
|
36
|
-
const
|
|
37
|
-
const { parent_id: parentId } = data.branch;
|
|
36
|
+
const parentId = (targetBranch?.id === targetBranchId ? targetBranch : (await api.getProjectBranch(projectId, targetBranchId)).data.branch).parent_id;
|
|
38
37
|
if (parentId == null) throw new PointInTimeParseError("Branch has no parent");
|
|
39
38
|
branchId = parentId;
|
|
40
39
|
} else branchId = await branchIdResolve({
|
|
41
40
|
branch: parsedPIT.branch,
|
|
42
41
|
projectId,
|
|
43
|
-
apiClient: api
|
|
42
|
+
apiClient: api,
|
|
43
|
+
branches
|
|
44
44
|
});
|
|
45
45
|
delete parsedPIT.branch;
|
|
46
46
|
return {
|
package/dist/writer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isCi } from "./env.js";
|
|
2
2
|
import { toSnakeCase } from "./utils/string.js";
|
|
3
|
-
import { formatHumanChunk, resolveOutputWidth } from "./human_table.js";
|
|
3
|
+
import { formatHumanChunk, resolveOutputWidth, stripAnsi } from "./human_table.js";
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
//#region src/writer.ts
|
|
6
6
|
const writeYaml = (chunks) => {
|
|
@@ -11,17 +11,20 @@ const writeJson = (chunks) => {
|
|
|
11
11
|
return json === void 0 ? json : `${json}\n`;
|
|
12
12
|
};
|
|
13
13
|
const writeTable = (chunks, out, width) => {
|
|
14
|
+
const noColor = Boolean(process.env.NO_COLOR);
|
|
14
15
|
chunks.forEach(({ data, config }, i) => {
|
|
15
|
-
|
|
16
|
+
const title = config.humanTitle ?? config.title;
|
|
17
|
+
const text = formatHumanChunk({
|
|
16
18
|
data,
|
|
17
19
|
fields: config.fields,
|
|
18
|
-
title
|
|
20
|
+
title,
|
|
19
21
|
emptyMessage: config.emptyMessage,
|
|
20
22
|
renderColumns: config.renderColumns,
|
|
21
23
|
width,
|
|
22
24
|
colorTitle: !isCi(),
|
|
23
|
-
leadingBlank: i > 0 && Boolean(
|
|
24
|
-
})
|
|
25
|
+
leadingBlank: i > 0 && Boolean(title)
|
|
26
|
+
});
|
|
27
|
+
out.write(noColor ? stripAnsi(text) : text);
|
|
25
28
|
});
|
|
26
29
|
};
|
|
27
30
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.5",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"yaml": "^2.9.0",
|
|
65
65
|
"yargs": "17.7.2",
|
|
66
66
|
"yoctocolors": "^2.1.2",
|
|
67
|
+
"@neon/sdk": "7.0.0",
|
|
67
68
|
"@neon/config": "1.8.3",
|
|
68
|
-
"@neon/config-runtime": "1.7.3"
|
|
69
|
-
"@neon/sdk": "7.0.0"
|
|
69
|
+
"@neon/config-runtime": "1.7.3"
|
|
70
70
|
},
|
|
71
71
|
"optionalDependencies": {
|
|
72
72
|
"@napi-rs/keyring": "1.3.0",
|