@neon/config 0.0.0 → 0.9.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/README.md +148 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/lib/auth.d.ts +67 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +107 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/credentials.d.ts +37 -0
- package/dist/lib/credentials.d.ts.map +1 -0
- package/dist/lib/credentials.js +30 -0
- package/dist/lib/credentials.js.map +1 -0
- package/dist/lib/define-config.d.ts +123 -0
- package/dist/lib/define-config.d.ts.map +1 -0
- package/dist/lib/define-config.js +168 -0
- package/dist/lib/define-config.js.map +1 -0
- package/dist/lib/diff.d.ts +120 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +284 -0
- package/dist/lib/diff.js.map +1 -0
- package/dist/lib/duration.d.ts +68 -0
- package/dist/lib/duration.d.ts.map +1 -0
- package/dist/lib/duration.js +111 -0
- package/dist/lib/duration.js.map +1 -0
- package/dist/lib/errors.d.ts +140 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +185 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/loader.d.ts +44 -0
- package/dist/lib/loader.d.ts.map +1 -0
- package/dist/lib/loader.js +120 -0
- package/dist/lib/loader.js.map +1 -0
- package/dist/lib/neon-api-real.d.ts +92 -0
- package/dist/lib/neon-api-real.d.ts.map +1 -0
- package/dist/lib/neon-api-real.js +957 -0
- package/dist/lib/neon-api-real.js.map +1 -0
- package/dist/lib/neon-api.d.ts +373 -0
- package/dist/lib/neon-api.d.ts.map +1 -0
- package/dist/lib/neon-api.js +1 -0
- package/dist/lib/patterns.d.ts +43 -0
- package/dist/lib/patterns.d.ts.map +1 -0
- package/dist/lib/patterns.js +76 -0
- package/dist/lib/patterns.js.map +1 -0
- package/dist/lib/schema.d.ts +215 -0
- package/dist/lib/schema.d.ts.map +1 -0
- package/dist/lib/schema.js +284 -0
- package/dist/lib/schema.js.map +1 -0
- package/dist/lib/types.d.ts +546 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +18 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/wrap-neon-error.d.ts +30 -0
- package/dist/lib/wrap-neon-error.d.ts.map +1 -0
- package/dist/lib/wrap-neon-error.js +139 -0
- package/dist/lib/wrap-neon-error.js.map +1 -0
- package/dist/v1.d.ts +211 -0
- package/dist/v1.d.ts.map +1 -0
- package/dist/v1.js +82 -0
- package/dist/v1.js.map +1 -0
- package/package.json +57 -18
package/dist/lib/diff.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
//#region src/lib/diff.ts
|
|
2
|
+
/**
|
|
3
|
+
* Diff desired branch policy against the selected remote branch. Pure function.
|
|
4
|
+
*/
|
|
5
|
+
function diffConfig(config, remote, options) {
|
|
6
|
+
const conflicts = [];
|
|
7
|
+
const plan = [];
|
|
8
|
+
diffBranchConfig({
|
|
9
|
+
config,
|
|
10
|
+
remote,
|
|
11
|
+
options,
|
|
12
|
+
plan,
|
|
13
|
+
conflicts
|
|
14
|
+
});
|
|
15
|
+
diffServices({
|
|
16
|
+
config,
|
|
17
|
+
remote,
|
|
18
|
+
options,
|
|
19
|
+
plan,
|
|
20
|
+
conflicts
|
|
21
|
+
});
|
|
22
|
+
diffPreview({
|
|
23
|
+
config,
|
|
24
|
+
remote,
|
|
25
|
+
plan
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
plan,
|
|
29
|
+
conflicts
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Plan Preview features (functions, buckets). Like {@link diffServices}, this is
|
|
34
|
+
* **additive**: it creates desired buckets and (re-)deploys functions, but never deletes
|
|
35
|
+
* them. Teardown is destructive, so it stays explicit/manual — matching the existing
|
|
36
|
+
* auth / dataApi behaviour.
|
|
37
|
+
*
|
|
38
|
+
* The AI Gateway is intentionally NOT planned here: it is always available on a branch
|
|
39
|
+
* (credential-gated, not per-branch provisioned), so `preview.aiGateway` produces no plan
|
|
40
|
+
* step — it only drives the branch credential's `ai_gateway:invoke` scope and the gateway
|
|
41
|
+
* env vars (`@neon/env`). There is nothing to create, and nothing to probe.
|
|
42
|
+
*
|
|
43
|
+
* Functions are always (re-)deployed: deployments are versioned and the newest becomes
|
|
44
|
+
* active, so each push ships the current source. There is no separate create step — Neon
|
|
45
|
+
* creates the function on its first deployment — so a single `deploy-function` step is
|
|
46
|
+
* emitted per desired function, carrying `functionExists` so the apply can report it as a
|
|
47
|
+
* create (first deploy) or an update (re-deploy).
|
|
48
|
+
*/
|
|
49
|
+
function diffPreview(args) {
|
|
50
|
+
const { config, remote, plan } = args;
|
|
51
|
+
const preview = config.preview;
|
|
52
|
+
if (!preview) return;
|
|
53
|
+
const state = remote.preview ?? {
|
|
54
|
+
buckets: [],
|
|
55
|
+
functions: []
|
|
56
|
+
};
|
|
57
|
+
for (const bucket of preview.buckets) {
|
|
58
|
+
if (state.buckets.some((b) => b.name === bucket.name)) continue;
|
|
59
|
+
plan.push({
|
|
60
|
+
kind: "create-bucket",
|
|
61
|
+
projectId: remote.projectId,
|
|
62
|
+
branchId: remote.branch.id,
|
|
63
|
+
branchName: remote.branch.name,
|
|
64
|
+
bucketName: bucket.name,
|
|
65
|
+
accessLevel: bucket.access
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
for (const fn of preview.functions) {
|
|
69
|
+
const exists = state.functions.some((f) => f.slug === fn.slug);
|
|
70
|
+
plan.push({
|
|
71
|
+
kind: "deploy-function",
|
|
72
|
+
projectId: remote.projectId,
|
|
73
|
+
branchId: remote.branch.id,
|
|
74
|
+
branchName: remote.branch.name,
|
|
75
|
+
fn,
|
|
76
|
+
functionExists: exists
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Plan branch-scoped integrations. Enabling is additive (no existing resource to override).
|
|
82
|
+
* The Data API is the one integration that also has a reconcilable *update*: its runtime
|
|
83
|
+
* `settings` can drift once enabled, and reconciling them is an override (gated on
|
|
84
|
+
* `updateExisting`, like compute/TTL/protected). The auth provider / JWKS wiring is fixed at
|
|
85
|
+
* enable time, so it is never updated here. Disabling stays explicit/manual (destructive).
|
|
86
|
+
*/
|
|
87
|
+
function diffServices(args) {
|
|
88
|
+
const { config, remote, options, plan, conflicts } = args;
|
|
89
|
+
const state = remote.services;
|
|
90
|
+
if (config.authEnabled && !state.authEnabled) {
|
|
91
|
+
const step = {
|
|
92
|
+
kind: "enable-auth",
|
|
93
|
+
projectId: remote.projectId,
|
|
94
|
+
branchId: remote.branch.id,
|
|
95
|
+
branchName: remote.branch.name
|
|
96
|
+
};
|
|
97
|
+
if (state.databaseName) step.databaseName = state.databaseName;
|
|
98
|
+
plan.push(step);
|
|
99
|
+
}
|
|
100
|
+
if (config.dataApiEnabled) diffDataApi({
|
|
101
|
+
config,
|
|
102
|
+
remote,
|
|
103
|
+
options,
|
|
104
|
+
plan,
|
|
105
|
+
conflicts
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Plan the Data API: a first-time **enable** (carrying the create-time auth wiring +
|
|
110
|
+
* settings), or — when it already exists — a **settings update** if the policy's settings
|
|
111
|
+
* drift from the remote. The update is an override: applied as a plan step under
|
|
112
|
+
* `updateExisting`, otherwise reported as a conflict.
|
|
113
|
+
*/
|
|
114
|
+
function diffDataApi(args) {
|
|
115
|
+
const { config, remote, options, plan, conflicts } = args;
|
|
116
|
+
const state = remote.services;
|
|
117
|
+
const desired = config.dataApi;
|
|
118
|
+
if (!state.dataApiEnabled) {
|
|
119
|
+
const step = {
|
|
120
|
+
kind: "enable-data-api",
|
|
121
|
+
projectId: remote.projectId,
|
|
122
|
+
branchId: remote.branch.id,
|
|
123
|
+
branchName: remote.branch.name,
|
|
124
|
+
databaseName: state.databaseName
|
|
125
|
+
};
|
|
126
|
+
const input = desired ? enableInputFromResolved(desired) : void 0;
|
|
127
|
+
if (input) step.input = input;
|
|
128
|
+
plan.push(step);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const desiredSettings = desired?.settings;
|
|
132
|
+
if (!desiredSettings) return;
|
|
133
|
+
if (!dataApiSettingsDiffer(desiredSettings, state.dataApiSettings)) return;
|
|
134
|
+
if (options.updateExisting) plan.push({
|
|
135
|
+
kind: "update-data-api",
|
|
136
|
+
projectId: remote.projectId,
|
|
137
|
+
branchId: remote.branch.id,
|
|
138
|
+
branchName: remote.branch.name,
|
|
139
|
+
databaseName: state.databaseName,
|
|
140
|
+
settings: desiredSettings
|
|
141
|
+
});
|
|
142
|
+
else conflicts.push({
|
|
143
|
+
kind: "branch",
|
|
144
|
+
identifier: remote.branch.name,
|
|
145
|
+
field: "dataApi.settings",
|
|
146
|
+
current: state.dataApiSettings ?? void 0,
|
|
147
|
+
desired: desiredSettings,
|
|
148
|
+
reason: "Existing Data API has different settings. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply."
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Build the create-time {@link EnableDataApiInput} from a resolved Data API config. */
|
|
152
|
+
function enableInputFromResolved(resolved) {
|
|
153
|
+
const input = { authProvider: resolved.authProvider };
|
|
154
|
+
if (resolved.jwksUrl !== void 0) input.jwksUrl = resolved.jwksUrl;
|
|
155
|
+
if (resolved.providerName !== void 0) input.providerName = resolved.providerName;
|
|
156
|
+
if (resolved.jwtAudience !== void 0) input.jwtAudience = resolved.jwtAudience;
|
|
157
|
+
if (resolved.settings) input.settings = resolved.settings;
|
|
158
|
+
return input;
|
|
159
|
+
}
|
|
160
|
+
/** The camelCase keys of {@link DataApiSettings}, used to compare desired vs remote settings. */
|
|
161
|
+
const DATA_API_SETTING_KEYS = [
|
|
162
|
+
"dbAggregatesEnabled",
|
|
163
|
+
"dbAnonRole",
|
|
164
|
+
"dbExtraSearchPath",
|
|
165
|
+
"dbMaxRows",
|
|
166
|
+
"dbSchemas",
|
|
167
|
+
"jwtRoleClaimKey",
|
|
168
|
+
"jwtCacheMaxLifetime",
|
|
169
|
+
"openapiMode",
|
|
170
|
+
"serverCorsAllowedOrigins",
|
|
171
|
+
"serverTimingEnabled"
|
|
172
|
+
];
|
|
173
|
+
/**
|
|
174
|
+
* Whether the policy's Data API `settings` differ from the remote ones. Only the keys the
|
|
175
|
+
* policy actually set are compared (so unset fields never count as drift). When the remote
|
|
176
|
+
* settings are not reported (`null`/absent — non-SubZero), drift can't be computed and this
|
|
177
|
+
* returns `false` so no spurious update is planned.
|
|
178
|
+
*/
|
|
179
|
+
function dataApiSettingsDiffer(desired, current) {
|
|
180
|
+
if (!current) return false;
|
|
181
|
+
for (const key of DATA_API_SETTING_KEYS) {
|
|
182
|
+
if (desired[key] === void 0) continue;
|
|
183
|
+
if (JSON.stringify(desired[key]) !== JSON.stringify(current[key])) return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
function diffBranchConfig(args) {
|
|
188
|
+
const { config, remote, options, plan, conflicts } = args;
|
|
189
|
+
const branchName = remote.branch.name;
|
|
190
|
+
const computeSettings = config.postgres?.computeSettings;
|
|
191
|
+
if (computeSettings) {
|
|
192
|
+
const endpoint = remote.endpoint;
|
|
193
|
+
if (!endpoint) conflicts.push({
|
|
194
|
+
kind: "branch",
|
|
195
|
+
identifier: branchName,
|
|
196
|
+
field: "endpoint",
|
|
197
|
+
current: void 0,
|
|
198
|
+
desired: computeSettings,
|
|
199
|
+
reason: "Branch has no read-write endpoint; cannot apply compute settings."
|
|
200
|
+
});
|
|
201
|
+
else {
|
|
202
|
+
const drift = computeDriftBetween(computeSettings, endpoint);
|
|
203
|
+
if (drift) if (options.updateExisting) plan.push({
|
|
204
|
+
kind: "update-endpoint",
|
|
205
|
+
projectId: remote.projectId,
|
|
206
|
+
branchName,
|
|
207
|
+
endpointId: endpoint.id,
|
|
208
|
+
settings: computeSettings
|
|
209
|
+
});
|
|
210
|
+
else conflicts.push({
|
|
211
|
+
kind: "branch",
|
|
212
|
+
identifier: branchName,
|
|
213
|
+
field: "computeSettings",
|
|
214
|
+
current: drift.current,
|
|
215
|
+
desired: drift.desired,
|
|
216
|
+
reason: "Existing branch has different compute settings. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply."
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (config.protected !== void 0 && config.protected !== remote.branch.protected) if (options.updateExisting) plan.push({
|
|
221
|
+
kind: "update-branch-protected",
|
|
222
|
+
projectId: remote.projectId,
|
|
223
|
+
branchId: remote.branch.id,
|
|
224
|
+
branchName,
|
|
225
|
+
protected: config.protected
|
|
226
|
+
});
|
|
227
|
+
else conflicts.push({
|
|
228
|
+
kind: "branch",
|
|
229
|
+
identifier: branchName,
|
|
230
|
+
field: "protected",
|
|
231
|
+
current: remote.branch.protected,
|
|
232
|
+
desired: config.protected,
|
|
233
|
+
reason: "Existing branch has a different `protected` flag. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply."
|
|
234
|
+
});
|
|
235
|
+
if (config.ttlSeconds !== void 0) {
|
|
236
|
+
const current = remote.branch.expiresAt ? Math.max(0, Math.round((Date.parse(remote.branch.expiresAt) - Date.now()) / 1e3)) : void 0;
|
|
237
|
+
if (current === void 0 || Math.abs(current - config.ttlSeconds) > 30) {
|
|
238
|
+
const expiresAt = new Date(Date.now() + config.ttlSeconds * 1e3).toISOString();
|
|
239
|
+
if (options.updateExisting) plan.push({
|
|
240
|
+
kind: "update-branch-ttl",
|
|
241
|
+
projectId: remote.projectId,
|
|
242
|
+
branchId: remote.branch.id,
|
|
243
|
+
branchName,
|
|
244
|
+
expiresAt
|
|
245
|
+
});
|
|
246
|
+
else conflicts.push({
|
|
247
|
+
kind: "branch",
|
|
248
|
+
identifier: branchName,
|
|
249
|
+
field: "ttl",
|
|
250
|
+
current: remote.branch.expiresAt,
|
|
251
|
+
desired: expiresAt,
|
|
252
|
+
reason: "Existing branch has a different TTL. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply."
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function computeDriftBetween(desired, endpoint) {
|
|
258
|
+
const currentDrift = {};
|
|
259
|
+
const desiredDrift = {};
|
|
260
|
+
let drift = false;
|
|
261
|
+
if (desired.autoscalingLimitMinCu !== void 0 && desired.autoscalingLimitMinCu !== endpoint.autoscalingLimitMinCu) {
|
|
262
|
+
currentDrift.autoscalingLimitMinCu = endpoint.autoscalingLimitMinCu;
|
|
263
|
+
desiredDrift.autoscalingLimitMinCu = desired.autoscalingLimitMinCu;
|
|
264
|
+
drift = true;
|
|
265
|
+
}
|
|
266
|
+
if (desired.autoscalingLimitMaxCu !== void 0 && desired.autoscalingLimitMaxCu !== endpoint.autoscalingLimitMaxCu) {
|
|
267
|
+
currentDrift.autoscalingLimitMaxCu = endpoint.autoscalingLimitMaxCu;
|
|
268
|
+
desiredDrift.autoscalingLimitMaxCu = desired.autoscalingLimitMaxCu;
|
|
269
|
+
drift = true;
|
|
270
|
+
}
|
|
271
|
+
if (desired.suspendTimeout !== void 0 && desired.suspendTimeout !== endpoint.suspendTimeout) {
|
|
272
|
+
currentDrift.suspendTimeout = endpoint.suspendTimeout;
|
|
273
|
+
desiredDrift.suspendTimeout = desired.suspendTimeout;
|
|
274
|
+
drift = true;
|
|
275
|
+
}
|
|
276
|
+
return drift ? {
|
|
277
|
+
current: currentDrift,
|
|
278
|
+
desired: desiredDrift
|
|
279
|
+
} : null;
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
export { diffConfig };
|
|
283
|
+
|
|
284
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","names":[],"sources":["../../src/lib/diff.ts"],"sourcesContent":["import type {\n\tEnableDataApiInput,\n\tNeonBranchSnapshot,\n\tNeonBucketSnapshot,\n\tNeonEndpointSnapshot,\n\tNeonFunctionSnapshot,\n} from \"./neon-api.js\";\nimport type {\n\tBucketAccessLevel,\n\tComputeSettings,\n\tConflictReport,\n\tDataApiSettings,\n\tResolvedBranchConfig,\n\tResolvedDataApiConfig,\n\tResolvedFunctionConfig,\n} from \"./types.js\";\n\n/**\n * A planned action to perform a single mutation against the Neon API. The diff engine\n * produces a list of these for `pushConfig` to execute (or report).\n */\nexport type PlanStep =\n\t| {\n\t\t\tkind: \"update-branch-ttl\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\texpiresAt: string | null;\n\t }\n\t| {\n\t\t\tkind: \"update-branch-protected\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tprotected: boolean;\n\t }\n\t| {\n\t\t\tkind: \"update-endpoint\";\n\t\t\tprojectId: string;\n\t\t\tbranchName: string;\n\t\t\tendpointId: string;\n\t\t\tsettings: ComputeSettings;\n\t }\n\t| {\n\t\t\tkind: \"enable-auth\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tdatabaseName?: string;\n\t }\n\t| {\n\t\t\tkind: \"enable-data-api\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tdatabaseName: string;\n\t\t\t/** Create-time auth wiring + initial settings from the policy. */\n\t\t\tinput?: EnableDataApiInput;\n\t }\n\t| {\n\t\t\t/**\n\t\t\t * Reconcile the runtime settings of an already-enabled Data API integration.\n\t\t\t * Only `settings` are mutable post-create, so this is the lone Data API\n\t\t\t * *update* step — and it is an override (requires `updateExisting`).\n\t\t\t */\n\t\t\tkind: \"update-data-api\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tdatabaseName: string;\n\t\t\tsettings: DataApiSettings;\n\t }\n\t| {\n\t\t\tkind: \"create-bucket\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tbucketName: string;\n\t\t\taccessLevel: BucketAccessLevel;\n\t }\n\t| {\n\t\t\t/**\n\t\t\t * Deploy code to a function. Planned for every desired function — deployments are\n\t\t\t * versioned and the newest becomes active, so a push ships the current source each\n\t\t\t * time. Neon has no separate \"create function\" endpoint: the first deployment to a\n\t\t\t * slug creates the function. `functionExists` therefore only drives whether this\n\t\t\t * surfaces as a `create` (first deploy) or an `update` (re-deploy).\n\t\t\t */\n\t\t\tkind: \"deploy-function\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tfn: ResolvedFunctionConfig;\n\t\t\t/** Whether the function already existed remotely when the plan was computed. */\n\t\t\tfunctionExists: boolean;\n\t };\n\nexport interface RemoteServiceState {\n\tdatabaseName: string;\n\tauthEnabled: boolean;\n\tdataApiEnabled: boolean;\n\t/**\n\t * Current Data API runtime settings, when the integration is enabled and the API reports\n\t * them (SubZero only). `null`/absent means \"not reported\" — settings drift can't be\n\t * computed, so no update step is planned.\n\t */\n\tdataApiSettings?: DataApiSettings | null;\n}\n\n/**\n * Snapshot of the branch's current Preview-feature state. Absent (`undefined`) when the\n * policy has no `preview` block — `pushConfig` only fetches this when needed.\n */\nexport interface RemotePreviewState {\n\tbuckets: NeonBucketSnapshot[];\n\tfunctions: NeonFunctionSnapshot[];\n}\n\nexport interface RemoteState {\n\tprojectId: string;\n\tbranch: NeonBranchSnapshot;\n\tendpoint?: NeonEndpointSnapshot;\n\tservices: RemoteServiceState;\n\tpreview?: RemotePreviewState;\n}\n\nexport interface DiffOptions {\n\t/**\n\t * Apply mutable drift on the selected branch as plan steps instead of conflicts.\n\t * Default: `false`.\n\t */\n\tupdateExisting: boolean;\n}\n\nexport interface DiffResult {\n\tplan: PlanStep[];\n\tconflicts: ConflictReport[];\n}\n\n/**\n * Diff desired branch policy against the selected remote branch. Pure function.\n */\nexport function diffConfig(\n\tconfig: ResolvedBranchConfig,\n\tremote: RemoteState,\n\toptions: DiffOptions,\n): DiffResult {\n\tconst conflicts: ConflictReport[] = [];\n\tconst plan: PlanStep[] = [];\n\tdiffBranchConfig({ config, remote, options, plan, conflicts });\n\tdiffServices({ config, remote, options, plan, conflicts });\n\tdiffPreview({ config, remote, plan });\n\treturn { plan, conflicts };\n}\n\n/**\n * Plan Preview features (functions, buckets). Like {@link diffServices}, this is\n * **additive**: it creates desired buckets and (re-)deploys functions, but never deletes\n * them. Teardown is destructive, so it stays explicit/manual — matching the existing\n * auth / dataApi behaviour.\n *\n * The AI Gateway is intentionally NOT planned here: it is always available on a branch\n * (credential-gated, not per-branch provisioned), so `preview.aiGateway` produces no plan\n * step — it only drives the branch credential's `ai_gateway:invoke` scope and the gateway\n * env vars (`@neon/env`). There is nothing to create, and nothing to probe.\n *\n * Functions are always (re-)deployed: deployments are versioned and the newest becomes\n * active, so each push ships the current source. There is no separate create step — Neon\n * creates the function on its first deployment — so a single `deploy-function` step is\n * emitted per desired function, carrying `functionExists` so the apply can report it as a\n * create (first deploy) or an update (re-deploy).\n */\nfunction diffPreview(args: {\n\tconfig: ResolvedBranchConfig;\n\tremote: RemoteState;\n\tplan: PlanStep[];\n}): void {\n\tconst { config, remote, plan } = args;\n\tconst preview = config.preview;\n\tif (!preview) return;\n\t// `remote.preview` is only fetched when the policy has a preview block; treat a missing\n\t// snapshot as \"nothing exists yet\" so the diff is still well-defined.\n\tconst state: RemotePreviewState = remote.preview ?? {\n\t\tbuckets: [],\n\t\tfunctions: [],\n\t};\n\n\tfor (const bucket of preview.buckets) {\n\t\tif (state.buckets.some((b) => b.name === bucket.name)) continue;\n\t\tplan.push({\n\t\t\tkind: \"create-bucket\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t\tbucketName: bucket.name,\n\t\t\taccessLevel: bucket.access,\n\t\t});\n\t}\n\n\tfor (const fn of preview.functions) {\n\t\tconst exists = state.functions.some((f) => f.slug === fn.slug);\n\t\t// Neon creates the function on its first deployment (there is no separate create\n\t\t// endpoint), so always emit a single deploy step and let `functionExists` decide\n\t\t// whether it is reported as a create or an update.\n\t\tplan.push({\n\t\t\tkind: \"deploy-function\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t\tfn,\n\t\t\tfunctionExists: exists,\n\t\t});\n\t}\n}\n\n/**\n * Plan branch-scoped integrations. Enabling is additive (no existing resource to override).\n * The Data API is the one integration that also has a reconcilable *update*: its runtime\n * `settings` can drift once enabled, and reconciling them is an override (gated on\n * `updateExisting`, like compute/TTL/protected). The auth provider / JWKS wiring is fixed at\n * enable time, so it is never updated here. Disabling stays explicit/manual (destructive).\n */\nfunction diffServices(args: {\n\tconfig: ResolvedBranchConfig;\n\tremote: RemoteState;\n\toptions: DiffOptions;\n\tplan: PlanStep[];\n\tconflicts: ConflictReport[];\n}): void {\n\tconst { config, remote, options, plan, conflicts } = args;\n\tconst state = remote.services;\n\tif (config.authEnabled && !state.authEnabled) {\n\t\tconst step: PlanStep = {\n\t\t\tkind: \"enable-auth\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t};\n\t\tif (state.databaseName) step.databaseName = state.databaseName;\n\t\tplan.push(step);\n\t}\n\tif (config.dataApiEnabled) {\n\t\tdiffDataApi({ config, remote, options, plan, conflicts });\n\t}\n}\n\n/**\n * Plan the Data API: a first-time **enable** (carrying the create-time auth wiring +\n * settings), or — when it already exists — a **settings update** if the policy's settings\n * drift from the remote. The update is an override: applied as a plan step under\n * `updateExisting`, otherwise reported as a conflict.\n */\nfunction diffDataApi(args: {\n\tconfig: ResolvedBranchConfig;\n\tremote: RemoteState;\n\toptions: DiffOptions;\n\tplan: PlanStep[];\n\tconflicts: ConflictReport[];\n}): void {\n\tconst { config, remote, options, plan, conflicts } = args;\n\tconst state = remote.services;\n\tconst desired = config.dataApi;\n\n\tif (!state.dataApiEnabled) {\n\t\tconst step: PlanStep = {\n\t\t\tkind: \"enable-data-api\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t\tdatabaseName: state.databaseName,\n\t\t};\n\t\tconst input = desired ? enableInputFromResolved(desired) : undefined;\n\t\tif (input) step.input = input;\n\t\tplan.push(step);\n\t\treturn;\n\t}\n\n\t// Already enabled: the only reconcilable change is its runtime settings.\n\tconst desiredSettings = desired?.settings;\n\tif (!desiredSettings) return;\n\tif (!dataApiSettingsDiffer(desiredSettings, state.dataApiSettings)) return;\n\n\tif (options.updateExisting) {\n\t\tplan.push({\n\t\t\tkind: \"update-data-api\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t\tdatabaseName: state.databaseName,\n\t\t\tsettings: desiredSettings,\n\t\t});\n\t} else {\n\t\tconflicts.push({\n\t\t\tkind: \"branch\",\n\t\t\tidentifier: remote.branch.name,\n\t\t\tfield: \"dataApi.settings\",\n\t\t\tcurrent: state.dataApiSettings ?? undefined,\n\t\t\tdesired: desiredSettings,\n\t\t\treason: \"Existing Data API has different settings. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.\",\n\t\t});\n\t}\n}\n\n/** Build the create-time {@link EnableDataApiInput} from a resolved Data API config. */\nfunction enableInputFromResolved(\n\tresolved: ResolvedDataApiConfig,\n): EnableDataApiInput {\n\tconst input: EnableDataApiInput = { authProvider: resolved.authProvider };\n\tif (resolved.jwksUrl !== undefined) input.jwksUrl = resolved.jwksUrl;\n\tif (resolved.providerName !== undefined)\n\t\tinput.providerName = resolved.providerName;\n\tif (resolved.jwtAudience !== undefined)\n\t\tinput.jwtAudience = resolved.jwtAudience;\n\tif (resolved.settings) input.settings = resolved.settings;\n\treturn input;\n}\n\n/** The camelCase keys of {@link DataApiSettings}, used to compare desired vs remote settings. */\nconst DATA_API_SETTING_KEYS = [\n\t\"dbAggregatesEnabled\",\n\t\"dbAnonRole\",\n\t\"dbExtraSearchPath\",\n\t\"dbMaxRows\",\n\t\"dbSchemas\",\n\t\"jwtRoleClaimKey\",\n\t\"jwtCacheMaxLifetime\",\n\t\"openapiMode\",\n\t\"serverCorsAllowedOrigins\",\n\t\"serverTimingEnabled\",\n] as const satisfies ReadonlyArray<keyof DataApiSettings>;\n\n/**\n * Whether the policy's Data API `settings` differ from the remote ones. Only the keys the\n * policy actually set are compared (so unset fields never count as drift). When the remote\n * settings are not reported (`null`/absent — non-SubZero), drift can't be computed and this\n * returns `false` so no spurious update is planned.\n */\nfunction dataApiSettingsDiffer(\n\tdesired: DataApiSettings,\n\tcurrent: DataApiSettings | null | undefined,\n): boolean {\n\tif (!current) return false;\n\tfor (const key of DATA_API_SETTING_KEYS) {\n\t\tif (desired[key] === undefined) continue;\n\t\tif (JSON.stringify(desired[key]) !== JSON.stringify(current[key])) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\ninterface BranchConfigArgs {\n\tconfig: ResolvedBranchConfig;\n\tremote: RemoteState;\n\toptions: DiffOptions;\n\tplan: PlanStep[];\n\tconflicts: ConflictReport[];\n}\n\nfunction diffBranchConfig(args: BranchConfigArgs): void {\n\tconst { config, remote, options, plan, conflicts } = args;\n\tconst branchName = remote.branch.name;\n\tconst computeSettings = config.postgres?.computeSettings;\n\n\tif (computeSettings) {\n\t\tconst endpoint = remote.endpoint;\n\t\tif (!endpoint) {\n\t\t\tconflicts.push({\n\t\t\t\tkind: \"branch\",\n\t\t\t\tidentifier: branchName,\n\t\t\t\tfield: \"endpoint\",\n\t\t\t\tcurrent: undefined,\n\t\t\t\tdesired: computeSettings,\n\t\t\t\treason: \"Branch has no read-write endpoint; cannot apply compute settings.\",\n\t\t\t});\n\t\t} else {\n\t\t\tconst drift = computeDriftBetween(computeSettings, endpoint);\n\t\t\tif (drift) {\n\t\t\t\tif (options.updateExisting) {\n\t\t\t\t\tplan.push({\n\t\t\t\t\t\tkind: \"update-endpoint\",\n\t\t\t\t\t\tprojectId: remote.projectId,\n\t\t\t\t\t\tbranchName,\n\t\t\t\t\t\tendpointId: endpoint.id,\n\t\t\t\t\t\tsettings: computeSettings,\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tconflicts.push({\n\t\t\t\t\t\tkind: \"branch\",\n\t\t\t\t\t\tidentifier: branchName,\n\t\t\t\t\t\tfield: \"computeSettings\",\n\t\t\t\t\t\tcurrent: drift.current,\n\t\t\t\t\t\tdesired: drift.desired,\n\t\t\t\t\t\treason: \"Existing branch has different compute settings. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.\",\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (\n\t\tconfig.protected !== undefined &&\n\t\tconfig.protected !== remote.branch.protected\n\t) {\n\t\tif (options.updateExisting) {\n\t\t\tplan.push({\n\t\t\t\tkind: \"update-branch-protected\",\n\t\t\t\tprojectId: remote.projectId,\n\t\t\t\tbranchId: remote.branch.id,\n\t\t\t\tbranchName,\n\t\t\t\tprotected: config.protected,\n\t\t\t});\n\t\t} else {\n\t\t\tconflicts.push({\n\t\t\t\tkind: \"branch\",\n\t\t\t\tidentifier: branchName,\n\t\t\t\tfield: \"protected\",\n\t\t\t\tcurrent: remote.branch.protected,\n\t\t\t\tdesired: config.protected,\n\t\t\t\treason: \"Existing branch has a different `protected` flag. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.\",\n\t\t\t});\n\t\t}\n\t}\n\n\tif (config.ttlSeconds !== undefined) {\n\t\tconst current = remote.branch.expiresAt\n\t\t\t? Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tMath.round(\n\t\t\t\t\t\t(Date.parse(remote.branch.expiresAt) - Date.now()) /\n\t\t\t\t\t\t\t1000,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t: undefined;\n\t\tif (\n\t\t\tcurrent === undefined ||\n\t\t\tMath.abs(current - config.ttlSeconds) > 30\n\t\t) {\n\t\t\tconst expiresAt = new Date(\n\t\t\t\tDate.now() + config.ttlSeconds * 1000,\n\t\t\t).toISOString();\n\t\t\tif (options.updateExisting) {\n\t\t\t\tplan.push({\n\t\t\t\t\tkind: \"update-branch-ttl\",\n\t\t\t\t\tprojectId: remote.projectId,\n\t\t\t\t\tbranchId: remote.branch.id,\n\t\t\t\t\tbranchName,\n\t\t\t\t\texpiresAt,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconflicts.push({\n\t\t\t\t\tkind: \"branch\",\n\t\t\t\t\tidentifier: branchName,\n\t\t\t\t\tfield: \"ttl\",\n\t\t\t\t\tcurrent: remote.branch.expiresAt,\n\t\t\t\t\tdesired: expiresAt,\n\t\t\t\t\treason: \"Existing branch has a different TTL. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.\",\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction computeDriftBetween(\n\tdesired: ComputeSettings,\n\tendpoint: NeonEndpointSnapshot,\n): {\n\tcurrent: Partial<ComputeSettings>;\n\tdesired: Partial<ComputeSettings>;\n} | null {\n\tconst currentDrift: Partial<ComputeSettings> = {};\n\tconst desiredDrift: Partial<ComputeSettings> = {};\n\tlet drift = false;\n\n\tif (\n\t\tdesired.autoscalingLimitMinCu !== undefined &&\n\t\tdesired.autoscalingLimitMinCu !== endpoint.autoscalingLimitMinCu\n\t) {\n\t\tcurrentDrift.autoscalingLimitMinCu = endpoint.autoscalingLimitMinCu;\n\t\tdesiredDrift.autoscalingLimitMinCu = desired.autoscalingLimitMinCu;\n\t\tdrift = true;\n\t}\n\tif (\n\t\tdesired.autoscalingLimitMaxCu !== undefined &&\n\t\tdesired.autoscalingLimitMaxCu !== endpoint.autoscalingLimitMaxCu\n\t) {\n\t\tcurrentDrift.autoscalingLimitMaxCu = endpoint.autoscalingLimitMaxCu;\n\t\tdesiredDrift.autoscalingLimitMaxCu = desired.autoscalingLimitMaxCu;\n\t\tdrift = true;\n\t}\n\tif (\n\t\tdesired.suspendTimeout !== undefined &&\n\t\tdesired.suspendTimeout !== endpoint.suspendTimeout\n\t) {\n\t\tcurrentDrift.suspendTimeout = endpoint.suspendTimeout;\n\t\tdesiredDrift.suspendTimeout = desired.suspendTimeout;\n\t\tdrift = true;\n\t}\n\treturn drift ? { current: currentDrift, desired: desiredDrift } : null;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DurationString } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/duration.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse a duration value into whole seconds.
|
|
7
|
+
*
|
|
8
|
+
* Accepted formats:
|
|
9
|
+
* - a positive finite **number** → interpreted as seconds (must be an integer)
|
|
10
|
+
* - a **string** of the form `<integer><unit>` where unit is one of `s`, `m`, `h`, `d`, `w`
|
|
11
|
+
* (e.g. `30s`, `5m`, `1h`, `7d`, `2w`)
|
|
12
|
+
*
|
|
13
|
+
* A **unit is required** on strings: a bare numeric string like `"7"` is rejected — pass a
|
|
14
|
+
* `number` (`7`) for raw seconds instead. This removes the ambiguity where `"7"` silently
|
|
15
|
+
* meant 7 seconds rather than, say, `"7d"`.
|
|
16
|
+
*
|
|
17
|
+
* Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.
|
|
18
|
+
*/
|
|
19
|
+
declare function parseDuration(input: string | number): {
|
|
20
|
+
seconds: number;
|
|
21
|
+
} | {
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
/** Neon's branch-expiration ceiling: the API rejects an `expires_at` more than 30 days out. */
|
|
25
|
+
declare const MAX_BRANCH_TTL_SECONDS: number;
|
|
26
|
+
/**
|
|
27
|
+
* Parse a branch TTL into seconds, enforcing Neon's branch-expiration limit on top of the
|
|
28
|
+
* shared {@link parseDuration} rules: the result must be `> 0` and at most 30 days
|
|
29
|
+
* ({@link MAX_BRANCH_TTL_SECONDS}), since the API caps `expires_at` at 30 days from now.
|
|
30
|
+
*
|
|
31
|
+
* Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.
|
|
32
|
+
*/
|
|
33
|
+
declare function parseBranchTtl(input: string | number): {
|
|
34
|
+
seconds: number;
|
|
35
|
+
} | {
|
|
36
|
+
error: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Render a TTL in seconds back to the canonical "<n><unit>" form. Used for round-trip
|
|
40
|
+
* serialization when {@link pullConfig} emits a TTL value (it always falls back to seconds
|
|
41
|
+
* when no clean unit boundary matches). The output always carries a unit, so it is a valid
|
|
42
|
+
* {@link DurationString}.
|
|
43
|
+
*/
|
|
44
|
+
declare function formatDurationSeconds(totalSeconds: number): DurationString;
|
|
45
|
+
/**
|
|
46
|
+
* Parse a suspend timeout value into seconds for the Neon API.
|
|
47
|
+
*
|
|
48
|
+
* Accepted formats:
|
|
49
|
+
* - `false` → -1 (never suspend)
|
|
50
|
+
* - `undefined` → 0 (use platform default)
|
|
51
|
+
* - duration string → parsed seconds ("5m", "1h", "7d")
|
|
52
|
+
* - number → validated seconds (must be 60-604800 or -1/0)
|
|
53
|
+
*
|
|
54
|
+
* Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.
|
|
55
|
+
*/
|
|
56
|
+
declare function parseSuspendTimeout(input: false | string | number | undefined): {
|
|
57
|
+
seconds: number;
|
|
58
|
+
} | {
|
|
59
|
+
error: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Format a suspend timeout value from API seconds back to the user-facing type.
|
|
63
|
+
* Returns `false` for -1 (never suspend), `undefined` for 0 (default), or a duration string.
|
|
64
|
+
*/
|
|
65
|
+
declare function formatSuspendTimeout(seconds: number): false | DurationString | undefined;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { MAX_BRANCH_TTL_SECONDS, formatDurationSeconds, formatSuspendTimeout, parseBranchTtl, parseDuration, parseSuspendTimeout };
|
|
68
|
+
//# sourceMappingURL=duration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.d.ts","names":[],"sources":["../../src/lib/duration.ts"],"mappings":";;;;;;AAgBA;AAkDA;AASA;AAmBA;AA+BA;AA+CA;;;;;;;iBA5JgB,aAAA;;;;;;cAkDH;;;;;;;;iBASG,cAAA;;;;;;;;;;;iBAmBA,qBAAA,wBAA6C;;;;;;;;;;;;iBA+B7C,mBAAA;;;;;;;;;iBA+CA,oBAAA,2BAEL"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
//#region src/lib/duration.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parse a duration value into whole seconds.
|
|
4
|
+
*
|
|
5
|
+
* Accepted formats:
|
|
6
|
+
* - a positive finite **number** → interpreted as seconds (must be an integer)
|
|
7
|
+
* - a **string** of the form `<integer><unit>` where unit is one of `s`, `m`, `h`, `d`, `w`
|
|
8
|
+
* (e.g. `30s`, `5m`, `1h`, `7d`, `2w`)
|
|
9
|
+
*
|
|
10
|
+
* A **unit is required** on strings: a bare numeric string like `"7"` is rejected — pass a
|
|
11
|
+
* `number` (`7`) for raw seconds instead. This removes the ambiguity where `"7"` silently
|
|
12
|
+
* meant 7 seconds rather than, say, `"7d"`.
|
|
13
|
+
*
|
|
14
|
+
* Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.
|
|
15
|
+
*/
|
|
16
|
+
function parseDuration(input) {
|
|
17
|
+
if (typeof input === "number") {
|
|
18
|
+
if (!Number.isFinite(input)) return { error: `not a finite number: ${input}` };
|
|
19
|
+
if (!Number.isInteger(input)) return { error: `must be an integer when passed as number: ${input}` };
|
|
20
|
+
if (input <= 0) return { error: `must be > 0, got ${input}` };
|
|
21
|
+
return { seconds: input };
|
|
22
|
+
}
|
|
23
|
+
const trimmed = input.trim();
|
|
24
|
+
if (trimmed === "") return { error: "duration string is empty" };
|
|
25
|
+
if (/^\d+$/.test(trimmed)) return { error: `duration string "${input}" is missing a unit; add one of s, m, h, d, w (e.g. "${trimmed}d") or pass ${trimmed} as a number for seconds` };
|
|
26
|
+
const unitMatch = /^(\d+)([smhdw])$/i.exec(trimmed);
|
|
27
|
+
if (!unitMatch) return { error: `invalid duration "${input}"; expected an integer followed by one of: s, m, h, d, w (e.g. "30s", "1h", "7d")` };
|
|
28
|
+
const value = Number(unitMatch[1]);
|
|
29
|
+
const unit = unitMatch[2].toLowerCase();
|
|
30
|
+
if (value <= 0) return { error: `must be > 0, got "${trimmed}"` };
|
|
31
|
+
return { seconds: value * UNIT_SECONDS[unit] };
|
|
32
|
+
}
|
|
33
|
+
const UNIT_SECONDS = {
|
|
34
|
+
s: 1,
|
|
35
|
+
m: 60,
|
|
36
|
+
h: 3600,
|
|
37
|
+
d: 1440 * 60,
|
|
38
|
+
w: 10080 * 60
|
|
39
|
+
};
|
|
40
|
+
/** Neon's branch-expiration ceiling: the API rejects an `expires_at` more than 30 days out. */
|
|
41
|
+
const MAX_BRANCH_TTL_SECONDS = 30 * UNIT_SECONDS.d;
|
|
42
|
+
/**
|
|
43
|
+
* Parse a branch TTL into seconds, enforcing Neon's branch-expiration limit on top of the
|
|
44
|
+
* shared {@link parseDuration} rules: the result must be `> 0` and at most 30 days
|
|
45
|
+
* ({@link MAX_BRANCH_TTL_SECONDS}), since the API caps `expires_at` at 30 days from now.
|
|
46
|
+
*
|
|
47
|
+
* Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.
|
|
48
|
+
*/
|
|
49
|
+
function parseBranchTtl(input) {
|
|
50
|
+
const result = parseDuration(input);
|
|
51
|
+
if ("error" in result) return result;
|
|
52
|
+
if (result.seconds > MAX_BRANCH_TTL_SECONDS) return { error: `branch TTL must be at most 30 days (${MAX_BRANCH_TTL_SECONDS}s), got ${result.seconds}s` };
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Render a TTL in seconds back to the canonical "<n><unit>" form. Used for round-trip
|
|
57
|
+
* serialization when {@link pullConfig} emits a TTL value (it always falls back to seconds
|
|
58
|
+
* when no clean unit boundary matches). The output always carries a unit, so it is a valid
|
|
59
|
+
* {@link DurationString}.
|
|
60
|
+
*/
|
|
61
|
+
function formatDurationSeconds(totalSeconds) {
|
|
62
|
+
if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) throw new RangeError(`formatDurationSeconds expected a positive finite number, got ${totalSeconds}`);
|
|
63
|
+
const candidates = [
|
|
64
|
+
["w", UNIT_SECONDS.w],
|
|
65
|
+
["d", UNIT_SECONDS.d],
|
|
66
|
+
["h", UNIT_SECONDS.h],
|
|
67
|
+
["m", UNIT_SECONDS.m]
|
|
68
|
+
];
|
|
69
|
+
for (const [unit, perUnit] of candidates) if (totalSeconds % perUnit === 0) return `${totalSeconds / perUnit}${unit}`;
|
|
70
|
+
return `${totalSeconds}s`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse a suspend timeout value into seconds for the Neon API.
|
|
74
|
+
*
|
|
75
|
+
* Accepted formats:
|
|
76
|
+
* - `false` → -1 (never suspend)
|
|
77
|
+
* - `undefined` → 0 (use platform default)
|
|
78
|
+
* - duration string → parsed seconds ("5m", "1h", "7d")
|
|
79
|
+
* - number → validated seconds (must be 60-604800 or -1/0)
|
|
80
|
+
*
|
|
81
|
+
* Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.
|
|
82
|
+
*/
|
|
83
|
+
function parseSuspendTimeout(input) {
|
|
84
|
+
if (input === false) return { seconds: -1 };
|
|
85
|
+
if (input === void 0) return { seconds: 0 };
|
|
86
|
+
if (typeof input === "number") {
|
|
87
|
+
if (!Number.isFinite(input)) return { error: `not a finite number: ${input}` };
|
|
88
|
+
if (!Number.isInteger(input)) return { error: `must be an integer: ${input}` };
|
|
89
|
+
if (input === -1 || input === 0) return { seconds: input };
|
|
90
|
+
if (input < 60 || input > 604800) return { error: `suspend timeout must be between 60 and 604800 seconds (1 minute to 1 week), got ${input}` };
|
|
91
|
+
return { seconds: input };
|
|
92
|
+
}
|
|
93
|
+
const result = parseDuration(input);
|
|
94
|
+
if ("error" in result) return result;
|
|
95
|
+
const { seconds } = result;
|
|
96
|
+
if (seconds < 60 || seconds > 604800) return { error: `suspend timeout must be between 60 and 604800 seconds (1 minute to 1 week), "${input}" = ${seconds}s` };
|
|
97
|
+
return { seconds };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Format a suspend timeout value from API seconds back to the user-facing type.
|
|
101
|
+
* Returns `false` for -1 (never suspend), `undefined` for 0 (default), or a duration string.
|
|
102
|
+
*/
|
|
103
|
+
function formatSuspendTimeout(seconds) {
|
|
104
|
+
if (seconds === -1) return false;
|
|
105
|
+
if (seconds === 0) return void 0;
|
|
106
|
+
return formatDurationSeconds(seconds);
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { MAX_BRANCH_TTL_SECONDS, formatDurationSeconds, formatSuspendTimeout, parseBranchTtl, parseDuration, parseSuspendTimeout };
|
|
110
|
+
|
|
111
|
+
//# sourceMappingURL=duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.js","names":[],"sources":["../../src/lib/duration.ts"],"sourcesContent":["import type { DurationString } from \"./types.js\";\n\n/**\n * Parse a duration value into whole seconds.\n *\n * Accepted formats:\n * - a positive finite **number** → interpreted as seconds (must be an integer)\n * - a **string** of the form `<integer><unit>` where unit is one of `s`, `m`, `h`, `d`, `w`\n * (e.g. `30s`, `5m`, `1h`, `7d`, `2w`)\n *\n * A **unit is required** on strings: a bare numeric string like `\"7\"` is rejected — pass a\n * `number` (`7`) for raw seconds instead. This removes the ambiguity where `\"7\"` silently\n * meant 7 seconds rather than, say, `\"7d\"`.\n *\n * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.\n */\nexport function parseDuration(\n\tinput: string | number,\n): { seconds: number } | { error: string } {\n\tif (typeof input === \"number\") {\n\t\tif (!Number.isFinite(input))\n\t\t\treturn { error: `not a finite number: ${input}` };\n\t\tif (!Number.isInteger(input))\n\t\t\treturn {\n\t\t\t\terror: `must be an integer when passed as number: ${input}`,\n\t\t\t};\n\t\tif (input <= 0) return { error: `must be > 0, got ${input}` };\n\t\treturn { seconds: input };\n\t}\n\n\tconst trimmed = input.trim();\n\tif (trimmed === \"\") return { error: \"duration string is empty\" };\n\n\t// A bare numeric string is rejected on purpose: pass a number for raw seconds, or add a\n\t// unit (e.g. \"7d\"). Detected explicitly so we can give a targeted hint instead of the\n\t// generic \"invalid duration\" message.\n\tif (/^\\d+$/.test(trimmed)) {\n\t\treturn {\n\t\t\terror: `duration string \"${input}\" is missing a unit; add one of s, m, h, d, w (e.g. \"${trimmed}d\") or pass ${trimmed} as a number for seconds`,\n\t\t};\n\t}\n\n\tconst unitMatch = /^(\\d+)([smhdw])$/i.exec(trimmed);\n\tif (!unitMatch) {\n\t\treturn {\n\t\t\terror: `invalid duration \"${input}\"; expected an integer followed by one of: s, m, h, d, w (e.g. \"30s\", \"1h\", \"7d\")`,\n\t\t};\n\t}\n\n\tconst value = Number(unitMatch[1]);\n\tconst unit = unitMatch[2].toLowerCase() as \"s\" | \"m\" | \"h\" | \"d\" | \"w\";\n\tif (value <= 0) return { error: `must be > 0, got \"${trimmed}\"` };\n\n\tconst seconds = value * UNIT_SECONDS[unit];\n\treturn { seconds };\n}\n\nconst UNIT_SECONDS = {\n\ts: 1,\n\tm: 60,\n\th: 60 * 60,\n\td: 24 * 60 * 60,\n\tw: 7 * 24 * 60 * 60,\n} as const;\n\n/** Neon's branch-expiration ceiling: the API rejects an `expires_at` more than 30 days out. */\nexport const MAX_BRANCH_TTL_SECONDS = 30 * UNIT_SECONDS.d;\n\n/**\n * Parse a branch TTL into seconds, enforcing Neon's branch-expiration limit on top of the\n * shared {@link parseDuration} rules: the result must be `> 0` and at most 30 days\n * ({@link MAX_BRANCH_TTL_SECONDS}), since the API caps `expires_at` at 30 days from now.\n *\n * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.\n */\nexport function parseBranchTtl(\n\tinput: string | number,\n): { seconds: number } | { error: string } {\n\tconst result = parseDuration(input);\n\tif (\"error\" in result) return result;\n\tif (result.seconds > MAX_BRANCH_TTL_SECONDS) {\n\t\treturn {\n\t\t\terror: `branch TTL must be at most 30 days (${MAX_BRANCH_TTL_SECONDS}s), got ${result.seconds}s`,\n\t\t};\n\t}\n\treturn result;\n}\n\n/**\n * Render a TTL in seconds back to the canonical \"<n><unit>\" form. Used for round-trip\n * serialization when {@link pullConfig} emits a TTL value (it always falls back to seconds\n * when no clean unit boundary matches). The output always carries a unit, so it is a valid\n * {@link DurationString}.\n */\nexport function formatDurationSeconds(totalSeconds: number): DurationString {\n\tif (!Number.isFinite(totalSeconds) || totalSeconds <= 0) {\n\t\tthrow new RangeError(\n\t\t\t`formatDurationSeconds expected a positive finite number, got ${totalSeconds}`,\n\t\t);\n\t}\n\tconst candidates = [\n\t\t[\"w\", UNIT_SECONDS.w],\n\t\t[\"d\", UNIT_SECONDS.d],\n\t\t[\"h\", UNIT_SECONDS.h],\n\t\t[\"m\", UNIT_SECONDS.m],\n\t] as const;\n\tfor (const [unit, perUnit] of candidates) {\n\t\tif (totalSeconds % perUnit === 0) {\n\t\t\treturn `${totalSeconds / perUnit}${unit}`;\n\t\t}\n\t}\n\treturn `${totalSeconds}s`;\n}\n\n/**\n * Parse a suspend timeout value into seconds for the Neon API.\n *\n * Accepted formats:\n * - `false` → -1 (never suspend)\n * - `undefined` → 0 (use platform default)\n * - duration string → parsed seconds (\"5m\", \"1h\", \"7d\")\n * - number → validated seconds (must be 60-604800 or -1/0)\n *\n * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.\n */\nexport function parseSuspendTimeout(\n\tinput: false | string | number | undefined,\n): { seconds: number } | { error: string } {\n\t// false means \"never suspend\"\n\tif (input === false) return { seconds: -1 };\n\n\t// undefined means \"use platform default\"\n\tif (input === undefined) return { seconds: 0 };\n\n\t// If it's a number, validate the range\n\tif (typeof input === \"number\") {\n\t\tif (!Number.isFinite(input))\n\t\t\treturn { error: `not a finite number: ${input}` };\n\t\tif (!Number.isInteger(input))\n\t\t\treturn { error: `must be an integer: ${input}` };\n\n\t\t// Allow special values: -1 (never), 0 (default)\n\t\tif (input === -1 || input === 0) return { seconds: input };\n\n\t\t// Validate range for custom timeout: 60s (1 min) to 604800s (1 week)\n\t\tif (input < 60 || input > 604_800) {\n\t\t\treturn {\n\t\t\t\terror: `suspend timeout must be between 60 and 604800 seconds (1 minute to 1 week), got ${input}`,\n\t\t\t};\n\t\t}\n\t\treturn { seconds: input };\n\t}\n\n\t// Parse duration string\n\tconst result = parseDuration(input);\n\tif (\"error\" in result) return result;\n\n\t// Validate the parsed duration is in the valid range\n\tconst { seconds } = result;\n\tif (seconds < 60 || seconds > 604_800) {\n\t\treturn {\n\t\t\terror: `suspend timeout must be between 60 and 604800 seconds (1 minute to 1 week), \"${input}\" = ${seconds}s`,\n\t\t};\n\t}\n\n\treturn { seconds };\n}\n\n/**\n * Format a suspend timeout value from API seconds back to the user-facing type.\n * Returns `false` for -1 (never suspend), `undefined` for 0 (default), or a duration string.\n */\nexport function formatSuspendTimeout(\n\tseconds: number,\n): false | DurationString | undefined {\n\tif (seconds === -1) return false; // never suspend\n\tif (seconds === 0) return undefined; // platform default\n\treturn formatDurationSeconds(seconds);\n}\n"],"mappings":"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"}
|