neon 8.4.2 → 8.4.4
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 +29 -0
- package/dist/analytics.js +2 -2
- package/dist/api.js +26 -1
- package/dist/commands/dev.js +2 -2
- package/dist/commands/env.js +1 -1
- package/dist/commands/subscribe.js +167 -54
- package/dist/index.js +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -306,6 +306,35 @@ regression + TAP tests.
|
|
|
306
306
|
|
|
307
307
|
- **TLS cipher is runtime-dependent.** The negotiated TLS 1.3 ciphersuite is chosen by the host runtime's TLS library from an offer byte-identical to libpq's. Under Node (OpenSSL) that is `TLS_AES_256_GCM_SHA384`, matching vanilla psql; under Bun (BoringSSL) it is `TLS_AES_128_GCM_SHA256`. Both are TLS 1.3 AEAD suites with no practical security difference, and neither runtime exposes a client-side knob to steer the selection.
|
|
308
308
|
|
|
309
|
+
## Subscribe to a Realtime query
|
|
310
|
+
|
|
311
|
+
`neon subscribe` streams a live SQL query until interrupted:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
neon subscribe "SELECT id, title, completed FROM realtime_todos ORDER BY id"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The default terminal output is append-only. It identifies the selected project,
|
|
318
|
+
branch, and database; prints the initial result once; reports connection-state
|
|
319
|
+
changes; and then prints each published batch with its transaction IDs, a compact
|
|
320
|
+
`+insert ~update -delete` summary, and the affected rows. It does not take over the
|
|
321
|
+
terminal or require keyboard navigation. Pass `--changes` to display the raw reset
|
|
322
|
+
and batch events instead of the materialized view.
|
|
323
|
+
|
|
324
|
+
For scripts and agents, JSON output is a JSON Lines stream with one complete event
|
|
325
|
+
per line. YAML output uses one document per event:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
neon subscribe --changes --output json "SELECT * FROM realtime_todos" |
|
|
329
|
+
jq --unbuffered 'select(.type == "batch")'
|
|
330
|
+
|
|
331
|
+
neon subscribe --output yaml "SELECT * FROM realtime_todos"
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Machine-readable output retains the event stream: without `--changes`, each event is
|
|
335
|
+
a complete `snapshot`; with `--changes`, events are raw `reset` and `batch` objects.
|
|
336
|
+
Lifecycle labels and the target header are terminal-only.
|
|
337
|
+
|
|
309
338
|
## Configure autocompletion
|
|
310
339
|
|
|
311
340
|
The Neon CLI supports autocompletion, which you can configure in a few easy steps. See [Neon CLI commands — completion](https://neon.com/docs/cli/completion) for instructions.
|
package/dist/analytics.js
CHANGED
|
@@ -2,7 +2,7 @@ import { a as OAUTH, t as storeFor } from "./_chunks/credential_io-ByNuojoE.js";
|
|
|
2
2
|
import { getCliAgent, getGithubEnvVars, isCi } from "./env.js";
|
|
3
3
|
import { log } from "./log.js";
|
|
4
4
|
import pkg_default from "./pkg.js";
|
|
5
|
-
import { getApiClient, isNeonApiError } from "./api.js";
|
|
5
|
+
import { UnreadableResponseError, getApiClient, isNeonApiError } from "./api.js";
|
|
6
6
|
import { getAuthContext } from "./auth_context.js";
|
|
7
7
|
import { n as credentialsPath } from "./_chunks/paths-DMq0Lt7a.js";
|
|
8
8
|
import "./config.js";
|
|
@@ -271,7 +271,7 @@ const sendError = (err, errCode) => {
|
|
|
271
271
|
});
|
|
272
272
|
});
|
|
273
273
|
log.debug("Sent CLI error event: %s", errCode);
|
|
274
|
-
if (errCode === "UNKNOWN_ERROR" && isUnexpectedError(err)) {
|
|
274
|
+
if (errCode === "UNKNOWN_ERROR" && isUnexpectedError(err) || err instanceof UnreadableResponseError) {
|
|
275
275
|
const report = import("./error_reporting.js").then(({ reportUnexpectedError }) => reportUnexpectedError(err, redactCredentialTokenIds)).catch((reportError) => {
|
|
276
276
|
log.debug("Could not report the error to Sentry: %s", reportError);
|
|
277
277
|
});
|
package/dist/api.js
CHANGED
|
@@ -42,6 +42,16 @@ var NeonApiError = class extends Error {
|
|
|
42
42
|
this.code = init.code;
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* A 2xx response whose body could not be read or parsed. The request succeeded, so this is
|
|
47
|
+
* reported to Sentry as well as counted as an API error.
|
|
48
|
+
*/
|
|
49
|
+
var UnreadableResponseError = class extends NeonApiError {
|
|
50
|
+
constructor(message, init) {
|
|
51
|
+
super(message, init);
|
|
52
|
+
this.name = "UnreadableResponseError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
45
55
|
/** Narrow an unknown error to a {@link NeonApiError}. */
|
|
46
56
|
function isNeonApiError(err) {
|
|
47
57
|
return err instanceof NeonApiError;
|
|
@@ -170,6 +180,20 @@ function httpError(response, body) {
|
|
|
170
180
|
});
|
|
171
181
|
}
|
|
172
182
|
/**
|
|
183
|
+
* The cause's message is left out: `JSON.parse` quotes the input, and the body can carry a
|
|
184
|
+
* password or token that would reach stderr and analytics.
|
|
185
|
+
*/
|
|
186
|
+
function unreadableBodyError(response, cause) {
|
|
187
|
+
const base = httpError(response, void 0);
|
|
188
|
+
const reason = cause instanceof SyntaxError ? "the body is not valid JSON" : "the body could not be read";
|
|
189
|
+
return new UnreadableResponseError(`Could not read the Neon API response (${response.status} ${response.statusText}) to ${base.requestPath ?? "the request"}: ${reason}`, {
|
|
190
|
+
status: base.status,
|
|
191
|
+
statusText: base.statusText,
|
|
192
|
+
headers: base.headers,
|
|
193
|
+
requestPath: base.requestPath
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
173
197
|
* Translate a thrown `fetch` failure into a {@link NeonApiError}. A request
|
|
174
198
|
* timeout uses `ECONNABORTED` (matching the old axios behaviour, and excluded
|
|
175
199
|
* from `isNetworkError`'s connectivity codes so it's still reported as a
|
|
@@ -283,6 +307,7 @@ const getApiClient = ({ apiKey, apiHost, requestTimeoutMs = REQUEST_TIMEOUT_MS }
|
|
|
283
307
|
const response = result.response;
|
|
284
308
|
if (!response) throw networkError(result.error ?? /* @__PURE__ */ new Error("No response from Neon API"));
|
|
285
309
|
if (!response.ok) throw httpError(response, rawErrorBody(result.error) ?? result.data);
|
|
310
|
+
if (result.error !== void 0 && result.error !== null) throw unreadableBodyError(response, rawErrorBody(result.error));
|
|
286
311
|
return {
|
|
287
312
|
data: result.data,
|
|
288
313
|
status: response.status,
|
|
@@ -978,4 +1003,4 @@ const getApiClient = ({ apiKey, apiHost, requestTimeoutMs = REQUEST_TIMEOUT_MS }
|
|
|
978
1003
|
};
|
|
979
1004
|
};
|
|
980
1005
|
//#endregion
|
|
981
|
-
export { ContentType, NeonApiError, codeFromBody, getApiClient, isNeonApiError, messageFromBody, retryOnLock };
|
|
1006
|
+
export { ContentType, NeonApiError, UnreadableResponseError, codeFromBody, getApiClient, isNeonApiError, messageFromBody, retryOnLock };
|
package/dist/commands/dev.js
CHANGED
|
@@ -4,11 +4,11 @@ import { isClaimableEnvTarget } from "../claimable/state.js";
|
|
|
4
4
|
import { getCliName } from "../utils/cli_name.js";
|
|
5
5
|
import { readEnvFile, resolveEnvFilePath } from "../env_file.js";
|
|
6
6
|
import { l as functionBaseUrlKey } from "../_chunks/env-BpP3Dqpe.js";
|
|
7
|
-
import { branchIdResolve } from "../utils/enrichers.js";
|
|
8
|
-
import { bundleEntry } from "../utils/esbuild.js";
|
|
9
7
|
import { a as resolveDevEnv } from "../_chunks/env-4WGRW2nw.js";
|
|
10
8
|
import { findConfigFunctionBySource, resolveFunctionsFromConfig } from "../dev/functions.js";
|
|
11
9
|
import { resolveWatchInputs } from "../dev/inputs.js";
|
|
10
|
+
import { branchIdResolve } from "../utils/enrichers.js";
|
|
11
|
+
import { bundleEntry } from "../utils/esbuild.js";
|
|
12
12
|
import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
package/dist/commands/env.js
CHANGED
|
@@ -8,9 +8,9 @@ import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
|
|
|
8
8
|
import { t as NEON_ENV_VAR_KEYS, u as isFunctionBaseUrlKey } from "../_chunks/env-BpP3Dqpe.js";
|
|
9
9
|
import { ENV_PULL_KEY_HELP, ENV_PULL_SERVICES, envKeysForSelection, envPullFlagValue, ownedEnvServiceKeys, parseEnvPullKeys } from "../env_services.js";
|
|
10
10
|
import { listedServices } from "../utils/staging.js";
|
|
11
|
+
import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-4WGRW2nw.js";
|
|
11
12
|
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
12
13
|
import { warnAiGateway } from "../utils/ai_gateway_notice.js";
|
|
13
|
-
import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-4WGRW2nw.js";
|
|
14
14
|
import { formatPulledEnv, pulledBranch } from "./env_output.js";
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
16
|
import chalk from "chalk";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
2
2
|
import { writer } from "../writer.js";
|
|
3
|
-
import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
|
|
4
3
|
import { o as resolveNeonEnvVars } from "../_chunks/env-4WGRW2nw.js";
|
|
4
|
+
import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
|
|
5
5
|
import YAML from "yaml";
|
|
6
6
|
//#region src/commands/subscribe.ts
|
|
7
7
|
var subscribe_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -26,11 +26,11 @@ const builder = (argv) => argv.usage("$0 subscribe \"<query>\" [options]").posit
|
|
|
26
26
|
type: "string"
|
|
27
27
|
},
|
|
28
28
|
changes: {
|
|
29
|
-
describe: "Show raw resets and transaction batches
|
|
29
|
+
describe: "Show raw resets and transaction batches",
|
|
30
30
|
type: "boolean",
|
|
31
31
|
default: false
|
|
32
32
|
}
|
|
33
|
-
}).example("$0 subscribe \"SELECT * FROM items\"", "Print the
|
|
33
|
+
}).example("$0 subscribe \"SELECT * FROM items\"", "Print the initial result followed by a readable transaction log").example("$0 subscribe --changes \"SELECT * FROM items\"", "Print raw reset and delta events").epilogue("The subscription runs until interrupted. Terminal output is an append-only snapshot and transaction log; JSON output is newline-delimited; YAML output uses one document per event.").middleware(fillSingleProject).strict();
|
|
34
34
|
const handler = async (props) => {
|
|
35
35
|
const controller = new AbortController();
|
|
36
36
|
const stop = (exitCode) => {
|
|
@@ -56,6 +56,7 @@ const subscribe = async (props, dependencies = {}) => {
|
|
|
56
56
|
const resolveBranchId = dependencies.resolveBranchId ?? branchIdFromProps;
|
|
57
57
|
const resolveEnv = dependencies.resolveEnv ?? resolveNeonEnvVars;
|
|
58
58
|
const connect = dependencies.connect ?? connectRealtime;
|
|
59
|
+
const now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
|
|
59
60
|
const branchId = await resolveBranchId(props);
|
|
60
61
|
const { vars } = await resolveEnv({
|
|
61
62
|
cwd: props.cwd ?? process.cwd(),
|
|
@@ -67,12 +68,20 @@ const subscribe = async (props, dependencies = {}) => {
|
|
|
67
68
|
services: ["postgres", "realtime"],
|
|
68
69
|
...props.runtimeApi ? { api: props.runtimeApi } : {}
|
|
69
70
|
});
|
|
71
|
+
const url = requiredEnv(vars, "NEON_REALTIME_URL");
|
|
72
|
+
const secret = requiredEnv(vars, "NEON_REALTIME_SECRET");
|
|
73
|
+
const database = await databaseName(requiredEnv(vars, "DATABASE_URL"));
|
|
70
74
|
const realtime = await connect({
|
|
71
|
-
url
|
|
72
|
-
secret
|
|
73
|
-
database
|
|
75
|
+
url,
|
|
76
|
+
secret,
|
|
77
|
+
database
|
|
74
78
|
});
|
|
75
|
-
const render = createRenderer(props
|
|
79
|
+
const render = createRenderer(props, {
|
|
80
|
+
project: props.projectId,
|
|
81
|
+
branch: branchDisplayName(props, branchId),
|
|
82
|
+
database,
|
|
83
|
+
query: props.query
|
|
84
|
+
}, now);
|
|
76
85
|
let subscription;
|
|
77
86
|
try {
|
|
78
87
|
if (props.changes) {
|
|
@@ -81,7 +90,8 @@ const subscribe = async (props, dependencies = {}) => {
|
|
|
81
90
|
} else {
|
|
82
91
|
const materialized = await realtime.subscribeMaterialized(props.query);
|
|
83
92
|
subscription = materialized;
|
|
84
|
-
await
|
|
93
|
+
if (props.output === "table") await consumeChanges(materialized, render, props.signal);
|
|
94
|
+
else await consumeSnapshots(materialized, render, props.signal);
|
|
85
95
|
}
|
|
86
96
|
} finally {
|
|
87
97
|
subscription?.unsubscribe();
|
|
@@ -123,10 +133,23 @@ const databaseName = async (connectionString) => {
|
|
|
123
133
|
if (database) return database;
|
|
124
134
|
throw new Error("DATABASE_URL must include a database name.");
|
|
125
135
|
};
|
|
126
|
-
const createRenderer = (props) => {
|
|
136
|
+
const createRenderer = (props, context, now) => {
|
|
127
137
|
const out = props.out ?? process.stdout;
|
|
128
|
-
let
|
|
138
|
+
let hasBaseline = false;
|
|
139
|
+
let lastStatus;
|
|
140
|
+
const rowsById = /* @__PURE__ */ new Map();
|
|
141
|
+
if (props.output === "table") renderHumanHeader(out, context, props.changes);
|
|
129
142
|
return (event) => {
|
|
143
|
+
if (event.type === "status") {
|
|
144
|
+
if (props.output === "table") {
|
|
145
|
+
const status = humanStatus(event.state);
|
|
146
|
+
if (status !== lastStatus) {
|
|
147
|
+
lastStatus = status;
|
|
148
|
+
out.write(`${timestamp(now)} ${status}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
130
153
|
if (props.output === "json") {
|
|
131
154
|
out.write(`${JSON.stringify(event)}\n`);
|
|
132
155
|
return;
|
|
@@ -135,22 +158,25 @@ const createRenderer = (props) => {
|
|
|
135
158
|
out.write(`---\n${YAML.stringify(event)}`);
|
|
136
159
|
return;
|
|
137
160
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
out.write("\n");
|
|
162
|
+
if (event.type === "reset" && !props.changes) {
|
|
163
|
+
const label = hasBaseline ? "Reset" : "Initial snapshot";
|
|
164
|
+
hasBaseline = true;
|
|
165
|
+
rowsById.clear();
|
|
166
|
+
for (const { rowId, row } of event.rows) rowsById.set(rowId, row);
|
|
167
|
+
renderMaterializedRows(out, event.rows.map(({ row }) => row), `${timestamp(now)} ${label}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (event.type === "batch" && !props.changes) {
|
|
171
|
+
renderHumanBatch(out, event, rowsById, now);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
renderTableEvent(out, event, now);
|
|
141
175
|
};
|
|
142
176
|
};
|
|
143
|
-
const renderTableEvent = (out, event) => {
|
|
177
|
+
const renderTableEvent = (out, event, now) => {
|
|
144
178
|
if (event.type === "snapshot") {
|
|
145
|
-
|
|
146
|
-
writer({
|
|
147
|
-
output: "table",
|
|
148
|
-
out
|
|
149
|
-
}).end(event.rows, {
|
|
150
|
-
fields,
|
|
151
|
-
humanTitle: "Query result",
|
|
152
|
-
emptyMessage: "Query returned no rows."
|
|
153
|
-
});
|
|
179
|
+
renderMaterializedRows(out, event.rows, `${timestamp(now)} Snapshot`);
|
|
154
180
|
return;
|
|
155
181
|
}
|
|
156
182
|
if (event.type === "reset") {
|
|
@@ -158,14 +184,7 @@ const renderTableEvent = (out, event) => {
|
|
|
158
184
|
row_id: rowId,
|
|
159
185
|
row
|
|
160
186
|
}));
|
|
161
|
-
|
|
162
|
-
output: "table",
|
|
163
|
-
out
|
|
164
|
-
}).end(rows, {
|
|
165
|
-
fields: ["row_id", "row"],
|
|
166
|
-
humanTitle: "Reset",
|
|
167
|
-
emptyMessage: "Reset contains no rows."
|
|
168
|
-
});
|
|
187
|
+
renderRows(out, rows, ["row_id", "row"], `${timestamp(now)} Reset · ${rowCount(rows.length)}`, "Reset contains no rows.");
|
|
169
188
|
return;
|
|
170
189
|
}
|
|
171
190
|
const rows = event.changes.map((change) => ({
|
|
@@ -173,46 +192,135 @@ const renderTableEvent = (out, event) => {
|
|
|
173
192
|
row_id: change.rowId,
|
|
174
193
|
row: change.type === "upsert" ? change.row : void 0
|
|
175
194
|
}));
|
|
195
|
+
renderRows(out, rows, [
|
|
196
|
+
"type",
|
|
197
|
+
"row_id",
|
|
198
|
+
"row"
|
|
199
|
+
], `${timestamp(now)} ${transactionLabel(event.txids)} · ${rawChangeSummary(event.changes)}`, "Batch contains no row changes.");
|
|
200
|
+
};
|
|
201
|
+
const renderHumanHeader = (out, context, rawChanges) => {
|
|
202
|
+
const target = {
|
|
203
|
+
project: context.project,
|
|
204
|
+
branch: context.branch,
|
|
205
|
+
database: context.database,
|
|
206
|
+
mode: rawChanges ? "Raw changes" : "Materialized result",
|
|
207
|
+
query: context.query
|
|
208
|
+
};
|
|
176
209
|
writer({
|
|
177
210
|
output: "table",
|
|
178
211
|
out
|
|
179
|
-
}).end(
|
|
212
|
+
}).end(target, {
|
|
180
213
|
fields: [
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
214
|
+
"project",
|
|
215
|
+
"branch",
|
|
216
|
+
"database",
|
|
217
|
+
"mode",
|
|
218
|
+
"query"
|
|
184
219
|
],
|
|
185
|
-
humanTitle:
|
|
186
|
-
emptyMessage: "Batch contains no row changes."
|
|
220
|
+
humanTitle: "Live query"
|
|
187
221
|
});
|
|
188
222
|
};
|
|
223
|
+
const renderMaterializedRows = (out, rows, title) => {
|
|
224
|
+
const humanTitle = `${title} · ${rowCount(rows.length)}`;
|
|
225
|
+
renderRows(out, rows, rowFields(rows), humanTitle, "Query returned no rows.");
|
|
226
|
+
};
|
|
227
|
+
const renderHumanBatch = (out, event, rowsById, now) => {
|
|
228
|
+
const changes = classifyChanges(event.changes, rowsById);
|
|
229
|
+
const inserted = changes.filter((change) => change.operation === "+ insert");
|
|
230
|
+
const updated = changes.filter((change) => change.operation === "~ update");
|
|
231
|
+
const deleted = changes.filter((change) => change.operation === "- delete");
|
|
232
|
+
const rowFieldsInBatch = rowFields(changes.flatMap((change) => change.row ? [change.row] : []));
|
|
233
|
+
const operationField = uniqueOperationField(rowFieldsInBatch);
|
|
234
|
+
const rows = changes.map((change) => ({
|
|
235
|
+
...change.row ?? {},
|
|
236
|
+
[operationField]: change.operation
|
|
237
|
+
}));
|
|
238
|
+
renderRows(out, rows, [operationField, ...rowFieldsInBatch], `${timestamp(now)} ${transactionLabel(event.txids)} · +${inserted.length} ~${updated.length} -${deleted.length}`, "Transaction contains no row changes.");
|
|
239
|
+
};
|
|
240
|
+
const renderRows = (out, rows, fields, title, emptyMessage) => {
|
|
241
|
+
writer({
|
|
242
|
+
output: "table",
|
|
243
|
+
out
|
|
244
|
+
}).end(rows, {
|
|
245
|
+
fields,
|
|
246
|
+
humanTitle: title
|
|
247
|
+
});
|
|
248
|
+
if (rows.length === 0) out.write(`${emptyMessage}\n`);
|
|
249
|
+
};
|
|
250
|
+
const classifyChanges = (changes, rowsById) => changes.map((change) => {
|
|
251
|
+
if (change.type === "upsert") {
|
|
252
|
+
const operation = rowsById.has(change.rowId) ? "~ update" : "+ insert";
|
|
253
|
+
rowsById.set(change.rowId, change.row);
|
|
254
|
+
return {
|
|
255
|
+
operation,
|
|
256
|
+
row: change.row
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const row = rowsById.get(change.rowId);
|
|
260
|
+
rowsById.delete(change.rowId);
|
|
261
|
+
return {
|
|
262
|
+
operation: "- delete",
|
|
263
|
+
...row ? { row } : {}
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
const uniqueOperationField = (fields) => {
|
|
267
|
+
for (const candidate of [
|
|
268
|
+
"change",
|
|
269
|
+
"operation",
|
|
270
|
+
"event"
|
|
271
|
+
]) if (!fields.includes(candidate)) return candidate;
|
|
272
|
+
let suffix = 1;
|
|
273
|
+
while (fields.includes(`change_${suffix}`)) suffix += 1;
|
|
274
|
+
return `change_${suffix}`;
|
|
275
|
+
};
|
|
276
|
+
const transactionLabel = (txids) => {
|
|
277
|
+
if (txids.length === 0) return "Update (no transaction ID)";
|
|
278
|
+
return `${txids.length === 1 ? "Transaction" : "Transactions"} ${txids.join(", ")}`;
|
|
279
|
+
};
|
|
280
|
+
const rawChangeSummary = (changes) => {
|
|
281
|
+
const upserts = changes.filter((change) => change.type === "upsert").length;
|
|
282
|
+
const removals = changes.length - upserts;
|
|
283
|
+
return `${upserts} upsert${upserts === 1 ? "" : "s"}, ${removals} removal${removals === 1 ? "" : "s"}`;
|
|
284
|
+
};
|
|
285
|
+
const rowCount = (count) => `${count} ${count === 1 ? "row" : "rows"}`;
|
|
286
|
+
const timestamp = (now) => `[${now().toISOString()}]`;
|
|
287
|
+
const humanStatus = (state) => {
|
|
288
|
+
switch (state.status) {
|
|
289
|
+
case "connecting": return "Connecting…";
|
|
290
|
+
case "live": return "Live — waiting for changes.";
|
|
291
|
+
case "stale": return "Stale — showing the last known result.";
|
|
292
|
+
case "closed": return "Subscription closed.";
|
|
293
|
+
case "error": return `Subscription failed — ${state.error.message}`;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const branchDisplayName = (props, branchId) => {
|
|
297
|
+
const branch = "branch" in props ? props.branch : props.id;
|
|
298
|
+
return branch === branchId ? branchId : `${branch} (${branchId})`;
|
|
299
|
+
};
|
|
189
300
|
const rowFields = (rows) => {
|
|
190
301
|
const seen = /* @__PURE__ */ new Set();
|
|
191
302
|
for (const row of rows) for (const key of Object.keys(row)) seen.add(key);
|
|
192
303
|
return [...seen];
|
|
193
304
|
};
|
|
194
|
-
const consumeSnapshots = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.
|
|
195
|
-
const rows = subscription.getSnapshot().data;
|
|
196
|
-
if (rows !== void 0) render({
|
|
197
|
-
type: "snapshot",
|
|
198
|
-
rows
|
|
199
|
-
});
|
|
200
|
-
})), subscription.onBatch(() => attempt(fail, () => {
|
|
305
|
+
const consumeSnapshots = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.onUpdate(() => attempt(fail, () => {
|
|
201
306
|
const rows = subscription.getSnapshot().data;
|
|
202
307
|
if (rows !== void 0) render({
|
|
203
308
|
type: "snapshot",
|
|
204
309
|
rows
|
|
205
310
|
});
|
|
206
|
-
}))], signal);
|
|
207
|
-
const consumeChanges = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.
|
|
208
|
-
type
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
311
|
+
}))], render, signal);
|
|
312
|
+
const consumeChanges = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.onUpdate((update) => {
|
|
313
|
+
if (update.type === "rows") attempt(fail, () => render({
|
|
314
|
+
type: "reset",
|
|
315
|
+
rows: update.rows
|
|
316
|
+
}));
|
|
317
|
+
else attempt(fail, () => render({
|
|
318
|
+
type: "batch",
|
|
319
|
+
txids: update.batch.txids,
|
|
320
|
+
changes: update.changes
|
|
321
|
+
}));
|
|
322
|
+
})], render, signal);
|
|
323
|
+
const consumeSubscription = (subscription, listenForData, render, signal) => new Promise((resolve, reject) => {
|
|
216
324
|
let settled = false;
|
|
217
325
|
let detachData = [];
|
|
218
326
|
let detachState = () => void 0;
|
|
@@ -227,6 +335,11 @@ const consumeSubscription = (subscription, listenForData, signal) => new Promise
|
|
|
227
335
|
};
|
|
228
336
|
const fail = (error) => finish(reject, error);
|
|
229
337
|
const onState = (state) => {
|
|
338
|
+
attempt(fail, () => render({
|
|
339
|
+
type: "status",
|
|
340
|
+
state
|
|
341
|
+
}));
|
|
342
|
+
if (settled) return;
|
|
230
343
|
if (state.status === "error") fail(state.error);
|
|
231
344
|
else if (state.status === "closed") finish(resolve);
|
|
232
345
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { log } from "./log.js";
|
|
2
2
|
import pkg_default from "./pkg.js";
|
|
3
|
-
import { codeFromBody, isNeonApiError, messageFromBody } from "./api.js";
|
|
3
|
+
import { UnreadableResponseError, codeFromBody, isNeonApiError, messageFromBody } from "./api.js";
|
|
4
4
|
import { getAuthContext } from "./auth_context.js";
|
|
5
5
|
import { r as defaultDir } from "./_chunks/paths-DMq0Lt7a.js";
|
|
6
6
|
import { ensureConfigDir } from "./config.js";
|
|
@@ -188,6 +188,7 @@ async function handleError(msg, err, canRetry) {
|
|
|
188
188
|
} else {
|
|
189
189
|
const serverMessage = messageFromBody(err.data);
|
|
190
190
|
if (serverMessage) log.error(serverMessage);
|
|
191
|
+
else if (err instanceof UnreadableResponseError) log.error(err.message);
|
|
191
192
|
else log.error([`HTTP ${err.status ?? ""} ${err.statusText ?? ""}`.trim(), err.requestPath].filter(Boolean).join(" | "));
|
|
192
193
|
log.debug("status: %d %s | path: %s", err.status, err.statusText, err.requestPath);
|
|
193
194
|
sendError(err, "API_ERROR");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "8.4.
|
|
3
|
+
"version": "8.4.4",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"yoctocolors": "^2.1.2",
|
|
69
69
|
"@neon/config": "1.9.1",
|
|
70
70
|
"@neon/config-runtime": "1.9.1",
|
|
71
|
-
"@neon/realtime": "0.0.
|
|
71
|
+
"@neon/realtime": "0.0.3",
|
|
72
72
|
"@neon/sdk": "7.2.0"
|
|
73
73
|
},
|
|
74
74
|
"optionalDependencies": {
|