@neondatabase/config 0.2.1 → 0.4.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 +20 -7
- package/dist/index.d.ts +2 -2
- package/dist/lib/define-config.d.ts +32 -15
- package/dist/lib/define-config.d.ts.map +1 -1
- package/dist/lib/define-config.js +79 -58
- package/dist/lib/define-config.js.map +1 -1
- package/dist/lib/neon-api-real.d.ts +14 -2
- package/dist/lib/neon-api-real.d.ts.map +1 -1
- package/dist/lib/neon-api-real.js +23 -10
- package/dist/lib/neon-api-real.js.map +1 -1
- package/dist/lib/neon-api.d.ts +1 -2
- package/dist/lib/neon-api.d.ts.map +1 -1
- package/dist/lib/schema.d.ts +52 -32
- package/dist/lib/schema.d.ts.map +1 -1
- package/dist/lib/schema.js +42 -59
- package/dist/lib/schema.js.map +1 -1
- package/dist/lib/types.d.ts +112 -62
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/v1.d.ts +40 -39
- package/dist/v1.d.ts.map +1 -1
- package/dist/v1.js +9 -7
- package/dist/v1.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,17 +16,30 @@ npm install @neondatabase/config
|
|
|
16
16
|
// neon.ts
|
|
17
17
|
import { defineConfig } from "@neondatabase/config/v1";
|
|
18
18
|
|
|
19
|
-
export default defineConfig(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
// Static: what *exists* on every branch. GA service toggles drive the typed env.
|
|
21
|
+
auth: true,
|
|
22
|
+
dataApi: false,
|
|
23
|
+
// Beta (Preview) features, keyed by slug / name.
|
|
24
|
+
preview: {
|
|
25
|
+
functions: {
|
|
26
|
+
hello: { name: "Hello", source: "./functions/hello.ts", dev: { port: 8787 } },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
// Dynamic: per-branch tuning only. Cannot add/remove services or functions.
|
|
30
|
+
branch: (branch) => ({
|
|
31
|
+
protected: branch.name === "main",
|
|
32
|
+
...(branch.name === "main" ? {} : { parent: "main", ttl: "7d" }),
|
|
33
|
+
}),
|
|
24
34
|
});
|
|
25
35
|
```
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
A policy is split into a **static** existential set and a **dynamic** `branch` closure:
|
|
28
38
|
|
|
29
|
-
`
|
|
39
|
+
- **Static top-level** — `auth` / `dataApi` (GA service toggles) and the beta `preview` block (`aiGateway`, `functions` keyed by slug, `buckets` keyed by name). Because this is static, the secret set is known at the type level, so `parseEnv` / `fetchEnv` from `@neondatabase/env` return an exact `NeonEnv`.
|
|
40
|
+
- **`branch` closure** — receives a **read-only descriptor** (`BranchTarget`) of the branch being evaluated (`name`, `id`, `exists`, `isDefault`, `isProtected`, `parentId`, `expiresAt`) and returns per-branch *tuning*: `parent`, `ttl`, `protected`, `postgres.computeSettings`, and per-function `runtime`. Function memory is fixed at `2048` MiB for now and is not user-configurable. It runs both against existing branches and during pre-create evaluation (`exists: false`). It **cannot** change which services or functions exist — that is what keeps the static secret set sound.
|
|
41
|
+
|
|
42
|
+
Service toggles accept `true` / `{}` / `{ enabled: true }` (enabled) and `false` / `{ enabled: false }` (disabled). Function slugs (record keys) must match `^[a-z0-9]{1,20}$`.
|
|
30
43
|
|
|
31
44
|
## Functions
|
|
32
45
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppliedChange,
|
|
1
|
+
import { AppliedChange, BranchTarget, BranchTuning, BranchTuningFn, BucketAccessLevel, BucketDef, ComputeSettings, Config, ConflictReport, FunctionDef, FunctionDevConfig, FunctionRuntime, FunctionTuning, PostgresConfig, PreviewInput, PreviewTuning, PushResult, ResolvedBranchConfig, ResolvedBucketConfig, ResolvedFunctionConfig, ResolvedPreviewConfig, ServiceToggle, ServiceToggleInput } from "./lib/types.js";
|
|
2
2
|
import { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError } from "./lib/errors.js";
|
|
3
3
|
import { CreateBranchInput, CreateBucketInput, CreateProjectInput, DeployFunctionInput, GetConnectionUriInput, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBucketSnapshot, NeonDataApiSnapshot, NeonDatabaseSnapshot, NeonEndpointSnapshot, NeonFunctionDeploymentSnapshot, NeonFunctionSnapshot, NeonProjectSnapshot, NeonRoleSnapshot, UpdateBranchInput } from "./lib/neon-api.js";
|
|
4
4
|
import { createNeonApiFromOptions, resolveApiKey } from "./lib/auth.js";
|
|
@@ -7,4 +7,4 @@ import { DiffOptions, DiffResult, PlanStep, RemotePreviewState, RemoteServiceSta
|
|
|
7
7
|
import { LoadConfigOptions, loadConfigFromFile } from "./lib/loader.js";
|
|
8
8
|
import { createRealNeonApi } from "./lib/neon-api-real.js";
|
|
9
9
|
import { errors, schemas } from "./v1.js";
|
|
10
|
-
export { AppliedChange,
|
|
10
|
+
export { AppliedChange, BranchTarget, BranchTuning, BranchTuningFn, BucketAccessLevel, BucketDef, ComputeSettings, Config, ConfigLoadError, ConfigValidationError, ConflictReport, CreateBranchInput, CreateBucketInput, CreateProjectInput, DeployFunctionInput, DiffOptions, DiffResult, ErrorCode, FunctionDef, FunctionDevConfig, FunctionRuntime, FunctionTuning, GetConnectionUriInput, LoadConfigOptions, MissingContextError, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBucketSnapshot, NeonDataApiSnapshot, NeonDatabaseSnapshot, NeonEndpointSnapshot, NeonFunctionDeploymentSnapshot, NeonFunctionSnapshot, NeonProjectSnapshot, NeonRoleSnapshot, PlanStep, PlatformError, PostgresConfig, PreviewInput, PreviewTuning, PushAbortedError, PushConflictError, PushResult, RemotePreviewState, RemoteServiceState, RemoteState, ResolvedBranchConfig, ResolvedBucketConfig, ResolvedFunctionConfig, ResolvedPreviewConfig, ServiceToggle, ServiceToggleInput, UpdateBranchInput, createNeonApiFromOptions, createRealNeonApi, defineConfig, diffConfig, errors, loadConfigFromFile, resolveApiKey, resolveConfig, schemas };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BranchTarget, Config, ResolvedBranchConfig } from "./types.js";
|
|
1
|
+
import { BranchTarget, BranchTuningFn, Config, PreviewInput, ResolvedBranchConfig, ServiceToggleInput } from "./types.js";
|
|
2
2
|
|
|
3
3
|
//#region src/lib/define-config.d.ts
|
|
4
4
|
|
|
@@ -9,27 +9,44 @@ import { BranchTarget, Config, ResolvedBranchConfig } from "./types.js";
|
|
|
9
9
|
* ```ts
|
|
10
10
|
* import { defineConfig } from "@neondatabase/config/v1";
|
|
11
11
|
*
|
|
12
|
-
* export default defineConfig(
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* export default defineConfig({
|
|
13
|
+
* auth: true,
|
|
14
|
+
* preview: {
|
|
15
|
+
* functions: {
|
|
16
|
+
* hello: { name: "Hello", source: "./functions/hello.ts", dev: { port: 8787 } },
|
|
17
|
+
* },
|
|
18
|
+
* },
|
|
19
|
+
* branch: (branch) => ({ protected: branch.name === "main" }),
|
|
17
20
|
* });
|
|
18
21
|
* ```
|
|
19
22
|
*
|
|
20
|
-
* The
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* Neon) and during pre-create evaluation (`exists: false`, `id` undefined).
|
|
23
|
+
* The policy is split into a **static** existential set (top-level `auth` / `dataApi`
|
|
24
|
+
* toggles and the beta `preview` block) and a **dynamic** per-branch `branch` closure. The
|
|
25
|
+
* static half determines which secrets exist — so `NeonEnv<typeof config>` and `parseEnv`
|
|
26
|
+
* are exact — while the closure can only *tune* a branch (lifecycle, compute, per-function
|
|
27
|
+
* deploy settings), never change what exists.
|
|
26
28
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
+
* The `branch` callback receives a read-only {@link BranchTarget} descriptor of the branch
|
|
30
|
+
* being decided for (not a live handle); switch on its facts (`branch.name`,
|
|
31
|
+
* `branch.isDefault`, `branch.exists`, …) and **return** the desired tuning. It runs in two
|
|
32
|
+
* modes: against an existing branch (fields populated from Neon) and during pre-create
|
|
33
|
+
* evaluation (`exists: false`, `id` undefined).
|
|
34
|
+
*
|
|
35
|
+
* Pure: no I/O, no side effects. The static parts are validated here; the closure's output
|
|
36
|
+
* is validated every time it is evaluated so errors point at the concrete branch target.
|
|
29
37
|
*/
|
|
30
|
-
declare function defineConfig<const
|
|
38
|
+
declare function defineConfig<const Auth extends ServiceToggleInput | undefined = undefined, const DataApi extends ServiceToggleInput | undefined = undefined, const Preview extends PreviewInput | undefined = undefined>(input: {
|
|
39
|
+
auth?: Auth;
|
|
40
|
+
dataApi?: DataApi;
|
|
41
|
+
preview?: Preview;
|
|
42
|
+
branch?: BranchTuningFn<Preview>;
|
|
43
|
+
}): Config<Auth, DataApi, Preview>;
|
|
31
44
|
/**
|
|
32
45
|
* Evaluate a branch policy for a specific branch target and return a normalized config.
|
|
46
|
+
*
|
|
47
|
+
* Merges the static existential set (services + preview functions/buckets) with the
|
|
48
|
+
* per-branch tuning returned by the `branch` closure into the same {@link
|
|
49
|
+
* ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.
|
|
33
50
|
*/
|
|
34
51
|
declare function resolveConfig(config: Config, branch: BranchTarget): ResolvedBranchConfig;
|
|
35
52
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-config.d.ts","names":[],"sources":["../../src/lib/define-config.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"define-config.d.ts","names":[],"sources":["../../src/lib/define-config.ts"],"mappings":";;;;;;AA2DA;;;;;;;;;;;;;;AASU;AA4BV;;;;;AAGuB;AA6GvB;;;;;;;;;;iBArJgB,gCACI,kEACG,kEACA;SAEf;YACG;YACA;WACD,eAAe;IACrB,OAAO,MAAM,SAAS;;;;;;;;iBA4BV,aAAA,SACP,gBACA,eACN;;;;;;iBA6Ga,eAAA"}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { ConfigValidationError } from "./errors.js";
|
|
2
2
|
import { parseDuration } from "./duration.js";
|
|
3
|
-
import {
|
|
3
|
+
import { branchTuningSchema, configInputSchema, formatZodIssues } from "./schema.js";
|
|
4
4
|
//#region src/lib/define-config.ts
|
|
5
5
|
/** Default deploy parameters applied to functions that omit them in `neon.ts`. */
|
|
6
6
|
const DEFAULT_FUNCTION_RUNTIME = "nodejs24";
|
|
7
|
-
const DEFAULT_FUNCTION_MEMORY_MIB = 512;
|
|
8
7
|
const REGION_PREFIX = /^(aws|azure|gcp)-/;
|
|
9
8
|
/**
|
|
10
9
|
* Validate and freeze a Neon Platform branch policy.
|
|
@@ -13,88 +12,110 @@ const REGION_PREFIX = /^(aws|azure|gcp)-/;
|
|
|
13
12
|
* ```ts
|
|
14
13
|
* import { defineConfig } from "@neondatabase/config/v1";
|
|
15
14
|
*
|
|
16
|
-
* export default defineConfig(
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* export default defineConfig({
|
|
16
|
+
* auth: true,
|
|
17
|
+
* preview: {
|
|
18
|
+
* functions: {
|
|
19
|
+
* hello: { name: "Hello", source: "./functions/hello.ts", dev: { port: 8787 } },
|
|
20
|
+
* },
|
|
21
|
+
* },
|
|
22
|
+
* branch: (branch) => ({ protected: branch.name === "main" }),
|
|
21
23
|
* });
|
|
22
24
|
* ```
|
|
23
25
|
*
|
|
24
|
-
* The
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* Neon) and during pre-create evaluation (`exists: false`, `id` undefined).
|
|
26
|
+
* The policy is split into a **static** existential set (top-level `auth` / `dataApi`
|
|
27
|
+
* toggles and the beta `preview` block) and a **dynamic** per-branch `branch` closure. The
|
|
28
|
+
* static half determines which secrets exist — so `NeonEnv<typeof config>` and `parseEnv`
|
|
29
|
+
* are exact — while the closure can only *tune* a branch (lifecycle, compute, per-function
|
|
30
|
+
* deploy settings), never change what exists.
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
32
|
+
* The `branch` callback receives a read-only {@link BranchTarget} descriptor of the branch
|
|
33
|
+
* being decided for (not a live handle); switch on its facts (`branch.name`,
|
|
34
|
+
* `branch.isDefault`, `branch.exists`, …) and **return** the desired tuning. It runs in two
|
|
35
|
+
* modes: against an existing branch (fields populated from Neon) and during pre-create
|
|
36
|
+
* evaluation (`exists: false`, `id` undefined).
|
|
37
|
+
*
|
|
38
|
+
* Pure: no I/O, no side effects. The static parts are validated here; the closure's output
|
|
39
|
+
* is validated every time it is evaluated so errors point at the concrete branch target.
|
|
33
40
|
*/
|
|
34
41
|
function defineConfig(input) {
|
|
35
|
-
if (typeof input
|
|
36
|
-
|
|
42
|
+
if (typeof input === "function") throw new ConfigValidationError(["defineConfig now expects an object, not a function: `export default defineConfig({ auth: true, preview: { … }, branch: (branch) => ({ … }) })`.", "The static services/preview set moved to the top level; per-branch logic moved into the `branch` closure."]);
|
|
43
|
+
if (input === null || typeof input !== "object") throw new ConfigValidationError(["defineConfig expects a configuration object: `export default defineConfig({ … })`."]);
|
|
44
|
+
const parsed = configInputSchema.safeParse(input);
|
|
45
|
+
if (!parsed.success) throw new ConfigValidationError(formatZodIssues(parsed.error));
|
|
46
|
+
return Object.freeze({ ...input });
|
|
37
47
|
}
|
|
38
48
|
/**
|
|
39
49
|
* Evaluate a branch policy for a specific branch target and return a normalized config.
|
|
50
|
+
*
|
|
51
|
+
* Merges the static existential set (services + preview functions/buckets) with the
|
|
52
|
+
* per-branch tuning returned by the `branch` closure into the same {@link
|
|
53
|
+
* ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.
|
|
40
54
|
*/
|
|
41
55
|
function resolveConfig(config, branch) {
|
|
56
|
+
const tuning = evaluateBranchTuning(config.branch, branch);
|
|
57
|
+
const resolved = {
|
|
58
|
+
authEnabled: isServiceEnabled(config.auth),
|
|
59
|
+
dataApiEnabled: isServiceEnabled(config.dataApi)
|
|
60
|
+
};
|
|
61
|
+
if (tuning.parent !== void 0) resolved.parent = tuning.parent;
|
|
62
|
+
if (tuning.ttl !== void 0) {
|
|
63
|
+
const parsedTtl = parseDuration(tuning.ttl);
|
|
64
|
+
if (!("error" in parsedTtl)) resolved.ttlSeconds = parsedTtl.seconds;
|
|
65
|
+
}
|
|
66
|
+
if (tuning.protected !== void 0) resolved.protected = tuning.protected;
|
|
67
|
+
if (tuning.postgres?.computeSettings) resolved.postgres = { computeSettings: { ...tuning.postgres.computeSettings } };
|
|
68
|
+
const preview = resolvePreviewConfig(config.preview, tuning);
|
|
69
|
+
if (preview) resolved.preview = preview;
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Run the `branch` closure (when present) for the target and validate its output. The
|
|
74
|
+
* closure is optional — a fully static policy resolves with empty tuning.
|
|
75
|
+
*/
|
|
76
|
+
function evaluateBranchTuning(branchFn, target) {
|
|
77
|
+
if (!branchFn) return {};
|
|
42
78
|
let raw;
|
|
43
79
|
try {
|
|
44
|
-
raw =
|
|
80
|
+
raw = branchFn(Object.freeze({ ...target }));
|
|
45
81
|
} catch (cause) {
|
|
46
|
-
throw new ConfigValidationError([`
|
|
82
|
+
throw new ConfigValidationError([`Branch policy threw while evaluating branch "${target.name}".`, cause?.message ?? String(cause)]);
|
|
47
83
|
}
|
|
48
|
-
const parsed =
|
|
84
|
+
const parsed = branchTuningSchema.safeParse(raw ?? {});
|
|
49
85
|
if (!parsed.success) throw new ConfigValidationError(formatZodIssues(parsed.error));
|
|
50
|
-
|
|
51
|
-
const issues = [];
|
|
52
|
-
let ttlSeconds;
|
|
53
|
-
if (cfg.ttl !== void 0) {
|
|
54
|
-
const parsedTtl = parseDuration(cfg.ttl);
|
|
55
|
-
if ("error" in parsedTtl) issues.push(`ttl: ${parsedTtl.error}`);
|
|
56
|
-
else ttlSeconds = parsedTtl.seconds;
|
|
57
|
-
}
|
|
58
|
-
if (issues.length > 0) throw new ConfigValidationError(issues);
|
|
59
|
-
const resolved = {
|
|
60
|
-
authEnabled: isServiceEnabled(cfg.auth),
|
|
61
|
-
dataApiEnabled: isServiceEnabled(cfg.dataApi)
|
|
62
|
-
};
|
|
63
|
-
if (cfg.parent !== void 0) resolved.parent = cfg.parent;
|
|
64
|
-
if (ttlSeconds !== void 0) resolved.ttlSeconds = ttlSeconds;
|
|
65
|
-
if (cfg.protected !== void 0) resolved.protected = cfg.protected;
|
|
66
|
-
if (cfg.postgres) resolved.postgres = { ...cfg.postgres.computeSettings ? { computeSettings: { ...cfg.postgres.computeSettings } } : {} };
|
|
67
|
-
if (cfg.preview) resolved.preview = resolvePreviewConfig(cfg.preview);
|
|
68
|
-
return resolved;
|
|
86
|
+
return parsed.data;
|
|
69
87
|
}
|
|
70
|
-
function isServiceEnabled(
|
|
71
|
-
|
|
88
|
+
function isServiceEnabled(toggle) {
|
|
89
|
+
if (toggle === void 0) return false;
|
|
90
|
+
if (typeof toggle === "boolean") return toggle;
|
|
91
|
+
return toggle.enabled !== false;
|
|
72
92
|
}
|
|
73
93
|
/**
|
|
74
|
-
* Normalize
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
94
|
+
* Normalize the static {@link PreviewInput} (merged with per-branch function tuning) into a
|
|
95
|
+
* {@link ResolvedPreviewConfig}. Returns `undefined` when the policy declares no `preview`
|
|
96
|
+
* block so the field can be omitted entirely. Function slugs / bucket names come from the
|
|
97
|
+
* record keys.
|
|
78
98
|
*/
|
|
79
|
-
function resolvePreviewConfig(preview) {
|
|
99
|
+
function resolvePreviewConfig(preview, tuning) {
|
|
100
|
+
if (!preview) return void 0;
|
|
101
|
+
const fnTuning = tuning.preview?.functions ?? {};
|
|
80
102
|
return {
|
|
81
|
-
functions: (preview.functions ??
|
|
82
|
-
buckets: (preview.buckets ??
|
|
83
|
-
name
|
|
84
|
-
access:
|
|
103
|
+
functions: Object.entries(preview.functions ?? {}).map(([slug, def]) => resolveFunctionConfig(slug, def, fnTuning[slug] ?? {})),
|
|
104
|
+
buckets: Object.entries(preview.buckets ?? {}).map(([name, def]) => ({
|
|
105
|
+
name,
|
|
106
|
+
access: def.access ?? "private"
|
|
85
107
|
})),
|
|
86
108
|
aiGatewayEnabled: isServiceEnabled(preview.aiGateway)
|
|
87
109
|
};
|
|
88
110
|
}
|
|
89
|
-
function resolveFunctionConfig(
|
|
111
|
+
function resolveFunctionConfig(slug, def, tuning) {
|
|
90
112
|
return {
|
|
91
|
-
slug
|
|
92
|
-
name:
|
|
93
|
-
source:
|
|
94
|
-
env: { ...
|
|
95
|
-
runtime:
|
|
96
|
-
|
|
97
|
-
...fn.dev ? { dev: fn.dev } : {}
|
|
113
|
+
slug,
|
|
114
|
+
name: def.name,
|
|
115
|
+
source: def.source,
|
|
116
|
+
env: { ...def.env ?? {} },
|
|
117
|
+
runtime: tuning.runtime ?? DEFAULT_FUNCTION_RUNTIME,
|
|
118
|
+
...def.dev ? { dev: def.dev } : {}
|
|
98
119
|
};
|
|
99
120
|
}
|
|
100
121
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-config.js","names":[],"sources":["../../src/lib/define-config.ts"],"sourcesContent":["import { parseDuration } from \"./duration.js\";\nimport { ConfigValidationError } from \"./errors.js\";\nimport {
|
|
1
|
+
{"version":3,"file":"define-config.js","names":[],"sources":["../../src/lib/define-config.ts"],"sourcesContent":["import { parseDuration } from \"./duration.js\";\nimport { ConfigValidationError } from \"./errors.js\";\nimport {\n\tbranchTuningSchema,\n\tconfigInputSchema,\n\tformatZodIssues,\n} from \"./schema.js\";\nimport type {\n\tBranchTarget,\n\tBranchTuning,\n\tBranchTuningFn,\n\tConfig,\n\tFunctionDef,\n\tFunctionTuning,\n\tPreviewInput,\n\tResolvedBranchConfig,\n\tResolvedFunctionConfig,\n\tResolvedPreviewConfig,\n\tServiceToggleInput,\n} from \"./types.js\";\n\n/** Default deploy parameters applied to functions that omit them in `neon.ts`. */\nconst DEFAULT_FUNCTION_RUNTIME = \"nodejs24\" as const;\n\nconst REGION_PREFIX = /^(aws|azure|gcp)-/;\n\n/**\n * Validate and freeze a Neon Platform branch policy.\n *\n * Used at the top of `neon.ts`:\n * ```ts\n * import { defineConfig } from \"@neondatabase/config/v1\";\n *\n * export default defineConfig({\n * auth: true,\n * preview: {\n * functions: {\n * hello: { name: \"Hello\", source: \"./functions/hello.ts\", dev: { port: 8787 } },\n * },\n * },\n * branch: (branch) => ({ protected: branch.name === \"main\" }),\n * });\n * ```\n *\n * The policy is split into a **static** existential set (top-level `auth` / `dataApi`\n * toggles and the beta `preview` block) and a **dynamic** per-branch `branch` closure. The\n * static half determines which secrets exist — so `NeonEnv<typeof config>` and `parseEnv`\n * are exact — while the closure can only *tune* a branch (lifecycle, compute, per-function\n * deploy settings), never change what exists.\n *\n * The `branch` callback receives a read-only {@link BranchTarget} descriptor of the branch\n * being decided for (not a live handle); switch on its facts (`branch.name`,\n * `branch.isDefault`, `branch.exists`, …) and **return** the desired tuning. It runs in two\n * modes: against an existing branch (fields populated from Neon) and during pre-create\n * evaluation (`exists: false`, `id` undefined).\n *\n * Pure: no I/O, no side effects. The static parts are validated here; the closure's output\n * is validated every time it is evaluated so errors point at the concrete branch target.\n */\nexport function defineConfig<\n\tconst Auth extends ServiceToggleInput | undefined = undefined,\n\tconst DataApi extends ServiceToggleInput | undefined = undefined,\n\tconst Preview extends PreviewInput | undefined = undefined,\n>(input: {\n\tauth?: Auth;\n\tdataApi?: DataApi;\n\tpreview?: Preview;\n\tbranch?: BranchTuningFn<Preview>;\n}): Config<Auth, DataApi, Preview> {\n\tif (typeof input === \"function\") {\n\t\tthrow new ConfigValidationError([\n\t\t\t\"defineConfig now expects an object, not a function: `export default defineConfig({ auth: true, preview: { … }, branch: (branch) => ({ … }) })`.\",\n\t\t\t\"The static services/preview set moved to the top level; per-branch logic moved into the `branch` closure.\",\n\t\t]);\n\t}\n\tif (input === null || typeof input !== \"object\") {\n\t\tthrow new ConfigValidationError([\n\t\t\t\"defineConfig expects a configuration object: `export default defineConfig({ … })`.\",\n\t\t]);\n\t}\n\n\tconst parsed = configInputSchema.safeParse(input);\n\tif (!parsed.success) {\n\t\tthrow new ConfigValidationError(formatZodIssues(parsed.error));\n\t}\n\n\treturn Object.freeze({ ...input }) as Config<Auth, DataApi, Preview>;\n}\n\n/**\n * Evaluate a branch policy for a specific branch target and return a normalized config.\n *\n * Merges the static existential set (services + preview functions/buckets) with the\n * per-branch tuning returned by the `branch` closure into the same {@link\n * ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.\n */\nexport function resolveConfig(\n\tconfig: Config,\n\tbranch: BranchTarget,\n): ResolvedBranchConfig {\n\tconst tuning = evaluateBranchTuning(config.branch, branch);\n\n\tconst resolved: ResolvedBranchConfig = {\n\t\tauthEnabled: isServiceEnabled(config.auth),\n\t\tdataApiEnabled: isServiceEnabled(config.dataApi),\n\t};\n\tif (tuning.parent !== undefined) resolved.parent = tuning.parent;\n\tif (tuning.ttl !== undefined) {\n\t\t// `branchTuningSchema` already validated `ttl` with the same `parseDuration`, so\n\t\t// this only converts the validated value to seconds — it cannot fail here.\n\t\tconst parsedTtl = parseDuration(tuning.ttl);\n\t\tif (!(\"error\" in parsedTtl)) resolved.ttlSeconds = parsedTtl.seconds;\n\t}\n\tif (tuning.protected !== undefined) resolved.protected = tuning.protected;\n\tif (tuning.postgres?.computeSettings) {\n\t\tresolved.postgres = {\n\t\t\tcomputeSettings: { ...tuning.postgres.computeSettings },\n\t\t};\n\t}\n\n\tconst preview = resolvePreviewConfig(config.preview, tuning);\n\tif (preview) resolved.preview = preview;\n\n\treturn resolved;\n}\n\n/**\n * Run the `branch` closure (when present) for the target and validate its output. The\n * closure is optional — a fully static policy resolves with empty tuning.\n */\nfunction evaluateBranchTuning(\n\tbranchFn: BranchTuningFn | undefined,\n\ttarget: BranchTarget,\n): BranchTuning {\n\tif (!branchFn) return {};\n\tlet raw: unknown;\n\ttry {\n\t\traw = branchFn(Object.freeze({ ...target }));\n\t} catch (cause) {\n\t\tthrow new ConfigValidationError([\n\t\t\t`Branch policy threw while evaluating branch \"${target.name}\".`,\n\t\t\t(cause as Error)?.message ?? String(cause),\n\t\t]);\n\t}\n\tconst parsed = branchTuningSchema.safeParse(raw ?? {});\n\tif (!parsed.success) {\n\t\tthrow new ConfigValidationError(formatZodIssues(parsed.error));\n\t}\n\treturn parsed.data as BranchTuning;\n}\n\nfunction isServiceEnabled(toggle: ServiceToggleInput | undefined): boolean {\n\tif (toggle === undefined) return false;\n\tif (typeof toggle === \"boolean\") return toggle;\n\treturn toggle.enabled !== false;\n}\n\n/**\n * Normalize the static {@link PreviewInput} (merged with per-branch function tuning) into a\n * {@link ResolvedPreviewConfig}. Returns `undefined` when the policy declares no `preview`\n * block so the field can be omitted entirely. Function slugs / bucket names come from the\n * record keys.\n */\nfunction resolvePreviewConfig(\n\tpreview: PreviewInput | undefined,\n\ttuning: BranchTuning,\n): ResolvedPreviewConfig | undefined {\n\tif (!preview) return undefined;\n\tconst fnTuning = tuning.preview?.functions ?? {};\n\tconst functions: ResolvedFunctionConfig[] = Object.entries(\n\t\tpreview.functions ?? {},\n\t).map(([slug, def]) =>\n\t\tresolveFunctionConfig(slug, def, fnTuning[slug] ?? {}),\n\t);\n\tconst buckets = Object.entries(preview.buckets ?? {}).map(\n\t\t([name, def]) => ({\n\t\t\tname,\n\t\t\taccess: def.access ?? \"private\",\n\t\t}),\n\t);\n\treturn {\n\t\tfunctions,\n\t\tbuckets,\n\t\taiGatewayEnabled: isServiceEnabled(preview.aiGateway),\n\t};\n}\n\nfunction resolveFunctionConfig(\n\tslug: string,\n\tdef: FunctionDef,\n\ttuning: FunctionTuning,\n): ResolvedFunctionConfig {\n\treturn {\n\t\tslug,\n\t\tname: def.name,\n\t\tsource: def.source,\n\t\tenv: { ...(def.env ?? {}) },\n\t\truntime: tuning.runtime ?? DEFAULT_FUNCTION_RUNTIME,\n\t\t// Passed through untouched (no defaults); only `neon dev` reads it.\n\t\t...(def.dev ? { dev: def.dev } : {}),\n\t};\n}\n\n/**\n * Normalize a region identifier to Neon's `<cloud>-<region>` format. When the user writes\n * `us-east-1` we assume `aws-us-east-1`. Pure helper used by both the validator and the\n * NeonApi adapter.\n */\nexport function normalizeRegion(region: string): string {\n\tif (REGION_PREFIX.test(region)) return region;\n\treturn `aws-${region}`;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NeonApi } from "./neon-api.js";
|
|
1
|
+
import { DeployFunctionInput, NeonApi } from "./neon-api.js";
|
|
2
2
|
|
|
3
3
|
//#region src/lib/neon-api-real.d.ts
|
|
4
4
|
interface CreateNeonAuthRestInput {
|
|
@@ -58,6 +58,18 @@ declare function previewUnavailableError(err: unknown, featureLabel: string): un
|
|
|
58
58
|
declare function createNeonAuthRestInput(input: {
|
|
59
59
|
databaseName?: string;
|
|
60
60
|
}): CreateNeonAuthRestInput;
|
|
61
|
+
/**
|
|
62
|
+
* Build the `multipart/form-data` body for a function deployment, matching the public
|
|
63
|
+
* `FunctionDeployRequest` schema (`POST .../functions/{slug}/deployments`):
|
|
64
|
+
*
|
|
65
|
+
* - `zip` — the bundle as a binary part (named `bundle.zip`).
|
|
66
|
+
* - `runtime` — the function runtime.
|
|
67
|
+
* - `environment` — a single JSON-encoded string→string map (multipart can't carry a typed
|
|
68
|
+
* object part), omitted entirely when there are no env vars.
|
|
69
|
+
*
|
|
70
|
+
* Pure (no I/O) so it can be unit-tested against the spec without stubbing `fetch`.
|
|
71
|
+
*/
|
|
72
|
+
declare function buildFunctionDeployForm(input: DeployFunctionInput): FormData;
|
|
61
73
|
/**
|
|
62
74
|
* Read a response body as JSON, tolerating non-JSON. Some Neon routes return a plain-text
|
|
63
75
|
* body (e.g. a 404 `"this route does not exist"` for a Preview feature not enabled in the
|
|
@@ -70,5 +82,5 @@ declare function createNeonAuthRestInput(input: {
|
|
|
70
82
|
*/
|
|
71
83
|
declare function readJsonBody(res: Response): Promise<unknown>;
|
|
72
84
|
//#endregion
|
|
73
|
-
export { createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
|
|
85
|
+
export { buildFunctionDeployForm, createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
|
|
74
86
|
//# sourceMappingURL=neon-api-real.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"neon-api-real.d.ts","names":[],"sources":["../../src/lib/neon-api-real.ts"],"mappings":";;;UAoFU,uBAAA;;EAAA,aAAA,CAAA,EAAA,MAAA;AAeV;AAyCC;AAeD;;;;AAES,iBA1DO,iBAAA,CA0DP,OAAA,EAAA;QACE,EAAA,MAAA;SAAR,CAAA,EAAA,MAAA;EAAO;
|
|
1
|
+
{"version":3,"file":"neon-api-real.d.ts","names":[],"sources":["../../src/lib/neon-api-real.ts"],"mappings":";;;UAoFU,uBAAA;;EAAA,aAAA,CAAA,EAAA,MAAA;AAeV;AAyCC;AAeD;;;;AAES,iBA1DO,iBAAA,CA0DP,OAAA,EAAA;QACE,EAAA,MAAA;SAAR,CAAA,EAAA,MAAA;EAAO;AAwzBV;AAsBA;AAmCA;AAoBA;EAAuC,aAAA,CAAA,EAAA;IAAQ,WAAA,CAAA,EAAA,MAAA;IAAsB,cAAA,CAAA,EAAA,MAAA;IAAQ,UAAA,CAAA,EAAA,MAAA;EAwBvD,CAAA;CAAY,CAAA,EA38B9B,OA28B8B;UA76BxB,WAAA,CA66B8B;aAAW,EAAA,MAAA;EAAO,cAAA,EAAA,MAAA;;;;;;;;;;iBAh6BpC,2BACX,QAAQ,YACV,cACN,QAAQ;;;;;;;;;;;;iBAwzBK,2BAAA;;;;;;iBAsBA,uBAAA;iBAmCA,uBAAA;;IAEZ;;;;;;;;;;;;iBAkBY,uBAAA,QAA+B,sBAAsB;;;;;;;;;;;iBAwB/C,YAAA,MAAkB,WAAW"}
|
|
@@ -284,17 +284,12 @@ var RealNeonApi = class {
|
|
|
284
284
|
return this.request("DELETE", path);
|
|
285
285
|
}
|
|
286
286
|
/**
|
|
287
|
-
* Upload a built function bundle via `multipart/form-data` to the deploy endpoint
|
|
288
|
-
*
|
|
287
|
+
* Upload a built function bundle via `multipart/form-data` to the deploy endpoint
|
|
288
|
+
* (`POST .../functions/{slug}/deployments`). Body shape lives in the pure
|
|
289
|
+
* {@link buildFunctionDeployForm} helper so it can be unit-tested against the spec.
|
|
289
290
|
*/
|
|
290
291
|
async postMultipart(path, input) {
|
|
291
|
-
|
|
292
|
-
form.set("file", new Blob([input.bundle], { type: "application/zip" }), "bundle.zip");
|
|
293
|
-
form.set("memory_mib", String(input.memoryMib));
|
|
294
|
-
form.set("concurrency", "1");
|
|
295
|
-
form.set("runtime", input.runtime);
|
|
296
|
-
for (const [key, value] of Object.entries(input.environment)) form.set(`environment[${key}]`, value);
|
|
297
|
-
return this.request("POST", path, { body: form });
|
|
292
|
+
return this.request("POST", path, { body: buildFunctionDeployForm(input) });
|
|
298
293
|
}
|
|
299
294
|
async request(method, path, init = {}) {
|
|
300
295
|
const url = `${this.restConfig.baseUrl.replace(/\/+$/, "")}${path}`;
|
|
@@ -532,6 +527,24 @@ function createNeonAuthRestInput(input) {
|
|
|
532
527
|
};
|
|
533
528
|
}
|
|
534
529
|
/**
|
|
530
|
+
* Build the `multipart/form-data` body for a function deployment, matching the public
|
|
531
|
+
* `FunctionDeployRequest` schema (`POST .../functions/{slug}/deployments`):
|
|
532
|
+
*
|
|
533
|
+
* - `zip` — the bundle as a binary part (named `bundle.zip`).
|
|
534
|
+
* - `runtime` — the function runtime.
|
|
535
|
+
* - `environment` — a single JSON-encoded string→string map (multipart can't carry a typed
|
|
536
|
+
* object part), omitted entirely when there are no env vars.
|
|
537
|
+
*
|
|
538
|
+
* Pure (no I/O) so it can be unit-tested against the spec without stubbing `fetch`.
|
|
539
|
+
*/
|
|
540
|
+
function buildFunctionDeployForm(input) {
|
|
541
|
+
const form = new FormData();
|
|
542
|
+
form.set("zip", new Blob([input.bundle], { type: "application/zip" }), "bundle.zip");
|
|
543
|
+
form.set("runtime", input.runtime);
|
|
544
|
+
if (Object.keys(input.environment).length > 0) form.set("environment", JSON.stringify(input.environment));
|
|
545
|
+
return form;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
535
548
|
* Read a response body as JSON, tolerating non-JSON. Some Neon routes return a plain-text
|
|
536
549
|
* body (e.g. a 404 `"this route does not exist"` for a Preview feature not enabled in the
|
|
537
550
|
* project/region). Parsing that with `JSON.parse` used to throw a cryptic
|
|
@@ -630,6 +643,6 @@ function defaultsToComputeSettings(defaults) {
|
|
|
630
643
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
631
644
|
}
|
|
632
645
|
//#endregion
|
|
633
|
-
export { createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
|
|
646
|
+
export { buildFunctionDeployForm, createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
|
|
634
647
|
|
|
635
648
|
//# sourceMappingURL=neon-api-real.js.map
|