@neondatabase/config 0.0.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/.env.example +5 -0
- package/README.md +92 -0
- package/e2e/errors.e2e.test.ts +52 -0
- package/e2e/helpers.ts +205 -0
- package/e2e/load-env.ts +29 -0
- package/e2e/setup.ts +24 -0
- package/package.json +18 -0
- package/src/index.ts +5 -0
- package/src/lib/auth.test.ts +166 -0
- package/src/lib/auth.ts +124 -0
- package/src/lib/define-config.test.ts +161 -0
- package/src/lib/define-config.ts +152 -0
- package/src/lib/diff.test.ts +142 -0
- package/src/lib/diff.ts +391 -0
- package/src/lib/duration.test.ts +105 -0
- package/src/lib/duration.ts +147 -0
- package/src/lib/errors.test.ts +26 -0
- package/src/lib/errors.ts +220 -0
- package/src/lib/fake-neon-api.ts +782 -0
- package/src/lib/loader.test.ts +35 -0
- package/src/lib/loader.ts +215 -0
- package/src/lib/neon-api-real.test.ts +72 -0
- package/src/lib/neon-api-real.ts +1123 -0
- package/src/lib/neon-api.ts +356 -0
- package/src/lib/patterns.test.ts +80 -0
- package/src/lib/patterns.ts +98 -0
- package/src/lib/schema.test.ts +88 -0
- package/src/lib/schema.ts +252 -0
- package/src/lib/test-utils.ts +83 -0
- package/src/lib/types.ts +268 -0
- package/src/lib/wrap-neon-error.test.ts +145 -0
- package/src/lib/wrap-neon-error.ts +204 -0
- package/src/v1.test.ts +33 -0
- package/src/v1.ts +148 -0
- package/tsconfig.json +4 -0
- package/tsdown.config.ts +19 -0
- package/vitest.config.ts +19 -0
- package/vitest.e2e.config.ts +29 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { parseDuration, parseSuspendTimeout } from "./duration.js";
|
|
3
|
+
import { isWildcardPattern, validatePattern } from "./patterns.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Zod schema for {@link import("./types.js").ComputeSettings}.
|
|
7
|
+
*
|
|
8
|
+
* - CU values must be one of: 0.25, 0.5, 1, 2, 4, 8
|
|
9
|
+
* - `suspendTimeout` can be:
|
|
10
|
+
* - `false` (never suspend)
|
|
11
|
+
* - duration string like "5m", "1h" (must be 60s-604800s when parsed)
|
|
12
|
+
* - number in seconds (60-604800, or -1/0 for special values)
|
|
13
|
+
* - `undefined` (use platform default)
|
|
14
|
+
*
|
|
15
|
+
* Cross-field invariants (min <= max) are enforced via `superRefine`.
|
|
16
|
+
*/
|
|
17
|
+
export const computeSettingsSchema = z
|
|
18
|
+
.strictObject({
|
|
19
|
+
autoscalingLimitMinCu: z
|
|
20
|
+
.union([
|
|
21
|
+
z.literal(0.25),
|
|
22
|
+
z.literal(0.5),
|
|
23
|
+
z.literal(1),
|
|
24
|
+
z.literal(2),
|
|
25
|
+
z.literal(4),
|
|
26
|
+
z.literal(8),
|
|
27
|
+
])
|
|
28
|
+
.optional(),
|
|
29
|
+
autoscalingLimitMaxCu: z
|
|
30
|
+
.union([
|
|
31
|
+
z.literal(0.25),
|
|
32
|
+
z.literal(0.5),
|
|
33
|
+
z.literal(1),
|
|
34
|
+
z.literal(2),
|
|
35
|
+
z.literal(4),
|
|
36
|
+
z.literal(8),
|
|
37
|
+
])
|
|
38
|
+
.optional(),
|
|
39
|
+
suspendTimeout: z
|
|
40
|
+
.union([z.literal(false), z.string(), z.number()])
|
|
41
|
+
.optional()
|
|
42
|
+
.superRefine((value, ctx) => {
|
|
43
|
+
if (value === undefined) return; // undefined is valid (use platform default)
|
|
44
|
+
const result = parseSuspendTimeout(value);
|
|
45
|
+
if ("error" in result) {
|
|
46
|
+
ctx.addIssue({
|
|
47
|
+
code: "custom",
|
|
48
|
+
message: result.error,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
})
|
|
53
|
+
.superRefine((settings, ctx) => {
|
|
54
|
+
const { autoscalingLimitMinCu: min, autoscalingLimitMaxCu: max } =
|
|
55
|
+
settings;
|
|
56
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
57
|
+
ctx.addIssue({
|
|
58
|
+
code: "custom",
|
|
59
|
+
path: ["autoscalingLimitMinCu"],
|
|
60
|
+
message: `autoscalingLimitMinCu (${min}) must be <= autoscalingLimitMaxCu (${max})`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const serviceToggleSchema = z.strictObject({
|
|
66
|
+
enabled: z.boolean().optional(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const postgresConfigSchema = z.strictObject({
|
|
70
|
+
computeSettings: computeSettingsSchema.optional(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Branch-unique function slug. Mirrors the Neon Functions API path-segment rule
|
|
75
|
+
* (`platform/internal/platform/functions/name.go`): lowercase DNS label, 1–40 chars.
|
|
76
|
+
*/
|
|
77
|
+
const functionSlugSchema = z
|
|
78
|
+
.string()
|
|
79
|
+
.regex(
|
|
80
|
+
/^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/,
|
|
81
|
+
"function slug must be a lowercase DNS label (1-40 chars, letters/digits/hyphens, no leading/trailing hyphen)",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Per-function environment map. Every value must be a defined string: a `process.env.X`
|
|
86
|
+
* that is unset surfaces as `undefined` and is rejected here (rather than silently
|
|
87
|
+
* shipping `undefined` into the deployment).
|
|
88
|
+
*/
|
|
89
|
+
const functionEnvSchema = z.record(z.string(), z.string());
|
|
90
|
+
|
|
91
|
+
export const functionConfigSchema = z.strictObject({
|
|
92
|
+
slug: functionSlugSchema,
|
|
93
|
+
name: z.string().min(1).max(255),
|
|
94
|
+
source: z.string().min(1),
|
|
95
|
+
env: functionEnvSchema.optional(),
|
|
96
|
+
runtime: z.literal("nodejs24").optional(),
|
|
97
|
+
memoryMib: z
|
|
98
|
+
.union([
|
|
99
|
+
z.literal(256),
|
|
100
|
+
z.literal(512),
|
|
101
|
+
z.literal(1024),
|
|
102
|
+
z.literal(2048),
|
|
103
|
+
z.literal(4096),
|
|
104
|
+
z.literal(8192),
|
|
105
|
+
])
|
|
106
|
+
.optional(),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const bucketConfigSchema = z.strictObject({
|
|
110
|
+
name: z.string().min(1).max(255),
|
|
111
|
+
access: z
|
|
112
|
+
.union([z.literal("private"), z.literal("public_read")])
|
|
113
|
+
.optional(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const previewConfigSchema = z
|
|
117
|
+
.strictObject({
|
|
118
|
+
functions: z.array(functionConfigSchema).optional(),
|
|
119
|
+
buckets: z.array(bucketConfigSchema).optional(),
|
|
120
|
+
aiGateway: serviceToggleSchema.optional(),
|
|
121
|
+
})
|
|
122
|
+
.superRefine((preview, ctx) => {
|
|
123
|
+
assertUnique({
|
|
124
|
+
ctx,
|
|
125
|
+
path: ["functions"],
|
|
126
|
+
items: preview.functions ?? [],
|
|
127
|
+
key: (fn) => fn.slug,
|
|
128
|
+
label: "function slug",
|
|
129
|
+
});
|
|
130
|
+
assertUnique({
|
|
131
|
+
ctx,
|
|
132
|
+
path: ["buckets"],
|
|
133
|
+
items: preview.buckets ?? [],
|
|
134
|
+
key: (bucket) => bucket.name,
|
|
135
|
+
label: "bucket name",
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Flag duplicate keys within a Preview collection so a typo in two function slugs (or two
|
|
141
|
+
* buckets) surfaces as a config error rather than the second silently clobbering the first
|
|
142
|
+
* at apply time.
|
|
143
|
+
*/
|
|
144
|
+
function assertUnique<T>(args: {
|
|
145
|
+
ctx: z.RefinementCtx;
|
|
146
|
+
path: (string | number)[];
|
|
147
|
+
items: T[];
|
|
148
|
+
key: (item: T) => string;
|
|
149
|
+
label: string;
|
|
150
|
+
}): void {
|
|
151
|
+
const { ctx, path, items, key, label } = args;
|
|
152
|
+
const seen = new Set<string>();
|
|
153
|
+
items.forEach((item, index) => {
|
|
154
|
+
const value = key(item);
|
|
155
|
+
if (seen.has(value)) {
|
|
156
|
+
ctx.addIssue({
|
|
157
|
+
code: "custom",
|
|
158
|
+
path: [...path, index],
|
|
159
|
+
message: `duplicate ${label}: ${JSON.stringify(value)}`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
seen.add(value);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const branchConfigSchema = z
|
|
167
|
+
.strictObject({
|
|
168
|
+
parent: z.string().optional(),
|
|
169
|
+
protected: z.boolean().optional(),
|
|
170
|
+
ttl: z
|
|
171
|
+
.union([z.string(), z.number()])
|
|
172
|
+
.optional()
|
|
173
|
+
.superRefine((value, ctx) => {
|
|
174
|
+
if (value === undefined) return;
|
|
175
|
+
const result = parseDuration(value);
|
|
176
|
+
if ("error" in result) {
|
|
177
|
+
ctx.addIssue({ code: "custom", message: result.error });
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
postgres: postgresConfigSchema.optional(),
|
|
181
|
+
auth: serviceToggleSchema.optional(),
|
|
182
|
+
dataApi: serviceToggleSchema.optional(),
|
|
183
|
+
preview: previewConfigSchema.optional(),
|
|
184
|
+
})
|
|
185
|
+
.superRefine((cfg, ctx) => {
|
|
186
|
+
validateParentReference({
|
|
187
|
+
ctx,
|
|
188
|
+
path: ["parent"],
|
|
189
|
+
parent: cfg.parent,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
function validateParentReference(args: {
|
|
194
|
+
ctx: z.RefinementCtx;
|
|
195
|
+
path: (string | number)[];
|
|
196
|
+
parent: string | undefined;
|
|
197
|
+
}): void {
|
|
198
|
+
const { ctx, path, parent } = args;
|
|
199
|
+
if (parent === undefined) return;
|
|
200
|
+
|
|
201
|
+
const patternCheck = validatePattern(parent);
|
|
202
|
+
if ("error" in patternCheck) {
|
|
203
|
+
ctx.addIssue({ code: "custom", path, message: patternCheck.error });
|
|
204
|
+
} else if (isWildcardPattern(parent)) {
|
|
205
|
+
ctx.addIssue({
|
|
206
|
+
code: "custom",
|
|
207
|
+
path,
|
|
208
|
+
message: `parent must be a concrete branch name (no wildcards), got "${parent}"`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const configSchema = z.function({
|
|
214
|
+
input: [z.unknown()],
|
|
215
|
+
output: z.unknown(),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Convert the structured {@link z.ZodError} produced by `configSchema.safeParse` into the
|
|
220
|
+
* `string[]` shape used by {@link import("./errors.js").ConfigValidationError}.
|
|
221
|
+
*
|
|
222
|
+
* Issue paths are rendered as dot-separated property accesses (`postgres.computeSettings`)
|
|
223
|
+
* and unknown-key issues from `strictObject` are normalised so the message contains the
|
|
224
|
+
* substring "unknown key" — keeping pre-zod assertions in test suites and downstream tools
|
|
225
|
+
* stable.
|
|
226
|
+
*/
|
|
227
|
+
export function formatZodIssues(error: z.ZodError): string[] {
|
|
228
|
+
return error.issues.map((issue) => {
|
|
229
|
+
const path = renderPath(issue.path);
|
|
230
|
+
const message = normaliseIssueMessage(issue);
|
|
231
|
+
return path ? `${path}: ${message}` : message;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderPath(path: ReadonlyArray<PropertyKey>): string {
|
|
236
|
+
let out = "";
|
|
237
|
+
for (const segment of path) {
|
|
238
|
+
if (typeof segment === "number") out += `[${segment}]`;
|
|
239
|
+
else if (out === "") out += String(segment);
|
|
240
|
+
else out += `.${String(segment)}`;
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function normaliseIssueMessage(issue: z.core.$ZodIssue): string {
|
|
246
|
+
if (issue.code === "unrecognized_keys") {
|
|
247
|
+
const keys = (issue as z.core.$ZodIssueUnrecognizedKeys).keys ?? [];
|
|
248
|
+
const formatted = keys.map((k) => JSON.stringify(k)).join(", ");
|
|
249
|
+
return `unknown key${keys.length === 1 ? "" : "s"}: ${formatted}`;
|
|
250
|
+
}
|
|
251
|
+
return issue.message;
|
|
252
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Every Neon env var (and adjacent OS env var) the platform package reads at runtime.
|
|
8
|
+
* Used by {@link stubCleanNeonEnv} to give each test a deterministic, empty starting
|
|
9
|
+
* env regardless of the developer's local `~/.config/neonctl`, exported `NEON_*` vars,
|
|
10
|
+
* or anything else carried over from the parent shell.
|
|
11
|
+
*/
|
|
12
|
+
const NEON_AND_RELATED_ENV_KEYS = [
|
|
13
|
+
"NEON_API_KEY",
|
|
14
|
+
"NEON_PROJECT_ID",
|
|
15
|
+
"NEON_BRANCH_ID",
|
|
16
|
+
"NEON_ORG_ID",
|
|
17
|
+
"NEON_AUTH_BASE_URL",
|
|
18
|
+
"NEON_DATA_API_URL",
|
|
19
|
+
"DATABASE_URL",
|
|
20
|
+
"DATABASE_URL_UNPOOLED",
|
|
21
|
+
"NEONCTL_CONFIG_DIR",
|
|
22
|
+
"HOME",
|
|
23
|
+
"USERPROFILE",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Stub every Neon-related env var to `undefined` (so they look unset to the code under
|
|
28
|
+
* test). Tests can override individual keys with `vi.stubEnv(key, value)` after calling
|
|
29
|
+
* this. `vitest.config.ts` sets `unstubEnvs: true` so each test's stubs are auto-reset.
|
|
30
|
+
*
|
|
31
|
+
* Call this from a `beforeEach` in any test file that touches `process.env`-driven
|
|
32
|
+
* resolution (api key / context / connection-string env vars).
|
|
33
|
+
*/
|
|
34
|
+
export function stubCleanNeonEnv(): void {
|
|
35
|
+
for (const key of NEON_AND_RELATED_ENV_KEYS) {
|
|
36
|
+
// vitest 3.x: passing `undefined` deletes the env var (rather than setting it
|
|
37
|
+
// to the literal string "undefined") — exactly what we want, because empty
|
|
38
|
+
// strings would still trip code that reads `env.HOME` to build paths.
|
|
39
|
+
vi.stubEnv(key, undefined);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build a transient project tree under the OS temp directory.
|
|
45
|
+
*
|
|
46
|
+
* Returns the absolute root path plus a `cleanup()` that removes the tree. Tests should
|
|
47
|
+
* call `cleanup()` from an `afterEach`/`afterAll` hook.
|
|
48
|
+
*
|
|
49
|
+
* `files` is a flat map of relative paths → contents; intermediate directories are created
|
|
50
|
+
* automatically. Directories themselves can be created by passing `null` as the value.
|
|
51
|
+
*
|
|
52
|
+
* A `.git/HEAD` marker is seeded at the root by default so the platform's upward walkers
|
|
53
|
+
* (which stop at `.git`) don't escape the synthetic repo and read the developer's real
|
|
54
|
+
* `~/.neon`. Pass an explicit `.git` entry in `files` to override or position it elsewhere
|
|
55
|
+
* (e.g. for tests that exercise the boundary behaviour itself).
|
|
56
|
+
*/
|
|
57
|
+
export function makeTempRepo(files: Record<string, string | null>): {
|
|
58
|
+
root: string;
|
|
59
|
+
cleanup: () => void;
|
|
60
|
+
} {
|
|
61
|
+
const root = mkdtempSync(join(tmpdir(), "neon-ts-test-"));
|
|
62
|
+
const callerSpecifiesGit = Object.keys(files).some(
|
|
63
|
+
(p) => p === ".git" || p.startsWith(".git/"),
|
|
64
|
+
);
|
|
65
|
+
const entries: Array<[string, string | null]> = callerSpecifiesGit
|
|
66
|
+
? Object.entries(files)
|
|
67
|
+
: [[".git/HEAD", "ref: refs/heads/main\n"], ...Object.entries(files)];
|
|
68
|
+
for (const [relPath, contents] of entries) {
|
|
69
|
+
const abs = join(root, relPath);
|
|
70
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
71
|
+
if (contents !== null) {
|
|
72
|
+
writeFileSync(abs, contents, "utf-8");
|
|
73
|
+
} else {
|
|
74
|
+
mkdirSync(abs, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
root,
|
|
79
|
+
cleanup: () => {
|
|
80
|
+
rmSync(root, { recursive: true, force: true });
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Valid Neon Compute Unit values.
|
|
3
|
+
* Most plans support 0.25, 0.5, 1, 2, 4, 8. Higher values may be available on Business plans.
|
|
4
|
+
*/
|
|
5
|
+
export type ComputeUnit = 0.25 | 0.5 | 1 | 2 | 4 | 8;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compute settings applied to the read/write endpoint of a branch.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the subset of {@link https://api-docs.neon.tech/reference/getting-started-with-neon-api Neon endpoint}
|
|
11
|
+
* fields that we expose as IaC primitives. Anything left undefined falls back to the project's
|
|
12
|
+
* `default_endpoint_settings` (which themselves fall back to Neon platform defaults).
|
|
13
|
+
*/
|
|
14
|
+
export interface ComputeSettings {
|
|
15
|
+
/**
|
|
16
|
+
* Minimum number of Compute Units. Set to 0.25 for true scale-to-zero.
|
|
17
|
+
* @example 0.25 // scale-to-zero
|
|
18
|
+
* @example 1 // always-on with 1 CU minimum
|
|
19
|
+
*/
|
|
20
|
+
autoscalingLimitMinCu?: ComputeUnit;
|
|
21
|
+
/**
|
|
22
|
+
* Maximum number of Compute Units for autoscaling.
|
|
23
|
+
* @example 2
|
|
24
|
+
* @example 8
|
|
25
|
+
*/
|
|
26
|
+
autoscalingLimitMaxCu?: ComputeUnit;
|
|
27
|
+
/**
|
|
28
|
+
* How long to wait before suspending an idle compute.
|
|
29
|
+
*
|
|
30
|
+
* - `false` — never suspend (always-on compute)
|
|
31
|
+
* - `"5m"` — duration string (supports "30s", "5m", "1h", "7d", etc)
|
|
32
|
+
* - `300` — custom timeout in seconds (60-604800)
|
|
33
|
+
* - `undefined` — use Neon platform default (currently 300s / 5 minutes)
|
|
34
|
+
*
|
|
35
|
+
* @example false // never suspend
|
|
36
|
+
* @example "5m" // 5 minutes
|
|
37
|
+
* @example "1h" // 1 hour
|
|
38
|
+
* @example 300 // 5 minutes in seconds
|
|
39
|
+
*/
|
|
40
|
+
suspendTimeout?: false | "5m" | "1h" | string | number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read-only descriptor of the branch a {@link Config} policy is being evaluated for — the
|
|
45
|
+
* `branch` argument passed to your `defineConfig((branch) => …)` callback. It describes
|
|
46
|
+
* **which** branch this invocation decides for; it is not a live branch handle and must not
|
|
47
|
+
* be mutated. Switch on its fields and return the desired {@link BranchConfig}.
|
|
48
|
+
*/
|
|
49
|
+
export interface BranchTarget {
|
|
50
|
+
/** Branch name being evaluated. For `branch dev`, this is the generated branch name. */
|
|
51
|
+
name: string;
|
|
52
|
+
/** Neon branch id when the branch already exists. Undefined during pre-create eval. */
|
|
53
|
+
id?: string;
|
|
54
|
+
/** Whether this branch already exists on Neon. */
|
|
55
|
+
exists: boolean;
|
|
56
|
+
/** Parent branch id from Neon when known. */
|
|
57
|
+
parentId?: string;
|
|
58
|
+
/** Whether Neon marks this branch as the project default. */
|
|
59
|
+
isDefault?: boolean;
|
|
60
|
+
/** Whether Neon currently marks this branch protected. */
|
|
61
|
+
isProtected?: boolean;
|
|
62
|
+
/** Current expiration timestamp from Neon, when set. */
|
|
63
|
+
expiresAt?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ServiceToggle {
|
|
67
|
+
/** Defaults to `true` when the service namespace is present. Set `false` to opt out. */
|
|
68
|
+
enabled?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PostgresConfig {
|
|
72
|
+
computeSettings?: ComputeSettings;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Supported function runtimes. Mirrors the Neon Functions deploy API `runtime` enum.
|
|
77
|
+
* Only `nodejs24` exists today; kept as a union so adding runtimes later is a
|
|
78
|
+
* non-breaking, type-checked change.
|
|
79
|
+
*/
|
|
80
|
+
export type FunctionRuntime = "nodejs24";
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Memory sizes (MiB) accepted by the Neon Functions deploy API. Mirrors the
|
|
84
|
+
* `memory_mib` enum in the spec.
|
|
85
|
+
*/
|
|
86
|
+
export type FunctionMemoryMib = 256 | 512 | 1024 | 2048 | 4096 | 8192;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A single Neon Function deployed to a branch (Preview feature).
|
|
90
|
+
*
|
|
91
|
+
* A function is invoked like a Cloudflare/Vercel handler — its source module
|
|
92
|
+
* `export default { fetch }` or `export async function handler(req): Response`. The
|
|
93
|
+
* `source` path is bundled (esbuild) and uploaded as a deployment; the newest
|
|
94
|
+
* deployment becomes active.
|
|
95
|
+
*/
|
|
96
|
+
export interface FunctionConfig {
|
|
97
|
+
/**
|
|
98
|
+
* Branch-unique, lowercase DNS-label used as the path segment in the function's
|
|
99
|
+
* invocation URL. Immutable once created. 1–40 chars, `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`.
|
|
100
|
+
* @example "hello-world"
|
|
101
|
+
*/
|
|
102
|
+
slug: string;
|
|
103
|
+
/** Free-form display name. @example "Hello World" */
|
|
104
|
+
name: string;
|
|
105
|
+
/**
|
|
106
|
+
* Path to the function's entry module, **relative to `neon.ts`** (or absolute). The
|
|
107
|
+
* module's default export (`{ fetch }`) or `handler` export is the function entry. This
|
|
108
|
+
* path is resolved against the loaded `neon.ts` location and bundled with esbuild at
|
|
109
|
+
* deploy time.
|
|
110
|
+
*
|
|
111
|
+
* We require a string path rather than an imported handler because a JS function value
|
|
112
|
+
* carries no reference back to its source file, so esbuild has nothing to bundle from.
|
|
113
|
+
* @example "./functions/hello-world.ts"
|
|
114
|
+
*/
|
|
115
|
+
source: string;
|
|
116
|
+
/**
|
|
117
|
+
* Environment variables injected into the deployed function. Every value must be a
|
|
118
|
+
* defined string — a `process.env.X` that is `undefined` (unset) errors at validation
|
|
119
|
+
* time rather than silently shipping `undefined`.
|
|
120
|
+
* @example { RESEND_API_KEY: process.env.RESEND_API_KEY }
|
|
121
|
+
*/
|
|
122
|
+
env?: Record<string, string>;
|
|
123
|
+
/** Runtime to execute the function with. Defaults to `"nodejs24"`. */
|
|
124
|
+
runtime?: FunctionRuntime;
|
|
125
|
+
/** Memory allotted to each invocation, in MiB. Defaults to `512`. */
|
|
126
|
+
memoryMib?: FunctionMemoryMib;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Anonymous-access level for a branchable object-storage bucket. */
|
|
130
|
+
export type BucketAccessLevel = "private" | "public_read";
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A branchable object-storage bucket on a branch (Preview feature).
|
|
134
|
+
*/
|
|
135
|
+
export interface BucketConfig {
|
|
136
|
+
/** Bucket name, unique within a branch. 1–255 chars. */
|
|
137
|
+
name: string;
|
|
138
|
+
/**
|
|
139
|
+
* Anonymous access level. `private` (default) requires authenticated reads/writes;
|
|
140
|
+
* `public_read` allows anonymous GetObject/HeadObject.
|
|
141
|
+
*/
|
|
142
|
+
access?: BucketAccessLevel;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Branch-scoped Preview features. Grouped under `preview` to signal they are backed by
|
|
147
|
+
* Neon `x-stability-level: beta` endpoints and may change before GA.
|
|
148
|
+
*/
|
|
149
|
+
export interface PreviewConfig {
|
|
150
|
+
/** Functions to deploy on the branch. */
|
|
151
|
+
functions?: FunctionConfig[];
|
|
152
|
+
/** Object-storage buckets to create on the branch. */
|
|
153
|
+
buckets?: BucketConfig[];
|
|
154
|
+
/** Enable/disable the AI Gateway on the branch (toggle, like auth / dataApi). */
|
|
155
|
+
aiGateway?: ServiceToggle;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface BranchConfigBase {
|
|
159
|
+
/** Parent branch name used when creating a new branch. Not a Postgres setting. */
|
|
160
|
+
parent?: string;
|
|
161
|
+
/** Time-to-live applied when creating a new branch, or reconciled on existing branches. */
|
|
162
|
+
ttl?: string | number;
|
|
163
|
+
/** Whether the selected branch should be protected. Undefined means "leave as-is". */
|
|
164
|
+
protected?: boolean;
|
|
165
|
+
postgres?: PostgresConfig;
|
|
166
|
+
/**
|
|
167
|
+
* Branch-scoped Preview features (functions, object-storage buckets, AI Gateway).
|
|
168
|
+
* Backed by Neon `x-stability-level: beta` endpoints — see {@link PreviewConfig}.
|
|
169
|
+
*/
|
|
170
|
+
preview?: PreviewConfig;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type BranchServiceConfig =
|
|
174
|
+
| { auth?: never; dataApi?: never }
|
|
175
|
+
| { auth: ServiceToggle; dataApi?: never }
|
|
176
|
+
| { auth?: never; dataApi: ServiceToggle }
|
|
177
|
+
| { auth: ServiceToggle; dataApi: ServiceToggle };
|
|
178
|
+
|
|
179
|
+
export type BranchConfig = BranchConfigBase & BranchServiceConfig;
|
|
180
|
+
|
|
181
|
+
export type Config = (branch: BranchTarget) => BranchConfig;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* A function with all deploy defaults applied. `resolveConfig` fills in `runtime` and
|
|
185
|
+
* `memoryMib` so downstream diff/apply never has to re-derive them.
|
|
186
|
+
*/
|
|
187
|
+
export interface ResolvedFunctionConfig {
|
|
188
|
+
slug: string;
|
|
189
|
+
name: string;
|
|
190
|
+
source: string;
|
|
191
|
+
env: Record<string, string>;
|
|
192
|
+
runtime: FunctionRuntime;
|
|
193
|
+
memoryMib: FunctionMemoryMib;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** A bucket with its access level defaulted to `private`. */
|
|
197
|
+
export interface ResolvedBucketConfig {
|
|
198
|
+
name: string;
|
|
199
|
+
access: BucketAccessLevel;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Normalized {@link PreviewConfig}. Only present on {@link ResolvedBranchConfig} when the
|
|
204
|
+
* policy returned a `preview` block. `aiGatewayEnabled` follows the same
|
|
205
|
+
* "present-and-not-`false`" semantics as `authEnabled` / `dataApiEnabled`.
|
|
206
|
+
*/
|
|
207
|
+
export interface ResolvedPreviewConfig {
|
|
208
|
+
functions: ResolvedFunctionConfig[];
|
|
209
|
+
buckets: ResolvedBucketConfig[];
|
|
210
|
+
aiGatewayEnabled: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface ResolvedBranchConfig {
|
|
214
|
+
parent?: string;
|
|
215
|
+
ttlSeconds?: number;
|
|
216
|
+
protected?: boolean;
|
|
217
|
+
postgres?: PostgresConfig;
|
|
218
|
+
authEnabled: boolean;
|
|
219
|
+
dataApiEnabled: boolean;
|
|
220
|
+
preview?: ResolvedPreviewConfig;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* One concrete change `pushConfig` made (or, in dry-run, would make) on the remote.
|
|
225
|
+
*/
|
|
226
|
+
export interface AppliedChange {
|
|
227
|
+
/**
|
|
228
|
+
* `service` covers branch-scoped integrations driven by the branch policy (e.g.
|
|
229
|
+
* Neon Auth, Data API).
|
|
230
|
+
*/
|
|
231
|
+
kind: "branch" | "service";
|
|
232
|
+
action: "create" | "update" | "noop";
|
|
233
|
+
identifier: string;
|
|
234
|
+
details?: Record<string, unknown>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* A diff entry that conflicts with the desired config. `pushConfig` throws
|
|
239
|
+
* {@link PushConflictError} on the first call when conflicts exist; pass
|
|
240
|
+
* `updateExisting: true` to apply mutable drift (settings, `protected`, TTL, project
|
|
241
|
+
* rename). Immutable fields (region, Postgres major version) are always conflicts —
|
|
242
|
+
* recreate the project to change them.
|
|
243
|
+
*/
|
|
244
|
+
export interface ConflictReport {
|
|
245
|
+
kind: "branch";
|
|
246
|
+
identifier: string;
|
|
247
|
+
field: string;
|
|
248
|
+
current: unknown;
|
|
249
|
+
desired: unknown;
|
|
250
|
+
reason: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Result of a `pushConfig` invocation.
|
|
255
|
+
*/
|
|
256
|
+
export interface PushResult {
|
|
257
|
+
projectId: string;
|
|
258
|
+
orgId?: string;
|
|
259
|
+
branchId: string;
|
|
260
|
+
branchName: string;
|
|
261
|
+
/**
|
|
262
|
+
* `true` when `pushConfig` was called with `{ dryRun: true }`. `applied` then records
|
|
263
|
+
* what **would** be applied on a real push; no API mutations were performed.
|
|
264
|
+
*/
|
|
265
|
+
dryRun: boolean;
|
|
266
|
+
applied: AppliedChange[];
|
|
267
|
+
conflicts: ConflictReport[];
|
|
268
|
+
}
|