neonctl 2.27.1 → 2.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -3
- package/dist/analytics.js +52 -34
- package/dist/api.js +643 -13
- package/dist/auth.js +50 -44
- package/dist/cli.js +8 -1
- package/dist/commands/auth.js +64 -51
- package/dist/commands/bootstrap.js +115 -157
- package/dist/commands/branches.js +160 -150
- package/dist/commands/bucket.js +183 -146
- package/dist/commands/checkout.js +51 -51
- package/dist/commands/config.js +228 -82
- package/dist/commands/connection_string.js +62 -62
- package/dist/commands/data_api.js +100 -101
- package/dist/commands/databases.js +29 -26
- package/dist/commands/deploy.js +12 -12
- package/dist/commands/dev.js +114 -114
- package/dist/commands/env.js +43 -43
- package/dist/commands/functions.js +101 -104
- package/dist/commands/index.js +27 -25
- package/dist/commands/init.js +23 -22
- package/dist/commands/ip_allow.js +29 -29
- package/dist/commands/link.js +232 -182
- package/dist/commands/neon_auth.js +385 -370
- package/dist/commands/operations.js +11 -11
- package/dist/commands/orgs.js +8 -8
- package/dist/commands/projects.js +103 -101
- package/dist/commands/psql.js +31 -31
- package/dist/commands/roles.js +27 -24
- package/dist/commands/schema_diff.js +25 -26
- package/dist/commands/set_context.js +17 -17
- package/dist/commands/status.js +40 -0
- package/dist/commands/user.js +5 -5
- package/dist/commands/vpc_endpoints.js +50 -50
- package/dist/config.js +7 -7
- package/dist/config_format.js +5 -5
- package/dist/context.js +37 -14
- package/dist/current_branch_fast_path.js +55 -0
- package/dist/dev/env.js +33 -33
- package/dist/dev/functions.js +4 -4
- package/dist/dev/inputs.js +6 -6
- package/dist/dev/runtime.js +25 -25
- package/dist/env.js +14 -14
- package/dist/env_file.js +13 -13
- package/dist/errors.js +68 -5
- package/dist/functions_api.js +10 -10
- package/dist/help.js +15 -15
- package/dist/index.js +110 -107
- package/dist/log.js +2 -2
- package/dist/parameters.gen.js +14 -14
- package/dist/pkg.js +5 -5
- package/dist/psql/cli.js +4 -2
- package/dist/psql/command/cmd_cond.js +61 -61
- package/dist/psql/command/cmd_connect.js +159 -154
- package/dist/psql/command/cmd_copy.js +107 -97
- package/dist/psql/command/cmd_describe.js +368 -363
- package/dist/psql/command/cmd_format.js +276 -263
- package/dist/psql/command/cmd_io.js +269 -263
- package/dist/psql/command/cmd_lo.js +74 -66
- package/dist/psql/command/cmd_meta.js +148 -148
- package/dist/psql/command/cmd_misc.js +17 -17
- package/dist/psql/command/cmd_pipeline.js +142 -135
- package/dist/psql/command/cmd_restrict.js +25 -25
- package/dist/psql/command/cmd_show.js +183 -168
- package/dist/psql/command/dispatch.js +26 -26
- package/dist/psql/command/shared.js +14 -14
- package/dist/psql/complete/filenames.js +16 -16
- package/dist/psql/complete/index.js +4 -4
- package/dist/psql/complete/matcher.js +33 -32
- package/dist/psql/complete/psqlVars.js +173 -173
- package/dist/psql/complete/queries.js +5 -3
- package/dist/psql/complete/rules.js +900 -863
- package/dist/psql/core/common.js +136 -133
- package/dist/psql/core/help.js +343 -343
- package/dist/psql/core/mainloop.js +160 -153
- package/dist/psql/core/prompt.js +126 -123
- package/dist/psql/core/settings.js +111 -111
- package/dist/psql/core/sqlHelp.js +150 -150
- package/dist/psql/core/startup.js +211 -205
- package/dist/psql/core/syncVars.js +14 -14
- package/dist/psql/core/variables.js +24 -24
- package/dist/psql/describe/formatters.js +302 -289
- package/dist/psql/describe/processNamePattern.js +28 -28
- package/dist/psql/describe/queries.js +656 -651
- package/dist/psql/index.js +436 -411
- package/dist/psql/io/history.js +36 -36
- package/dist/psql/io/input.js +15 -15
- package/dist/psql/io/lineEditor/buffer.js +27 -25
- package/dist/psql/io/lineEditor/complete.js +15 -15
- package/dist/psql/io/lineEditor/filename.js +22 -22
- package/dist/psql/io/lineEditor/index.js +65 -62
- package/dist/psql/io/lineEditor/keymap.js +325 -318
- package/dist/psql/io/lineEditor/vt100.js +60 -60
- package/dist/psql/io/pgpass.js +18 -18
- package/dist/psql/io/pgservice.js +14 -14
- package/dist/psql/io/psqlrc.js +46 -46
- package/dist/psql/print/aligned.js +175 -166
- package/dist/psql/print/asciidoc.js +51 -51
- package/dist/psql/print/crosstab.js +34 -31
- package/dist/psql/print/csv.js +25 -22
- package/dist/psql/print/html.js +54 -54
- package/dist/psql/print/json.js +12 -12
- package/dist/psql/print/latex.js +118 -118
- package/dist/psql/print/pager.js +28 -26
- package/dist/psql/print/troff.js +48 -48
- package/dist/psql/print/unaligned.js +15 -14
- package/dist/psql/print/units.js +17 -17
- package/dist/psql/scanner/slash.js +48 -46
- package/dist/psql/scanner/sql.js +88 -84
- package/dist/psql/scanner/stringutils.js +21 -17
- package/dist/psql/types/index.js +7 -7
- package/dist/psql/types/scanner.js +8 -8
- package/dist/psql/wire/connection.js +341 -327
- package/dist/psql/wire/copy.js +7 -7
- package/dist/psql/wire/pipeline.js +26 -24
- package/dist/psql/wire/protocol.js +102 -102
- package/dist/psql/wire/sasl.js +62 -62
- package/dist/psql/wire/tls.js +79 -73
- package/dist/storage_api.js +22 -23
- package/dist/test_utils/fixtures.js +74 -41
- package/dist/test_utils/oauth_server.js +5 -5
- package/dist/utils/api_enums.js +33 -0
- package/dist/utils/branch_notice.js +5 -5
- package/dist/utils/branch_picker.js +26 -26
- package/dist/utils/compute_units.js +4 -4
- package/dist/utils/enrichers.js +28 -16
- package/dist/utils/esbuild.js +28 -28
- package/dist/utils/formats.js +1 -1
- package/dist/utils/middlewares.js +3 -3
- package/dist/utils/package_manager.js +68 -0
- package/dist/utils/point_in_time.js +12 -12
- package/dist/utils/psql.js +30 -30
- package/dist/utils/string.js +2 -2
- package/dist/utils/ui.js +9 -9
- package/dist/utils/zip.js +1 -1
- package/dist/writer.js +17 -17
- package/package.json +10 -12
|
@@ -1,77 +1,110 @@
|
|
|
1
|
-
import { fork } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
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
|
+
});
|
|
8
29
|
export const test = originalTest.extend({
|
|
9
30
|
// eslint-disable-next-line no-empty-pattern
|
|
10
31
|
runMockServer: async ({}, use) => {
|
|
11
|
-
let
|
|
32
|
+
let startedServer;
|
|
12
33
|
await use(async (mockDir) => {
|
|
13
34
|
const app = express();
|
|
14
35
|
app.use(express.json());
|
|
15
|
-
app.use(
|
|
16
|
-
|
|
36
|
+
app.use("/", emocks(join(process.cwd(), "mocks", mockDir), {
|
|
37
|
+
"404": (_req, res) => res.status(404).send({ message: "Not Found" }),
|
|
17
38
|
}));
|
|
18
|
-
await new Promise((resolve) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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);
|
|
22
43
|
});
|
|
23
44
|
});
|
|
45
|
+
startedServer = server;
|
|
24
46
|
return server;
|
|
25
47
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
}
|
|
34
62
|
},
|
|
35
63
|
testCliCommand: async ({ runMockServer }, use) => {
|
|
36
64
|
await use(async (args, options = {}) => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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",
|
|
48
78
|
...args,
|
|
49
79
|
], {
|
|
50
|
-
stdio:
|
|
80
|
+
stdio: "pipe",
|
|
81
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
51
82
|
env: {
|
|
52
|
-
PATH:
|
|
83
|
+
PATH: `${join(process.cwd(), "mocks/bin")}:${process.env.PATH}`,
|
|
53
84
|
...(options.env ?? {}),
|
|
54
85
|
},
|
|
55
86
|
});
|
|
56
87
|
return new Promise((resolve, reject) => {
|
|
57
|
-
cp.stdout?.on(
|
|
88
|
+
cp.stdout?.on("data", (data) => {
|
|
58
89
|
output += data.toString();
|
|
59
90
|
});
|
|
60
|
-
cp.stderr?.on(
|
|
91
|
+
cp.stderr?.on("data", (data) => {
|
|
61
92
|
error += data.toString();
|
|
62
93
|
log.error(data.toString());
|
|
63
94
|
});
|
|
64
|
-
cp.on(
|
|
95
|
+
cp.on("error", (err) => {
|
|
65
96
|
log.error(err);
|
|
66
97
|
throw err;
|
|
67
98
|
});
|
|
68
|
-
cp.on(
|
|
99
|
+
cp.on("close", (code) => {
|
|
69
100
|
try {
|
|
70
101
|
expect(code).toBe(options?.code ?? 0);
|
|
71
102
|
expect(output).toMatchSnapshot();
|
|
72
103
|
if (options.stderr !== undefined) {
|
|
73
|
-
expect(strip(error).replace(/\s+/g,
|
|
74
|
-
? options.stderr
|
|
104
|
+
expect(strip(error).replace(/\s+/g, " ").trim()).toEqual(typeof options.stderr === "string"
|
|
105
|
+
? options.stderr
|
|
106
|
+
.toString()
|
|
107
|
+
.replace(/\s+/g, " ")
|
|
75
108
|
: options.stderr);
|
|
76
109
|
}
|
|
77
110
|
resolve();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { OAuth2Server } from "oauth2-mock-server";
|
|
2
|
+
import { log } from "../log";
|
|
3
3
|
export const startOauthServer = async () => {
|
|
4
4
|
const server = new OAuth2Server();
|
|
5
|
-
await server.issuer.keys.generate(
|
|
6
|
-
await server.start(0,
|
|
7
|
-
log.debug(
|
|
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
8
|
return server;
|
|
9
9
|
};
|
|
@@ -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
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import chalk from
|
|
2
|
-
import { log } from
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { log } from "../log.js";
|
|
3
3
|
/**
|
|
4
4
|
* Print a one-line "this command is targeting <branch>" notice to **stderr** so
|
|
5
5
|
* the user can sanity-check they're acting on the branch they think they are —
|
|
@@ -14,9 +14,9 @@ import { log } from '../log.js';
|
|
|
14
14
|
* `→ Planning against branch main (br-…)`.
|
|
15
15
|
*/
|
|
16
16
|
export const announceTargetBranch = (props, branch, verb) => {
|
|
17
|
-
if (props.output ===
|
|
17
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
const suffix = branch.usedDefault ? chalk.dim(
|
|
21
|
-
log.info(
|
|
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
22
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { log } from
|
|
5
|
-
import {
|
|
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
6
|
/** Sentinel `value` for the "create a new branch" choice (no branch id can collide). */
|
|
7
|
-
const CREATE_BRANCH_CHOICE = Symbol(
|
|
7
|
+
const CREATE_BRANCH_CHOICE = Symbol("create-branch");
|
|
8
8
|
/**
|
|
9
9
|
* Render a branch's display name with the same word labels as `neonctl branch list`
|
|
10
10
|
* (`[default]`, `[protected]`) instead of symbols, so the picker reads clearly.
|
|
@@ -12,13 +12,13 @@ const CREATE_BRANCH_CHOICE = Symbol('create-branch');
|
|
|
12
12
|
const branchLabel = (branch) => {
|
|
13
13
|
const labels = [];
|
|
14
14
|
if (branch.default) {
|
|
15
|
-
labels.push(
|
|
15
|
+
labels.push("[default]");
|
|
16
16
|
}
|
|
17
17
|
if (branch.protected) {
|
|
18
|
-
labels.push(
|
|
18
|
+
labels.push("[protected]");
|
|
19
19
|
}
|
|
20
20
|
labels.push(branch.name);
|
|
21
|
-
return labels.join(
|
|
21
|
+
return labels.join(" ");
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
24
|
* Prompt the user to pick a branch from `branches`, with a "+ Create a new branch…" option
|
|
@@ -36,11 +36,11 @@ export const pickBranchInteractively = async (branches, opts) => {
|
|
|
36
36
|
const defaultBranchIndex = branches.findIndex((b) => b.default);
|
|
37
37
|
const initial = defaultBranchIndex >= 0 ? defaultBranchIndex + 1 : 0;
|
|
38
38
|
const { choice } = await prompts({
|
|
39
|
-
type:
|
|
40
|
-
name:
|
|
39
|
+
type: "select",
|
|
40
|
+
name: "choice",
|
|
41
41
|
message: opts.message,
|
|
42
42
|
choices: [
|
|
43
|
-
{ title:
|
|
43
|
+
{ title: "+ Create a new branch…", value: CREATE_BRANCH_CHOICE },
|
|
44
44
|
...branches.map((b) => ({
|
|
45
45
|
title: `${branchLabel(b)} (${b.id})`,
|
|
46
46
|
value: b.id,
|
|
@@ -49,12 +49,12 @@ export const pickBranchInteractively = async (branches, opts) => {
|
|
|
49
49
|
initial,
|
|
50
50
|
});
|
|
51
51
|
if (choice === undefined) {
|
|
52
|
-
throw new Error(
|
|
52
|
+
throw new Error("Aborted: no branch selected.");
|
|
53
53
|
}
|
|
54
54
|
if (choice === CREATE_BRANCH_CHOICE) {
|
|
55
|
-
return { kind:
|
|
55
|
+
return { kind: "create", name: await promptNewBranchName(branches) };
|
|
56
56
|
}
|
|
57
|
-
return { kind:
|
|
57
|
+
return { kind: "existing", branchId: choice };
|
|
58
58
|
};
|
|
59
59
|
/**
|
|
60
60
|
* Prompt for a new branch name, rejecting empty input and names already taken on the
|
|
@@ -63,21 +63,21 @@ export const pickBranchInteractively = async (branches, opts) => {
|
|
|
63
63
|
export const promptNewBranchName = async (branches) => {
|
|
64
64
|
const existing = new Set(branches.map((b) => b.name));
|
|
65
65
|
const { name } = await prompts({
|
|
66
|
-
type:
|
|
67
|
-
name:
|
|
68
|
-
message:
|
|
66
|
+
type: "text",
|
|
67
|
+
name: "name",
|
|
68
|
+
message: "New branch name:",
|
|
69
69
|
validate: (value) => {
|
|
70
70
|
const trimmed = value.trim();
|
|
71
|
-
if (trimmed ===
|
|
72
|
-
return
|
|
71
|
+
if (trimmed === "")
|
|
72
|
+
return "Branch name cannot be empty.";
|
|
73
73
|
if (existing.has(trimmed))
|
|
74
74
|
return `A branch named "${trimmed}" already exists.`;
|
|
75
75
|
return true;
|
|
76
76
|
},
|
|
77
77
|
});
|
|
78
|
-
const trimmed = typeof name ===
|
|
79
|
-
if (trimmed ===
|
|
80
|
-
throw new Error(
|
|
78
|
+
const trimmed = typeof name === "string" ? name.trim() : "";
|
|
79
|
+
if (trimmed === "") {
|
|
80
|
+
throw new Error("Aborted: no branch name provided.");
|
|
81
81
|
}
|
|
82
82
|
return trimmed;
|
|
83
83
|
};
|
|
@@ -89,15 +89,15 @@ export const promptNewBranchName = async (branches) => {
|
|
|
89
89
|
export const createBranch = async (apiClient, projectId, name, branches) => {
|
|
90
90
|
const defaultBranch = branches.find((b) => b.default);
|
|
91
91
|
if (!defaultBranch) {
|
|
92
|
-
throw new Error(
|
|
92
|
+
throw new Error("No default branch found");
|
|
93
93
|
}
|
|
94
94
|
const { data } = await retryOnLock(() => apiClient.createProjectBranch(projectId, {
|
|
95
95
|
branch: { name, parent_id: defaultBranch.id },
|
|
96
96
|
endpoints: [{ type: EndpointType.ReadWrite }],
|
|
97
97
|
}));
|
|
98
98
|
if (defaultBranch.protected) {
|
|
99
|
-
log.warning(
|
|
99
|
+
log.warning("The parent branch is protected; a unique role password has been generated for the new branch.");
|
|
100
100
|
}
|
|
101
|
-
log.info(
|
|
101
|
+
log.info("Created branch %s (%s).", data.branch.name, data.branch.id);
|
|
102
102
|
return data.branch.id;
|
|
103
103
|
};
|
|
@@ -6,20 +6,20 @@ export const getComputeUnits = (autoscaling) => {
|
|
|
6
6
|
autoscaling_limit_max_cu: fixedSizeAutoscaling,
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
|
-
if (!autoscaling.includes(
|
|
9
|
+
if (!autoscaling.includes("-")) {
|
|
10
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
11
|
}
|
|
12
|
-
const [min, max] = autoscaling.split(
|
|
12
|
+
const [min, max] = autoscaling.split("-");
|
|
13
13
|
if (!min || !max) {
|
|
14
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
15
|
}
|
|
16
16
|
const minAutoscaling = Number(min);
|
|
17
17
|
const maxAutoscaling = Number(max);
|
|
18
18
|
if (isNaN(minAutoscaling)) {
|
|
19
|
-
throw new Error(
|
|
19
|
+
throw new Error("Autoscaling min should be a number");
|
|
20
20
|
}
|
|
21
21
|
if (isNaN(maxAutoscaling)) {
|
|
22
|
-
throw new Error(
|
|
22
|
+
throw new Error("Autoscaling max should be a number");
|
|
23
23
|
}
|
|
24
24
|
return {
|
|
25
25
|
autoscaling_limit_min_cu: minAutoscaling,
|
package/dist/utils/enrichers.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isNeonApiError, messageFromBody } from "../api.js";
|
|
2
|
+
import { isConfigInit, isCurrentBranchProbe } from "../context.js";
|
|
3
|
+
import { looksLikeBranchId } from "./formats.js";
|
|
3
4
|
export const branchIdResolve = async ({ branch, apiClient, projectId, }) => {
|
|
4
5
|
branch = branch.toString();
|
|
5
6
|
if (looksLikeBranchId(branch)) {
|
|
@@ -12,12 +13,12 @@ export const branchIdResolve = async ({ branch, apiClient, projectId, }) => {
|
|
|
12
13
|
if (!branchData) {
|
|
13
14
|
throw new Error(`Branch ${branch} not found.\nAvailable branches: ${data.branches
|
|
14
15
|
.map((b) => b.name)
|
|
15
|
-
.join(
|
|
16
|
+
.join(", ")}`);
|
|
16
17
|
}
|
|
17
18
|
return branchData.id;
|
|
18
19
|
};
|
|
19
20
|
const getBranchIdFromProps = async (props) => {
|
|
20
|
-
const branch =
|
|
21
|
+
const branch = "branch" in props && typeof props.branch === "string"
|
|
21
22
|
? props.branch
|
|
22
23
|
: props.id;
|
|
23
24
|
if (branch) {
|
|
@@ -34,14 +35,14 @@ const getBranchIdFromProps = async (props) => {
|
|
|
34
35
|
if (defaultBranch) {
|
|
35
36
|
return defaultBranch.id;
|
|
36
37
|
}
|
|
37
|
-
throw new Error(
|
|
38
|
+
throw new Error("No default branch found");
|
|
38
39
|
};
|
|
39
40
|
export const branchIdFromProps = async (props) => {
|
|
40
41
|
props.branchId = await getBranchIdFromProps(props);
|
|
41
42
|
return props.branchId;
|
|
42
43
|
};
|
|
43
44
|
export const resolveBranchRef = async (props) => {
|
|
44
|
-
const branch =
|
|
45
|
+
const branch = "branch" in props && typeof props.branch === "string"
|
|
45
46
|
? props.branch
|
|
46
47
|
: props.id;
|
|
47
48
|
const { data } = await props.apiClient.listProjectBranches({
|
|
@@ -67,11 +68,11 @@ export const resolveBranchRef = async (props) => {
|
|
|
67
68
|
}
|
|
68
69
|
throw new Error(`Branch ${ref} not found.\nAvailable branches: ${branches
|
|
69
70
|
.map((b) => b.name)
|
|
70
|
-
.join(
|
|
71
|
+
.join(", ")}`);
|
|
71
72
|
}
|
|
72
73
|
const defaultBranch = branches.find((b) => b.default);
|
|
73
74
|
if (!defaultBranch) {
|
|
74
|
-
throw new Error(
|
|
75
|
+
throw new Error("No default branch found");
|
|
75
76
|
}
|
|
76
77
|
return {
|
|
77
78
|
branchId: defaultBranch.id,
|
|
@@ -84,7 +85,7 @@ export const resolveSingleDatabase = async (props) => {
|
|
|
84
85
|
const databases = data.databases;
|
|
85
86
|
if (props.database !== undefined) {
|
|
86
87
|
if (!databases.find((d) => d.name === props.database)) {
|
|
87
|
-
throw new Error(`Database not found: ${props.database}. Available databases on branch ${props.branchId}: ${databases.map((d) => d.name).join(
|
|
88
|
+
throw new Error(`Database not found: ${props.database}. Available databases on branch ${props.branchId}: ${databases.map((d) => d.name).join(", ")}`);
|
|
88
89
|
}
|
|
89
90
|
return props.database;
|
|
90
91
|
}
|
|
@@ -94,9 +95,20 @@ export const resolveSingleDatabase = async (props) => {
|
|
|
94
95
|
if (databases.length === 1) {
|
|
95
96
|
return databases[0].name;
|
|
96
97
|
}
|
|
97
|
-
throw new Error(`Multiple databases found for the branch, please provide one with the --database option: ${databases.map((d) => d.name).join(
|
|
98
|
+
throw new Error(`Multiple databases found for the branch, please provide one with the --database option: ${databases.map((d) => d.name).join(", ")}`);
|
|
98
99
|
};
|
|
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
|
+
}
|
|
100
112
|
if (props.projectId) {
|
|
101
113
|
return { ...props, projectId: props.projectId };
|
|
102
114
|
}
|
|
@@ -114,7 +126,7 @@ export const fillSingleProject = async (props) => {
|
|
|
114
126
|
org_id: orgId,
|
|
115
127
|
});
|
|
116
128
|
if (data.projects.length === 0) {
|
|
117
|
-
throw new Error(
|
|
129
|
+
throw new Error("No projects found");
|
|
118
130
|
}
|
|
119
131
|
if (data.projects.length > 1) {
|
|
120
132
|
throw new Error(`Multiple projects found, please provide one with the --project-id option`);
|
|
@@ -126,10 +138,10 @@ export const fillSingleProject = async (props) => {
|
|
|
126
138
|
}
|
|
127
139
|
catch (error) {
|
|
128
140
|
// If the API error is about missing org_id, provide a user-friendly message
|
|
129
|
-
if (
|
|
130
|
-
error.
|
|
131
|
-
error.
|
|
132
|
-
throw new Error(
|
|
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");
|
|
133
145
|
}
|
|
134
146
|
throw error;
|
|
135
147
|
}
|
|
@@ -140,7 +152,7 @@ export const fillSingleOrg = async (props) => {
|
|
|
140
152
|
}
|
|
141
153
|
const { data } = await props.apiClient.getCurrentUserOrganizations();
|
|
142
154
|
if (data.organizations.length === 0) {
|
|
143
|
-
throw new Error(
|
|
155
|
+
throw new Error("No organizations found");
|
|
144
156
|
}
|
|
145
157
|
if (data.organizations.length > 1) {
|
|
146
158
|
throw new Error(`Multiple organizations found, please provide one with the --org-id option`);
|
package/dist/utils/esbuild.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { spawn } from
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from
|
|
3
|
-
import { tmpdir } from
|
|
4
|
-
import { basename, join } from
|
|
5
|
-
import which from
|
|
6
|
-
const NOT_FOUND =
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
import which from "which";
|
|
6
|
+
const NOT_FOUND = "esbuild not found. neonctl ships esbuild for most platforms; if you see " +
|
|
7
|
+
"this, install esbuild and ensure it is on your PATH (e.g. `npm i -g " +
|
|
8
|
+
"esbuild`), or set NEON_ESBUILD_PATH to an esbuild binary.";
|
|
9
9
|
// Prepended to the ESM bundle. Bundled dependencies are frequently CommonJS, but an ESM
|
|
10
10
|
// output (`--format=esm`) has no `require` / `__filename` / `__dirname` in scope — so any
|
|
11
11
|
// bundled CJS code that calls `require(...)` would fail at load with
|
|
@@ -39,7 +39,7 @@ const bundleViaModule = async (source, loadEsbuild) => {
|
|
|
39
39
|
// to the binary path), so keeping this specifier invisible to the scanners is
|
|
40
40
|
// what keeps esbuild out of the snapshot.
|
|
41
41
|
// Do NOT "simplify" this back to import('esbuild').
|
|
42
|
-
const name = [
|
|
42
|
+
const name = ["es", "build"].join("");
|
|
43
43
|
let esbuild;
|
|
44
44
|
try {
|
|
45
45
|
esbuild = await loadEsbuild(name);
|
|
@@ -57,16 +57,16 @@ const bundleViaModule = async (source, loadEsbuild) => {
|
|
|
57
57
|
// Emit `index.mjs` (not `out.js`): the Functions runtime imports the archive's entry
|
|
58
58
|
// by the conventional `index.{js,mjs}` name, and `.mjs` makes Node treat the ESM
|
|
59
59
|
// output as a module without needing a `package.json` type marker alongside it.
|
|
60
|
-
outfile:
|
|
60
|
+
outfile: "index.mjs",
|
|
61
61
|
write: false,
|
|
62
62
|
minify: true,
|
|
63
|
-
format:
|
|
64
|
-
platform:
|
|
63
|
+
format: "esm",
|
|
64
|
+
platform: "node",
|
|
65
65
|
// Bundle dependencies into the entry so the deployed archive is self-contained (the
|
|
66
66
|
// Functions runtime has no node_modules). Node built-ins stay external on
|
|
67
67
|
// platform:'node'. The banner re-creates require/__filename/__dirname for bundled CJS.
|
|
68
68
|
banner: { js: ESM_CJS_INTEROP_BANNER },
|
|
69
|
-
logLevel:
|
|
69
|
+
logLevel: "silent",
|
|
70
70
|
})
|
|
71
71
|
.catch((err) => {
|
|
72
72
|
throw new Error(`Failed to bundle function from ${source}. ${message(err)}`.trim());
|
|
@@ -90,12 +90,12 @@ const resolveEsbuild = () => {
|
|
|
90
90
|
return override;
|
|
91
91
|
throw new Error(NOT_FOUND);
|
|
92
92
|
}
|
|
93
|
-
const onPath = which.sync(
|
|
93
|
+
const onPath = which.sync("esbuild", { nothrow: true });
|
|
94
94
|
if (onPath)
|
|
95
95
|
return onPath;
|
|
96
96
|
// CWD-relative (not install-relative): helps the dev checkout where esbuild is
|
|
97
97
|
// a devDependency. In `npm i -g` and pkg installs the PATH branch above wins.
|
|
98
|
-
const local = join(process.cwd(),
|
|
98
|
+
const local = join(process.cwd(), "node_modules", ".bin", "esbuild");
|
|
99
99
|
if (existsSync(local))
|
|
100
100
|
return local;
|
|
101
101
|
throw new Error(NOT_FOUND);
|
|
@@ -103,30 +103,30 @@ const resolveEsbuild = () => {
|
|
|
103
103
|
const runEsbuild = (bin, args) => new Promise((resolve, reject) => {
|
|
104
104
|
// stderr is captured (NOT inherited): with --log-level=error a success emits
|
|
105
105
|
// nothing, and a failure's diagnostic is read out below. Never use 'inherit'.
|
|
106
|
-
const child = spawn(bin, args, { stdio: [
|
|
107
|
-
let stderr =
|
|
108
|
-
child.stderr.on(
|
|
106
|
+
const child = spawn(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
107
|
+
let stderr = "";
|
|
108
|
+
child.stderr.on("data", (chunk) => {
|
|
109
109
|
stderr += chunk.toString();
|
|
110
110
|
});
|
|
111
|
-
child.on(
|
|
112
|
-
child.on(
|
|
111
|
+
child.on("error", reject);
|
|
112
|
+
child.on("close", (code) => {
|
|
113
113
|
resolve({ code, stderr });
|
|
114
114
|
});
|
|
115
115
|
});
|
|
116
116
|
const bundleViaBinary = async (source) => {
|
|
117
117
|
const bin = resolveEsbuild();
|
|
118
|
-
const outDir = mkdtempSync(join(tmpdir(),
|
|
119
|
-
const outfile = join(outDir,
|
|
118
|
+
const outDir = mkdtempSync(join(tmpdir(), "neon-fn-bundle-"));
|
|
119
|
+
const outfile = join(outDir, "index.mjs");
|
|
120
120
|
try {
|
|
121
121
|
const { code, stderr } = await runEsbuild(bin, [
|
|
122
122
|
source,
|
|
123
|
-
|
|
123
|
+
"--bundle",
|
|
124
124
|
`--outfile=${outfile}`,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
"--minify",
|
|
126
|
+
"--format=esm",
|
|
127
|
+
"--platform=node",
|
|
128
128
|
`--banner:js=${ESM_CJS_INTEROP_BANNER}`,
|
|
129
|
-
|
|
129
|
+
"--log-level=error",
|
|
130
130
|
]);
|
|
131
131
|
if (code !== 0) {
|
|
132
132
|
throw new Error(`Failed to bundle function from ${source}. ${stderr.trim()}`.trim());
|
|
@@ -134,7 +134,7 @@ const bundleViaBinary = async (source) => {
|
|
|
134
134
|
// No `--sourcemap`: the Functions runtime has no source-map support, so an uploaded
|
|
135
135
|
// `index.mjs.map` is never consumed — emitting it only inflated the archive.
|
|
136
136
|
return {
|
|
137
|
-
|
|
137
|
+
"index.mjs": new Uint8Array(readFileSync(outfile)),
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
finally {
|
package/dist/utils/formats.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const HAIKU_REGEX = /^[a-z0-9]+-[a-z0-9]+-[a-z0-9]+$/;
|
|
2
|
-
export const looksLikeBranchId = (branch) => branch.startsWith(
|
|
2
|
+
export const looksLikeBranchId = (branch) => branch.startsWith("br-") && HAIKU_REGEX.test(branch.substring(3));
|
|
3
3
|
const LSN_REGEX = /^[a-fA-F0-9]{1,8}\/[a-fA-F0-9]{1,8}$/;
|
|
4
4
|
export const looksLikeLSN = (lsn) => LSN_REGEX.test(lsn);
|
|
5
5
|
export const looksLikeTimestamp = (timestamp) => {
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const fillInArgs = (args, currentArgs = args, acc = []) => {
|
|
7
7
|
Object.entries(currentArgs).forEach(([k, v]) => {
|
|
8
|
-
if (k ===
|
|
8
|
+
if (k === "_" || k === "--") {
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
// check if the value is an Object
|
|
12
|
-
if (typeof v ===
|
|
12
|
+
if (typeof v === "object" && v !== null) {
|
|
13
13
|
fillInArgs(args, v, [...acc, k]);
|
|
14
14
|
}
|
|
15
15
|
else if (acc.length > 0) {
|
|
16
16
|
// if it's not an object, and we have a path, fill it in
|
|
17
|
-
args[acc.join(
|
|
17
|
+
args[acc.join(".") + "." + k] = v;
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
};
|