neon 8.4.3 → 8.4.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/README.md CHANGED
@@ -308,10 +308,10 @@ regression + TAP tests.
308
308
 
309
309
  ## Subscribe to a Realtime query
310
310
 
311
- `neon subscribe` streams a live SQL query until interrupted:
311
+ `neon realtime subscribe` streams a live SQL query until interrupted:
312
312
 
313
313
  ```bash
314
- neon subscribe "SELECT id, title, completed FROM realtime_todos ORDER BY id"
314
+ neon realtime subscribe "SELECT id, title, completed FROM realtime_todos ORDER BY id"
315
315
  ```
316
316
 
317
317
  The default terminal output is append-only. It identifies the selected project,
@@ -325,10 +325,10 @@ For scripts and agents, JSON output is a JSON Lines stream with one complete eve
325
325
  per line. YAML output uses one document per event:
326
326
 
327
327
  ```bash
328
- neon subscribe --changes --output json "SELECT * FROM realtime_todos" |
328
+ neon realtime subscribe --changes --output json "SELECT * FROM realtime_todos" |
329
329
  jq --unbuffered 'select(.type == "batch")'
330
330
 
331
- neon subscribe --output yaml "SELECT * FROM realtime_todos"
331
+ neon realtime subscribe --output yaml "SELECT * FROM realtime_todos"
332
332
  ```
333
333
 
334
334
  Machine-readable output retains the event stream: without `--changes`, each event is
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 };
@@ -39,7 +39,6 @@ import { t as roles_exports } from "./roles.js";
39
39
  import { t as set_context_exports } from "./set_context.js";
40
40
  import { t as snapshots_exports } from "./snapshots.js";
41
41
  import { t as status_exports } from "./status.js";
42
- import { t as subscribe_exports } from "./subscribe.js";
43
42
  import { t as triggers_exports } from "./triggers.js";
44
43
  import { t as user_exports } from "./user.js";
45
44
  import { t as vpc_endpoints_exports } from "./vpc_endpoints.js";
@@ -78,7 +77,6 @@ var commands_default = [
78
77
  feedback_exports,
79
78
  data_api_exports,
80
79
  realtime_exports,
81
- subscribe_exports,
82
80
  functions_exports,
83
81
  triggers_exports,
84
82
  credentials_exports,
@@ -3,7 +3,7 @@ import { REALTIME_COMING_SOON } from "../utils/staging.js";
3
3
  import { Parser } from "yargs/helpers";
4
4
  //#region src/commands/manifest.ts
5
5
  /**
6
- * Every top-level command, in help order, with the metadata yargs needs to list and route it.
6
+ * Every top-level command, with the metadata yargs needs to list and route it.
7
7
  * A command's module (and everything it imports) loads only when yargs selects that command,
8
8
  * so `neon --help` and `neon projects list` don't pay for `init`, `dev`, or `psql`.
9
9
  *
@@ -193,12 +193,6 @@ const commandManifest = [
193
193
  load: () => import("./realtime.js"),
194
194
  stagingOnly: REALTIME_COMING_SOON
195
195
  },
196
- {
197
- command: "subscribe <query>",
198
- describe: "Subscribe to a live SQL query with Neon Realtime",
199
- load: () => import("./subscribe.js"),
200
- stagingOnly: REALTIME_COMING_SOON
201
- },
202
196
  {
203
197
  command: "functions",
204
198
  aliases: ["function"],
@@ -2,6 +2,7 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { retryOnLock } from "../api.js";
4
4
  import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
5
+ import { builder as builder$1, handler as handler$1 } from "./subscribe.js";
5
6
  import { defineConfig } from "@neon/config";
6
7
  //#region src/commands/realtime.ts
7
8
  var realtime_exports = /* @__PURE__ */ __exportAll({
@@ -44,7 +45,7 @@ const builder = (argv) => argv.usage("$0 realtime <sub-command> [options]").opti
44
45
  }), (args) => enable({
45
46
  ...args,
46
47
  ...args.allowedOrigin !== void 0 ? { allowedOrigins: args.allowedOrigin } : {}
47
- })).command("disable", "Disable Realtime on a branch", (yargs) => yargs.strict(), (args) => disable(args));
48
+ })).command("disable", "Disable Realtime on a branch", (yargs) => yargs.strict(), (args) => disable(args)).command("subscribe <query>", "Subscribe to a live SQL query with Neon Realtime", builder$1, handler$1);
48
49
  const handler = (_args) => {};
49
50
  //#endregion
50
51
  export { builder, command, describe, disable, enable, handler, realtime_exports as t };
@@ -1,37 +1,18 @@
1
- import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
1
  import { writer } from "../writer.js";
3
- import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
2
+ import { branchIdFromProps } from "../utils/enrichers.js";
4
3
  import { o as resolveNeonEnvVars } from "../_chunks/env-4WGRW2nw.js";
5
4
  import YAML from "yaml";
6
5
  //#region src/commands/subscribe.ts
7
- var subscribe_exports = /* @__PURE__ */ __exportAll({
8
- builder: () => builder,
9
- command: () => command,
10
- describe: () => describe,
11
- handler: () => handler,
12
- subscribe: () => subscribe
13
- });
14
- const command = "subscribe <query>";
15
- const describe = "Subscribe to a live SQL query with Neon Realtime";
16
- const builder = (argv) => argv.usage("$0 subscribe \"<query>\" [options]").positional("query", {
6
+ const builder = (argv) => argv.usage("$0 realtime subscribe \"<query>\" [options]").positional("query", {
17
7
  describe: "SELECT query to subscribe to",
18
8
  type: "string"
19
- }).options({
20
- "project-id": {
21
- describe: "Project ID",
22
- type: "string"
23
- },
24
- branch: {
25
- describe: "Branch ID or name",
26
- type: "string"
27
- },
28
- changes: {
29
- describe: "Show raw resets and transaction batches",
30
- type: "boolean",
31
- default: false
32
- }
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
- const handler = async (props) => {
9
+ }).options({ changes: {
10
+ describe: "Show raw resets and transaction batches",
11
+ type: "boolean",
12
+ default: false
13
+ } }).example("$0 realtime subscribe \"SELECT * FROM items\"", "Print the initial result followed by a readable transaction log").example("$0 realtime 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.").strict();
14
+ const handler = async (args) => {
15
+ const props = args;
35
16
  const controller = new AbortController();
36
17
  const stop = (exitCode) => {
37
18
  controller.abort();
@@ -302,27 +283,24 @@ const rowFields = (rows) => {
302
283
  for (const row of rows) for (const key of Object.keys(row)) seen.add(key);
303
284
  return [...seen];
304
285
  };
305
- const consumeSnapshots = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.onReset(() => attempt(fail, () => {
306
- const rows = subscription.getSnapshot().data;
307
- if (rows !== void 0) render({
308
- type: "snapshot",
309
- rows
310
- });
311
- })), subscription.onBatch(() => attempt(fail, () => {
286
+ const consumeSnapshots = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.onUpdate(() => attempt(fail, () => {
312
287
  const rows = subscription.getSnapshot().data;
313
288
  if (rows !== void 0) render({
314
289
  type: "snapshot",
315
290
  rows
316
291
  });
317
292
  }))], render, signal);
318
- const consumeChanges = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.onReset((rows) => attempt(fail, () => render({
319
- type: "reset",
320
- rows
321
- }))), subscription.onBatch((changes, batch) => attempt(fail, () => render({
322
- type: "batch",
323
- txids: batch.txids,
324
- changes
325
- })))], render, signal);
293
+ const consumeChanges = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.onUpdate((update) => {
294
+ if (update.type === "rows") attempt(fail, () => render({
295
+ type: "reset",
296
+ rows: update.rows
297
+ }));
298
+ else attempt(fail, () => render({
299
+ type: "batch",
300
+ txids: update.batch.txids,
301
+ changes: update.changes
302
+ }));
303
+ })], render, signal);
326
304
  const consumeSubscription = (subscription, listenForData, render, signal) => new Promise((resolve, reject) => {
327
305
  let settled = false;
328
306
  let detachData = [];
@@ -360,4 +338,4 @@ const attempt = (fail, action) => {
360
338
  }
361
339
  };
362
340
  //#endregion
363
- export { builder, command, describe, handler, subscribe, subscribe_exports as t };
341
+ export { builder, handler, subscribe };
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";
@@ -30,7 +30,6 @@ const NO_SUBCOMMANDS_VERBS = [
30
30
  "cs",
31
31
  "connection-string",
32
32
  "psql",
33
- "subscribe",
34
33
  "set-context",
35
34
  "checkout",
36
35
  "link",
@@ -52,6 +51,7 @@ const NO_SUBCOMMANDS_VERBS = [
52
51
  let builder = yargs(hideBin(process.argv));
53
52
  builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command> [options]").parserConfiguration({
54
53
  "populate--": true,
54
+ "sort-commands": true,
55
55
  "parse-positional-numbers": false
56
56
  }).help().option("output", {
57
57
  alias: "o",
@@ -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",
3
+ "version": "8.4.5",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -66,10 +66,10 @@
66
66
  "yaml": "^2.9.0",
67
67
  "yargs": "17.7.2",
68
68
  "yoctocolors": "^2.1.2",
69
- "@neon/config-runtime": "1.9.1",
70
69
  "@neon/config": "1.9.1",
71
- "@neon/sdk": "7.2.0",
72
- "@neon/realtime": "0.0.1"
70
+ "@neon/config-runtime": "1.9.1",
71
+ "@neon/realtime": "0.1.0",
72
+ "@neon/sdk": "7.2.0"
73
73
  },
74
74
  "optionalDependencies": {
75
75
  "@napi-rs/keyring": "1.3.0",
@@ -77,7 +77,6 @@
77
77
  },
78
78
  "devDependencies": {
79
79
  "@apidevtools/swagger-parser": "12.1.0",
80
- "hono": "^4.12.25",
81
80
  "@rollup/plugin-commonjs": "25.0.8",
82
81
  "@rollup/plugin-json": "6.1.0",
83
82
  "@rollup/plugin-node-resolve": "15.2.3",
@@ -94,6 +93,7 @@
94
93
  "@yao-pkg/pkg": "6.10.0",
95
94
  "emocks": "3.0.4",
96
95
  "express": "4.19.2",
96
+ "hono": "^4.12.25",
97
97
  "node-pty": "1.1.0",
98
98
  "oauth2-mock-server": "8.1.0",
99
99
  "openapi-types": "12.1.3",
@@ -103,8 +103,9 @@
103
103
  "tsx": "4.22.3",
104
104
  "typescript": "^5.9.0",
105
105
  "vitest": "^3.0.9",
106
- "@neon-internals/cli-core": "0.0.0",
107
106
  "@neon-internals/env-core": "0.0.32",
107
+ "@neon-internals/cli-core": "0.0.0",
108
+ "@neon/env": "1.6.2",
108
109
  "@neon/e2e-harness": "0.0.0",
109
110
  "@neon/functions": "0.11.0"
110
111
  },
@@ -137,7 +138,7 @@
137
138
  "lint:fix": "pnpm typecheck && biome check src --write",
138
139
  "test": "pnpm --filter neonctl... build && vitest run",
139
140
  "test:ci": "pnpm build && vitest run",
140
- "test:e2e": "pnpm build && vitest run --config vitest.e2e.config.ts",
141
+ "test:e2e": "pnpm --filter \"@neon/env...\" build && pnpm build && vitest run --config vitest.e2e.config.ts",
141
142
  "test:perf": "pnpm --filter neon... build && vitest run src/perf_budget.test.ts",
142
143
  "perf": "tsx scripts/perf/perf.ts",
143
144
  "perf:ci-report": "tsx scripts/perf/ci-report.ts",