neon 2.1.1 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +601 -0
  3. package/dist/analytics.js +156 -0
  4. package/dist/api.js +665 -0
  5. package/dist/auth.js +127 -0
  6. package/dist/callback.html +51 -0
  7. package/dist/cli.js +9 -0
  8. package/dist/commands/auth.js +214 -0
  9. package/dist/commands/bootstrap.js +481 -0
  10. package/dist/commands/branches.js +481 -0
  11. package/dist/commands/bucket.js +543 -0
  12. package/dist/commands/checkout.js +289 -0
  13. package/dist/commands/config.js +544 -0
  14. package/dist/commands/connection_string.js +172 -0
  15. package/dist/commands/data_api.js +285 -0
  16. package/dist/commands/databases.js +82 -0
  17. package/dist/commands/deploy.js +26 -0
  18. package/dist/commands/dev.js +698 -0
  19. package/dist/commands/env.js +166 -0
  20. package/dist/commands/functions.js +373 -0
  21. package/dist/commands/index.js +54 -0
  22. package/dist/commands/init.js +73 -0
  23. package/dist/commands/ip_allow.js +137 -0
  24. package/dist/commands/link.js +1121 -0
  25. package/dist/commands/neon_auth.js +1028 -0
  26. package/dist/commands/operations.js +28 -0
  27. package/dist/commands/orgs.js +24 -0
  28. package/dist/commands/projects.js +372 -0
  29. package/dist/commands/psql.js +62 -0
  30. package/dist/commands/roles.js +65 -0
  31. package/dist/commands/schema_diff.js +151 -0
  32. package/dist/commands/set_context.js +29 -0
  33. package/dist/commands/status.js +40 -0
  34. package/dist/commands/user.js +15 -0
  35. package/dist/commands/vpc_endpoints.js +134 -0
  36. package/dist/config.js +11 -0
  37. package/dist/config_format.js +72 -0
  38. package/dist/context.js +177 -0
  39. package/dist/current_branch_fast_path.js +55 -0
  40. package/dist/dev/env.js +240 -0
  41. package/dist/dev/functions.js +70 -0
  42. package/dist/dev/inputs.js +63 -0
  43. package/dist/dev/runtime.js +146 -0
  44. package/dist/env.js +36 -0
  45. package/dist/env_file.js +159 -0
  46. package/dist/errors.js +80 -0
  47. package/dist/functions_api.js +44 -0
  48. package/dist/help.js +146 -0
  49. package/dist/index.js +234 -0
  50. package/dist/log.js +18 -0
  51. package/dist/parameters.gen.js +480 -0
  52. package/dist/pkg.js +25 -0
  53. package/dist/psql/cli.js +53 -0
  54. package/dist/psql/command/cmd_cond.js +437 -0
  55. package/dist/psql/command/cmd_connect.js +820 -0
  56. package/dist/psql/command/cmd_copy.js +1035 -0
  57. package/dist/psql/command/cmd_describe.js +1815 -0
  58. package/dist/psql/command/cmd_format.js +922 -0
  59. package/dist/psql/command/cmd_io.js +2193 -0
  60. package/dist/psql/command/cmd_lo.js +393 -0
  61. package/dist/psql/command/cmd_meta.js +970 -0
  62. package/dist/psql/command/cmd_misc.js +187 -0
  63. package/dist/psql/command/cmd_pipeline.js +1148 -0
  64. package/dist/psql/command/cmd_restrict.js +171 -0
  65. package/dist/psql/command/cmd_show.js +766 -0
  66. package/dist/psql/command/dispatch.js +343 -0
  67. package/dist/psql/command/inputQueue.js +42 -0
  68. package/dist/psql/command/shared.js +71 -0
  69. package/dist/psql/complete/filenames.js +139 -0
  70. package/dist/psql/complete/index.js +104 -0
  71. package/dist/psql/complete/matcher.js +315 -0
  72. package/dist/psql/complete/psqlVars.js +247 -0
  73. package/dist/psql/complete/queries.js +493 -0
  74. package/dist/psql/complete/rules.js +2424 -0
  75. package/dist/psql/core/common.js +1253 -0
  76. package/dist/psql/core/help.js +576 -0
  77. package/dist/psql/core/mainloop.js +1360 -0
  78. package/dist/psql/core/prompt.js +440 -0
  79. package/dist/psql/core/settings.js +684 -0
  80. package/dist/psql/core/sqlHelp.js +1066 -0
  81. package/dist/psql/core/startup.js +846 -0
  82. package/dist/psql/core/syncVars.js +116 -0
  83. package/dist/psql/core/variables.js +287 -0
  84. package/dist/psql/describe/formatters.js +1290 -0
  85. package/dist/psql/describe/processNamePattern.js +270 -0
  86. package/dist/psql/describe/queries.js +2378 -0
  87. package/dist/psql/describe/versionGate.js +43 -0
  88. package/dist/psql/index.js +2030 -0
  89. package/dist/psql/io/history.js +299 -0
  90. package/dist/psql/io/input.js +120 -0
  91. package/dist/psql/io/lineEditor/buffer.js +325 -0
  92. package/dist/psql/io/lineEditor/complete.js +227 -0
  93. package/dist/psql/io/lineEditor/filename.js +159 -0
  94. package/dist/psql/io/lineEditor/index.js +894 -0
  95. package/dist/psql/io/lineEditor/keymap.js +745 -0
  96. package/dist/psql/io/lineEditor/vt100.js +363 -0
  97. package/dist/psql/io/pgpass.js +202 -0
  98. package/dist/psql/io/pgservice.js +194 -0
  99. package/dist/psql/io/psqlrc.js +422 -0
  100. package/dist/psql/print/aligned.js +1765 -0
  101. package/dist/psql/print/asciidoc.js +248 -0
  102. package/dist/psql/print/crosstab.js +463 -0
  103. package/dist/psql/print/csv.js +95 -0
  104. package/dist/psql/print/html.js +258 -0
  105. package/dist/psql/print/json.js +96 -0
  106. package/dist/psql/print/latex.js +396 -0
  107. package/dist/psql/print/pager.js +267 -0
  108. package/dist/psql/print/troff.js +258 -0
  109. package/dist/psql/print/unaligned.js +119 -0
  110. package/dist/psql/print/units.js +135 -0
  111. package/dist/psql/scanner/slash.js +515 -0
  112. package/dist/psql/scanner/sql.js +914 -0
  113. package/dist/psql/scanner/stringutils.js +394 -0
  114. package/dist/psql/types/backslash.js +1 -0
  115. package/dist/psql/types/connection.js +1 -0
  116. package/dist/psql/types/index.js +7 -0
  117. package/dist/psql/types/printer.js +1 -0
  118. package/dist/psql/types/repl.js +1 -0
  119. package/dist/psql/types/scanner.js +24 -0
  120. package/dist/psql/types/settings.js +1 -0
  121. package/dist/psql/types/variables.js +1 -0
  122. package/dist/psql/wire/connection.js +2858 -0
  123. package/dist/psql/wire/copy.js +108 -0
  124. package/dist/psql/wire/notify.js +59 -0
  125. package/dist/psql/wire/pipeline.js +521 -0
  126. package/dist/psql/wire/protocol.js +466 -0
  127. package/dist/psql/wire/sasl.js +296 -0
  128. package/dist/psql/wire/tls.js +602 -0
  129. package/dist/storage_api.js +147 -0
  130. package/dist/test_utils/fixtures.js +122 -0
  131. package/dist/test_utils/oauth_server.js +9 -0
  132. package/dist/types.js +1 -0
  133. package/dist/utils/api_enums.js +33 -0
  134. package/dist/utils/auth.js +5 -0
  135. package/dist/utils/branch_notice.js +22 -0
  136. package/dist/utils/branch_picker.js +103 -0
  137. package/dist/utils/compute_units.js +28 -0
  138. package/dist/utils/enrichers.js +161 -0
  139. package/dist/utils/esbuild.js +158 -0
  140. package/dist/utils/formats.js +18 -0
  141. package/dist/utils/middlewares.js +20 -0
  142. package/dist/utils/package_manager.js +68 -0
  143. package/dist/utils/point_in_time.js +56 -0
  144. package/dist/utils/psql.js +120 -0
  145. package/dist/utils/string.js +5 -0
  146. package/dist/utils/ui.js +59 -0
  147. package/dist/utils/zip.js +4 -0
  148. package/dist/writer.js +97 -0
  149. package/package.json +117 -14
  150. package/.jshintrc +0 -3
  151. package/CHANGELOG.markdown +0 -22
  152. package/LICENSE +0 -21
  153. package/README.markdown +0 -76
  154. package/bower.json +0 -23
  155. package/license.txt +0 -9
  156. package/neon.js +0 -234
  157. package/stdlib/bubbling_support.js +0 -32
  158. package/stdlib/custom_event.js +0 -54
  159. package/stdlib/custom_event_support.js +0 -195
  160. package/stdlib/index.js +0 -7
  161. package/stdlib/node_support.js +0 -123
  162. package/stdlib/widget.js +0 -340
  163. package/test/neon_browser.html +0 -11
  164. package/test/neon_stdlib_browser.html +0 -15
  165. package/test/neon_stdlib_test.js +0 -84
  166. package/test/neon_test.js +0 -64
@@ -0,0 +1,147 @@
1
+ // Typed client helpers for the branch object-storage (bucket/object) API.
2
+ //
3
+ // These endpoints are part of the Neon object-storage surface (the "Buckets"
4
+ // tag in the public API). They are not yet exposed as typed methods on
5
+ // `@neon/sdk`, so the request/response types and the thin call helpers live
6
+ // here. They are implemented on top of the API client's low-level `request()`
7
+ // method, which reuses the exact same authentication, base URL, headers and
8
+ // retry behaviour as every other neonctl command. When the SDK gains these
9
+ // methods, the call sites in `src/commands/bucket.ts` can switch over with no
10
+ // behavioural change.
11
+ const bucketsPath = (projectId, branchId) => `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/buckets`;
12
+ const bucketPath = (projectId, branchId, bucketName) => `${bucketsPath(projectId, branchId)}/${encodeURIComponent(bucketName)}`;
13
+ /**
14
+ * Create a bucket on a branch.
15
+ *
16
+ * @request POST /projects/{project_id}/branches/{branch_id}/buckets
17
+ */
18
+ export const createProjectBranchBucket = (apiClient, { projectId, branchId, name, accessLevel, }) => {
19
+ const body = { name };
20
+ // Omit access_level entirely so the server default (`private`) applies.
21
+ if (accessLevel !== undefined) {
22
+ body.access_level = accessLevel;
23
+ }
24
+ return apiClient.request({
25
+ path: bucketsPath(projectId, branchId),
26
+ method: "POST",
27
+ body,
28
+ format: "json",
29
+ secure: true,
30
+ });
31
+ };
32
+ /**
33
+ * List the buckets on a branch.
34
+ *
35
+ * @request GET /projects/{project_id}/branches/{branch_id}/buckets
36
+ */
37
+ export const listProjectBranchBuckets = (apiClient, { projectId, branchId }) => apiClient.request({
38
+ path: bucketsPath(projectId, branchId),
39
+ method: "GET",
40
+ format: "json",
41
+ secure: true,
42
+ });
43
+ /**
44
+ * Delete a bucket from a branch.
45
+ *
46
+ * @request DELETE /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}
47
+ */
48
+ export const deleteProjectBranchBucket = (apiClient, { projectId, branchId, bucketName, }) => apiClient.request({
49
+ path: bucketPath(projectId, branchId, bucketName),
50
+ method: "DELETE",
51
+ secure: true,
52
+ });
53
+ /**
54
+ * List objects (and collapsed folders) in a bucket on a branch.
55
+ *
56
+ * @request GET /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}/objects
57
+ */
58
+ export const listProjectBranchBucketObjects = (apiClient, { projectId, branchId, bucketName, ...query }) => apiClient.request({
59
+ path: `${bucketPath(projectId, branchId, bucketName)}/objects`,
60
+ method: "GET",
61
+ query,
62
+ format: "json",
63
+ secure: true,
64
+ });
65
+ /**
66
+ * Download an object's raw bytes from a bucket on a branch.
67
+ *
68
+ * The server returns the body as `application/octet-stream` with a
69
+ * `Content-Disposition: attachment` header; the helper requests the body as a
70
+ * stream (`responseType: 'stream'`), so `.data` is a Node `Readable` the caller
71
+ * can pipe straight to disk without buffering the whole object in memory. The
72
+ * response headers are returned alongside so the caller can derive a filename
73
+ * from `Content-Disposition`.
74
+ *
75
+ * The object key may contain `/`; it is percent-encoded into a single path
76
+ * segment so nested keys are routed to the `{object_key}` parameter.
77
+ *
78
+ * @request GET /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}/objects/{object_key}/download
79
+ */
80
+ export const getProjectBranchBucketObject = (apiClient, { projectId, branchId, bucketName, objectKey, }) => apiClient.request({
81
+ path: `${bucketPath(projectId, branchId, bucketName)}/objects/${encodeURIComponent(objectKey)}/download`,
82
+ method: "GET",
83
+ format: "stream",
84
+ secure: true,
85
+ });
86
+ /**
87
+ * Delete an object from a bucket on a branch.
88
+ *
89
+ * The object key may contain `/`; it is percent-encoded into a single path
90
+ * segment so nested keys are routed to the `{object_key}` parameter.
91
+ *
92
+ * @request DELETE /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}/objects/{object_key}
93
+ */
94
+ export const deleteProjectBranchBucketObject = (apiClient, { projectId, branchId, bucketName, objectKey, }) => apiClient.request({
95
+ path: `${bucketPath(projectId, branchId, bucketName)}/objects/${encodeURIComponent(objectKey)}`,
96
+ method: "DELETE",
97
+ secure: true,
98
+ });
99
+ /**
100
+ * Delete every object under a key prefix (folder) in a bucket on a branch.
101
+ *
102
+ * `prefix` must be non-empty and end with `/`; every object on this branch
103
+ * whose key starts with the prefix is soft-deleted in a single call.
104
+ *
105
+ * @request DELETE /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}/objects-by-prefix
106
+ */
107
+ export const deleteProjectBranchBucketObjectsByPrefix = (apiClient, { projectId, branchId, bucketName, prefix, }) => apiClient.request({
108
+ path: `${bucketPath(projectId, branchId, bucketName)}/objects-by-prefix`,
109
+ method: "DELETE",
110
+ query: { prefix },
111
+ format: "json",
112
+ secure: true,
113
+ });
114
+ /**
115
+ * Request a presigned PUT URL for uploading an object to a bucket on a branch.
116
+ *
117
+ * Returns the URL, the headers that must accompany the PUT for the signature to
118
+ * verify, and the expiry. The actual upload (a `PUT` to the returned `url` with
119
+ * the returned `headers` and the file stream) is performed by the caller, NOT
120
+ * through this api-client, since it targets the branch S3 data-plane endpoint
121
+ * rather than the console API. No SigV4 or credential handling happens here.
122
+ *
123
+ * The object key may contain `/`; it is percent-encoded into a single path
124
+ * segment so nested keys are routed to the `{object_key}` parameter.
125
+ *
126
+ * This targets the unified presign endpoint, passing `operation: "upload"` to
127
+ * request a presigned PUT. (The same endpoint also serves `download`, but that
128
+ * path is API-only and has no neonctl command.)
129
+ *
130
+ * @request POST /projects/{project_id}/branches/{branch_id}/buckets/{bucket_name}/objects/{object_key}/presign
131
+ */
132
+ export const presignUpload = (apiClient, { projectId, branchId, bucketName, objectKey, contentType, expiresInSeconds, }) => {
133
+ const body = { operation: "upload" };
134
+ if (contentType !== undefined) {
135
+ body.content_type = contentType;
136
+ }
137
+ if (expiresInSeconds !== undefined) {
138
+ body.expires_in_seconds = expiresInSeconds;
139
+ }
140
+ return apiClient.request({
141
+ path: `${bucketPath(projectId, branchId, bucketName)}/objects/${encodeURIComponent(objectKey)}/presign`,
142
+ method: "POST",
143
+ body,
144
+ format: "json",
145
+ secure: true,
146
+ });
147
+ };
@@ -0,0 +1,122 @@
1
+ import { fork } from "node:child_process";
2
+ import { createServer } from "node:http";
3
+ import { join } from "node:path";
4
+ import emocks from "emocks";
5
+ import express from "express";
6
+ import strip from "strip-ansi";
7
+ import { expect, test as originalTest } from "vitest";
8
+ import { log } from "../log";
9
+ /**
10
+ * Reserve a localhost port and close its listener, returning a URL that is guaranteed to
11
+ * refuse connections right now. Lets a test drive the CLI into a real `ECONNREFUSED`
12
+ * (the same shape a network blip produces) deterministically and fast.
13
+ */
14
+ const reserveClosedPort = () => new Promise((resolve, reject) => {
15
+ const probe = createServer();
16
+ probe.on("error", reject);
17
+ probe.listen(0, "127.0.0.1", () => {
18
+ const { port } = probe.address();
19
+ probe.close((err) => {
20
+ if (err) {
21
+ reject(err);
22
+ }
23
+ else {
24
+ resolve(`http://127.0.0.1:${port}`);
25
+ }
26
+ });
27
+ });
28
+ });
29
+ export const test = originalTest.extend({
30
+ // eslint-disable-next-line no-empty-pattern
31
+ runMockServer: async ({}, use) => {
32
+ let startedServer;
33
+ await use(async (mockDir) => {
34
+ const app = express();
35
+ app.use(express.json());
36
+ app.use("/", emocks(join(process.cwd(), "mocks", mockDir), {
37
+ "404": (_req, res) => res.status(404).send({ message: "Not Found" }),
38
+ }));
39
+ const server = await new Promise((resolve) => {
40
+ const s = app.listen(0, () => {
41
+ log.debug("Mock server listening at %d", s.address().port);
42
+ resolve(s);
43
+ });
44
+ });
45
+ startedServer = server;
46
+ return server;
47
+ });
48
+ // `unreachableHost` tests never start the server, so only close it when it ran.
49
+ const server = startedServer;
50
+ if (server) {
51
+ await new Promise((resolve, reject) => {
52
+ server.close((err) => {
53
+ if (err) {
54
+ reject(err instanceof Error ? err : new Error(String(err)));
55
+ }
56
+ else {
57
+ resolve();
58
+ }
59
+ });
60
+ });
61
+ }
62
+ },
63
+ testCliCommand: async ({ runMockServer }, use) => {
64
+ await use(async (args, options = {}) => {
65
+ const apiHost = options.unreachableHost
66
+ ? await reserveClosedPort()
67
+ : `http://localhost:${(await runMockServer(options.mockDir || "main")).address().port}`;
68
+ let output = "";
69
+ let error = "";
70
+ const cp = fork(join(process.cwd(), "./dist/index.js"), [
71
+ "--api-host",
72
+ apiHost,
73
+ "--output",
74
+ options.output ?? (options.outputTable ? "table" : "yaml"),
75
+ "--api-key",
76
+ "test-key",
77
+ "--no-analytics",
78
+ ...args,
79
+ ], {
80
+ stdio: "pipe",
81
+ ...(options.cwd ? { cwd: options.cwd } : {}),
82
+ env: {
83
+ PATH: `${join(process.cwd(), "mocks/bin")}:${process.env.PATH}`,
84
+ ...(options.env ?? {}),
85
+ },
86
+ });
87
+ return new Promise((resolve, reject) => {
88
+ cp.stdout?.on("data", (data) => {
89
+ output += data.toString();
90
+ });
91
+ cp.stderr?.on("data", (data) => {
92
+ error += data.toString();
93
+ log.error(data.toString());
94
+ });
95
+ cp.on("error", (err) => {
96
+ log.error(err);
97
+ throw err;
98
+ });
99
+ cp.on("close", (code) => {
100
+ try {
101
+ expect(code).toBe(options?.code ?? 0);
102
+ expect(output).toMatchSnapshot();
103
+ if (options.stderr !== undefined) {
104
+ expect(strip(error).replace(/\s+/g, " ").trim()).toEqual(typeof options.stderr === "string"
105
+ ? options.stderr
106
+ .toString()
107
+ .replace(/\s+/g, " ")
108
+ : options.stderr);
109
+ }
110
+ resolve();
111
+ }
112
+ catch (err) {
113
+ reject(err instanceof Error ? err : new Error(String(err)));
114
+ }
115
+ });
116
+ }).catch((err) => {
117
+ log.error(err);
118
+ throw err;
119
+ });
120
+ });
121
+ },
122
+ });
@@ -0,0 +1,9 @@
1
+ import { OAuth2Server } from "oauth2-mock-server";
2
+ import { log } from "../log";
3
+ export const startOauthServer = async () => {
4
+ const server = new OAuth2Server();
5
+ await server.issuer.keys.generate("RS256");
6
+ await server.start(0, "localhost");
7
+ log.debug("Started OAuth server on port %d", server.address().port);
8
+ return server;
9
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ // Runtime enum-like constants for Neon API string unions.
2
+ //
3
+ // `@neondatabase/api-client` generated TypeScript `enum`s (real runtime objects)
4
+ // for fields like the compute endpoint type. `@neon/sdk` instead models these as
5
+ // plain string-literal union *types*, which have no runtime value — so code that
6
+ // read `EndpointType.ReadWrite` or `Object.values(EndpointType)` no longer works.
7
+ //
8
+ // These `as const` objects restore that runtime surface, and each is paired with
9
+ // a same-named type whose union is identical to the corresponding `@neon/sdk`
10
+ // type, so values stay assignable in both directions.
11
+ export const EndpointType = {
12
+ ReadOnly: "read_only",
13
+ ReadWrite: "read_write",
14
+ };
15
+ export const NeonAuthOauthProviderId = {
16
+ Google: "google",
17
+ Github: "github",
18
+ Microsoft: "microsoft",
19
+ Vercel: "vercel",
20
+ };
21
+ export const NeonAuthOauthProviderType = {
22
+ Standard: "standard",
23
+ Shared: "shared",
24
+ };
25
+ export const NeonAuthSupportedAuthProvider = {
26
+ Mock: "mock",
27
+ Stack: "stack",
28
+ BetterAuth: "better_auth",
29
+ };
30
+ export const NeonAuthEmailVerificationMethod = {
31
+ Link: "link",
32
+ Otp: "otp",
33
+ };
@@ -0,0 +1,5 @@
1
+ export const extendTokenSet = (tokenSet) => {
2
+ const exp = new Date();
3
+ exp.setSeconds(exp.getSeconds() + (tokenSet.expires_in ?? 0));
4
+ return { ...tokenSet, expires_at: exp.getTime() };
5
+ };
@@ -0,0 +1,22 @@
1
+ import chalk from "chalk";
2
+ import { log } from "../log.js";
3
+ /**
4
+ * Print a one-line "this command is targeting <branch>" notice to **stderr** so
5
+ * the user can sanity-check they're acting on the branch they think they are —
6
+ * before a `status` / `plan` / `apply` / `env pull` does its work. This is the
7
+ * cheap guardrail that catches "I planned against the wrong branch" / "I pulled
8
+ * env from the wrong branch" before it bites.
9
+ *
10
+ * - Skipped for machine-readable output (`--output json|yaml`) so it never has
11
+ * to be reasoned about by a script; it's stderr-only regardless, keeping
12
+ * `--output table` stdout clean for piping too.
13
+ * - `verb` is the leading phrase, e.g. `'Planning against branch'` →
14
+ * `→ Planning against branch main (br-…)`.
15
+ */
16
+ export const announceTargetBranch = (props, branch, verb) => {
17
+ if (props.output === "json" || props.output === "yaml") {
18
+ return;
19
+ }
20
+ const suffix = branch.usedDefault ? chalk.dim(" · project default") : "";
21
+ log.info("%s %s %s %s%s", chalk.dim("→"), verb, chalk.cyan.bold(branch.branchName), chalk.dim(`(${branch.branchId})`), suffix);
22
+ };
@@ -0,0 +1,103 @@
1
+ import prompts from "prompts";
2
+ import { retryOnLock } from "../api.js";
3
+ import { isCi } from "../env.js";
4
+ import { log } from "../log.js";
5
+ import { EndpointType } from "./api_enums.js";
6
+ /** Sentinel `value` for the "create a new branch" choice (no branch id can collide). */
7
+ const CREATE_BRANCH_CHOICE = Symbol("create-branch");
8
+ /**
9
+ * Render a branch's display name with the same word labels as `neonctl branch list`
10
+ * (`[default]`, `[protected]`) instead of symbols, so the picker reads clearly.
11
+ */
12
+ const branchLabel = (branch) => {
13
+ const labels = [];
14
+ if (branch.default) {
15
+ labels.push("[default]");
16
+ }
17
+ if (branch.protected) {
18
+ labels.push("[protected]");
19
+ }
20
+ labels.push(branch.name);
21
+ return labels.join(" ");
22
+ };
23
+ /**
24
+ * Prompt the user to pick a branch from `branches`, with a "+ Create a new branch…" option
25
+ * pinned to the top (mirroring the project/org pickers). The default selection is the
26
+ * project's default branch (the create option sits at index 0, so the default index is
27
+ * offset by one).
28
+ *
29
+ * Throws `opts.nonInteractiveMessage` when there is no TTY (or in CI): the caller knows the
30
+ * right guidance for its command, so the message is supplied rather than hard-coded here.
31
+ */
32
+ export const pickBranchInteractively = async (branches, opts) => {
33
+ if (isCi() || !process.stdout.isTTY) {
34
+ throw new Error(opts.nonInteractiveMessage);
35
+ }
36
+ const defaultBranchIndex = branches.findIndex((b) => b.default);
37
+ const initial = defaultBranchIndex >= 0 ? defaultBranchIndex + 1 : 0;
38
+ const { choice } = await prompts({
39
+ type: "select",
40
+ name: "choice",
41
+ message: opts.message,
42
+ choices: [
43
+ { title: "+ Create a new branch…", value: CREATE_BRANCH_CHOICE },
44
+ ...branches.map((b) => ({
45
+ title: `${branchLabel(b)} (${b.id})`,
46
+ value: b.id,
47
+ })),
48
+ ],
49
+ initial,
50
+ });
51
+ if (choice === undefined) {
52
+ throw new Error("Aborted: no branch selected.");
53
+ }
54
+ if (choice === CREATE_BRANCH_CHOICE) {
55
+ return { kind: "create", name: await promptNewBranchName(branches) };
56
+ }
57
+ return { kind: "existing", branchId: choice };
58
+ };
59
+ /**
60
+ * Prompt for a new branch name, rejecting empty input and names already taken on the
61
+ * project (so we never silently select a different, pre-existing branch).
62
+ */
63
+ export const promptNewBranchName = async (branches) => {
64
+ const existing = new Set(branches.map((b) => b.name));
65
+ const { name } = await prompts({
66
+ type: "text",
67
+ name: "name",
68
+ message: "New branch name:",
69
+ validate: (value) => {
70
+ const trimmed = value.trim();
71
+ if (trimmed === "")
72
+ return "Branch name cannot be empty.";
73
+ if (existing.has(trimmed))
74
+ return `A branch named "${trimmed}" already exists.`;
75
+ return true;
76
+ },
77
+ });
78
+ const trimmed = typeof name === "string" ? name.trim() : "";
79
+ if (trimmed === "") {
80
+ throw new Error("Aborted: no branch name provided.");
81
+ }
82
+ return trimmed;
83
+ };
84
+ /**
85
+ * Create a branch with the same defaults as `neonctl branch create --name <name>`:
86
+ * branched from the project's default branch with a read-write compute endpoint. Returns
87
+ * the new branch id.
88
+ */
89
+ export const createBranch = async (apiClient, projectId, name, branches) => {
90
+ const defaultBranch = branches.find((b) => b.default);
91
+ if (!defaultBranch) {
92
+ throw new Error("No default branch found");
93
+ }
94
+ const { data } = await retryOnLock(() => apiClient.createProjectBranch(projectId, {
95
+ branch: { name, parent_id: defaultBranch.id },
96
+ endpoints: [{ type: EndpointType.ReadWrite }],
97
+ }));
98
+ if (defaultBranch.protected) {
99
+ log.warning("The parent branch is protected; a unique role password has been generated for the new branch.");
100
+ }
101
+ log.info("Created branch %s (%s).", data.branch.name, data.branch.id);
102
+ return data.branch.id;
103
+ };
@@ -0,0 +1,28 @@
1
+ export const getComputeUnits = (autoscaling) => {
2
+ const fixedSizeAutoscaling = Number(autoscaling);
3
+ if (!isNaN(fixedSizeAutoscaling)) {
4
+ return {
5
+ autoscaling_limit_min_cu: fixedSizeAutoscaling,
6
+ autoscaling_limit_max_cu: fixedSizeAutoscaling,
7
+ };
8
+ }
9
+ if (!autoscaling.includes("-")) {
10
+ throw new Error('Autoscaling should be either fixed size (e.g. 2) or min and max sizes delimited with a dash (e.g. "0.5-1")');
11
+ }
12
+ const [min, max] = autoscaling.split("-");
13
+ if (!min || !max) {
14
+ throw new Error('Autoscaling should be either fixed size (e.g. 2) or min and max sizes delimited with a dash (e.g. "0.5-1")');
15
+ }
16
+ const minAutoscaling = Number(min);
17
+ const maxAutoscaling = Number(max);
18
+ if (isNaN(minAutoscaling)) {
19
+ throw new Error("Autoscaling min should be a number");
20
+ }
21
+ if (isNaN(maxAutoscaling)) {
22
+ throw new Error("Autoscaling max should be a number");
23
+ }
24
+ return {
25
+ autoscaling_limit_min_cu: minAutoscaling,
26
+ autoscaling_limit_max_cu: maxAutoscaling,
27
+ };
28
+ };
@@ -0,0 +1,161 @@
1
+ import { isNeonApiError, messageFromBody } from "../api.js";
2
+ import { isConfigInit, isCurrentBranchProbe } from "../context.js";
3
+ import { looksLikeBranchId } from "./formats.js";
4
+ export const branchIdResolve = async ({ branch, apiClient, projectId, }) => {
5
+ branch = branch.toString();
6
+ if (looksLikeBranchId(branch)) {
7
+ return branch;
8
+ }
9
+ const { data } = await apiClient.listProjectBranches({
10
+ projectId,
11
+ });
12
+ const branchData = data.branches.find((b) => b.name === branch);
13
+ if (!branchData) {
14
+ throw new Error(`Branch ${branch} not found.\nAvailable branches: ${data.branches
15
+ .map((b) => b.name)
16
+ .join(", ")}`);
17
+ }
18
+ return branchData.id;
19
+ };
20
+ const getBranchIdFromProps = async (props) => {
21
+ const branch = "branch" in props && typeof props.branch === "string"
22
+ ? props.branch
23
+ : props.id;
24
+ if (branch) {
25
+ return await branchIdResolve({
26
+ branch,
27
+ apiClient: props.apiClient,
28
+ projectId: props.projectId,
29
+ });
30
+ }
31
+ const { data } = await props.apiClient.listProjectBranches({
32
+ projectId: props.projectId,
33
+ });
34
+ const defaultBranch = data.branches.find((b) => b.default);
35
+ if (defaultBranch) {
36
+ return defaultBranch.id;
37
+ }
38
+ throw new Error("No default branch found");
39
+ };
40
+ export const branchIdFromProps = async (props) => {
41
+ props.branchId = await getBranchIdFromProps(props);
42
+ return props.branchId;
43
+ };
44
+ export const resolveBranchRef = async (props) => {
45
+ const branch = "branch" in props && typeof props.branch === "string"
46
+ ? props.branch
47
+ : props.id;
48
+ const { data } = await props.apiClient.listProjectBranches({
49
+ projectId: props.projectId,
50
+ });
51
+ const branches = data.branches;
52
+ if (branch) {
53
+ const ref = branch.toString();
54
+ const found = looksLikeBranchId(ref)
55
+ ? branches.find((b) => b.id === ref)
56
+ : branches.find((b) => b.name === ref);
57
+ if (found) {
58
+ return {
59
+ branchId: found.id,
60
+ branchName: found.name ?? found.id,
61
+ usedDefault: false,
62
+ };
63
+ }
64
+ // A `br-…` id absent from the listing is still usable as an id (trust it like
65
+ // branchIdResolve does); only an unresolved *name* is a genuine error.
66
+ if (looksLikeBranchId(ref)) {
67
+ return { branchId: ref, branchName: ref, usedDefault: false };
68
+ }
69
+ throw new Error(`Branch ${ref} not found.\nAvailable branches: ${branches
70
+ .map((b) => b.name)
71
+ .join(", ")}`);
72
+ }
73
+ const defaultBranch = branches.find((b) => b.default);
74
+ if (!defaultBranch) {
75
+ throw new Error("No default branch found");
76
+ }
77
+ return {
78
+ branchId: defaultBranch.id,
79
+ branchName: defaultBranch.name ?? defaultBranch.id,
80
+ usedDefault: true,
81
+ };
82
+ };
83
+ export const resolveSingleDatabase = async (props) => {
84
+ const { data } = await props.apiClient.listProjectBranchDatabases(props.projectId, props.branchId);
85
+ const databases = data.databases;
86
+ if (props.database !== undefined) {
87
+ if (!databases.find((d) => d.name === props.database)) {
88
+ throw new Error(`Database not found: ${props.database}. Available databases on branch ${props.branchId}: ${databases.map((d) => d.name).join(", ")}`);
89
+ }
90
+ return props.database;
91
+ }
92
+ if (databases.length === 0) {
93
+ throw new Error(`No databases found for the branch: ${props.branchId}`);
94
+ }
95
+ if (databases.length === 1) {
96
+ return databases[0].name;
97
+ }
98
+ throw new Error(`Multiple databases found for the branch, please provide one with the --database option: ${databases.map((d) => d.name).join(", ")}`);
99
+ };
100
+ export const fillSingleProject = async (props) => {
101
+ // The offline `--current-branch` probe needs no project at all and runs with no
102
+ // API client (auth was skipped), so resolving a single project here would both
103
+ // hit the network and dereference a null client. Skip it entirely.
104
+ if (isCurrentBranchProbe(props)) {
105
+ return props;
106
+ }
107
+ // `config init` is purely local (scaffold + npm install) and runs with no API
108
+ // client, so resolving a single project here would dereference a null client.
109
+ if (isConfigInit(props)) {
110
+ return props;
111
+ }
112
+ if (props.projectId) {
113
+ return { ...props, projectId: props.projectId };
114
+ }
115
+ // If no orgId is provided, try to auto-fill it if there's only one org
116
+ let orgId = props.orgId;
117
+ if (!orgId) {
118
+ const { data: orgsData } = await props.apiClient.getCurrentUserOrganizations();
119
+ if (orgsData.organizations.length === 1) {
120
+ orgId = orgsData.organizations[0].id;
121
+ }
122
+ }
123
+ try {
124
+ const { data } = await props.apiClient.listProjects({
125
+ limit: 2,
126
+ org_id: orgId,
127
+ });
128
+ if (data.projects.length === 0) {
129
+ throw new Error("No projects found");
130
+ }
131
+ if (data.projects.length > 1) {
132
+ throw new Error(`Multiple projects found, please provide one with the --project-id option`);
133
+ }
134
+ return {
135
+ ...props,
136
+ projectId: data.projects[0].id,
137
+ };
138
+ }
139
+ catch (error) {
140
+ // If the API error is about missing org_id, provide a user-friendly message
141
+ if (isNeonApiError(error) &&
142
+ error.status === 400 &&
143
+ messageFromBody(error.data)?.includes("org_id is required")) {
144
+ throw new Error("Multiple projects found, please provide one with the --project-id option");
145
+ }
146
+ throw error;
147
+ }
148
+ };
149
+ export const fillSingleOrg = async (props) => {
150
+ if (props.orgId) {
151
+ return props;
152
+ }
153
+ const { data } = await props.apiClient.getCurrentUserOrganizations();
154
+ if (data.organizations.length === 0) {
155
+ throw new Error("No organizations found");
156
+ }
157
+ if (data.organizations.length > 1) {
158
+ throw new Error(`Multiple organizations found, please provide one with the --org-id option`);
159
+ }
160
+ return { ...props, orgId: data.organizations[0].id };
161
+ };