neonctl 2.37.1 → 2.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +15 -700
  2. package/bin/cli.js +3 -0
  3. package/package.json +8 -75
  4. package/dist/analytics.js +0 -168
  5. package/dist/api.js +0 -720
  6. package/dist/auth.js +0 -125
  7. package/dist/callback.html +0 -60
  8. package/dist/cli.js +0 -9
  9. package/dist/commands/api.js +0 -278
  10. package/dist/commands/auth.js +0 -214
  11. package/dist/commands/bootstrap.js +0 -481
  12. package/dist/commands/branches.js +0 -488
  13. package/dist/commands/bucket.js +0 -549
  14. package/dist/commands/checkout.js +0 -321
  15. package/dist/commands/config.js +0 -626
  16. package/dist/commands/connection_string.js +0 -172
  17. package/dist/commands/data_api.js +0 -284
  18. package/dist/commands/databases.js +0 -82
  19. package/dist/commands/deploy.js +0 -26
  20. package/dist/commands/dev.js +0 -698
  21. package/dist/commands/diff.js +0 -222
  22. package/dist/commands/env.js +0 -189
  23. package/dist/commands/functions.js +0 -373
  24. package/dist/commands/index.js +0 -62
  25. package/dist/commands/init.js +0 -73
  26. package/dist/commands/inspect.js +0 -65
  27. package/dist/commands/ip_allow.js +0 -137
  28. package/dist/commands/link.js +0 -1121
  29. package/dist/commands/neon_auth.js +0 -1028
  30. package/dist/commands/operations.js +0 -28
  31. package/dist/commands/orgs.js +0 -24
  32. package/dist/commands/projects.js +0 -413
  33. package/dist/commands/psql.js +0 -62
  34. package/dist/commands/roles.js +0 -65
  35. package/dist/commands/schema_diff.js +0 -151
  36. package/dist/commands/set_context.js +0 -29
  37. package/dist/commands/snapshots.js +0 -455
  38. package/dist/commands/status.js +0 -40
  39. package/dist/commands/user.js +0 -15
  40. package/dist/commands/vpc_endpoints.js +0 -134
  41. package/dist/config.js +0 -11
  42. package/dist/config_format.js +0 -72
  43. package/dist/context.js +0 -231
  44. package/dist/current_branch_fast_path.js +0 -55
  45. package/dist/dev/env.js +0 -244
  46. package/dist/dev/functions.js +0 -70
  47. package/dist/dev/inputs.js +0 -63
  48. package/dist/dev/runtime.js +0 -146
  49. package/dist/env.js +0 -36
  50. package/dist/env_file.js +0 -159
  51. package/dist/errors.js +0 -80
  52. package/dist/functions_api.js +0 -48
  53. package/dist/help.js +0 -146
  54. package/dist/index.js +0 -240
  55. package/dist/log.js +0 -18
  56. package/dist/parameters.gen.js +0 -480
  57. package/dist/pkg.js +0 -25
  58. package/dist/psql/cli.js +0 -53
  59. package/dist/psql/command/cmd_cond.js +0 -437
  60. package/dist/psql/command/cmd_connect.js +0 -820
  61. package/dist/psql/command/cmd_copy.js +0 -1035
  62. package/dist/psql/command/cmd_describe.js +0 -1815
  63. package/dist/psql/command/cmd_format.js +0 -948
  64. package/dist/psql/command/cmd_io.js +0 -2193
  65. package/dist/psql/command/cmd_lo.js +0 -393
  66. package/dist/psql/command/cmd_meta.js +0 -969
  67. package/dist/psql/command/cmd_misc.js +0 -187
  68. package/dist/psql/command/cmd_pipeline.js +0 -1148
  69. package/dist/psql/command/cmd_restrict.js +0 -171
  70. package/dist/psql/command/cmd_show.js +0 -766
  71. package/dist/psql/command/dispatch.js +0 -343
  72. package/dist/psql/command/inputQueue.js +0 -42
  73. package/dist/psql/command/shared.js +0 -71
  74. package/dist/psql/complete/filenames.js +0 -139
  75. package/dist/psql/complete/index.js +0 -104
  76. package/dist/psql/complete/matcher.js +0 -315
  77. package/dist/psql/complete/psqlVars.js +0 -249
  78. package/dist/psql/complete/queries.js +0 -493
  79. package/dist/psql/complete/rules.js +0 -2424
  80. package/dist/psql/core/common.js +0 -1253
  81. package/dist/psql/core/help.js +0 -576
  82. package/dist/psql/core/mainloop.js +0 -1360
  83. package/dist/psql/core/prompt.js +0 -439
  84. package/dist/psql/core/settings.js +0 -686
  85. package/dist/psql/core/sqlHelp.js +0 -1066
  86. package/dist/psql/core/startup.js +0 -846
  87. package/dist/psql/core/syncVars.js +0 -116
  88. package/dist/psql/core/variables.js +0 -287
  89. package/dist/psql/describe/formatters.js +0 -1330
  90. package/dist/psql/describe/processNamePattern.js +0 -270
  91. package/dist/psql/describe/queries.js +0 -2452
  92. package/dist/psql/describe/versionGate.js +0 -44
  93. package/dist/psql/index.js +0 -2030
  94. package/dist/psql/io/history.js +0 -299
  95. package/dist/psql/io/input.js +0 -120
  96. package/dist/psql/io/lineEditor/buffer.js +0 -325
  97. package/dist/psql/io/lineEditor/complete.js +0 -227
  98. package/dist/psql/io/lineEditor/filename.js +0 -159
  99. package/dist/psql/io/lineEditor/index.js +0 -893
  100. package/dist/psql/io/lineEditor/keymap.js +0 -745
  101. package/dist/psql/io/lineEditor/vt100.js +0 -363
  102. package/dist/psql/io/pgpass.js +0 -202
  103. package/dist/psql/io/pgservice.js +0 -194
  104. package/dist/psql/io/psqlrc.js +0 -422
  105. package/dist/psql/print/aligned.js +0 -1748
  106. package/dist/psql/print/asciidoc.js +0 -230
  107. package/dist/psql/print/crosstab.js +0 -463
  108. package/dist/psql/print/csv.js +0 -76
  109. package/dist/psql/print/html.js +0 -240
  110. package/dist/psql/print/json.js +0 -96
  111. package/dist/psql/print/latex.js +0 -379
  112. package/dist/psql/print/pager.js +0 -267
  113. package/dist/psql/print/troff.js +0 -240
  114. package/dist/psql/print/unaligned.js +0 -99
  115. package/dist/psql/print/units.js +0 -188
  116. package/dist/psql/scanner/slash.js +0 -515
  117. package/dist/psql/scanner/sql.js +0 -914
  118. package/dist/psql/scanner/stringutils.js +0 -394
  119. package/dist/psql/types/backslash.js +0 -1
  120. package/dist/psql/types/connection.js +0 -1
  121. package/dist/psql/types/index.js +0 -7
  122. package/dist/psql/types/printer.js +0 -1
  123. package/dist/psql/types/repl.js +0 -1
  124. package/dist/psql/types/scanner.js +0 -24
  125. package/dist/psql/types/settings.js +0 -1
  126. package/dist/psql/types/variables.js +0 -1
  127. package/dist/psql/wire/connection.js +0 -2857
  128. package/dist/psql/wire/copy.js +0 -108
  129. package/dist/psql/wire/notify.js +0 -59
  130. package/dist/psql/wire/pipeline.js +0 -521
  131. package/dist/psql/wire/protocol.js +0 -466
  132. package/dist/psql/wire/sasl.js +0 -294
  133. package/dist/psql/wire/tls.js +0 -602
  134. package/dist/storage_api.js +0 -147
  135. package/dist/test_utils/fixtures.js +0 -121
  136. package/dist/test_utils/oauth_server.js +0 -9
  137. package/dist/types.js +0 -1
  138. package/dist/utils/ai_gateway_notice.js +0 -180
  139. package/dist/utils/api_enums.js +0 -33
  140. package/dist/utils/auth.js +0 -5
  141. package/dist/utils/branch_notice.js +0 -22
  142. package/dist/utils/branch_picker.js +0 -103
  143. package/dist/utils/compute_units.js +0 -28
  144. package/dist/utils/config_diff.js +0 -185
  145. package/dist/utils/enrichers.js +0 -161
  146. package/dist/utils/esbuild.js +0 -158
  147. package/dist/utils/formats.js +0 -18
  148. package/dist/utils/git_diff.js +0 -90
  149. package/dist/utils/inspect_db.js +0 -153
  150. package/dist/utils/inspect_queries.js +0 -372
  151. package/dist/utils/middlewares.js +0 -20
  152. package/dist/utils/openapi.js +0 -114
  153. package/dist/utils/package_manager.js +0 -68
  154. package/dist/utils/point_in_time.js +0 -56
  155. package/dist/utils/psql.js +0 -120
  156. package/dist/utils/string.js +0 -5
  157. package/dist/utils/ui.js +0 -59
  158. package/dist/utils/zip.js +0 -4
  159. package/dist/writer.js +0 -97
@@ -1,549 +0,0 @@
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"];
13
- // Single-PUT upload cap. Objects larger than this must use multipart upload,
14
- // which is out of scope for v1; we reject them client-side before any HTTP so
15
- // the user gets an immediate, clear error rather than a server-side rejection
16
- // part-way through a large transfer.
17
- const MAX_OBJECT_BYTES = 100 * 1024 * 1024; // 100 MB
18
- // Ambient scope shared by every bucket sub-command. The bucket name (and the
19
- // object key/prefix) is always a positional, never a flag.
20
- const scopeOptions = {
21
- "project-id": {
22
- describe: "Project ID",
23
- type: "string",
24
- },
25
- branch: {
26
- describe: "Branch ID or name",
27
- type: "string",
28
- },
29
- };
30
- // Split an object target into its bucket and the remainder (key or prefix) on
31
- // the FIRST `/`. Bucket names are DNS-safe so they never contain a slash; the
32
- // remainder may contain further slashes and is returned verbatim. When the
33
- // target has no slash, `rest` is the empty string.
34
- export const splitBucketTarget = (target) => {
35
- const slash = target.indexOf("/");
36
- if (slash === -1) {
37
- return { bucket: target, rest: "" };
38
- }
39
- return {
40
- bucket: target.slice(0, slash),
41
- rest: target.slice(slash + 1),
42
- };
43
- };
44
- export const command = "buckets";
45
- export const describe = "Manage branch object-storage buckets and their objects";
46
- export const aliases = ["bucket"];
47
- export const builder = (argv) => argv
48
- .usage("$0 bucket <sub-command> [options]")
49
- .options({
50
- "project-id": {
51
- describe: "Project ID",
52
- type: "string",
53
- },
54
- })
55
- .middleware(fillSingleProject)
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",
61
- demandOption: true,
62
- })
63
- .options({
64
- ...scopeOptions,
65
- "access-level": {
66
- describe: "The visibility of the bucket",
67
- type: "string",
68
- choices: ACCESS_LEVELS,
69
- default: "private",
70
- },
71
- }), (args) => createBucket(args))
72
- .command({
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),
77
- handler: (args) => listBuckets(args),
78
- })
79
- .command({
80
- command: "delete <name>",
81
- aliases: ["rm"],
82
- describe: "Delete a bucket from a branch",
83
- builder: (yargs) => yargs
84
- .usage("$0 bucket delete <name> [options]")
85
- .positional("name", {
86
- describe: "The bucket name to delete",
87
- type: "string",
88
- demandOption: true,
89
- })
90
- .options(scopeOptions),
91
- handler: (args) => deleteBucket(args),
92
- })
93
- .command("object <sub-command>", "List, download, upload or delete objects in a bucket", (yargs) => yargs
94
- .usage("$0 bucket object <sub-command> [options]")
95
- .command({
96
- command: "list <target>",
97
- aliases: ["ls"],
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',
99
- builder: (yargs) => yargs
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",
104
- demandOption: true,
105
- })
106
- .options({
107
- ...scopeOptions,
108
- recursive: {
109
- describe: 'List every key flat, descending into nested folders (no delimiter). Mutually exclusive with --delimiter. Mirrors "aws s3 ls --recursive"',
110
- type: "boolean",
111
- default: false,
112
- },
113
- delimiter: {
114
- describe: 'Collapse keys sharing this prefix separator into folders. Defaults to "/" (folder view); ignored when --recursive is set',
115
- type: "string",
116
- },
117
- cursor: {
118
- describe: "Pagination cursor returned as next_cursor by a previous call",
119
- type: "string",
120
- },
121
- limit: {
122
- describe: "Maximum number of items (objects + folders) to return",
123
- type: "number",
124
- },
125
- }),
126
- handler: (args) => listObjects(args),
127
- })
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",
133
- demandOption: true,
134
- })
135
- .options({
136
- ...scopeOptions,
137
- file: {
138
- describe: "Path to write the downloaded object to (defaults to the object filename in the current directory)",
139
- type: "string",
140
- },
141
- }), (args) => getObject(args))
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",
147
- demandOption: true,
148
- })
149
- .options({
150
- ...scopeOptions,
151
- file: {
152
- describe: "Path to the local file to upload",
153
- type: "string",
154
- demandOption: true,
155
- },
156
- "content-type": {
157
- describe: "Content-Type to store the object with (e.g. text/plain)",
158
- type: "string",
159
- },
160
- }), (args) => putObject(args))
161
- .command({
162
- command: "delete <target>",
163
- aliases: ["rm"],
164
- describe: "Delete an object, or every object under a prefix",
165
- builder: (yargs) => yargs
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",
170
- demandOption: true,
171
- })
172
- .options({
173
- ...scopeOptions,
174
- recursive: {
175
- describe: 'Delete every object under the given prefix. The prefix must end with "/"',
176
- type: "boolean",
177
- default: false,
178
- },
179
- }),
180
- handler: (args) => deleteObject(args),
181
- })
182
- .demandCommand(1, "")
183
- .strictCommands())
184
- .demandCommand(1, "");
185
- export const handler = (args) => {
186
- return args;
187
- };
188
- const createBucket = async (props) => {
189
- const branchId = await branchIdFromProps(props);
190
- const { data } = await retryOnLock(() => createProjectBranchBucket(props.apiClient, {
191
- projectId: props.projectId,
192
- branchId,
193
- name: props.name,
194
- accessLevel: props.accessLevel,
195
- }));
196
- log.info(`Bucket "${data.bucket.name}" (${data.bucket.access_level}) created on branch ${branchId}`);
197
- };
198
- const listBuckets = async (props) => {
199
- const branchId = await branchIdFromProps(props);
200
- const { data } = await listProjectBranchBuckets(props.apiClient, {
201
- projectId: props.projectId,
202
- branchId,
203
- });
204
- if (props.output === "json" || props.output === "yaml") {
205
- writer(props).end(data.buckets, { fields: BUCKET_FIELDS });
206
- return;
207
- }
208
- writer(props).end(data.buckets, {
209
- fields: BUCKET_FIELDS,
210
- title: "buckets",
211
- emptyMessage: "No buckets found.",
212
- });
213
- };
214
- const deleteBucket = async (props) => {
215
- const branchId = await branchIdFromProps(props);
216
- try {
217
- await retryOnLock(() => deleteProjectBranchBucket(props.apiClient, {
218
- projectId: props.projectId,
219
- branchId,
220
- bucketName: props.name,
221
- }));
222
- }
223
- catch (err) {
224
- if (isNeonApiError(err) && err.status === 404) {
225
- throw new Error(`Bucket "${props.name}" not found on branch ${branchId}.`);
226
- }
227
- throw err;
228
- }
229
- log.info(`Bucket "${props.name}" deleted from branch ${branchId}`);
230
- };
231
- // Resolve the delimiter to send to the backend, mirroring `aws s3 ls`:
232
- // - default (neither flag): "/" so the listing is folder-collapsed;
233
- // - --recursive: no delimiter, so every nested key is returned flat;
234
- // - explicit --delimiter <x>: that value (an empty string lists flat too).
235
- // `--recursive` together with an explicit `--delimiter` is nonsensical and is
236
- // rejected client-side before any HTTP request is made.
237
- export const resolveListDelimiter = (props) => {
238
- if (props.recursive && props.delimiter !== undefined) {
239
- throw new Error("--recursive and --delimiter cannot be used together. Use --recursive for a flat listing, or --delimiter to collapse on a separator.");
240
- }
241
- if (props.recursive) {
242
- return undefined;
243
- }
244
- if (props.delimiter !== undefined) {
245
- return props.delimiter;
246
- }
247
- return "/";
248
- };
249
- const listObjects = async (props) => {
250
- const delimiter = resolveListDelimiter(props);
251
- const branchId = await branchIdFromProps(props);
252
- const { bucket, rest } = splitBucketTarget(props.target);
253
- const { data } = await listProjectBranchBucketObjects(props.apiClient, {
254
- projectId: props.projectId,
255
- branchId,
256
- bucketName: bucket,
257
- prefix: rest === "" ? undefined : rest,
258
- delimiter,
259
- cursor: props.cursor,
260
- limit: props.limit,
261
- });
262
- if (props.output === "json" || props.output === "yaml") {
263
- writer(props).end(data, {
264
- fields: [
265
- "folders",
266
- "objects",
267
- "prefix",
268
- "next_cursor",
269
- "is_truncated",
270
- ],
271
- });
272
- return;
273
- }
274
- const w = writer(props);
275
- if (data.folders.length > 0) {
276
- w.write(data.folders.map((name) => ({ name })), { fields: ["name"], title: "folders" });
277
- }
278
- w.write(data.objects, {
279
- fields: OBJECT_FIELDS,
280
- title: "objects",
281
- emptyMessage: "No objects found.",
282
- });
283
- w.end();
284
- if (data.is_truncated && data.next_cursor) {
285
- log.info(`More results available. Re-run with --cursor ${data.next_cursor} to fetch the next page.`);
286
- }
287
- };
288
- // Pull a filename out of a `Content-Disposition` header, falling back to the
289
- // last segment of the object key. Handles the plain and RFC 5987 (`filename*=`)
290
- // forms the download endpoint may emit.
291
- const filenameFromContentDisposition = (contentDisposition, key) => {
292
- if (contentDisposition) {
293
- const extended = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(contentDisposition);
294
- if (extended?.[1]) {
295
- try {
296
- return basename(decodeURIComponent(extended[1].trim()));
297
- }
298
- catch {
299
- // Fall through to the plain form / key on malformed encoding.
300
- }
301
- }
302
- const plain = /filename="?([^";]+)"?/i.exec(contentDisposition);
303
- if (plain?.[1]) {
304
- return basename(plain[1].trim());
305
- }
306
- }
307
- return basename(key) || key;
308
- };
309
- // Pull the `message` field out of a server error body, returning undefined when
310
- // the body is absent, not an object, or carries no usable message.
311
- const serverErrorMessage = (body) => {
312
- const message = body?.message;
313
- return typeof message === "string" && message.trim() !== ""
314
- ? message
315
- : undefined;
316
- };
317
- // Drain a streamed error body (the form an `octet-stream` download 404 takes)
318
- // and parse its `message`. Returns undefined on any read/parse failure so the
319
- // caller falls back to its default message.
320
- const streamErrorMessage = async (stream) => {
321
- if (typeof stream?.[Symbol.asyncIterator] !== "function") {
322
- return undefined;
323
- }
324
- try {
325
- const chunks = [];
326
- for await (const chunk of stream) {
327
- chunks.push(Buffer.from(chunk));
328
- }
329
- return serverErrorMessage(JSON.parse(Buffer.concat(chunks).toString()));
330
- }
331
- catch {
332
- return undefined;
333
- }
334
- };
335
- const objectNotFoundFallback = (key, bucket, branchId) => `Object "${key}" not found in bucket "${bucket}" on branch ${branchId}.`;
336
- // Prefer the server's error message when present so a missing bucket is not
337
- // misreported as a missing object; otherwise fall back to a clean default. Used
338
- // for the JSON (non-streamed) endpoints where the body is already parsed.
339
- const objectNotFoundMessage = (err, key, bucket, branchId) => {
340
- if (isNeonApiError(err)) {
341
- const serverMessage = serverErrorMessage(err.data);
342
- if (serverMessage !== undefined) {
343
- return serverMessage;
344
- }
345
- }
346
- return objectNotFoundFallback(key, bucket, branchId);
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
- // `createReadStream` with no encoding always emits Buffers at
358
- // runtime; @types/node types the `data` chunk as `string | Buffer`,
359
- // so normalize (a string would only appear if an encoding were set).
360
- const bytes = typeof chunk === "string"
361
- ? new TextEncoder().encode(chunk)
362
- : new Uint8Array(chunk);
363
- controller.enqueue(bytes);
364
- if ((controller.desiredSize ?? 0) <= 0)
365
- source.pause();
366
- });
367
- source.on("end", () => {
368
- controller.close();
369
- });
370
- source.on("error", (err) => {
371
- controller.error(err instanceof Error ? err : new Error(String(err)));
372
- });
373
- },
374
- pull() {
375
- source.resume();
376
- },
377
- cancel() {
378
- source.destroy();
379
- },
380
- });
381
- };
382
- const getObject = async (props) => {
383
- const branchId = await branchIdFromProps(props);
384
- const { bucket, rest: key } = splitBucketTarget(props.target);
385
- if (key === "") {
386
- throw new Error("Object target must be in the form <bucket>/<key>.");
387
- }
388
- let response;
389
- try {
390
- response = await getProjectBranchBucketObject(props.apiClient, {
391
- projectId: props.projectId,
392
- branchId,
393
- bucketName: bucket,
394
- objectKey: key,
395
- });
396
- }
397
- catch (err) {
398
- if (isNeonApiError(err) && err.status === 404) {
399
- // The download response is a stream, so a 404 body arrives as a stream
400
- // too; drain and parse it to recover the server's message (which
401
- // distinguishes a missing bucket from a missing object).
402
- const serverMessage = await streamErrorMessage(err.data);
403
- throw new Error(serverMessage ?? objectNotFoundFallback(key, bucket, branchId));
404
- }
405
- throw err;
406
- }
407
- const contentDisposition = response.headers["content-disposition"];
408
- const destination = props.file ?? filenameFromContentDisposition(contentDisposition, key);
409
- try {
410
- await pipeline(response.data, createWriteStream(destination));
411
- }
412
- catch (err) {
413
- // Best-effort cleanup of the partial file before rethrowing.
414
- await unlink(destination).catch(() => undefined);
415
- throw err;
416
- }
417
- log.info(`Object "${key}" downloaded from bucket "${bucket}" on branch ${branchId} to ${destination}`);
418
- };
419
- const putObject = async (props) => {
420
- const branchId = await branchIdFromProps(props);
421
- const { bucket, rest: key } = splitBucketTarget(props.target);
422
- if (bucket === "" || key === "") {
423
- throw new Error("Object target must be in the form <bucket>/<key>.");
424
- }
425
- // Stat the file first so we fail fast on a missing/unreadable file and can
426
- // enforce the single-PUT size cap BEFORE any network round-trip. We also
427
- // reuse the byte count as the PUT Content-Length so the stream is uploaded
428
- // without buffering the whole file in memory.
429
- let fileSize;
430
- try {
431
- const fileStat = await stat(props.file);
432
- if (!fileStat.isFile()) {
433
- throw new Error(`"${props.file}" is not a regular file.`);
434
- }
435
- fileSize = fileStat.size;
436
- }
437
- catch (err) {
438
- if (err?.code === "ENOENT") {
439
- throw new Error(`File "${props.file}" does not exist.`);
440
- }
441
- throw err;
442
- }
443
- if (fileSize > MAX_OBJECT_BYTES) {
444
- throw new Error(`File "${props.file}" is ${fileSize} bytes, which exceeds the ${MAX_OBJECT_BYTES}-byte (100 MB) single-upload limit. Larger objects are not supported yet.`);
445
- }
446
- // Ask the console for a presigned PUT URL plus the headers that must travel
447
- // with the upload for the signature to verify. No SigV4 happens in neonctl.
448
- let presign;
449
- try {
450
- ({ data: presign } = await presignUpload(props.apiClient, {
451
- projectId: props.projectId,
452
- branchId,
453
- bucketName: bucket,
454
- objectKey: key,
455
- contentType: props.contentType,
456
- }));
457
- }
458
- catch (err) {
459
- if (isNeonApiError(err)) {
460
- const status = err.status;
461
- if (status === 404) {
462
- throw new Error(objectNotFoundMessage(err, key, bucket, branchId));
463
- }
464
- // Any other HTTP error from the console (e.g. 403 when the caller lacks
465
- // write permission on the bucket) carries the same JSON `{ message }`
466
- // body, so surface that rather than a bare error. When the body has no
467
- // usable message, fall back to a clean status-bearing error.
468
- const serverMessage = serverErrorMessage(err.data);
469
- throw new Error(serverMessage ??
470
- `Failed to presign upload for "${key}" in bucket "${bucket}" on branch ${branchId}${status !== undefined ? ` (HTTP ${status})` : ""}: ${err.message}`);
471
- }
472
- throw err;
473
- }
474
- // Stream the file straight into the PUT body via `fetch`; never buffer the
475
- // whole file. The presigned URL targets the branch S3 data-plane endpoint
476
- // directly, so this PUT bypasses the console API entirely.
477
- //
478
- // `presign.headers` carries the signature-relevant headers (e.g. host,
479
- // content-type); the server does not sign Content-Length, so we set it
480
- // ourselves from the stat'd size. `redirect: 'error'` ensures we never resend
481
- // the file bytes and signed headers to a different host if the data-plane
482
- // endpoint were to answer with a redirect. `duplex: 'half'` is required by
483
- // fetch when streaming a request body.
484
- const upload = {
485
- method: "PUT",
486
- headers: {
487
- ...presign.headers,
488
- "Content-Length": String(fileSize),
489
- },
490
- body: fileToWebStream(props.file),
491
- redirect: "error",
492
- duplex: "half",
493
- };
494
- let uploadResponse;
495
- try {
496
- uploadResponse = await fetch(presign.url, upload);
497
- }
498
- catch (err) {
499
- // A transport-level failure (DNS, connection reset, redirect when none is
500
- // allowed). Surface a clean message without leaking the signed URL.
501
- throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId}: ${err instanceof Error ? err.message : String(err)}`);
502
- }
503
- if (!uploadResponse.ok) {
504
- // The upload targets the S3 data plane, whose error bodies are XML rather
505
- // than the JSON `{ message }` the console returns, so surface the status
506
- // rather than the body. Never include the presigned URL, which carries the
507
- // signature.
508
- 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}`);
509
- }
510
- log.info(`File "${props.file}" uploaded to "${key}" in bucket "${bucket}" on branch ${branchId}`);
511
- };
512
- const deleteObject = async (props) => {
513
- const branchId = await branchIdFromProps(props);
514
- const { bucket, rest } = splitBucketTarget(props.target);
515
- if (props.recursive) {
516
- if (rest === "") {
517
- throw new Error('Recursive delete requires a non-empty prefix ending in "/".');
518
- }
519
- if (!rest.endsWith("/")) {
520
- throw new Error(`Recursive delete requires a prefix ending in "/" (got "${rest}").`);
521
- }
522
- const { data } = await retryOnLock(() => deleteProjectBranchBucketObjectsByPrefix(props.apiClient, {
523
- projectId: props.projectId,
524
- branchId,
525
- bucketName: bucket,
526
- prefix: rest,
527
- }));
528
- log.info(`Deleted ${data.deleted} object(s) under prefix "${rest}" from bucket "${bucket}" on branch ${branchId}`);
529
- return;
530
- }
531
- if (rest === "") {
532
- throw new Error("Object target must be in the form <bucket>/<key>.");
533
- }
534
- try {
535
- await retryOnLock(() => deleteProjectBranchBucketObject(props.apiClient, {
536
- projectId: props.projectId,
537
- branchId,
538
- bucketName: bucket,
539
- objectKey: rest,
540
- }));
541
- }
542
- catch (err) {
543
- if (isNeonApiError(err) && err.status === 404) {
544
- throw new Error(objectNotFoundMessage(err, rest, bucket, branchId));
545
- }
546
- throw err;
547
- }
548
- log.info(`Object "${rest}" deleted from bucket "${bucket}" on branch ${branchId}`);
549
- };