neonctl 2.27.1 → 2.29.0

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.
Files changed (136) hide show
  1. package/README.md +35 -3
  2. package/dist/analytics.js +52 -34
  3. package/dist/api.js +643 -13
  4. package/dist/auth.js +50 -44
  5. package/dist/cli.js +8 -1
  6. package/dist/commands/auth.js +64 -51
  7. package/dist/commands/bootstrap.js +115 -157
  8. package/dist/commands/branches.js +160 -150
  9. package/dist/commands/bucket.js +183 -146
  10. package/dist/commands/checkout.js +51 -51
  11. package/dist/commands/config.js +228 -82
  12. package/dist/commands/connection_string.js +62 -62
  13. package/dist/commands/data_api.js +100 -101
  14. package/dist/commands/databases.js +29 -26
  15. package/dist/commands/deploy.js +12 -12
  16. package/dist/commands/dev.js +114 -114
  17. package/dist/commands/env.js +43 -43
  18. package/dist/commands/functions.js +101 -104
  19. package/dist/commands/index.js +27 -25
  20. package/dist/commands/init.js +23 -22
  21. package/dist/commands/ip_allow.js +29 -29
  22. package/dist/commands/link.js +232 -182
  23. package/dist/commands/neon_auth.js +385 -370
  24. package/dist/commands/operations.js +11 -11
  25. package/dist/commands/orgs.js +8 -8
  26. package/dist/commands/projects.js +103 -101
  27. package/dist/commands/psql.js +31 -31
  28. package/dist/commands/roles.js +27 -24
  29. package/dist/commands/schema_diff.js +25 -26
  30. package/dist/commands/set_context.js +17 -17
  31. package/dist/commands/status.js +40 -0
  32. package/dist/commands/user.js +5 -5
  33. package/dist/commands/vpc_endpoints.js +50 -50
  34. package/dist/config.js +7 -7
  35. package/dist/config_format.js +5 -5
  36. package/dist/context.js +37 -14
  37. package/dist/current_branch_fast_path.js +55 -0
  38. package/dist/dev/env.js +33 -33
  39. package/dist/dev/functions.js +4 -4
  40. package/dist/dev/inputs.js +6 -6
  41. package/dist/dev/runtime.js +25 -25
  42. package/dist/env.js +14 -14
  43. package/dist/env_file.js +13 -13
  44. package/dist/errors.js +68 -5
  45. package/dist/functions_api.js +10 -10
  46. package/dist/help.js +15 -15
  47. package/dist/index.js +110 -107
  48. package/dist/log.js +2 -2
  49. package/dist/parameters.gen.js +14 -14
  50. package/dist/pkg.js +5 -5
  51. package/dist/psql/cli.js +4 -2
  52. package/dist/psql/command/cmd_cond.js +61 -61
  53. package/dist/psql/command/cmd_connect.js +159 -154
  54. package/dist/psql/command/cmd_copy.js +107 -97
  55. package/dist/psql/command/cmd_describe.js +368 -363
  56. package/dist/psql/command/cmd_format.js +276 -263
  57. package/dist/psql/command/cmd_io.js +269 -263
  58. package/dist/psql/command/cmd_lo.js +74 -66
  59. package/dist/psql/command/cmd_meta.js +148 -148
  60. package/dist/psql/command/cmd_misc.js +17 -17
  61. package/dist/psql/command/cmd_pipeline.js +142 -135
  62. package/dist/psql/command/cmd_restrict.js +25 -25
  63. package/dist/psql/command/cmd_show.js +183 -168
  64. package/dist/psql/command/dispatch.js +26 -26
  65. package/dist/psql/command/shared.js +14 -14
  66. package/dist/psql/complete/filenames.js +16 -16
  67. package/dist/psql/complete/index.js +4 -4
  68. package/dist/psql/complete/matcher.js +33 -32
  69. package/dist/psql/complete/psqlVars.js +173 -173
  70. package/dist/psql/complete/queries.js +5 -3
  71. package/dist/psql/complete/rules.js +900 -863
  72. package/dist/psql/core/common.js +136 -133
  73. package/dist/psql/core/help.js +343 -343
  74. package/dist/psql/core/mainloop.js +160 -153
  75. package/dist/psql/core/prompt.js +126 -123
  76. package/dist/psql/core/settings.js +111 -111
  77. package/dist/psql/core/sqlHelp.js +150 -150
  78. package/dist/psql/core/startup.js +211 -205
  79. package/dist/psql/core/syncVars.js +14 -14
  80. package/dist/psql/core/variables.js +24 -24
  81. package/dist/psql/describe/formatters.js +302 -289
  82. package/dist/psql/describe/processNamePattern.js +28 -28
  83. package/dist/psql/describe/queries.js +656 -651
  84. package/dist/psql/index.js +436 -411
  85. package/dist/psql/io/history.js +36 -36
  86. package/dist/psql/io/input.js +15 -15
  87. package/dist/psql/io/lineEditor/buffer.js +27 -25
  88. package/dist/psql/io/lineEditor/complete.js +15 -15
  89. package/dist/psql/io/lineEditor/filename.js +22 -22
  90. package/dist/psql/io/lineEditor/index.js +65 -62
  91. package/dist/psql/io/lineEditor/keymap.js +325 -318
  92. package/dist/psql/io/lineEditor/vt100.js +60 -60
  93. package/dist/psql/io/pgpass.js +18 -18
  94. package/dist/psql/io/pgservice.js +14 -14
  95. package/dist/psql/io/psqlrc.js +46 -46
  96. package/dist/psql/print/aligned.js +175 -166
  97. package/dist/psql/print/asciidoc.js +51 -51
  98. package/dist/psql/print/crosstab.js +34 -31
  99. package/dist/psql/print/csv.js +25 -22
  100. package/dist/psql/print/html.js +54 -54
  101. package/dist/psql/print/json.js +12 -12
  102. package/dist/psql/print/latex.js +118 -118
  103. package/dist/psql/print/pager.js +28 -26
  104. package/dist/psql/print/troff.js +48 -48
  105. package/dist/psql/print/unaligned.js +15 -14
  106. package/dist/psql/print/units.js +17 -17
  107. package/dist/psql/scanner/slash.js +48 -46
  108. package/dist/psql/scanner/sql.js +88 -84
  109. package/dist/psql/scanner/stringutils.js +21 -17
  110. package/dist/psql/types/index.js +7 -7
  111. package/dist/psql/types/scanner.js +8 -8
  112. package/dist/psql/wire/connection.js +341 -327
  113. package/dist/psql/wire/copy.js +7 -7
  114. package/dist/psql/wire/pipeline.js +26 -24
  115. package/dist/psql/wire/protocol.js +102 -102
  116. package/dist/psql/wire/sasl.js +62 -62
  117. package/dist/psql/wire/tls.js +79 -73
  118. package/dist/storage_api.js +22 -23
  119. package/dist/test_utils/fixtures.js +74 -41
  120. package/dist/test_utils/oauth_server.js +5 -5
  121. package/dist/utils/api_enums.js +33 -0
  122. package/dist/utils/branch_notice.js +5 -5
  123. package/dist/utils/branch_picker.js +26 -26
  124. package/dist/utils/compute_units.js +4 -4
  125. package/dist/utils/enrichers.js +28 -16
  126. package/dist/utils/esbuild.js +28 -28
  127. package/dist/utils/formats.js +1 -1
  128. package/dist/utils/middlewares.js +3 -3
  129. package/dist/utils/package_manager.js +68 -0
  130. package/dist/utils/point_in_time.js +12 -12
  131. package/dist/utils/psql.js +30 -30
  132. package/dist/utils/string.js +2 -2
  133. package/dist/utils/ui.js +9 -9
  134. package/dist/utils/zip.js +1 -1
  135. package/dist/writer.js +17 -17
  136. package/package.json +10 -12
@@ -1,16 +1,15 @@
1
- import { createReadStream, createWriteStream } from 'node:fs';
2
- import { stat, unlink } from 'node:fs/promises';
3
- import { basename } from 'node:path';
4
- import { pipeline } from 'node:stream/promises';
5
- import axios, { isAxiosError } from 'axios';
6
- import { retryOnLock } from '../api.js';
7
- import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
8
- import { log } from '../log.js';
9
- import { writer } from '../writer.js';
10
- import { createProjectBranchBucket, listProjectBranchBuckets, deleteProjectBranchBucket, listProjectBranchBucketObjects, getProjectBranchBucketObject, deleteProjectBranchBucketObject, deleteProjectBranchBucketObjectsByPrefix, presignUpload, } from '../storage_api.js';
11
- const OBJECT_FIELDS = ['key', 'size', 'last_modified', 'etag'];
12
- const BUCKET_FIELDS = ['name', 'access_level'];
13
- const ACCESS_LEVELS = ['private', 'public_read'];
1
+ import { createReadStream, createWriteStream } from "node:fs";
2
+ import { stat, unlink } from "node:fs/promises";
3
+ import { basename } from "node:path";
4
+ import { pipeline } from "node:stream/promises";
5
+ import { isNeonApiError, retryOnLock } from "../api.js";
6
+ import { log } from "../log.js";
7
+ import { createProjectBranchBucket, deleteProjectBranchBucket, deleteProjectBranchBucketObject, deleteProjectBranchBucketObjectsByPrefix, getProjectBranchBucketObject, listProjectBranchBucketObjects, listProjectBranchBuckets, presignUpload, } from "../storage_api.js";
8
+ import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
9
+ import { writer } from "../writer.js";
10
+ const OBJECT_FIELDS = ["key", "size", "last_modified", "etag"];
11
+ const BUCKET_FIELDS = ["name", "access_level"];
12
+ const ACCESS_LEVELS = ["private", "public_read"];
14
13
  // Single-PUT upload cap. Objects larger than this must use multipart upload,
15
14
  // which is out of scope for v1; we reject them client-side before any HTTP so
16
15
  // the user gets an immediate, clear error rather than a server-side rejection
@@ -19,13 +18,13 @@ const MAX_OBJECT_BYTES = 100 * 1024 * 1024; // 100 MB
19
18
  // Ambient scope shared by every bucket sub-command. The bucket name (and the
20
19
  // object key/prefix) is always a positional, never a flag.
21
20
  const scopeOptions = {
22
- 'project-id': {
23
- describe: 'Project ID',
24
- type: 'string',
21
+ "project-id": {
22
+ describe: "Project ID",
23
+ type: "string",
25
24
  },
26
25
  branch: {
27
- describe: 'Branch ID or name',
28
- type: 'string',
26
+ describe: "Branch ID or name",
27
+ type: "string",
29
28
  },
30
29
  };
31
30
  // Split an object target into its bucket and the remainder (key or prefix) on
@@ -33,156 +32,156 @@ const scopeOptions = {
33
32
  // remainder may contain further slashes and is returned verbatim. When the
34
33
  // target has no slash, `rest` is the empty string.
35
34
  export const splitBucketTarget = (target) => {
36
- const slash = target.indexOf('/');
35
+ const slash = target.indexOf("/");
37
36
  if (slash === -1) {
38
- return { bucket: target, rest: '' };
37
+ return { bucket: target, rest: "" };
39
38
  }
40
39
  return {
41
40
  bucket: target.slice(0, slash),
42
41
  rest: target.slice(slash + 1),
43
42
  };
44
43
  };
45
- export const command = 'buckets';
46
- export const describe = 'Manage branch object-storage buckets and their objects';
47
- export const aliases = ['bucket'];
44
+ export const command = "buckets";
45
+ export const describe = "Manage branch object-storage buckets and their objects";
46
+ export const aliases = ["bucket"];
48
47
  export const builder = (argv) => argv
49
- .usage('$0 bucket <sub-command> [options]')
48
+ .usage("$0 bucket <sub-command> [options]")
50
49
  .options({
51
- 'project-id': {
52
- describe: 'Project ID',
53
- type: 'string',
50
+ "project-id": {
51
+ describe: "Project ID",
52
+ type: "string",
54
53
  },
55
54
  })
56
55
  .middleware(fillSingleProject)
57
- .command('create <name>', 'Create a bucket on a branch', (yargs) => yargs
58
- .usage('$0 bucket create <name> [options]')
59
- .positional('name', {
60
- describe: 'The bucket name to create',
61
- type: 'string',
56
+ .command("create <name>", "Create a bucket on a branch", (yargs) => yargs
57
+ .usage("$0 bucket create <name> [options]")
58
+ .positional("name", {
59
+ describe: "The bucket name to create",
60
+ type: "string",
62
61
  demandOption: true,
63
62
  })
64
63
  .options({
65
64
  ...scopeOptions,
66
- 'access-level': {
67
- describe: 'The visibility of the bucket',
68
- type: 'string',
65
+ "access-level": {
66
+ describe: "The visibility of the bucket",
67
+ type: "string",
69
68
  choices: ACCESS_LEVELS,
70
- default: 'private',
69
+ default: "private",
71
70
  },
72
71
  }), (args) => createBucket(args))
73
72
  .command({
74
- command: 'list',
75
- aliases: ['ls'],
76
- describe: 'List the buckets on a branch',
77
- builder: (yargs) => yargs.usage('$0 bucket list [options]').options(scopeOptions),
73
+ command: "list",
74
+ aliases: ["ls"],
75
+ describe: "List the buckets on a branch",
76
+ builder: (yargs) => yargs.usage("$0 bucket list [options]").options(scopeOptions),
78
77
  handler: (args) => listBuckets(args),
79
78
  })
80
79
  .command({
81
- command: 'delete <name>',
82
- aliases: ['rm'],
83
- describe: 'Delete a bucket from a branch',
80
+ command: "delete <name>",
81
+ aliases: ["rm"],
82
+ describe: "Delete a bucket from a branch",
84
83
  builder: (yargs) => yargs
85
- .usage('$0 bucket delete <name> [options]')
86
- .positional('name', {
87
- describe: 'The bucket name to delete',
88
- type: 'string',
84
+ .usage("$0 bucket delete <name> [options]")
85
+ .positional("name", {
86
+ describe: "The bucket name to delete",
87
+ type: "string",
89
88
  demandOption: true,
90
89
  })
91
90
  .options(scopeOptions),
92
91
  handler: (args) => deleteBucket(args),
93
92
  })
94
- .command('object <sub-command>', 'List, download, upload or delete objects in a bucket', (yargs) => yargs
95
- .usage('$0 bucket object <sub-command> [options]')
93
+ .command("object <sub-command>", "List, download, upload or delete objects in a bucket", (yargs) => yargs
94
+ .usage("$0 bucket object <sub-command> [options]")
96
95
  .command({
97
- command: 'list <target>',
98
- aliases: ['ls'],
96
+ command: "list <target>",
97
+ aliases: ["ls"],
99
98
  describe: 'List objects in a bucket. By default folders are collapsed (like "aws s3 ls"); pass --recursive for a flat listing of every key',
100
99
  builder: (yargs) => yargs
101
- .usage('$0 bucket object list <bucket>[/<prefix>] [options]')
102
- .positional('target', {
103
- describe: 'The bucket to list, optionally with a key prefix: <bucket>[/<prefix>]',
104
- type: 'string',
100
+ .usage("$0 bucket object list <bucket>[/<prefix>] [options]")
101
+ .positional("target", {
102
+ describe: "The bucket to list, optionally with a key prefix: <bucket>[/<prefix>]",
103
+ type: "string",
105
104
  demandOption: true,
106
105
  })
107
106
  .options({
108
107
  ...scopeOptions,
109
108
  recursive: {
110
109
  describe: 'List every key flat, descending into nested folders (no delimiter). Mutually exclusive with --delimiter. Mirrors "aws s3 ls --recursive"',
111
- type: 'boolean',
110
+ type: "boolean",
112
111
  default: false,
113
112
  },
114
113
  delimiter: {
115
114
  describe: 'Collapse keys sharing this prefix separator into folders. Defaults to "/" (folder view); ignored when --recursive is set',
116
- type: 'string',
115
+ type: "string",
117
116
  },
118
117
  cursor: {
119
- describe: 'Pagination cursor returned as next_cursor by a previous call',
120
- type: 'string',
118
+ describe: "Pagination cursor returned as next_cursor by a previous call",
119
+ type: "string",
121
120
  },
122
121
  limit: {
123
- describe: 'Maximum number of items (objects + folders) to return',
124
- type: 'number',
122
+ describe: "Maximum number of items (objects + folders) to return",
123
+ type: "number",
125
124
  },
126
125
  }),
127
126
  handler: (args) => listObjects(args),
128
127
  })
129
- .command('get <target>', 'Download an object from a bucket to a local file', (yargs) => yargs
130
- .usage('$0 bucket object get <bucket>/<key> [options]')
131
- .positional('target', {
132
- describe: 'The object to download: <bucket>/<key>',
133
- type: 'string',
128
+ .command("get <target>", "Download an object from a bucket to a local file", (yargs) => yargs
129
+ .usage("$0 bucket object get <bucket>/<key> [options]")
130
+ .positional("target", {
131
+ describe: "The object to download: <bucket>/<key>",
132
+ type: "string",
134
133
  demandOption: true,
135
134
  })
136
135
  .options({
137
136
  ...scopeOptions,
138
137
  file: {
139
- describe: 'Path to write the downloaded object to (defaults to the object filename in the current directory)',
140
- type: 'string',
138
+ describe: "Path to write the downloaded object to (defaults to the object filename in the current directory)",
139
+ type: "string",
141
140
  },
142
141
  }), (args) => getObject(args))
143
- .command('put <target>', 'Upload a local file to a bucket as an object', (yargs) => yargs
144
- .usage('$0 bucket object put <bucket>/<key> [options]')
145
- .positional('target', {
146
- describe: 'The object to upload to: <bucket>/<key>',
147
- type: 'string',
142
+ .command("put <target>", "Upload a local file to a bucket as an object", (yargs) => yargs
143
+ .usage("$0 bucket object put <bucket>/<key> [options]")
144
+ .positional("target", {
145
+ describe: "The object to upload to: <bucket>/<key>",
146
+ type: "string",
148
147
  demandOption: true,
149
148
  })
150
149
  .options({
151
150
  ...scopeOptions,
152
151
  file: {
153
- describe: 'Path to the local file to upload',
154
- type: 'string',
152
+ describe: "Path to the local file to upload",
153
+ type: "string",
155
154
  demandOption: true,
156
155
  },
157
- 'content-type': {
158
- describe: 'Content-Type to store the object with (e.g. text/plain)',
159
- type: 'string',
156
+ "content-type": {
157
+ describe: "Content-Type to store the object with (e.g. text/plain)",
158
+ type: "string",
160
159
  },
161
160
  }), (args) => putObject(args))
162
161
  .command({
163
- command: 'delete <target>',
164
- aliases: ['rm'],
165
- describe: 'Delete an object, or every object under a prefix',
162
+ command: "delete <target>",
163
+ aliases: ["rm"],
164
+ describe: "Delete an object, or every object under a prefix",
166
165
  builder: (yargs) => yargs
167
- .usage('$0 bucket object delete <bucket>/<key> [options]')
168
- .positional('target', {
169
- describe: 'The object to delete: <bucket>/<key>, or <bucket>/<prefix>/ with --recursive',
170
- type: 'string',
166
+ .usage("$0 bucket object delete <bucket>/<key> [options]")
167
+ .positional("target", {
168
+ describe: "The object to delete: <bucket>/<key>, or <bucket>/<prefix>/ with --recursive",
169
+ type: "string",
171
170
  demandOption: true,
172
171
  })
173
172
  .options({
174
173
  ...scopeOptions,
175
174
  recursive: {
176
175
  describe: 'Delete every object under the given prefix. The prefix must end with "/"',
177
- type: 'boolean',
176
+ type: "boolean",
178
177
  default: false,
179
178
  },
180
179
  }),
181
180
  handler: (args) => deleteObject(args),
182
181
  })
183
- .demandCommand(1, '')
182
+ .demandCommand(1, "")
184
183
  .strictCommands())
185
- .demandCommand(1, '');
184
+ .demandCommand(1, "");
186
185
  export const handler = (args) => {
187
186
  return args;
188
187
  };
@@ -202,14 +201,14 @@ const listBuckets = async (props) => {
202
201
  projectId: props.projectId,
203
202
  branchId,
204
203
  });
205
- if (props.output === 'json' || props.output === 'yaml') {
204
+ if (props.output === "json" || props.output === "yaml") {
206
205
  writer(props).end(data.buckets, { fields: BUCKET_FIELDS });
207
206
  return;
208
207
  }
209
208
  writer(props).end(data.buckets, {
210
209
  fields: BUCKET_FIELDS,
211
- title: 'buckets',
212
- emptyMessage: 'No buckets found.',
210
+ title: "buckets",
211
+ emptyMessage: "No buckets found.",
213
212
  });
214
213
  };
215
214
  const deleteBucket = async (props) => {
@@ -222,7 +221,7 @@ const deleteBucket = async (props) => {
222
221
  }));
223
222
  }
224
223
  catch (err) {
225
- if (isAxiosError(err) && err.response?.status === 404) {
224
+ if (isNeonApiError(err) && err.status === 404) {
226
225
  throw new Error(`Bucket "${props.name}" not found on branch ${branchId}.`);
227
226
  }
228
227
  throw err;
@@ -237,7 +236,7 @@ const deleteBucket = async (props) => {
237
236
  // rejected client-side before any HTTP request is made.
238
237
  export const resolveListDelimiter = (props) => {
239
238
  if (props.recursive && props.delimiter !== undefined) {
240
- throw new Error('--recursive and --delimiter cannot be used together. Use --recursive for a flat listing, or --delimiter to collapse on a separator.');
239
+ throw new Error("--recursive and --delimiter cannot be used together. Use --recursive for a flat listing, or --delimiter to collapse on a separator.");
241
240
  }
242
241
  if (props.recursive) {
243
242
  return undefined;
@@ -245,7 +244,7 @@ export const resolveListDelimiter = (props) => {
245
244
  if (props.delimiter !== undefined) {
246
245
  return props.delimiter;
247
246
  }
248
- return '/';
247
+ return "/";
249
248
  };
250
249
  const listObjects = async (props) => {
251
250
  const delimiter = resolveListDelimiter(props);
@@ -255,25 +254,31 @@ const listObjects = async (props) => {
255
254
  projectId: props.projectId,
256
255
  branchId,
257
256
  bucketName: bucket,
258
- prefix: rest === '' ? undefined : rest,
257
+ prefix: rest === "" ? undefined : rest,
259
258
  delimiter,
260
259
  cursor: props.cursor,
261
260
  limit: props.limit,
262
261
  });
263
- if (props.output === 'json' || props.output === 'yaml') {
262
+ if (props.output === "json" || props.output === "yaml") {
264
263
  writer(props).end(data, {
265
- fields: ['folders', 'objects', 'prefix', 'next_cursor', 'is_truncated'],
264
+ fields: [
265
+ "folders",
266
+ "objects",
267
+ "prefix",
268
+ "next_cursor",
269
+ "is_truncated",
270
+ ],
266
271
  });
267
272
  return;
268
273
  }
269
274
  const w = writer(props);
270
275
  if (data.folders.length > 0) {
271
- w.write(data.folders.map((name) => ({ name })), { fields: ['name'], title: 'folders' });
276
+ w.write(data.folders.map((name) => ({ name })), { fields: ["name"], title: "folders" });
272
277
  }
273
278
  w.write(data.objects, {
274
279
  fields: OBJECT_FIELDS,
275
- title: 'objects',
276
- emptyMessage: 'No objects found.',
280
+ title: "objects",
281
+ emptyMessage: "No objects found.",
277
282
  });
278
283
  w.end();
279
284
  if (data.is_truncated && data.next_cursor) {
@@ -305,7 +310,7 @@ const filenameFromContentDisposition = (contentDisposition, key) => {
305
310
  // the body is absent, not an object, or carries no usable message.
306
311
  const serverErrorMessage = (body) => {
307
312
  const message = body?.message;
308
- return typeof message === 'string' && message.trim() !== ''
313
+ return typeof message === "string" && message.trim() !== ""
309
314
  ? message
310
315
  : undefined;
311
316
  };
@@ -313,7 +318,7 @@ const serverErrorMessage = (body) => {
313
318
  // and parse its `message`. Returns undefined on any read/parse failure so the
314
319
  // caller falls back to its default message.
315
320
  const streamErrorMessage = async (stream) => {
316
- if (typeof stream?.[Symbol.asyncIterator] !== 'function') {
321
+ if (typeof stream?.[Symbol.asyncIterator] !== "function") {
317
322
  return undefined;
318
323
  }
319
324
  try {
@@ -332,19 +337,47 @@ const objectNotFoundFallback = (key, bucket, branchId) => `Object "${key}" not f
332
337
  // misreported as a missing object; otherwise fall back to a clean default. Used
333
338
  // for the JSON (non-streamed) endpoints where the body is already parsed.
334
339
  const objectNotFoundMessage = (err, key, bucket, branchId) => {
335
- if (isAxiosError(err)) {
336
- const serverMessage = serverErrorMessage(err.response?.data);
340
+ if (isNeonApiError(err)) {
341
+ const serverMessage = serverErrorMessage(err.data);
337
342
  if (serverMessage !== undefined) {
338
343
  return serverMessage;
339
344
  }
340
345
  }
341
346
  return objectNotFoundFallback(key, bucket, branchId);
342
347
  };
348
+ // Stream a file from disk as a WHATWG `ReadableStream` suitable for a `fetch`
349
+ // request body, applying backpressure so we never read faster than the upload
350
+ // drains. Uses the global `ReadableStream` (what `fetch` expects) directly, so
351
+ // there's no Node-vs-DOM stream type bridging.
352
+ const fileToWebStream = (path) => {
353
+ const source = createReadStream(path);
354
+ return new ReadableStream({
355
+ start(controller) {
356
+ source.on("data", (chunk) => {
357
+ controller.enqueue(new Uint8Array(chunk));
358
+ if ((controller.desiredSize ?? 0) <= 0)
359
+ source.pause();
360
+ });
361
+ source.on("end", () => {
362
+ controller.close();
363
+ });
364
+ source.on("error", (err) => {
365
+ controller.error(err instanceof Error ? err : new Error(String(err)));
366
+ });
367
+ },
368
+ pull() {
369
+ source.resume();
370
+ },
371
+ cancel() {
372
+ source.destroy();
373
+ },
374
+ });
375
+ };
343
376
  const getObject = async (props) => {
344
377
  const branchId = await branchIdFromProps(props);
345
378
  const { bucket, rest: key } = splitBucketTarget(props.target);
346
- if (key === '') {
347
- throw new Error('Object target must be in the form <bucket>/<key>.');
379
+ if (key === "") {
380
+ throw new Error("Object target must be in the form <bucket>/<key>.");
348
381
  }
349
382
  let response;
350
383
  try {
@@ -356,16 +389,16 @@ const getObject = async (props) => {
356
389
  });
357
390
  }
358
391
  catch (err) {
359
- if (isAxiosError(err) && err.response?.status === 404) {
392
+ if (isNeonApiError(err) && err.status === 404) {
360
393
  // The download response is a stream, so a 404 body arrives as a stream
361
394
  // too; drain and parse it to recover the server's message (which
362
395
  // distinguishes a missing bucket from a missing object).
363
- const serverMessage = await streamErrorMessage(err.response.data);
396
+ const serverMessage = await streamErrorMessage(err.data);
364
397
  throw new Error(serverMessage ?? objectNotFoundFallback(key, bucket, branchId));
365
398
  }
366
399
  throw err;
367
400
  }
368
- const contentDisposition = response.headers['content-disposition'];
401
+ const contentDisposition = response.headers["content-disposition"];
369
402
  const destination = props.file ?? filenameFromContentDisposition(contentDisposition, key);
370
403
  try {
371
404
  await pipeline(response.data, createWriteStream(destination));
@@ -380,8 +413,8 @@ const getObject = async (props) => {
380
413
  const putObject = async (props) => {
381
414
  const branchId = await branchIdFromProps(props);
382
415
  const { bucket, rest: key } = splitBucketTarget(props.target);
383
- if (bucket === '' || key === '') {
384
- throw new Error('Object target must be in the form <bucket>/<key>.');
416
+ if (bucket === "" || key === "") {
417
+ throw new Error("Object target must be in the form <bucket>/<key>.");
385
418
  }
386
419
  // Stat the file first so we fail fast on a missing/unreadable file and can
387
420
  // enforce the single-PUT size cap BEFORE any network round-trip. We also
@@ -396,7 +429,7 @@ const putObject = async (props) => {
396
429
  fileSize = fileStat.size;
397
430
  }
398
431
  catch (err) {
399
- if (err?.code === 'ENOENT') {
432
+ if (err?.code === "ENOENT") {
400
433
  throw new Error(`File "${props.file}" does not exist.`);
401
434
  }
402
435
  throw err;
@@ -417,52 +450,56 @@ const putObject = async (props) => {
417
450
  }));
418
451
  }
419
452
  catch (err) {
420
- if (isAxiosError(err)) {
421
- const status = err.response?.status;
453
+ if (isNeonApiError(err)) {
454
+ const status = err.status;
422
455
  if (status === 404) {
423
456
  throw new Error(objectNotFoundMessage(err, key, bucket, branchId));
424
457
  }
425
458
  // Any other HTTP error from the console (e.g. 403 when the caller lacks
426
459
  // write permission on the bucket) carries the same JSON `{ message }`
427
- // body, so surface that rather than a bare axios message. When the body
428
- // has no usable message, fall back to a clean status-bearing error.
429
- const serverMessage = serverErrorMessage(err.response?.data);
460
+ // body, so surface that rather than a bare error. When the body has no
461
+ // usable message, fall back to a clean status-bearing error.
462
+ const serverMessage = serverErrorMessage(err.data);
430
463
  throw new Error(serverMessage ??
431
- `Failed to presign upload for "${key}" in bucket "${bucket}" on branch ${branchId}${status !== undefined ? ` (HTTP ${status})` : ''}: ${err.message}`);
464
+ `Failed to presign upload for "${key}" in bucket "${bucket}" on branch ${branchId}${status !== undefined ? ` (HTTP ${status})` : ""}: ${err.message}`);
432
465
  }
433
466
  throw err;
434
467
  }
435
- // Stream the file straight into the PUT body; never buffer the whole file.
436
- // The presigned URL targets the branch S3 data-plane endpoint directly, so
437
- // this PUT goes through a plain axios call rather than the console api-client.
468
+ // Stream the file straight into the PUT body via `fetch`; never buffer the
469
+ // whole file. The presigned URL targets the branch S3 data-plane endpoint
470
+ // directly, so this PUT bypasses the console API entirely.
438
471
  //
439
472
  // `presign.headers` carries the signature-relevant headers (e.g. host,
440
473
  // content-type); the server does not sign Content-Length, so we set it
441
- // ourselves from the stat'd size to keep the upload streamed, not chunked.
442
- // `maxRedirects: 0` ensures we never resend the file bytes and signed headers
443
- // to a different host if the data-plane endpoint were to answer with a
444
- // redirect.
474
+ // ourselves from the stat'd size. `redirect: 'error'` ensures we never resend
475
+ // the file bytes and signed headers to a different host if the data-plane
476
+ // endpoint were to answer with a redirect. `duplex: 'half'` is required by
477
+ // fetch when streaming a request body.
478
+ const upload = {
479
+ method: "PUT",
480
+ headers: {
481
+ ...presign.headers,
482
+ "Content-Length": String(fileSize),
483
+ },
484
+ body: fileToWebStream(props.file),
485
+ redirect: "error",
486
+ duplex: "half",
487
+ };
488
+ let uploadResponse;
445
489
  try {
446
- await axios.put(presign.url, createReadStream(props.file), {
447
- headers: {
448
- ...presign.headers,
449
- 'Content-Length': fileSize,
450
- },
451
- maxBodyLength: Infinity,
452
- maxContentLength: Infinity,
453
- maxRedirects: 0,
454
- });
490
+ uploadResponse = await fetch(presign.url, upload);
455
491
  }
456
492
  catch (err) {
493
+ // A transport-level failure (DNS, connection reset, redirect when none is
494
+ // allowed). Surface a clean message without leaking the signed URL.
495
+ throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId}: ${err instanceof Error ? err.message : String(err)}`);
496
+ }
497
+ if (!uploadResponse.ok) {
457
498
  // The upload targets the S3 data plane, whose error bodies are XML rather
458
499
  // than the JSON `{ message }` the console returns, so surface the status
459
- // (and axios message) rather than leaking a raw error. Never include the
460
- // presigned URL, which carries the signature.
461
- if (isAxiosError(err)) {
462
- const status = err.response?.status;
463
- throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId}${status !== undefined ? ` (HTTP ${status})` : ''}: ${err.message}`);
464
- }
465
- throw err;
500
+ // rather than the body. Never include the presigned URL, which carries the
501
+ // signature.
502
+ throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId} (HTTP ${uploadResponse.status}): Request failed with status code ${uploadResponse.status}`);
466
503
  }
467
504
  log.info(`File "${props.file}" uploaded to "${key}" in bucket "${bucket}" on branch ${branchId}`);
468
505
  };
@@ -470,10 +507,10 @@ const deleteObject = async (props) => {
470
507
  const branchId = await branchIdFromProps(props);
471
508
  const { bucket, rest } = splitBucketTarget(props.target);
472
509
  if (props.recursive) {
473
- if (rest === '') {
510
+ if (rest === "") {
474
511
  throw new Error('Recursive delete requires a non-empty prefix ending in "/".');
475
512
  }
476
- if (!rest.endsWith('/')) {
513
+ if (!rest.endsWith("/")) {
477
514
  throw new Error(`Recursive delete requires a prefix ending in "/" (got "${rest}").`);
478
515
  }
479
516
  const { data } = await retryOnLock(() => deleteProjectBranchBucketObjectsByPrefix(props.apiClient, {
@@ -485,8 +522,8 @@ const deleteObject = async (props) => {
485
522
  log.info(`Deleted ${data.deleted} object(s) under prefix "${rest}" from bucket "${bucket}" on branch ${branchId}`);
486
523
  return;
487
524
  }
488
- if (rest === '') {
489
- throw new Error('Object target must be in the form <bucket>/<key>.');
525
+ if (rest === "") {
526
+ throw new Error("Object target must be in the form <bucket>/<key>.");
490
527
  }
491
528
  try {
492
529
  await retryOnLock(() => deleteProjectBranchBucketObject(props.apiClient, {
@@ -497,7 +534,7 @@ const deleteObject = async (props) => {
497
534
  }));
498
535
  }
499
536
  catch (err) {
500
- if (isAxiosError(err) && err.response?.status === 404) {
537
+ if (isNeonApiError(err) && err.status === 404) {
501
538
  throw new Error(objectNotFoundMessage(err, rest, bucket, branchId));
502
539
  }
503
540
  throw err;