neon 2.47.0 → 3.1.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 (198) hide show
  1. package/README.md +54 -0
  2. package/dist/_shared/auth_selection.js +76 -79
  3. package/dist/_shared/credentials.js +166 -187
  4. package/dist/_shared/env-core/env.js +395 -0
  5. package/dist/_shared/env-core/reuse-secrets.js +179 -0
  6. package/dist/_shared/paths.js +129 -126
  7. package/dist/_shared/profiles.js +192 -242
  8. package/dist/_shared/secure_file.js +36 -38
  9. package/dist/_virtual/_rolldown/runtime.js +13 -0
  10. package/dist/analytics.js +163 -207
  11. package/dist/api.js +815 -758
  12. package/dist/auth.js +121 -141
  13. package/dist/auth_context.js +39 -53
  14. package/dist/cli.js +4 -7
  15. package/dist/commands/api.js +220 -250
  16. package/dist/commands/api_keys.js +251 -314
  17. package/dist/commands/auth.js +283 -328
  18. package/dist/commands/bootstrap.js +372 -437
  19. package/dist/commands/branches.js +304 -455
  20. package/dist/commands/bucket.js +374 -514
  21. package/dist/commands/checkout.js +213 -298
  22. package/dist/commands/config.js +575 -658
  23. package/dist/commands/connection_string.js +137 -165
  24. package/dist/commands/data_api.js +238 -260
  25. package/dist/commands/databases.js +67 -76
  26. package/dist/commands/deploy.js +31 -25
  27. package/dist/commands/dev.js +642 -681
  28. package/dist/commands/diff.js +156 -200
  29. package/dist/commands/env.js +243 -303
  30. package/dist/commands/functions.js +275 -341
  31. package/dist/commands/index.js +70 -65
  32. package/dist/commands/init.js +84 -119
  33. package/dist/commands/inspect.js +55 -55
  34. package/dist/commands/ip_allow.js +88 -120
  35. package/dist/commands/link.js +874 -1019
  36. package/dist/commands/logs.js +291 -0
  37. package/dist/commands/neon_auth.js +725 -933
  38. package/dist/commands/operations.js +34 -25
  39. package/dist/commands/orgs.js +28 -18
  40. package/dist/commands/profile.js +614 -845
  41. package/dist/commands/projects.js +313 -373
  42. package/dist/commands/psql.js +60 -58
  43. package/dist/commands/roles.js +55 -58
  44. package/dist/commands/schema_diff.js +87 -131
  45. package/dist/commands/set_context.js +34 -26
  46. package/dist/commands/snapshots.js +288 -413
  47. package/dist/commands/status.js +41 -37
  48. package/dist/commands/user.js +21 -10
  49. package/dist/commands/vpc_endpoints.js +85 -113
  50. package/dist/config.js +7 -6
  51. package/dist/config_format.js +50 -66
  52. package/dist/config_template.js +128 -157
  53. package/dist/context.js +183 -235
  54. package/dist/current_branch_fast_path.js +40 -49
  55. package/dist/dev/env.js +313 -394
  56. package/dist/dev/functions.js +54 -64
  57. package/dist/dev/inputs.js +46 -58
  58. package/dist/dev/runtime.js +135 -164
  59. package/dist/dev/websocket.js +766 -959
  60. package/dist/env.js +27 -33
  61. package/dist/env_file.js +118 -132
  62. package/dist/env_services.js +36 -38
  63. package/dist/errors.js +57 -68
  64. package/dist/functions_api.js +45 -43
  65. package/dist/help.js +189 -140
  66. package/dist/index.js +182 -257
  67. package/dist/init/agents.js +137 -118
  68. package/dist/init/auth.js +58 -68
  69. package/dist/init/bootstrap.js +325 -396
  70. package/dist/init/build_config.js +4 -2
  71. package/dist/init/detect_agent.js +56 -101
  72. package/dist/init/editors.js +35 -52
  73. package/dist/init/enrich_output.js +51 -66
  74. package/dist/init/extension.js +134 -171
  75. package/dist/init/inspect.js +179 -266
  76. package/dist/init/interactive.js +510 -622
  77. package/dist/init/neonctl.js +117 -168
  78. package/dist/init/orchestrate.js +157 -173
  79. package/dist/init/phases/auth.js +188 -202
  80. package/dist/init/phases/cleanup.js +23 -23
  81. package/dist/init/phases/db.js +251 -277
  82. package/dist/init/phases/getting_started.js +213 -223
  83. package/dist/init/phases/mcp.js +174 -224
  84. package/dist/init/phases/migrations.js +247 -248
  85. package/dist/init/phases/neon_auth.js +114 -133
  86. package/dist/init/phases/setup.js +546 -703
  87. package/dist/init/phases/skills.js +75 -86
  88. package/dist/init/phases/status.js +72 -67
  89. package/dist/init/resolve_context.js +102 -99
  90. package/dist/init/route_command.js +91 -98
  91. package/dist/init/skills.js +174 -218
  92. package/dist/init/vsix.js +77 -99
  93. package/dist/log.js +17 -16
  94. package/dist/neon_services.js +104 -129
  95. package/dist/parameters.gen.js +481 -471
  96. package/dist/pkg.js +17 -19
  97. package/dist/profile_keys.js +44 -47
  98. package/dist/psql/cli.js +44 -47
  99. package/dist/psql/command/cmd_cond.js +231 -406
  100. package/dist/psql/command/cmd_connect.js +557 -764
  101. package/dist/psql/command/cmd_copy.js +727 -983
  102. package/dist/psql/command/cmd_describe.js +1499 -1688
  103. package/dist/psql/command/cmd_format.js +733 -905
  104. package/dist/psql/command/cmd_io.js +1293 -2082
  105. package/dist/psql/command/cmd_lo.js +297 -359
  106. package/dist/psql/command/cmd_meta.js +727 -878
  107. package/dist/psql/command/cmd_misc.js +138 -172
  108. package/dist/psql/command/cmd_pipeline.js +547 -1099
  109. package/dist/psql/command/cmd_restrict.js +119 -155
  110. package/dist/psql/command/cmd_show.js +529 -688
  111. package/dist/psql/command/dispatch.js +261 -325
  112. package/dist/psql/command/inputQueue.js +35 -33
  113. package/dist/psql/command/shared.js +49 -63
  114. package/dist/psql/complete/filenames.js +90 -133
  115. package/dist/psql/complete/index.js +59 -97
  116. package/dist/psql/complete/matcher.js +236 -300
  117. package/dist/psql/complete/psqlVars.js +218 -223
  118. package/dist/psql/complete/queries.js +159 -177
  119. package/dist/psql/complete/rules.js +1493 -2299
  120. package/dist/psql/core/common.js +762 -1180
  121. package/dist/psql/core/help.js +456 -546
  122. package/dist/psql/core/mainloop.js +692 -1302
  123. package/dist/psql/core/prompt.js +391 -408
  124. package/dist/psql/core/settings.js +429 -644
  125. package/dist/psql/core/sqlHelp.js +480 -554
  126. package/dist/psql/core/startup.js +626 -815
  127. package/dist/psql/core/syncVars.js +67 -110
  128. package/dist/psql/core/variables.js +156 -278
  129. package/dist/psql/describe/formatters.js +884 -1285
  130. package/dist/psql/describe/processNamePattern.js +173 -260
  131. package/dist/psql/describe/queries.js +1368 -2403
  132. package/dist/psql/describe/versionGate.js +32 -41
  133. package/dist/psql/index.js +1414 -1927
  134. package/dist/psql/io/history.js +232 -271
  135. package/dist/psql/io/input.js +103 -108
  136. package/dist/psql/io/lineEditor/buffer.js +238 -319
  137. package/dist/psql/io/lineEditor/complete.js +135 -213
  138. package/dist/psql/io/lineEditor/filename.js +139 -148
  139. package/dist/psql/io/lineEditor/index.js +653 -870
  140. package/dist/psql/io/lineEditor/keymap.js +544 -702
  141. package/dist/psql/io/lineEditor/vt100.js +294 -341
  142. package/dist/psql/io/pgpass.js +158 -187
  143. package/dist/psql/io/pgservice.js +146 -183
  144. package/dist/psql/io/psqlrc.js +328 -403
  145. package/dist/psql/print/aligned.js +1020 -1683
  146. package/dist/psql/print/asciidoc.js +180 -214
  147. package/dist/psql/print/crosstab.js +281 -442
  148. package/dist/psql/print/csv.js +48 -70
  149. package/dist/psql/print/html.js +195 -226
  150. package/dist/psql/print/json.js +75 -88
  151. package/dist/psql/print/latex.js +291 -364
  152. package/dist/psql/print/pager.js +171 -242
  153. package/dist/psql/print/troff.js +194 -226
  154. package/dist/psql/print/unaligned.js +69 -95
  155. package/dist/psql/print/units.js +167 -169
  156. package/dist/psql/scanner/slash.js +428 -483
  157. package/dist/psql/scanner/sql.js +445 -889
  158. package/dist/psql/scanner/stringutils.js +309 -379
  159. package/dist/psql/types/index.js +2 -7
  160. package/dist/psql/types/scanner.js +25 -22
  161. package/dist/psql/wire/connection.js +2042 -2803
  162. package/dist/psql/wire/copy.js +84 -100
  163. package/dist/psql/wire/notify.js +39 -59
  164. package/dist/psql/wire/pipeline.js +305 -518
  165. package/dist/psql/wire/protocol.js +349 -417
  166. package/dist/psql/wire/sasl.js +180 -265
  167. package/dist/psql/wire/tls.js +400 -561
  168. package/dist/storage_api.js +115 -129
  169. package/dist/test_utils/fixtures.js +94 -113
  170. package/dist/test_utils/oauth_server.js +10 -7
  171. package/dist/test_utils/project_dir.js +33 -0
  172. package/dist/utils/ai_gateway_notice.js +131 -162
  173. package/dist/utils/api_enums.js +21 -28
  174. package/dist/utils/auth.js +10 -4
  175. package/dist/utils/branch_notice.js +20 -19
  176. package/dist/utils/branch_picker.js +83 -89
  177. package/dist/utils/cli_name.js +15 -12
  178. package/dist/utils/compute_units.js +20 -27
  179. package/dist/utils/config_diff.js +127 -158
  180. package/dist/utils/enrichers.js +95 -148
  181. package/dist/utils/esbuild.js +133 -147
  182. package/dist/utils/flags.js +35 -47
  183. package/dist/utils/formats.js +8 -15
  184. package/dist/utils/git_diff.js +69 -80
  185. package/dist/utils/inspect_db.js +101 -143
  186. package/dist/utils/inspect_queries.js +179 -142
  187. package/dist/utils/middlewares.js +37 -44
  188. package/dist/utils/openapi.js +87 -99
  189. package/dist/utils/package_manager.js +312 -110
  190. package/dist/utils/point_in_time.js +49 -53
  191. package/dist/utils/psql.js +89 -106
  192. package/dist/utils/service_picker.js +55 -58
  193. package/dist/utils/string.js +5 -5
  194. package/dist/utils/ui.js +38 -55
  195. package/dist/utils/write_sync.js +26 -35
  196. package/dist/utils/zip.js +4 -3
  197. package/dist/writer.js +67 -87
  198. package/package.json +9 -7
@@ -1,549 +1,409 @@
1
+ import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
+ import { log } from "../log.js";
3
+ import { isNeonApiError, retryOnLock } from "../api.js";
4
+ import { writer } from "../writer.js";
5
+ import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
6
+ import { createProjectBranchBucket, deleteProjectBranchBucket, deleteProjectBranchBucketObject, deleteProjectBranchBucketObjectsByPrefix, getProjectBranchBucketObject, listProjectBranchBucketObjects, listProjectBranchBuckets, presignUpload } from "../storage_api.js";
1
7
  import { createReadStream, createWriteStream } from "node:fs";
2
- import { stat, unlink } from "node:fs/promises";
3
8
  import { basename } from "node:path";
9
+ import { stat, unlink } from "node:fs/promises";
4
10
  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
+ //#region src/commands/bucket.ts
12
+ var bucket_exports = /* @__PURE__ */ __exportAll({
13
+ aliases: () => aliases,
14
+ builder: () => builder,
15
+ command: () => command,
16
+ describe: () => describe,
17
+ handler: () => handler,
18
+ resolveListDelimiter: () => resolveListDelimiter,
19
+ splitBucketTarget: () => splitBucketTarget
20
+ });
21
+ const OBJECT_FIELDS = [
22
+ "key",
23
+ "size",
24
+ "last_modified",
25
+ "etag"
26
+ ];
11
27
  const BUCKET_FIELDS = ["name", "access_level"];
12
28
  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.
29
+ const MAX_OBJECT_BYTES = 104857600;
20
30
  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
- },
31
+ "project-id": {
32
+ describe: "Project ID",
33
+ type: "string"
34
+ },
35
+ branch: {
36
+ describe: "Branch ID or name",
37
+ type: "string"
38
+ }
29
39
  };
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
- };
40
+ const splitBucketTarget = (target) => {
41
+ const slash = target.indexOf("/");
42
+ if (slash === -1) return {
43
+ bucket: target,
44
+ rest: ""
45
+ };
46
+ return {
47
+ bucket: target.slice(0, slash),
48
+ rest: target.slice(slash + 1)
49
+ };
43
50
  };
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;
51
+ const command = "buckets";
52
+ const describe = "Manage branch object-storage buckets and their objects";
53
+ const aliases = ["bucket"];
54
+ const builder = (argv) => argv.usage("$0 bucket <sub-command> [options]").options({ "project-id": {
55
+ describe: "Project ID",
56
+ type: "string"
57
+ } }).middleware(fillSingleProject).command("create <name>", "Create a bucket on a branch", (yargs) => yargs.usage("$0 bucket create <name> [options]").positional("name", {
58
+ describe: "The bucket name to create",
59
+ type: "string",
60
+ demandOption: true
61
+ }).options({
62
+ ...scopeOptions,
63
+ "access-level": {
64
+ describe: "The visibility of the bucket",
65
+ type: "string",
66
+ choices: ACCESS_LEVELS,
67
+ default: "private"
68
+ }
69
+ }), (args) => createBucket(args)).command({
70
+ command: "list",
71
+ aliases: ["ls"],
72
+ describe: "List the buckets on a branch",
73
+ builder: (yargs) => yargs.usage("$0 bucket list [options]").options(scopeOptions),
74
+ handler: (args) => listBuckets(args)
75
+ }).command({
76
+ command: "delete <name>",
77
+ aliases: ["rm"],
78
+ describe: "Delete a bucket from a branch",
79
+ builder: (yargs) => yargs.usage("$0 bucket delete <name> [options]").positional("name", {
80
+ describe: "The bucket name to delete",
81
+ type: "string",
82
+ demandOption: true
83
+ }).options(scopeOptions),
84
+ handler: (args) => deleteBucket(args)
85
+ }).command("object <sub-command>", "List, download, upload or delete objects in a bucket", (yargs) => yargs.usage("$0 bucket object <sub-command> [options]").command({
86
+ command: "list <target>",
87
+ aliases: ["ls"],
88
+ describe: "List objects in a bucket. By default folders are collapsed (like \"aws s3 ls\"); pass --recursive for a flat listing of every key",
89
+ builder: (yargs) => yargs.usage("$0 bucket object list <bucket>[/<prefix>] [options]").positional("target", {
90
+ describe: "The bucket to list, optionally with a key prefix: <bucket>[/<prefix>]",
91
+ type: "string",
92
+ demandOption: true
93
+ }).options({
94
+ ...scopeOptions,
95
+ recursive: {
96
+ describe: "List every key flat, descending into nested folders (no delimiter). Mutually exclusive with --delimiter. Mirrors \"aws s3 ls --recursive\"",
97
+ type: "boolean",
98
+ default: false
99
+ },
100
+ delimiter: {
101
+ describe: "Collapse keys sharing this prefix separator into folders. Defaults to \"/\" (folder view); ignored when --recursive is set",
102
+ type: "string"
103
+ },
104
+ cursor: {
105
+ describe: "Pagination cursor returned as next_cursor by a previous call",
106
+ type: "string"
107
+ },
108
+ limit: {
109
+ describe: "Maximum number of items (objects + folders) to return",
110
+ type: "number"
111
+ }
112
+ }),
113
+ handler: (args) => listObjects(args)
114
+ }).command("get <target>", "Download an object from a bucket to a local file", (yargs) => yargs.usage("$0 bucket object get <bucket>/<key> [options]").positional("target", {
115
+ describe: "The object to download: <bucket>/<key>",
116
+ type: "string",
117
+ demandOption: true
118
+ }).options({
119
+ ...scopeOptions,
120
+ file: {
121
+ describe: "Path to write the downloaded object to (defaults to the object filename in the current directory)",
122
+ type: "string"
123
+ }
124
+ }), (args) => getObject(args)).command("put <target>", "Upload a local file to a bucket as an object", (yargs) => yargs.usage("$0 bucket object put <bucket>/<key> [options]").positional("target", {
125
+ describe: "The object to upload to: <bucket>/<key>",
126
+ type: "string",
127
+ demandOption: true
128
+ }).options({
129
+ ...scopeOptions,
130
+ file: {
131
+ describe: "Path to the local file to upload",
132
+ type: "string",
133
+ demandOption: true
134
+ },
135
+ "content-type": {
136
+ describe: "Content-Type to store the object with (e.g. text/plain)",
137
+ type: "string"
138
+ }
139
+ }), (args) => putObject(args)).command({
140
+ command: "delete <target>",
141
+ aliases: ["rm"],
142
+ describe: "Delete an object, or every object under a prefix",
143
+ builder: (yargs) => yargs.usage("$0 bucket object delete <bucket>/<key> [options]").positional("target", {
144
+ describe: "The object to delete: <bucket>/<key>, or <bucket>/<prefix>/ with --recursive",
145
+ type: "string",
146
+ demandOption: true
147
+ }).options({
148
+ ...scopeOptions,
149
+ recursive: {
150
+ describe: "Delete every object under the given prefix. The prefix must end with \"/\"",
151
+ type: "boolean",
152
+ default: false
153
+ }
154
+ }),
155
+ handler: (args) => deleteObject(args)
156
+ }).demandCommand(1, "").strictCommands()).demandCommand(1, "");
157
+ const handler = (args) => {
158
+ return args;
187
159
  };
188
160
  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}`);
161
+ const branchId = await branchIdFromProps(props);
162
+ const { data } = await retryOnLock(() => createProjectBranchBucket(props.apiClient, {
163
+ projectId: props.projectId,
164
+ branchId,
165
+ name: props.name,
166
+ accessLevel: props.accessLevel
167
+ }));
168
+ log.info(`Bucket "${data.bucket.name}" (${data.bucket.access_level}) created on branch ${branchId}`);
197
169
  };
198
170
  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
- });
171
+ const branchId = await branchIdFromProps(props);
172
+ const { data } = await listProjectBranchBuckets(props.apiClient, {
173
+ projectId: props.projectId,
174
+ branchId
175
+ });
176
+ if (props.output === "json" || props.output === "yaml") {
177
+ writer(props).end(data.buckets, { fields: BUCKET_FIELDS });
178
+ return;
179
+ }
180
+ writer(props).end(data.buckets, {
181
+ fields: BUCKET_FIELDS,
182
+ title: "buckets",
183
+ emptyMessage: "No buckets found."
184
+ });
213
185
  };
214
186
  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}`);
187
+ const branchId = await branchIdFromProps(props);
188
+ try {
189
+ await retryOnLock(() => deleteProjectBranchBucket(props.apiClient, {
190
+ projectId: props.projectId,
191
+ branchId,
192
+ bucketName: props.name
193
+ }));
194
+ } catch (err) {
195
+ if (isNeonApiError(err) && err.status === 404) throw new Error(`Bucket "${props.name}" not found on branch ${branchId}.`);
196
+ throw err;
197
+ }
198
+ log.info(`Bucket "${props.name}" deleted from branch ${branchId}`);
230
199
  };
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 "/";
200
+ const resolveListDelimiter = (props) => {
201
+ if (props.recursive && props.delimiter !== void 0) throw new Error("--recursive and --delimiter cannot be used together. Use --recursive for a flat listing, or --delimiter to collapse on a separator.");
202
+ if (props.recursive) return;
203
+ if (props.delimiter !== void 0) return props.delimiter;
204
+ return "/";
248
205
  };
249
206
  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
- }
207
+ const delimiter = resolveListDelimiter(props);
208
+ const branchId = await branchIdFromProps(props);
209
+ const { bucket, rest } = splitBucketTarget(props.target);
210
+ const { data } = await listProjectBranchBucketObjects(props.apiClient, {
211
+ projectId: props.projectId,
212
+ branchId,
213
+ bucketName: bucket,
214
+ prefix: rest === "" ? void 0 : rest,
215
+ delimiter,
216
+ cursor: props.cursor,
217
+ limit: props.limit
218
+ });
219
+ if (props.output === "json" || props.output === "yaml") {
220
+ writer(props).end(data, { fields: [
221
+ "folders",
222
+ "objects",
223
+ "prefix",
224
+ "next_cursor",
225
+ "is_truncated"
226
+ ] });
227
+ return;
228
+ }
229
+ const w = writer(props);
230
+ if (data.folders.length > 0) w.write(data.folders.map((name) => ({ name })), {
231
+ fields: ["name"],
232
+ title: "folders"
233
+ });
234
+ w.write(data.objects, {
235
+ fields: OBJECT_FIELDS,
236
+ title: "objects",
237
+ emptyMessage: "No objects found."
238
+ });
239
+ w.end();
240
+ if (data.is_truncated && data.next_cursor) log.info(`More results available. Re-run with --cursor ${data.next_cursor} to fetch the next page.`);
287
241
  };
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
242
  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;
243
+ if (contentDisposition) {
244
+ const extended = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(contentDisposition);
245
+ if (extended?.[1]) try {
246
+ return basename(decodeURIComponent(extended[1].trim()));
247
+ } catch {}
248
+ const plain = /filename="?([^";]+)"?/i.exec(contentDisposition);
249
+ if (plain?.[1]) return basename(plain[1].trim());
250
+ }
251
+ return basename(key) || key;
308
252
  };
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
253
  const serverErrorMessage = (body) => {
312
- const message = body?.message;
313
- return typeof message === "string" && message.trim() !== ""
314
- ? message
315
- : undefined;
254
+ const message = body?.message;
255
+ return typeof message === "string" && message.trim() !== "" ? message : void 0;
316
256
  };
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
257
  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
- }
258
+ if (typeof stream?.[Symbol.asyncIterator] !== "function") return;
259
+ try {
260
+ const chunks = [];
261
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
262
+ return serverErrorMessage(JSON.parse(Buffer.concat(chunks).toString()));
263
+ } catch {
264
+ return;
265
+ }
334
266
  };
335
267
  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
268
  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);
269
+ if (isNeonApiError(err)) {
270
+ const serverMessage = serverErrorMessage(err.data);
271
+ if (serverMessage !== void 0) return serverMessage;
272
+ }
273
+ return objectNotFoundFallback(key, bucket, branchId);
347
274
  };
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
275
  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
- });
276
+ const source = createReadStream(path);
277
+ return new ReadableStream({
278
+ start(controller) {
279
+ source.on("data", (chunk) => {
280
+ const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);
281
+ controller.enqueue(bytes);
282
+ if ((controller.desiredSize ?? 0) <= 0) source.pause();
283
+ });
284
+ source.on("end", () => {
285
+ controller.close();
286
+ });
287
+ source.on("error", (err) => {
288
+ controller.error(err instanceof Error ? err : new Error(String(err)));
289
+ });
290
+ },
291
+ pull() {
292
+ source.resume();
293
+ },
294
+ cancel() {
295
+ source.destroy();
296
+ }
297
+ });
381
298
  };
382
299
  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}`);
300
+ const branchId = await branchIdFromProps(props);
301
+ const { bucket, rest: key } = splitBucketTarget(props.target);
302
+ if (key === "") throw new Error("Object target must be in the form <bucket>/<key>.");
303
+ let response;
304
+ try {
305
+ response = await getProjectBranchBucketObject(props.apiClient, {
306
+ projectId: props.projectId,
307
+ branchId,
308
+ bucketName: bucket,
309
+ objectKey: key
310
+ });
311
+ } catch (err) {
312
+ if (isNeonApiError(err) && err.status === 404) {
313
+ const serverMessage = await streamErrorMessage(err.data);
314
+ throw new Error(serverMessage ?? objectNotFoundFallback(key, bucket, branchId));
315
+ }
316
+ throw err;
317
+ }
318
+ const contentDisposition = response.headers["content-disposition"];
319
+ const destination = props.file ?? filenameFromContentDisposition(contentDisposition, key);
320
+ try {
321
+ await pipeline(response.data, createWriteStream(destination));
322
+ } catch (err) {
323
+ await unlink(destination).catch(() => void 0);
324
+ throw err;
325
+ }
326
+ log.info(`Object "${key}" downloaded from bucket "${bucket}" on branch ${branchId} to ${destination}`);
418
327
  };
419
328
  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}`);
329
+ const branchId = await branchIdFromProps(props);
330
+ const { bucket, rest: key } = splitBucketTarget(props.target);
331
+ if (bucket === "" || key === "") throw new Error("Object target must be in the form <bucket>/<key>.");
332
+ let fileSize;
333
+ try {
334
+ const fileStat = await stat(props.file);
335
+ if (!fileStat.isFile()) throw new Error(`"${props.file}" is not a regular file.`);
336
+ fileSize = fileStat.size;
337
+ } catch (err) {
338
+ if (err?.code === "ENOENT") throw new Error(`File "${props.file}" does not exist.`);
339
+ throw err;
340
+ }
341
+ if (fileSize > MAX_OBJECT_BYTES) 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.`);
342
+ let presign;
343
+ try {
344
+ ({data: presign} = await presignUpload(props.apiClient, {
345
+ projectId: props.projectId,
346
+ branchId,
347
+ bucketName: bucket,
348
+ objectKey: key,
349
+ contentType: props.contentType
350
+ }));
351
+ } catch (err) {
352
+ if (isNeonApiError(err)) {
353
+ const status = err.status;
354
+ if (status === 404) throw new Error(objectNotFoundMessage(err, key, bucket, branchId));
355
+ const serverMessage = serverErrorMessage(err.data);
356
+ throw new Error(serverMessage ?? `Failed to presign upload for "${key}" in bucket "${bucket}" on branch ${branchId}${status !== void 0 ? ` (HTTP ${status})` : ""}: ${err.message}`);
357
+ }
358
+ throw err;
359
+ }
360
+ const upload = {
361
+ method: "PUT",
362
+ headers: {
363
+ ...presign.headers,
364
+ "Content-Length": String(fileSize)
365
+ },
366
+ body: fileToWebStream(props.file),
367
+ redirect: "error",
368
+ duplex: "half"
369
+ };
370
+ let uploadResponse;
371
+ try {
372
+ uploadResponse = await fetch(presign.url, upload);
373
+ } catch (err) {
374
+ throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId}: ${err instanceof Error ? err.message : String(err)}`);
375
+ }
376
+ if (!uploadResponse.ok) 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}`);
377
+ log.info(`File "${props.file}" uploaded to "${key}" in bucket "${bucket}" on branch ${branchId}`);
511
378
  };
512
379
  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}`);
380
+ const branchId = await branchIdFromProps(props);
381
+ const { bucket, rest } = splitBucketTarget(props.target);
382
+ if (props.recursive) {
383
+ if (rest === "") throw new Error("Recursive delete requires a non-empty prefix ending in \"/\".");
384
+ if (!rest.endsWith("/")) throw new Error(`Recursive delete requires a prefix ending in "/" (got "${rest}").`);
385
+ const { data } = await retryOnLock(() => deleteProjectBranchBucketObjectsByPrefix(props.apiClient, {
386
+ projectId: props.projectId,
387
+ branchId,
388
+ bucketName: bucket,
389
+ prefix: rest
390
+ }));
391
+ log.info(`Deleted ${data.deleted} object(s) under prefix "${rest}" from bucket "${bucket}" on branch ${branchId}`);
392
+ return;
393
+ }
394
+ if (rest === "") throw new Error("Object target must be in the form <bucket>/<key>.");
395
+ try {
396
+ await retryOnLock(() => deleteProjectBranchBucketObject(props.apiClient, {
397
+ projectId: props.projectId,
398
+ branchId,
399
+ bucketName: bucket,
400
+ objectKey: rest
401
+ }));
402
+ } catch (err) {
403
+ if (isNeonApiError(err) && err.status === 404) throw new Error(objectNotFoundMessage(err, rest, bucket, branchId));
404
+ throw err;
405
+ }
406
+ log.info(`Object "${rest}" deleted from bucket "${bucket}" on branch ${branchId}`);
549
407
  };
408
+ //#endregion
409
+ export { aliases, bucket_exports, builder, command, describe, handler, resolveListDelimiter, splitBucketTarget };