neon 3.0.0 → 3.1.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 (207) hide show
  1. package/README.md +70 -5
  2. package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/_chunks/credentials-MYdHdKah.js +188 -0
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
  7. package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
  8. package/dist/_chunks/profiles-Ir29rqns.js +217 -0
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
  12. package/dist/analytics.js +163 -207
  13. package/dist/api.js +815 -758
  14. package/dist/auth.js +121 -141
  15. package/dist/auth_context.js +39 -53
  16. package/dist/cli.js +4 -7
  17. package/dist/commands/api.js +220 -250
  18. package/dist/commands/api_keys.js +251 -314
  19. package/dist/commands/auth.js +283 -328
  20. package/dist/commands/bootstrap.js +372 -437
  21. package/dist/commands/branches.js +304 -455
  22. package/dist/commands/bucket.js +374 -514
  23. package/dist/commands/checkout.js +213 -298
  24. package/dist/commands/config.js +573 -690
  25. package/dist/commands/connection_string.js +137 -165
  26. package/dist/commands/data_api.js +238 -260
  27. package/dist/commands/databases.js +67 -76
  28. package/dist/commands/deploy.js +31 -25
  29. package/dist/commands/dev.js +639 -719
  30. package/dist/commands/diff.js +156 -200
  31. package/dist/commands/env.js +255 -305
  32. package/dist/commands/functions.js +275 -355
  33. package/dist/commands/index.js +70 -65
  34. package/dist/commands/init.js +84 -119
  35. package/dist/commands/inspect.js +55 -55
  36. package/dist/commands/ip_allow.js +88 -120
  37. package/dist/commands/link.js +874 -1019
  38. package/dist/commands/logs.js +291 -0
  39. package/dist/commands/neon_auth.js +725 -933
  40. package/dist/commands/operations.js +34 -25
  41. package/dist/commands/orgs.js +28 -18
  42. package/dist/commands/profile.js +615 -846
  43. package/dist/commands/projects.js +313 -373
  44. package/dist/commands/psql.js +60 -58
  45. package/dist/commands/roles.js +55 -58
  46. package/dist/commands/schema_diff.js +87 -131
  47. package/dist/commands/set_context.js +34 -26
  48. package/dist/commands/snapshots.js +288 -413
  49. package/dist/commands/status.js +41 -37
  50. package/dist/commands/user.js +21 -10
  51. package/dist/commands/vpc_endpoints.js +85 -113
  52. package/dist/config.js +7 -6
  53. package/dist/config_format.js +50 -66
  54. package/dist/config_template.js +128 -157
  55. package/dist/context.js +183 -235
  56. package/dist/current_branch_fast_path.js +40 -49
  57. package/dist/dev/env.js +2 -446
  58. package/dist/dev/functions.js +54 -68
  59. package/dist/dev/inputs.js +46 -58
  60. package/dist/dev/runtime.js +135 -164
  61. package/dist/dev/websocket.js +766 -959
  62. package/dist/env.js +27 -33
  63. package/dist/env_file.js +118 -132
  64. package/dist/env_services.js +2 -51
  65. package/dist/errors.js +57 -68
  66. package/dist/functions_api.js +45 -43
  67. package/dist/help.js +189 -140
  68. package/dist/index.js +182 -257
  69. package/dist/init/agents.js +137 -118
  70. package/dist/init/auth.js +58 -68
  71. package/dist/init/bootstrap.js +325 -396
  72. package/dist/init/build_config.js +4 -2
  73. package/dist/init/detect_agent.js +56 -101
  74. package/dist/init/editors.js +35 -52
  75. package/dist/init/enrich_output.js +51 -66
  76. package/dist/init/extension.js +134 -171
  77. package/dist/init/inspect.js +179 -266
  78. package/dist/init/interactive.js +510 -622
  79. package/dist/init/neonctl.js +117 -168
  80. package/dist/init/orchestrate.js +157 -173
  81. package/dist/init/phases/auth.js +188 -202
  82. package/dist/init/phases/cleanup.js +23 -23
  83. package/dist/init/phases/db.js +251 -277
  84. package/dist/init/phases/getting_started.js +213 -223
  85. package/dist/init/phases/mcp.js +174 -224
  86. package/dist/init/phases/migrations.js +247 -248
  87. package/dist/init/phases/neon_auth.js +114 -133
  88. package/dist/init/phases/setup.js +546 -703
  89. package/dist/init/phases/skills.js +75 -86
  90. package/dist/init/phases/status.js +72 -67
  91. package/dist/init/resolve_context.js +102 -99
  92. package/dist/init/route_command.js +91 -98
  93. package/dist/init/skills.js +174 -218
  94. package/dist/init/vsix.js +77 -99
  95. package/dist/log.js +17 -16
  96. package/dist/neon_services.js +104 -129
  97. package/dist/parameters.gen.js +481 -471
  98. package/dist/pkg.js +17 -19
  99. package/dist/profile_keys.js +44 -47
  100. package/dist/psql/cli.js +44 -47
  101. package/dist/psql/command/cmd_cond.js +231 -406
  102. package/dist/psql/command/cmd_connect.js +557 -764
  103. package/dist/psql/command/cmd_copy.js +728 -984
  104. package/dist/psql/command/cmd_describe.js +1499 -1688
  105. package/dist/psql/command/cmd_format.js +733 -905
  106. package/dist/psql/command/cmd_io.js +2 -2193
  107. package/dist/psql/command/cmd_lo.js +297 -359
  108. package/dist/psql/command/cmd_meta.js +727 -878
  109. package/dist/psql/command/cmd_misc.js +138 -172
  110. package/dist/psql/command/cmd_pipeline.js +2 -1148
  111. package/dist/psql/command/cmd_restrict.js +119 -155
  112. package/dist/psql/command/cmd_show.js +529 -688
  113. package/dist/psql/command/dispatch.js +260 -325
  114. package/dist/psql/command/inputQueue.js +35 -33
  115. package/dist/psql/command/shared.js +49 -63
  116. package/dist/psql/complete/filenames.js +90 -133
  117. package/dist/psql/complete/index.js +59 -97
  118. package/dist/psql/complete/matcher.js +236 -300
  119. package/dist/psql/complete/psqlVars.js +218 -223
  120. package/dist/psql/complete/queries.js +159 -177
  121. package/dist/psql/complete/rules.js +1493 -2299
  122. package/dist/psql/core/common.js +2 -1253
  123. package/dist/psql/core/help.js +456 -546
  124. package/dist/psql/core/mainloop.js +692 -1303
  125. package/dist/psql/core/prompt.js +391 -408
  126. package/dist/psql/core/settings.js +429 -644
  127. package/dist/psql/core/sqlHelp.js +480 -554
  128. package/dist/psql/core/startup.js +2 -846
  129. package/dist/psql/core/syncVars.js +67 -110
  130. package/dist/psql/core/variables.js +156 -278
  131. package/dist/psql/describe/formatters.js +884 -1285
  132. package/dist/psql/describe/processNamePattern.js +173 -260
  133. package/dist/psql/describe/queries.js +1368 -2403
  134. package/dist/psql/describe/versionGate.js +32 -41
  135. package/dist/psql/index.js +2 -2030
  136. package/dist/psql/io/history.js +232 -271
  137. package/dist/psql/io/input.js +103 -108
  138. package/dist/psql/io/lineEditor/buffer.js +238 -319
  139. package/dist/psql/io/lineEditor/complete.js +135 -213
  140. package/dist/psql/io/lineEditor/filename.js +139 -148
  141. package/dist/psql/io/lineEditor/index.js +653 -870
  142. package/dist/psql/io/lineEditor/keymap.js +544 -702
  143. package/dist/psql/io/lineEditor/vt100.js +294 -341
  144. package/dist/psql/io/pgpass.js +158 -187
  145. package/dist/psql/io/pgservice.js +146 -183
  146. package/dist/psql/io/psqlrc.js +328 -403
  147. package/dist/psql/print/aligned.js +1020 -1683
  148. package/dist/psql/print/asciidoc.js +180 -214
  149. package/dist/psql/print/crosstab.js +281 -442
  150. package/dist/psql/print/csv.js +48 -70
  151. package/dist/psql/print/html.js +195 -226
  152. package/dist/psql/print/json.js +75 -88
  153. package/dist/psql/print/latex.js +291 -364
  154. package/dist/psql/print/pager.js +171 -242
  155. package/dist/psql/print/troff.js +194 -226
  156. package/dist/psql/print/unaligned.js +69 -95
  157. package/dist/psql/print/units.js +167 -169
  158. package/dist/psql/scanner/slash.js +428 -483
  159. package/dist/psql/scanner/sql.js +445 -889
  160. package/dist/psql/scanner/stringutils.js +309 -379
  161. package/dist/psql/types/index.js +8 -7
  162. package/dist/psql/types/scanner.js +25 -22
  163. package/dist/psql/wire/connection.js +2042 -2803
  164. package/dist/psql/wire/copy.js +84 -100
  165. package/dist/psql/wire/notify.js +39 -59
  166. package/dist/psql/wire/pipeline.js +305 -518
  167. package/dist/psql/wire/protocol.js +349 -417
  168. package/dist/psql/wire/sasl.js +180 -265
  169. package/dist/psql/wire/tls.js +400 -561
  170. package/dist/storage_api.js +115 -129
  171. package/dist/test_utils/fixtures.js +94 -113
  172. package/dist/test_utils/oauth_server.js +10 -7
  173. package/dist/test_utils/project_dir.js +33 -0
  174. package/dist/utils/ai_gateway_notice.js +131 -162
  175. package/dist/utils/api_enums.js +21 -28
  176. package/dist/utils/auth.js +10 -4
  177. package/dist/utils/branch_notice.js +20 -19
  178. package/dist/utils/branch_picker.js +83 -89
  179. package/dist/utils/cli_name.js +15 -12
  180. package/dist/utils/compute_units.js +20 -27
  181. package/dist/utils/config_diff.js +127 -158
  182. package/dist/utils/enrichers.js +95 -148
  183. package/dist/utils/esbuild.js +130 -189
  184. package/dist/utils/flags.js +35 -47
  185. package/dist/utils/formats.js +8 -15
  186. package/dist/utils/git_diff.js +69 -80
  187. package/dist/utils/inspect_db.js +101 -143
  188. package/dist/utils/inspect_queries.js +179 -142
  189. package/dist/utils/middlewares.js +39 -45
  190. package/dist/utils/openapi.js +87 -99
  191. package/dist/utils/package_manager.js +312 -110
  192. package/dist/utils/point_in_time.js +49 -53
  193. package/dist/utils/psql.js +89 -106
  194. package/dist/utils/service_picker.js +55 -58
  195. package/dist/utils/string.js +5 -5
  196. package/dist/utils/ui.js +38 -55
  197. package/dist/utils/write_sync.js +26 -35
  198. package/dist/utils/zip.js +4 -3
  199. package/dist/writer.js +67 -87
  200. package/package.json +11 -6
  201. package/dist/_shared/auth_selection.js +0 -86
  202. package/dist/_shared/credentials.js +0 -209
  203. package/dist/_shared/env-core/env.js +0 -558
  204. package/dist/_shared/env-core/reuse-secrets.js +0 -223
  205. package/dist/_shared/paths.js +0 -148
  206. package/dist/_shared/profiles.js +0 -276
  207. package/dist/_shared/secure_file.js +0 -43
@@ -1,549 +1,409 @@
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-D7D4PA-g.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, builder, command, describe, handler, resolveListDelimiter, splitBucketTarget, bucket_exports as t };