@neondatabase/config-runtime 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +178 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/function-bundle.d.ts +25 -0
- package/dist/lib/function-bundle.d.ts.map +1 -0
- package/dist/lib/function-bundle.js +65 -0
- package/dist/lib/function-bundle.js.map +1 -0
- package/dist/lib/operations.d.ts +70 -0
- package/dist/lib/operations.d.ts.map +1 -0
- package/dist/lib/operations.js +60 -0
- package/dist/lib/operations.js.map +1 -0
- package/dist/lib/pull-config.d.ts +59 -0
- package/dist/lib/pull-config.d.ts.map +1 -0
- package/dist/lib/pull-config.js +93 -0
- package/dist/lib/pull-config.js.map +1 -0
- package/dist/lib/push-config.d.ts +104 -0
- package/dist/lib/push-config.d.ts.map +1 -0
- package/dist/lib/push-config.js +389 -0
- package/dist/lib/push-config.js.map +1 -0
- package/dist/v1.d.ts +6 -0
- package/dist/v1.js +6 -0
- package/package.json +69 -22
- package/e2e/conflict.e2e.test.ts +0 -34
- package/e2e/helpers.ts +0 -204
- package/e2e/lifecycle.e2e.test.ts +0 -50
- package/e2e/load-env.ts +0 -29
- package/e2e/setup.ts +0 -24
- package/src/index.ts +0 -5
- package/src/lib/fake-neon-api.ts +0 -782
- package/src/lib/function-bundle.test.ts +0 -60
- package/src/lib/function-bundle.ts +0 -104
- package/src/lib/operations.test.ts +0 -150
- package/src/lib/operations.ts +0 -103
- package/src/lib/pull-config.test.ts +0 -51
- package/src/lib/pull-config.ts +0 -215
- package/src/lib/push-config.test.ts +0 -421
- package/src/lib/push-config.ts +0 -619
- package/src/v1.test.ts +0 -30
- package/src/v1.ts +0 -74
- package/tsconfig.json +0 -4
- package/tsdown.config.ts +0 -22
- package/vitest.config.ts +0 -19
- package/vitest.e2e.config.ts +0 -29
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import type { ResolvedFunctionConfig } from "@neondatabase/config";
|
|
5
|
-
import { unzipSync } from "fflate";
|
|
6
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
7
|
-
import { buildFunctionBundle } from "./function-bundle.js";
|
|
8
|
-
|
|
9
|
-
let dir: string;
|
|
10
|
-
beforeAll(() => {
|
|
11
|
-
dir = mkdtempSync(join(tmpdir(), "neon-bundle-"));
|
|
12
|
-
});
|
|
13
|
-
afterAll(() => {
|
|
14
|
-
rmSync(dir, { recursive: true, force: true });
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
function fn(source: string): ResolvedFunctionConfig {
|
|
18
|
-
return {
|
|
19
|
-
slug: "hello-world",
|
|
20
|
-
name: "Hello World",
|
|
21
|
-
source,
|
|
22
|
-
env: {},
|
|
23
|
-
runtime: "nodejs24",
|
|
24
|
-
memoryMib: 512,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("buildFunctionBundle", () => {
|
|
29
|
-
test("bundles a handler with esbuild and returns a ZIP containing out.js + sourcemap", async () => {
|
|
30
|
-
const helper = join(dir, "shared.ts");
|
|
31
|
-
writeFileSync(helper, "export const greeting = 'hello from neon';\n");
|
|
32
|
-
const source = join(dir, "hello-world.ts");
|
|
33
|
-
// Importing a sibling proves esbuild actually *bundles* (not just copies) the entry.
|
|
34
|
-
writeFileSync(
|
|
35
|
-
source,
|
|
36
|
-
[
|
|
37
|
-
"import { greeting } from './shared.js';",
|
|
38
|
-
"export default { fetch(_req: Request): Response { return new Response(greeting); } };",
|
|
39
|
-
].join("\n"),
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const bundle = await buildFunctionBundle(fn(source));
|
|
43
|
-
expect(bundle.byteLength).toBeGreaterThan(0);
|
|
44
|
-
|
|
45
|
-
const files = unzipSync(bundle);
|
|
46
|
-
const names = Object.keys(files).sort();
|
|
47
|
-
expect(names).toContain("out.js");
|
|
48
|
-
expect(names).toContain("out.js.map");
|
|
49
|
-
|
|
50
|
-
// The bundled output should have inlined the imported constant.
|
|
51
|
-
const js = new TextDecoder().decode(files["out.js"]);
|
|
52
|
-
expect(js).toContain("hello from neon");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("throws a PlatformError when the source cannot be resolved", async () => {
|
|
56
|
-
await expect(
|
|
57
|
-
buildFunctionBundle(fn(join(dir, "does-not-exist.ts"))),
|
|
58
|
-
).rejects.toThrow(/Failed to bundle function "hello-world"/);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
ErrorCode,
|
|
4
|
-
PlatformError,
|
|
5
|
-
type ResolvedFunctionConfig,
|
|
6
|
-
} from "@neondatabase/config";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Build the deployable bundle (a ZIP archive of the esbuild-bundled source) for a function.
|
|
10
|
-
*
|
|
11
|
-
* This is the **imperative shell** step of function deploys, and the reason it lives in
|
|
12
|
-
* `@neondatabase/config-runtime` rather than `@neondatabase/config`: it pulls in `esbuild`
|
|
13
|
-
* (a native binary) and `fflate`. Keeping it out of `@neondatabase/config` means a `neon.ts`
|
|
14
|
-
* that only imports `defineConfig` never drags esbuild into the user's dependency tree or
|
|
15
|
-
* bundle. Deploy-side consumers (the neonctl CLI, CI) import this package and get esbuild as
|
|
16
|
-
* a normal, auto-installed dependency.
|
|
17
|
-
*
|
|
18
|
-
* esbuild and fflate are loaded with a dynamic `import()` (not a static top-level import) so
|
|
19
|
-
* that nothing in this package's static graph names esbuild until a deploy actually runs —
|
|
20
|
-
* a second layer of protection on top of the package split.
|
|
21
|
-
*
|
|
22
|
-
* Mirrors: `esbuild <source> --bundle --outfile=out.js --sourcemap --minify`, then zips the
|
|
23
|
-
* emitted files into the archive the Functions deploy endpoint expects.
|
|
24
|
-
*/
|
|
25
|
-
export async function buildFunctionBundle(
|
|
26
|
-
fn: ResolvedFunctionConfig,
|
|
27
|
-
): Promise<Uint8Array> {
|
|
28
|
-
const esbuild = await loadEsbuild();
|
|
29
|
-
|
|
30
|
-
let result: Awaited<ReturnType<typeof esbuild.build>>;
|
|
31
|
-
try {
|
|
32
|
-
result = await esbuild.build({
|
|
33
|
-
entryPoints: [fn.source],
|
|
34
|
-
bundle: true,
|
|
35
|
-
write: false,
|
|
36
|
-
// Set an explicit outfile so the emitted files are named `out.js` / `out.js.map`
|
|
37
|
-
// (with `write: false` and no outfile, esbuild labels the buffer `<stdout>`).
|
|
38
|
-
outfile: "out.js",
|
|
39
|
-
sourcemap: true,
|
|
40
|
-
minify: true,
|
|
41
|
-
format: "esm",
|
|
42
|
-
platform: "node",
|
|
43
|
-
// The Functions runtime provides Node built-ins; don't try to bundle them.
|
|
44
|
-
packages: "external",
|
|
45
|
-
logLevel: "silent",
|
|
46
|
-
});
|
|
47
|
-
} catch (cause) {
|
|
48
|
-
throw new PlatformError(
|
|
49
|
-
ErrorCode.InvalidConfig,
|
|
50
|
-
[
|
|
51
|
-
`Failed to bundle function "${fn.slug}" from ${fn.source}.`,
|
|
52
|
-
(cause as Error)?.message ?? String(cause),
|
|
53
|
-
].join(" "),
|
|
54
|
-
{ cause },
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const entries: Record<string, Uint8Array> = {};
|
|
59
|
-
// `write: false` guarantees `outputFiles`, but the type is optional — guard for safety.
|
|
60
|
-
for (const file of result.outputFiles ?? []) {
|
|
61
|
-
// esbuild returns absolute output paths; archive them under their basename
|
|
62
|
-
// (`out.js`, `out.js.map`) so the bundle layout is stable regardless of cwd.
|
|
63
|
-
entries[basename(file.path)] = file.contents;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return zipBundle(entries);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function zipBundle(
|
|
70
|
-
entries: Record<string, Uint8Array>,
|
|
71
|
-
): Promise<Uint8Array> {
|
|
72
|
-
const { zipSync } = await loadFflate();
|
|
73
|
-
return zipSync(entries, { level: 6 });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function loadEsbuild(): Promise<typeof import("esbuild")> {
|
|
77
|
-
try {
|
|
78
|
-
return await import("esbuild");
|
|
79
|
-
} catch (cause) {
|
|
80
|
-
throw new PlatformError(
|
|
81
|
-
ErrorCode.InvalidConfig,
|
|
82
|
-
[
|
|
83
|
-
"Deploying Neon Functions requires `esbuild`, which could not be loaded.",
|
|
84
|
-
"It is a dependency of @neondatabase/config-runtime — reinstall your dependencies (`pnpm install` / `npm install`).",
|
|
85
|
-
].join(" "),
|
|
86
|
-
{ cause },
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function loadFflate(): Promise<typeof import("fflate")> {
|
|
92
|
-
try {
|
|
93
|
-
return await import("fflate");
|
|
94
|
-
} catch (cause) {
|
|
95
|
-
throw new PlatformError(
|
|
96
|
-
ErrorCode.InvalidConfig,
|
|
97
|
-
[
|
|
98
|
-
"Deploying Neon Functions requires `fflate`, which could not be loaded.",
|
|
99
|
-
"It is a dependency of @neondatabase/config-runtime — reinstall your dependencies (`pnpm install` / `npm install`).",
|
|
100
|
-
].join(" "),
|
|
101
|
-
{ cause },
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { defineConfig, ErrorCode } from "@neondatabase/config";
|
|
2
|
-
import { describe, expect, test } from "vitest";
|
|
3
|
-
import { FakeNeonApi } from "./fake-neon-api.js";
|
|
4
|
-
import { apply, inspect, plan } from "./operations.js";
|
|
5
|
-
|
|
6
|
-
function seededFake(opts?: { protected?: boolean }) {
|
|
7
|
-
const api = new FakeNeonApi();
|
|
8
|
-
const projectId = "proj-ops";
|
|
9
|
-
api.seedProject({
|
|
10
|
-
project: {
|
|
11
|
-
id: projectId,
|
|
12
|
-
name: "ops-test",
|
|
13
|
-
regionId: "aws-us-east-1",
|
|
14
|
-
pgVersion: 17,
|
|
15
|
-
orgId: "org-ops",
|
|
16
|
-
},
|
|
17
|
-
branches: [
|
|
18
|
-
{
|
|
19
|
-
branch: {
|
|
20
|
-
id: "br-main",
|
|
21
|
-
name: "main",
|
|
22
|
-
isDefault: true,
|
|
23
|
-
protected: opts?.protected ?? false,
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
],
|
|
27
|
-
});
|
|
28
|
-
return { api, projectId };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe("inspect", () => {
|
|
32
|
-
test("returns the selected branch's live state", async () => {
|
|
33
|
-
const { api, projectId } = seededFake();
|
|
34
|
-
const result = await inspect({ api, projectId, branchId: "br-main" });
|
|
35
|
-
expect(result.project.name).toBe("ops-test");
|
|
36
|
-
expect(result.branch.name).toBe("main");
|
|
37
|
-
expect(result.config).toBeDefined();
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe("plan", () => {
|
|
42
|
-
test("computes a dry-run plan without mutating", async () => {
|
|
43
|
-
const { api, projectId } = seededFake();
|
|
44
|
-
const config = defineConfig(() => ({ auth: {} }));
|
|
45
|
-
const result = await plan(config, {
|
|
46
|
-
api,
|
|
47
|
-
projectId,
|
|
48
|
-
branchId: "br-main",
|
|
49
|
-
});
|
|
50
|
-
expect(result.dryRun).toBe(true);
|
|
51
|
-
expect(result.applied).toEqual(
|
|
52
|
-
expect.arrayContaining([
|
|
53
|
-
expect.objectContaining({
|
|
54
|
-
kind: "service",
|
|
55
|
-
identifier: "auth",
|
|
56
|
-
}),
|
|
57
|
-
]),
|
|
58
|
-
);
|
|
59
|
-
// No mutation happened: a fresh plan still shows the same enable.
|
|
60
|
-
const again = await plan(config, {
|
|
61
|
-
api,
|
|
62
|
-
projectId,
|
|
63
|
-
branchId: "br-main",
|
|
64
|
-
});
|
|
65
|
-
expect(again.applied).toEqual(result.applied);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("apply", () => {
|
|
70
|
-
test("applies the branch policy to the selected branch", async () => {
|
|
71
|
-
const { api, projectId } = seededFake();
|
|
72
|
-
const config = defineConfig(() => ({ auth: {} }));
|
|
73
|
-
const result = await apply(config, {
|
|
74
|
-
api,
|
|
75
|
-
projectId,
|
|
76
|
-
branchId: "br-main",
|
|
77
|
-
updateExisting: true,
|
|
78
|
-
});
|
|
79
|
-
expect(result.dryRun).toBe(false);
|
|
80
|
-
expect(result.applied).toEqual(
|
|
81
|
-
expect.arrayContaining([
|
|
82
|
-
expect.objectContaining({
|
|
83
|
-
kind: "service",
|
|
84
|
-
identifier: "auth",
|
|
85
|
-
}),
|
|
86
|
-
]),
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("surfaces drift as a PushConflictError without updateExisting", async () => {
|
|
91
|
-
const { api, projectId } = seededFake();
|
|
92
|
-
const config = defineConfig(() => ({
|
|
93
|
-
postgres: { computeSettings: { autoscalingLimitMaxCu: 4 } },
|
|
94
|
-
}));
|
|
95
|
-
await expect(
|
|
96
|
-
apply(config, { api, projectId, branchId: "br-main" }),
|
|
97
|
-
).rejects.toMatchObject({ code: ErrorCode.PushConflict });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("applies drift to a protected branch with both override flags", async () => {
|
|
101
|
-
const { api, projectId } = seededFake({ protected: true });
|
|
102
|
-
const config = defineConfig(() => ({
|
|
103
|
-
postgres: { computeSettings: { autoscalingLimitMaxCu: 4 } },
|
|
104
|
-
}));
|
|
105
|
-
const result = await apply(config, {
|
|
106
|
-
api,
|
|
107
|
-
projectId,
|
|
108
|
-
branchId: "br-main",
|
|
109
|
-
updateExisting: true,
|
|
110
|
-
allowProtectedBranch: true,
|
|
111
|
-
});
|
|
112
|
-
expect(result.dryRun).toBe(false);
|
|
113
|
-
expect(api.history.some((h) => h.method === "updateEndpoint")).toBe(
|
|
114
|
-
true,
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe("branch not found (id-only lookup)", () => {
|
|
120
|
-
test("inspect throws BranchNotFound for an unknown id", async () => {
|
|
121
|
-
const { api, projectId } = seededFake();
|
|
122
|
-
await expect(
|
|
123
|
-
inspect({ api, projectId, branchId: "br-does-not-exist" }),
|
|
124
|
-
).rejects.toMatchObject({ code: ErrorCode.BranchNotFound });
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("plan throws BranchNotFound for an unknown id", async () => {
|
|
128
|
-
const { api, projectId } = seededFake();
|
|
129
|
-
const config = defineConfig(() => ({}));
|
|
130
|
-
await expect(
|
|
131
|
-
plan(config, { api, projectId, branchId: "br-does-not-exist" }),
|
|
132
|
-
).rejects.toMatchObject({ code: ErrorCode.BranchNotFound });
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("apply throws BranchNotFound for an unknown id", async () => {
|
|
136
|
-
const { api, projectId } = seededFake();
|
|
137
|
-
const config = defineConfig(() => ({}));
|
|
138
|
-
await expect(
|
|
139
|
-
apply(config, { api, projectId, branchId: "br-does-not-exist" }),
|
|
140
|
-
).rejects.toMatchObject({ code: ErrorCode.BranchNotFound });
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("inspect rejects a branch name (ids only)", async () => {
|
|
144
|
-
const { api, projectId } = seededFake();
|
|
145
|
-
// "main" is the branch NAME; lookups match by id only, so this must fail.
|
|
146
|
-
await expect(
|
|
147
|
-
inspect({ api, projectId, branchId: "main" }),
|
|
148
|
-
).rejects.toMatchObject({ code: ErrorCode.BranchNotFound });
|
|
149
|
-
});
|
|
150
|
-
});
|
package/src/lib/operations.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import type { Config, PushResult } from "@neondatabase/config";
|
|
2
|
-
import { type PulledBranchConfig, pullConfig } from "./pull-config.js";
|
|
3
|
-
import { type PushConfigOptions, pushConfig } from "./push-config.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Where to run the operation and how to authenticate. Filesystem- and env-agnostic: the
|
|
7
|
-
* `projectId` and `branchId` are always passed explicitly by the caller (e.g. neonctl
|
|
8
|
-
* resolves them from `.neon` / `NEON_*` and forwards them here).
|
|
9
|
-
*/
|
|
10
|
-
export interface ConfigOperationOptions {
|
|
11
|
-
/**
|
|
12
|
-
* Neon project id. **Required** — the management API addresses branches through their
|
|
13
|
-
* project, so operations cannot run without it.
|
|
14
|
-
*/
|
|
15
|
-
projectId: string;
|
|
16
|
-
/**
|
|
17
|
-
* Neon branch id (`br-…`). **Required.** Must already exist on the project; resolve
|
|
18
|
-
* branch names to ids before calling.
|
|
19
|
-
*/
|
|
20
|
-
branchId: string;
|
|
21
|
-
/** Neon API key. Falls back to `NEON_API_KEY` / neonctl credentials. */
|
|
22
|
-
apiKey?: string;
|
|
23
|
-
/** Inject a custom NeonApi adapter (primarily for tests). */
|
|
24
|
-
api?: PushConfigOptions["api"];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Options accepted by {@link apply} on top of {@link ConfigOperationOptions}.
|
|
29
|
-
*/
|
|
30
|
-
export interface ApplyOptions extends ConfigOperationOptions {
|
|
31
|
-
/**
|
|
32
|
-
* Auto-confirm overriding existing remote settings (TTL, `protected`, compute
|
|
33
|
-
* settings) on the selected branch. Without it, drift is reported as a conflict.
|
|
34
|
-
*/
|
|
35
|
-
updateExisting?: boolean;
|
|
36
|
-
/** Auto-confirm applying to a branch marked `protected` on Neon. */
|
|
37
|
-
allowProtectedBranch?: boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Read a branch's live Neon state as a plain object (project + branch metadata and the
|
|
42
|
-
* reverse-engineered `BranchConfig`). Network read only — never mutates.
|
|
43
|
-
*
|
|
44
|
-
* `projectId` and `branchId` are **required** (both in `options`).
|
|
45
|
-
*/
|
|
46
|
-
export async function inspect(
|
|
47
|
-
options: ConfigOperationOptions,
|
|
48
|
-
): Promise<PulledBranchConfig> {
|
|
49
|
-
return pullConfig({
|
|
50
|
-
projectId: options.projectId,
|
|
51
|
-
branchId: options.branchId,
|
|
52
|
-
...(options.api ? { api: options.api } : {}),
|
|
53
|
-
...(options.apiKey ? { apiKey: options.apiKey } : {}),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Compute what {@link apply} would do for the given branch without mutating anything
|
|
59
|
-
* (dry-run plan). Returns the full {@link PushResult} with the planned changes in
|
|
60
|
-
* `applied` and any blocking drift in `conflicts` — the Neon equivalent of
|
|
61
|
-
* `terraform plan`.
|
|
62
|
-
*
|
|
63
|
-
* `projectId` and `branchId` are **required** (both in `options`).
|
|
64
|
-
*/
|
|
65
|
-
export async function plan(
|
|
66
|
-
config: Config,
|
|
67
|
-
options: ConfigOperationOptions,
|
|
68
|
-
): Promise<PushResult> {
|
|
69
|
-
return pushConfig(config, {
|
|
70
|
-
projectId: options.projectId,
|
|
71
|
-
branchId: options.branchId,
|
|
72
|
-
dryRun: true,
|
|
73
|
-
// Surface the full would-apply list as plan steps without mutating anything.
|
|
74
|
-
updateExisting: true,
|
|
75
|
-
...(options.api ? { api: options.api } : {}),
|
|
76
|
-
...(options.apiKey ? { apiKey: options.apiKey } : {}),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Apply a `neon.ts` policy to the given Neon branch and return the {@link PushResult}
|
|
82
|
-
* describing what changed — the Neon equivalent of `terraform apply`.
|
|
83
|
-
*
|
|
84
|
-
* `projectId` and `branchId` are **required** (both in `options`). Pass `updateExisting`
|
|
85
|
-
* to auto-confirm overriding existing remote settings and `allowProtectedBranch` to
|
|
86
|
-
* auto-confirm applying to a protected branch; otherwise drift is reported as a
|
|
87
|
-
* `PushConflictError`.
|
|
88
|
-
*
|
|
89
|
-
* Never creates projects or branches — both must already exist.
|
|
90
|
-
*/
|
|
91
|
-
export async function apply(
|
|
92
|
-
config: Config,
|
|
93
|
-
options: ApplyOptions,
|
|
94
|
-
): Promise<PushResult> {
|
|
95
|
-
return pushConfig(config, {
|
|
96
|
-
projectId: options.projectId,
|
|
97
|
-
branchId: options.branchId,
|
|
98
|
-
...(options.api ? { api: options.api } : {}),
|
|
99
|
-
...(options.apiKey ? { apiKey: options.apiKey } : {}),
|
|
100
|
-
...(options.updateExisting ? { updateExisting: true } : {}),
|
|
101
|
-
...(options.allowProtectedBranch ? { allowProtectedBranch: true } : {}),
|
|
102
|
-
});
|
|
103
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { FakeNeonApi } from "./fake-neon-api.js";
|
|
3
|
-
import { pullConfig } from "./pull-config.js";
|
|
4
|
-
|
|
5
|
-
describe("pullConfig", () => {
|
|
6
|
-
test("returns selected branch state as JSON-friendly branch config", async () => {
|
|
7
|
-
const api = new FakeNeonApi();
|
|
8
|
-
const projectId = "proj-pull";
|
|
9
|
-
api.seedProject({
|
|
10
|
-
project: {
|
|
11
|
-
id: projectId,
|
|
12
|
-
name: "pull-test",
|
|
13
|
-
regionId: "aws-us-east-1",
|
|
14
|
-
pgVersion: 17,
|
|
15
|
-
orgId: "org-pull",
|
|
16
|
-
},
|
|
17
|
-
branches: [
|
|
18
|
-
{ branch: { id: "br-main", name: "main", isDefault: true } },
|
|
19
|
-
{
|
|
20
|
-
branch: {
|
|
21
|
-
id: "br-dev",
|
|
22
|
-
name: "dev-a",
|
|
23
|
-
isDefault: false,
|
|
24
|
-
parentId: "br-main",
|
|
25
|
-
protected: true,
|
|
26
|
-
},
|
|
27
|
-
endpoint: { autoscalingLimitMaxCu: 2 },
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const pulled = await pullConfig({ api, projectId, branchId: "br-dev" });
|
|
33
|
-
|
|
34
|
-
expect(pulled.project).toMatchObject({
|
|
35
|
-
id: projectId,
|
|
36
|
-
name: "pull-test",
|
|
37
|
-
orgId: "org-pull",
|
|
38
|
-
});
|
|
39
|
-
expect(pulled.branch).toMatchObject({
|
|
40
|
-
id: "br-dev",
|
|
41
|
-
name: "dev-a",
|
|
42
|
-
parent: "main",
|
|
43
|
-
protected: true,
|
|
44
|
-
});
|
|
45
|
-
expect(pulled.config).toMatchObject({
|
|
46
|
-
parent: "main",
|
|
47
|
-
protected: true,
|
|
48
|
-
postgres: { computeSettings: { autoscalingLimitMaxCu: 2 } },
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
});
|
package/src/lib/pull-config.ts
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type BranchConfig,
|
|
3
|
-
type BucketConfig,
|
|
4
|
-
type ComputeSettings,
|
|
5
|
-
createNeonApiFromOptions,
|
|
6
|
-
ErrorCode,
|
|
7
|
-
type NeonApi,
|
|
8
|
-
type NeonBranchSnapshot,
|
|
9
|
-
type NeonBucketSnapshot,
|
|
10
|
-
type NeonEndpointSnapshot,
|
|
11
|
-
type NeonFunctionSnapshot,
|
|
12
|
-
type NeonProjectSnapshot,
|
|
13
|
-
PlatformError,
|
|
14
|
-
} from "@neondatabase/config";
|
|
15
|
-
|
|
16
|
-
export interface PullConfigOptions {
|
|
17
|
-
/** Neon project id (`<project>`). Required — the API addresses branches by project. */
|
|
18
|
-
projectId: string;
|
|
19
|
-
/** Neon branch id (`br-…`). Required. Resolve names to ids before calling. */
|
|
20
|
-
branchId: string;
|
|
21
|
-
/** Neon API key. Falls back to `NEON_API_KEY` / neonctl credentials. */
|
|
22
|
-
apiKey?: string;
|
|
23
|
-
/** Inject a custom NeonApi adapter (primarily for tests). */
|
|
24
|
-
api?: NeonApi;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Live Preview-feature state read back from a branch. Surfaced alongside `config` rather
|
|
29
|
-
* than inside it because functions cannot round-trip: the remote only knows the deployed
|
|
30
|
-
* bundle, not the local `source` path a {@link FunctionConfig} requires, so a pulled
|
|
31
|
-
* function is reported as `{ slug, name }` (no `source`).
|
|
32
|
-
*/
|
|
33
|
-
export interface PulledPreview {
|
|
34
|
-
buckets: BucketConfig[];
|
|
35
|
-
functions: Array<{ slug: string; name: string }>;
|
|
36
|
-
aiGatewayEnabled: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface PulledBranchConfig {
|
|
40
|
-
project: {
|
|
41
|
-
id: string;
|
|
42
|
-
name: string;
|
|
43
|
-
region: string;
|
|
44
|
-
pgVersion: number;
|
|
45
|
-
orgId?: string;
|
|
46
|
-
};
|
|
47
|
-
branch: {
|
|
48
|
-
id: string;
|
|
49
|
-
name: string;
|
|
50
|
-
parent?: string;
|
|
51
|
-
isDefault: boolean;
|
|
52
|
-
protected: boolean;
|
|
53
|
-
expiresAt?: string;
|
|
54
|
-
};
|
|
55
|
-
config: BranchConfig;
|
|
56
|
-
/**
|
|
57
|
-
* Live Preview-feature state, when the branch has any buckets/functions or an enabled
|
|
58
|
-
* AI Gateway. Omitted entirely when there is nothing to report.
|
|
59
|
-
*/
|
|
60
|
-
preview?: PulledPreview;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function pullConfig(
|
|
64
|
-
options: PullConfigOptions,
|
|
65
|
-
): Promise<PulledBranchConfig> {
|
|
66
|
-
const api = options.api ?? createApiFromOptions(options);
|
|
67
|
-
const projectId = options.projectId;
|
|
68
|
-
const project = await api.getProject(projectId);
|
|
69
|
-
const [branches, endpoints] = await Promise.all([
|
|
70
|
-
api.listBranches(projectId),
|
|
71
|
-
api.listEndpoints(projectId),
|
|
72
|
-
]);
|
|
73
|
-
const branch = resolveBranch(options.branchId, branches);
|
|
74
|
-
const endpoint = endpoints.find(
|
|
75
|
-
(ep) => ep.type === "read_write" && ep.branchId === branch.id,
|
|
76
|
-
);
|
|
77
|
-
const [buckets, functions, aiGatewayEnabled] = await Promise.all([
|
|
78
|
-
api.listBranchBuckets(projectId, branch.id),
|
|
79
|
-
api.listBranchFunctions(projectId, branch.id),
|
|
80
|
-
api.getAiGatewayEnabled(projectId, branch.id),
|
|
81
|
-
]);
|
|
82
|
-
return buildPulledBranchConfig(project, branch, branches, endpoint, {
|
|
83
|
-
buckets,
|
|
84
|
-
functions,
|
|
85
|
-
aiGatewayEnabled,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function createApiFromOptions(options: PullConfigOptions): NeonApi {
|
|
90
|
-
return createNeonApiFromOptions("pullConfig", {
|
|
91
|
-
...(options.apiKey ? { apiKey: options.apiKey } : {}),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function buildPulledBranchConfig(
|
|
96
|
-
project: NeonProjectSnapshot,
|
|
97
|
-
branch: NeonBranchSnapshot,
|
|
98
|
-
branches: NeonBranchSnapshot[],
|
|
99
|
-
endpoint: NeonEndpointSnapshot | undefined,
|
|
100
|
-
previewState?: {
|
|
101
|
-
buckets: NeonBucketSnapshot[];
|
|
102
|
-
functions: NeonFunctionSnapshot[];
|
|
103
|
-
aiGatewayEnabled: boolean;
|
|
104
|
-
},
|
|
105
|
-
): PulledBranchConfig {
|
|
106
|
-
const parent = branch.parentId
|
|
107
|
-
? branches.find((b) => b.id === branch.parentId)
|
|
108
|
-
: undefined;
|
|
109
|
-
const config: BranchConfig = {};
|
|
110
|
-
if (parent) config.parent = parent.name;
|
|
111
|
-
if (branch.expiresAt) config.ttl = branch.expiresAt;
|
|
112
|
-
if (branch.protected) config.protected = true;
|
|
113
|
-
if (endpoint) {
|
|
114
|
-
const compute = endpointToComputeSettings(endpoint, project);
|
|
115
|
-
if (compute) config.postgres = { computeSettings: compute };
|
|
116
|
-
}
|
|
117
|
-
const result: PulledBranchConfig = {
|
|
118
|
-
project: {
|
|
119
|
-
id: project.id,
|
|
120
|
-
name: project.name,
|
|
121
|
-
region: project.regionId,
|
|
122
|
-
pgVersion: project.pgVersion,
|
|
123
|
-
...(project.orgId ? { orgId: project.orgId } : {}),
|
|
124
|
-
},
|
|
125
|
-
branch: {
|
|
126
|
-
id: branch.id,
|
|
127
|
-
name: branch.name,
|
|
128
|
-
...(parent ? { parent: parent.name } : {}),
|
|
129
|
-
isDefault: branch.isDefault,
|
|
130
|
-
protected: branch.protected,
|
|
131
|
-
...(branch.expiresAt ? { expiresAt: branch.expiresAt } : {}),
|
|
132
|
-
},
|
|
133
|
-
config,
|
|
134
|
-
};
|
|
135
|
-
const preview = previewState ? buildPulledPreview(previewState) : undefined;
|
|
136
|
-
if (preview) result.preview = preview;
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Reverse-engineer the {@link PulledPreview} from remote snapshots. Returns `undefined` when
|
|
142
|
-
* the branch has no Preview features so the field can be omitted entirely.
|
|
143
|
-
*/
|
|
144
|
-
function buildPulledPreview(state: {
|
|
145
|
-
buckets: NeonBucketSnapshot[];
|
|
146
|
-
functions: NeonFunctionSnapshot[];
|
|
147
|
-
aiGatewayEnabled: boolean;
|
|
148
|
-
}): PulledPreview | undefined {
|
|
149
|
-
if (
|
|
150
|
-
state.buckets.length === 0 &&
|
|
151
|
-
state.functions.length === 0 &&
|
|
152
|
-
!state.aiGatewayEnabled
|
|
153
|
-
) {
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
buckets: state.buckets.map((b) => ({
|
|
158
|
-
name: b.name,
|
|
159
|
-
access: b.accessLevel,
|
|
160
|
-
})),
|
|
161
|
-
functions: state.functions.map((f) => ({
|
|
162
|
-
slug: f.slug,
|
|
163
|
-
name: f.name,
|
|
164
|
-
})),
|
|
165
|
-
aiGatewayEnabled: state.aiGatewayEnabled,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function resolveBranch(
|
|
170
|
-
branchId: string,
|
|
171
|
-
branches: NeonBranchSnapshot[],
|
|
172
|
-
): NeonBranchSnapshot {
|
|
173
|
-
const match = branches.find((b) => b.id === branchId);
|
|
174
|
-
if (match) return match;
|
|
175
|
-
throw new PlatformError(
|
|
176
|
-
ErrorCode.BranchNotFound,
|
|
177
|
-
[
|
|
178
|
-
`pullConfig: branch id ${JSON.stringify(branchId)} not found on project.`,
|
|
179
|
-
`Available branches: ${branches.map((b) => `${b.name} (${b.id})`).join(", ") || "(none)"}.`,
|
|
180
|
-
].join(" "),
|
|
181
|
-
{
|
|
182
|
-
details: {
|
|
183
|
-
branchId,
|
|
184
|
-
available: branches.map((b) => b.id),
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function endpointToComputeSettings(
|
|
191
|
-
endpoint: NeonEndpointSnapshot,
|
|
192
|
-
project: NeonProjectSnapshot,
|
|
193
|
-
): ComputeSettings | undefined {
|
|
194
|
-
const defaults = project.defaultEndpointSettings;
|
|
195
|
-
const out: ComputeSettings = {};
|
|
196
|
-
if (
|
|
197
|
-
endpoint.autoscalingLimitMinCu !== undefined &&
|
|
198
|
-
endpoint.autoscalingLimitMinCu !== defaults?.autoscalingLimitMinCu
|
|
199
|
-
) {
|
|
200
|
-
out.autoscalingLimitMinCu = endpoint.autoscalingLimitMinCu;
|
|
201
|
-
}
|
|
202
|
-
if (
|
|
203
|
-
endpoint.autoscalingLimitMaxCu !== undefined &&
|
|
204
|
-
endpoint.autoscalingLimitMaxCu !== defaults?.autoscalingLimitMaxCu
|
|
205
|
-
) {
|
|
206
|
-
out.autoscalingLimitMaxCu = endpoint.autoscalingLimitMaxCu;
|
|
207
|
-
}
|
|
208
|
-
if (
|
|
209
|
-
endpoint.suspendTimeout !== undefined &&
|
|
210
|
-
endpoint.suspendTimeout !== defaults?.suspendTimeout
|
|
211
|
-
) {
|
|
212
|
-
out.suspendTimeout = endpoint.suspendTimeout;
|
|
213
|
-
}
|
|
214
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
215
|
-
}
|