@neondatabase/config 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +178 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/lib/auth.d.ts +63 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +93 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/define-config.d.ts +43 -0
- package/dist/lib/define-config.d.ts.map +1 -0
- package/dist/lib/define-config.js +111 -0
- package/dist/lib/define-config.js.map +1 -0
- package/dist/lib/diff.d.ts +109 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +205 -0
- package/dist/lib/diff.js.map +1 -0
- package/dist/lib/duration.d.ts +46 -0
- package/dist/lib/duration.d.ts.map +1 -0
- package/dist/lib/duration.js +96 -0
- package/dist/lib/duration.js.map +1 -0
- package/dist/lib/errors.d.ts +129 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +168 -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 +119 -0
- package/dist/lib/loader.js.map +1 -0
- package/dist/lib/neon-api-real.d.ts +45 -0
- package/dist/lib/neon-api-real.d.ts.map +1 -0
- package/dist/lib/neon-api-real.js +582 -0
- package/dist/lib/neon-api-real.js.map +1 -0
- package/dist/lib/neon-api.d.ts +262 -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 +109 -0
- package/dist/lib/schema.d.ts.map +1 -0
- package/dist/lib/schema.js +199 -0
- package/dist/lib/schema.js.map +1 -0
- package/dist/lib/types.d.ts +259 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +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 +132 -0
- package/dist/v1.d.ts.map +1 -0
- package/dist/v1.js +69 -0
- package/dist/v1.js.map +1 -0
- package/package.json +67 -17
- package/.env.example +0 -5
- package/e2e/errors.e2e.test.ts +0 -52
- package/e2e/helpers.ts +0 -205
- package/e2e/load-env.ts +0 -29
- package/e2e/setup.ts +0 -24
- package/src/index.ts +0 -5
- package/src/lib/auth.test.ts +0 -166
- package/src/lib/auth.ts +0 -124
- package/src/lib/define-config.test.ts +0 -161
- package/src/lib/define-config.ts +0 -152
- package/src/lib/diff.test.ts +0 -142
- package/src/lib/diff.ts +0 -391
- package/src/lib/duration.test.ts +0 -105
- package/src/lib/duration.ts +0 -147
- package/src/lib/errors.test.ts +0 -26
- package/src/lib/errors.ts +0 -220
- package/src/lib/fake-neon-api.ts +0 -782
- package/src/lib/loader.test.ts +0 -35
- package/src/lib/loader.ts +0 -215
- package/src/lib/neon-api-real.test.ts +0 -72
- package/src/lib/neon-api-real.ts +0 -1123
- package/src/lib/neon-api.ts +0 -356
- package/src/lib/patterns.test.ts +0 -80
- package/src/lib/patterns.ts +0 -98
- package/src/lib/schema.test.ts +0 -88
- package/src/lib/schema.ts +0 -252
- package/src/lib/test-utils.ts +0 -83
- package/src/lib/types.ts +0 -268
- package/src/lib/wrap-neon-error.test.ts +0 -145
- package/src/lib/wrap-neon-error.ts +0 -204
- package/src/v1.test.ts +0 -33
- package/src/v1.ts +0 -148
- package/tsconfig.json +0 -4
- package/tsdown.config.ts +0 -19
- package/vitest.config.ts +0 -19
- package/vitest.e2e.config.ts +0 -29
package/src/lib/neon-api-real.ts
DELETED
|
@@ -1,1123 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Branch,
|
|
3
|
-
type BranchCreateRequest,
|
|
4
|
-
type BranchCreateRequestEndpointOptions,
|
|
5
|
-
type BranchUpdateRequest,
|
|
6
|
-
createApiClient,
|
|
7
|
-
type Database,
|
|
8
|
-
type DefaultEndpointSettings,
|
|
9
|
-
type Endpoint,
|
|
10
|
-
EndpointType,
|
|
11
|
-
type EndpointUpdateRequest,
|
|
12
|
-
type PgVersion,
|
|
13
|
-
type Project,
|
|
14
|
-
type ProjectCreateRequest,
|
|
15
|
-
type ProjectListItem,
|
|
16
|
-
type ProjectUpdateRequest,
|
|
17
|
-
type Role,
|
|
18
|
-
} from "@neondatabase/api-client";
|
|
19
|
-
import { z } from "zod";
|
|
20
|
-
import { formatSuspendTimeout, parseSuspendTimeout } from "./duration.js";
|
|
21
|
-
import { ErrorCode, PlatformError } from "./errors.js";
|
|
22
|
-
import type {
|
|
23
|
-
CreateBranchInput,
|
|
24
|
-
CreateBucketInput,
|
|
25
|
-
CreateProjectInput,
|
|
26
|
-
DeployFunctionInput,
|
|
27
|
-
GetConnectionUriInput,
|
|
28
|
-
NeonApi,
|
|
29
|
-
NeonAuthSnapshot,
|
|
30
|
-
NeonBranchSnapshot,
|
|
31
|
-
NeonBucketSnapshot,
|
|
32
|
-
NeonDataApiSnapshot,
|
|
33
|
-
NeonDatabaseSnapshot,
|
|
34
|
-
NeonEndpointSnapshot,
|
|
35
|
-
NeonFunctionDeploymentSnapshot,
|
|
36
|
-
NeonFunctionSnapshot,
|
|
37
|
-
NeonProjectSnapshot,
|
|
38
|
-
NeonRoleSnapshot,
|
|
39
|
-
UpdateBranchInput,
|
|
40
|
-
} from "./neon-api.js";
|
|
41
|
-
import type { BucketAccessLevel, ComputeSettings } from "./types.js";
|
|
42
|
-
import { wrapNeonError } from "./wrap-neon-error.js";
|
|
43
|
-
|
|
44
|
-
type ApiClient = ReturnType<typeof createApiClient>;
|
|
45
|
-
const DEFAULT_NEON_API_BASE_URL = "https://console.neon.tech/api/v2";
|
|
46
|
-
|
|
47
|
-
const neonAuthResponseSchema = z.object({
|
|
48
|
-
auth_provider_project_id: z.string(),
|
|
49
|
-
pub_client_key: z.string().optional(),
|
|
50
|
-
secret_server_key: z.string().optional(),
|
|
51
|
-
jwks_url: z.string(),
|
|
52
|
-
base_url: z.string().optional(),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// ─── Preview: buckets ──────────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
const bucketSchema = z.object({
|
|
58
|
-
name: z.string(),
|
|
59
|
-
access_level: z.string().optional(),
|
|
60
|
-
});
|
|
61
|
-
const bucketResponseSchema = z.object({ bucket: bucketSchema });
|
|
62
|
-
const bucketsListResponseSchema = z.object({ buckets: z.array(bucketSchema) });
|
|
63
|
-
|
|
64
|
-
// ─── Preview: functions ────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
const functionDeploymentSchema = z.object({
|
|
67
|
-
id: z.number(),
|
|
68
|
-
status: z.string(),
|
|
69
|
-
});
|
|
70
|
-
const neonFunctionSchema = z.object({
|
|
71
|
-
id: z.string(),
|
|
72
|
-
slug: z.string(),
|
|
73
|
-
name: z.string(),
|
|
74
|
-
invocation_url: z.string(),
|
|
75
|
-
active_deployment: functionDeploymentSchema.optional(),
|
|
76
|
-
});
|
|
77
|
-
const functionResponseSchema = z.object({ function: neonFunctionSchema });
|
|
78
|
-
const functionsListResponseSchema = z.object({
|
|
79
|
-
functions: z.array(neonFunctionSchema),
|
|
80
|
-
});
|
|
81
|
-
const functionDeploymentResponseSchema = z.object({
|
|
82
|
-
deployment: functionDeploymentSchema,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
interface CreateNeonAuthRestInput {
|
|
86
|
-
auth_provider: "better_auth";
|
|
87
|
-
database_name?: string;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
interface RestConfig {
|
|
91
|
-
apiKey: string;
|
|
92
|
-
baseUrl: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Adapt `@neondatabase/api-client` to the narrow {@link NeonApi} façade used by the rest of
|
|
97
|
-
* this package. Constructs are restricted to whole-object read/write of just the fields we
|
|
98
|
-
* model in {@link Config}; anything else stays untouched on the remote.
|
|
99
|
-
*/
|
|
100
|
-
export function createRealNeonApi(options: {
|
|
101
|
-
apiKey: string;
|
|
102
|
-
baseUrl?: string;
|
|
103
|
-
/**
|
|
104
|
-
* Tuning knob for the built-in 423 retry. Defaults: ~30s of total wait spread across
|
|
105
|
-
* 12 attempts with exponential backoff capped at 5s. Lowering this is mostly useful in
|
|
106
|
-
* tests; raising it is rarely needed because Neon operations are usually sub-second.
|
|
107
|
-
*/
|
|
108
|
-
retryOnLocked?: {
|
|
109
|
-
maxAttempts?: number;
|
|
110
|
-
initialDelayMs?: number;
|
|
111
|
-
maxDelayMs?: number;
|
|
112
|
-
};
|
|
113
|
-
}): NeonApi {
|
|
114
|
-
if (!options.apiKey || options.apiKey.trim() === "") {
|
|
115
|
-
throw new PlatformError(
|
|
116
|
-
ErrorCode.MissingApiKey,
|
|
117
|
-
[
|
|
118
|
-
"createRealNeonApi requires a non-empty `apiKey`.",
|
|
119
|
-
"Generate one at https://console.neon.tech/app/settings/api-keys and pass it as { apiKey: process.env.NEON_API_KEY }.",
|
|
120
|
-
].join(" "),
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const client = createApiClient({
|
|
125
|
-
apiKey: options.apiKey,
|
|
126
|
-
...(options.baseUrl ? { baseURL: options.baseUrl } : {}),
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
return new RealNeonApi(
|
|
130
|
-
client,
|
|
131
|
-
{
|
|
132
|
-
maxAttempts: options.retryOnLocked?.maxAttempts ?? 12,
|
|
133
|
-
initialDelayMs: options.retryOnLocked?.initialDelayMs ?? 250,
|
|
134
|
-
maxDelayMs: options.retryOnLocked?.maxDelayMs ?? 5_000,
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
apiKey: options.apiKey,
|
|
138
|
-
baseUrl: options.baseUrl ?? DEFAULT_NEON_API_BASE_URL,
|
|
139
|
-
},
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
interface RetryConfig {
|
|
144
|
-
maxAttempts: number;
|
|
145
|
-
initialDelayMs: number;
|
|
146
|
-
maxDelayMs: number;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Retry a function whenever it throws an HTTP 423 (Locked) — Neon's signal that a prior
|
|
151
|
-
* mutation on the same resource is still in flight. Uses exponential backoff capped at
|
|
152
|
-
* `maxDelayMs`. Any other error (and the last attempt) propagates.
|
|
153
|
-
*
|
|
154
|
-
* Exported only for tests; production callers go through the wrapped {@link NeonApi}.
|
|
155
|
-
*/
|
|
156
|
-
export async function retryOnLocked<T>(
|
|
157
|
-
fn: () => Promise<T>,
|
|
158
|
-
config: RetryConfig,
|
|
159
|
-
): Promise<T> {
|
|
160
|
-
let delay = config.initialDelayMs;
|
|
161
|
-
let lastError: unknown;
|
|
162
|
-
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
|
163
|
-
try {
|
|
164
|
-
return await fn();
|
|
165
|
-
} catch (err) {
|
|
166
|
-
lastError = err;
|
|
167
|
-
const status = readHttpStatusFromError(err);
|
|
168
|
-
if (status !== 423 || attempt === config.maxAttempts) throw err;
|
|
169
|
-
await sleep(delay);
|
|
170
|
-
delay = Math.min(delay * 2, config.maxDelayMs);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
throw lastError;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function readHttpStatusFromError(err: unknown): number | undefined {
|
|
177
|
-
if (err === null || typeof err !== "object") return undefined;
|
|
178
|
-
const response = (err as { response?: unknown }).response;
|
|
179
|
-
if (response === null || typeof response !== "object") return undefined;
|
|
180
|
-
const status = (response as { status?: unknown }).status;
|
|
181
|
-
return typeof status === "number" ? status : undefined;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function sleep(ms: number): Promise<void> {
|
|
185
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
class RealNeonApi implements NeonApi {
|
|
189
|
-
constructor(
|
|
190
|
-
private readonly client: ApiClient,
|
|
191
|
-
private readonly retryConfig: RetryConfig,
|
|
192
|
-
private readonly restConfig: RestConfig,
|
|
193
|
-
) {}
|
|
194
|
-
|
|
195
|
-
private retry<T>(fn: () => Promise<T>): Promise<T> {
|
|
196
|
-
return retryOnLocked(fn, this.retryConfig);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private async call<T>(
|
|
200
|
-
op: string,
|
|
201
|
-
fn: () => Promise<T>,
|
|
202
|
-
options: { projectId?: string; mutating?: boolean } = {},
|
|
203
|
-
): Promise<T> {
|
|
204
|
-
try {
|
|
205
|
-
return options.mutating ? await this.retry(fn) : await fn();
|
|
206
|
-
} catch (err) {
|
|
207
|
-
const wrapped = wrapNeonError(
|
|
208
|
-
err,
|
|
209
|
-
options.projectId
|
|
210
|
-
? { op, projectId: options.projectId }
|
|
211
|
-
: { op },
|
|
212
|
-
);
|
|
213
|
-
throw wrapped;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async listProjects(filter: {
|
|
218
|
-
orgId?: string;
|
|
219
|
-
}): Promise<NeonProjectSnapshot[]> {
|
|
220
|
-
return this.call(
|
|
221
|
-
filter.orgId ? `listProjects(org=${filter.orgId})` : "listProjects",
|
|
222
|
-
async () => {
|
|
223
|
-
const projects: ProjectListItem[] = [];
|
|
224
|
-
let cursor: string | undefined;
|
|
225
|
-
while (true) {
|
|
226
|
-
const res = await this.client.listProjects({
|
|
227
|
-
...(filter.orgId ? { org_id: filter.orgId } : {}),
|
|
228
|
-
...(cursor ? { cursor } : {}),
|
|
229
|
-
limit: 100,
|
|
230
|
-
});
|
|
231
|
-
projects.push(...res.data.projects);
|
|
232
|
-
const next = (
|
|
233
|
-
res.data as { pagination?: { next?: string } }
|
|
234
|
-
).pagination?.next;
|
|
235
|
-
if (!next || next === cursor) break;
|
|
236
|
-
cursor = next;
|
|
237
|
-
}
|
|
238
|
-
return projects.map(projectToSnapshot);
|
|
239
|
-
},
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async getProject(projectId: string): Promise<NeonProjectSnapshot> {
|
|
244
|
-
return this.call(
|
|
245
|
-
`getProject(${projectId})`,
|
|
246
|
-
async () => {
|
|
247
|
-
const res = await this.client.getProject(projectId);
|
|
248
|
-
return projectToSnapshot(res.data.project);
|
|
249
|
-
},
|
|
250
|
-
{ projectId },
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async createProject(
|
|
255
|
-
input: CreateProjectInput,
|
|
256
|
-
): Promise<NeonProjectSnapshot> {
|
|
257
|
-
const body: ProjectCreateRequest = {
|
|
258
|
-
project: {
|
|
259
|
-
name: input.name,
|
|
260
|
-
region_id: input.regionId,
|
|
261
|
-
...(input.pgVersion !== undefined
|
|
262
|
-
? { pg_version: input.pgVersion as PgVersion }
|
|
263
|
-
: {}),
|
|
264
|
-
...(input.orgId ? { org_id: input.orgId } : {}),
|
|
265
|
-
...(input.defaultEndpointSettings
|
|
266
|
-
? {
|
|
267
|
-
default_endpoint_settings:
|
|
268
|
-
computeSettingsToDefaults(
|
|
269
|
-
input.defaultEndpointSettings,
|
|
270
|
-
),
|
|
271
|
-
}
|
|
272
|
-
: {}),
|
|
273
|
-
...(input.defaultBranchName
|
|
274
|
-
? { branch: { name: input.defaultBranchName } }
|
|
275
|
-
: {}),
|
|
276
|
-
},
|
|
277
|
-
};
|
|
278
|
-
return this.call(
|
|
279
|
-
`createProject(${input.name})`,
|
|
280
|
-
async () => {
|
|
281
|
-
const res = await this.client.createProject(body);
|
|
282
|
-
return projectToSnapshot(res.data.project);
|
|
283
|
-
},
|
|
284
|
-
{ mutating: true },
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async updateProject(
|
|
289
|
-
projectId: string,
|
|
290
|
-
input: { name?: string; defaultEndpointSettings?: ComputeSettings },
|
|
291
|
-
): Promise<NeonProjectSnapshot> {
|
|
292
|
-
const body: ProjectUpdateRequest = {
|
|
293
|
-
project: {
|
|
294
|
-
...(input.name !== undefined ? { name: input.name } : {}),
|
|
295
|
-
...(input.defaultEndpointSettings
|
|
296
|
-
? {
|
|
297
|
-
default_endpoint_settings:
|
|
298
|
-
computeSettingsToDefaults(
|
|
299
|
-
input.defaultEndpointSettings,
|
|
300
|
-
),
|
|
301
|
-
}
|
|
302
|
-
: {}),
|
|
303
|
-
},
|
|
304
|
-
};
|
|
305
|
-
return this.call(
|
|
306
|
-
`updateProject(${projectId})`,
|
|
307
|
-
async () => {
|
|
308
|
-
const res = await this.client.updateProject(projectId, body);
|
|
309
|
-
return projectToSnapshot(res.data.project);
|
|
310
|
-
},
|
|
311
|
-
{ projectId, mutating: true },
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async listBranches(projectId: string): Promise<NeonBranchSnapshot[]> {
|
|
316
|
-
return this.call(
|
|
317
|
-
`listBranches(${projectId})`,
|
|
318
|
-
async () => {
|
|
319
|
-
const branches: Branch[] = [];
|
|
320
|
-
let cursor: string | undefined;
|
|
321
|
-
while (true) {
|
|
322
|
-
const res = await this.client.listProjectBranches({
|
|
323
|
-
projectId,
|
|
324
|
-
limit: 100,
|
|
325
|
-
...(cursor ? { cursor } : {}),
|
|
326
|
-
});
|
|
327
|
-
branches.push(...(res.data.branches as Branch[]));
|
|
328
|
-
const next = (
|
|
329
|
-
res.data as { pagination?: { next?: string } }
|
|
330
|
-
).pagination?.next;
|
|
331
|
-
if (!next || next === cursor) break;
|
|
332
|
-
cursor = next;
|
|
333
|
-
}
|
|
334
|
-
return branches.map(branchToSnapshot);
|
|
335
|
-
},
|
|
336
|
-
{ projectId },
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async createBranch(
|
|
341
|
-
projectId: string,
|
|
342
|
-
input: CreateBranchInput,
|
|
343
|
-
): Promise<{
|
|
344
|
-
branch: NeonBranchSnapshot;
|
|
345
|
-
endpoints: NeonEndpointSnapshot[];
|
|
346
|
-
}> {
|
|
347
|
-
const endpointOptions: BranchCreateRequestEndpointOptions | undefined =
|
|
348
|
-
input.computeSettings
|
|
349
|
-
? {
|
|
350
|
-
type: EndpointType.ReadWrite,
|
|
351
|
-
...computeSettingsToEndpointOptions(
|
|
352
|
-
input.computeSettings,
|
|
353
|
-
),
|
|
354
|
-
}
|
|
355
|
-
: { type: EndpointType.ReadWrite };
|
|
356
|
-
|
|
357
|
-
const body: BranchCreateRequest = {
|
|
358
|
-
branch: {
|
|
359
|
-
name: input.name,
|
|
360
|
-
...(input.parentId ? { parent_id: input.parentId } : {}),
|
|
361
|
-
...(input.expiresAt ? { expires_at: input.expiresAt } : {}),
|
|
362
|
-
...(input.protected !== undefined
|
|
363
|
-
? { protected: input.protected }
|
|
364
|
-
: {}),
|
|
365
|
-
},
|
|
366
|
-
endpoints: [endpointOptions],
|
|
367
|
-
};
|
|
368
|
-
return this.call(
|
|
369
|
-
`createBranch(${projectId}/${input.name})`,
|
|
370
|
-
async () => {
|
|
371
|
-
const res = await this.client.createProjectBranch(
|
|
372
|
-
projectId,
|
|
373
|
-
body,
|
|
374
|
-
);
|
|
375
|
-
return {
|
|
376
|
-
branch: branchToSnapshot(res.data.branch),
|
|
377
|
-
endpoints: (res.data.endpoints ?? []).map(
|
|
378
|
-
endpointToSnapshot,
|
|
379
|
-
),
|
|
380
|
-
};
|
|
381
|
-
},
|
|
382
|
-
{ projectId, mutating: true },
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async updateBranch(
|
|
387
|
-
projectId: string,
|
|
388
|
-
branchId: string,
|
|
389
|
-
input: UpdateBranchInput,
|
|
390
|
-
): Promise<NeonBranchSnapshot> {
|
|
391
|
-
const branch: BranchUpdateRequest["branch"] = {};
|
|
392
|
-
if (input.name !== undefined) branch.name = input.name;
|
|
393
|
-
if (input.expiresAt !== undefined) branch.expires_at = input.expiresAt;
|
|
394
|
-
if (input.protected !== undefined) branch.protected = input.protected;
|
|
395
|
-
return this.call(
|
|
396
|
-
`updateBranch(${projectId}/${branchId})`,
|
|
397
|
-
async () => {
|
|
398
|
-
const res = await this.client.updateProjectBranch(
|
|
399
|
-
projectId,
|
|
400
|
-
branchId,
|
|
401
|
-
{ branch },
|
|
402
|
-
);
|
|
403
|
-
return branchToSnapshot(res.data.branch);
|
|
404
|
-
},
|
|
405
|
-
{ projectId, mutating: true },
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async listEndpoints(projectId: string): Promise<NeonEndpointSnapshot[]> {
|
|
410
|
-
return this.call(
|
|
411
|
-
`listEndpoints(${projectId})`,
|
|
412
|
-
async () => {
|
|
413
|
-
const res = await this.client.listProjectEndpoints(projectId);
|
|
414
|
-
return (res.data.endpoints as Endpoint[]).map(
|
|
415
|
-
endpointToSnapshot,
|
|
416
|
-
);
|
|
417
|
-
},
|
|
418
|
-
{ projectId },
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async updateEndpoint(
|
|
423
|
-
projectId: string,
|
|
424
|
-
endpointId: string,
|
|
425
|
-
settings: ComputeSettings,
|
|
426
|
-
): Promise<NeonEndpointSnapshot> {
|
|
427
|
-
const endpoint: EndpointUpdateRequest["endpoint"] =
|
|
428
|
-
computeSettingsToEndpointOptions(settings);
|
|
429
|
-
return this.call(
|
|
430
|
-
`updateEndpoint(${projectId}/${endpointId})`,
|
|
431
|
-
async () => {
|
|
432
|
-
const res = await this.client.updateProjectEndpoint(
|
|
433
|
-
projectId,
|
|
434
|
-
endpointId,
|
|
435
|
-
{ endpoint },
|
|
436
|
-
);
|
|
437
|
-
return endpointToSnapshot(res.data.endpoint);
|
|
438
|
-
},
|
|
439
|
-
{ projectId, mutating: true },
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
async listBranchRoles(
|
|
444
|
-
projectId: string,
|
|
445
|
-
branchId: string,
|
|
446
|
-
): Promise<NeonRoleSnapshot[]> {
|
|
447
|
-
return this.call(
|
|
448
|
-
`listBranchRoles(${projectId}/${branchId})`,
|
|
449
|
-
async () => {
|
|
450
|
-
const res = await this.client.listProjectBranchRoles(
|
|
451
|
-
projectId,
|
|
452
|
-
branchId,
|
|
453
|
-
);
|
|
454
|
-
return (res.data.roles as Role[]).map(roleToSnapshot);
|
|
455
|
-
},
|
|
456
|
-
{ projectId },
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
async listBranchDatabases(
|
|
461
|
-
projectId: string,
|
|
462
|
-
branchId: string,
|
|
463
|
-
): Promise<NeonDatabaseSnapshot[]> {
|
|
464
|
-
return this.call(
|
|
465
|
-
`listBranchDatabases(${projectId}/${branchId})`,
|
|
466
|
-
async () => {
|
|
467
|
-
const res = await this.client.listProjectBranchDatabases(
|
|
468
|
-
projectId,
|
|
469
|
-
branchId,
|
|
470
|
-
);
|
|
471
|
-
return (res.data.databases as Database[]).map(
|
|
472
|
-
databaseToSnapshot,
|
|
473
|
-
);
|
|
474
|
-
},
|
|
475
|
-
{ projectId },
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
async getConnectionUri(
|
|
480
|
-
projectId: string,
|
|
481
|
-
input: GetConnectionUriInput,
|
|
482
|
-
): Promise<{ uri: string }> {
|
|
483
|
-
const op = `getConnectionUri(${projectId}/${input.databaseName}@${input.roleName}${input.pooled ? " pooled" : ""})`;
|
|
484
|
-
// Always send `pooled` explicitly. The Neon API has switched its default
|
|
485
|
-
// to returning the pooled URI when the parameter is omitted, so we have
|
|
486
|
-
// to be explicit to get the direct URI back.
|
|
487
|
-
const pooled = input.pooled === true;
|
|
488
|
-
return this.call(
|
|
489
|
-
op,
|
|
490
|
-
async () => {
|
|
491
|
-
const res = await this.client.getConnectionUri({
|
|
492
|
-
projectId,
|
|
493
|
-
database_name: input.databaseName,
|
|
494
|
-
role_name: input.roleName,
|
|
495
|
-
...(input.branchId ? { branch_id: input.branchId } : {}),
|
|
496
|
-
...(input.endpointId
|
|
497
|
-
? { endpoint_id: input.endpointId }
|
|
498
|
-
: {}),
|
|
499
|
-
pooled,
|
|
500
|
-
});
|
|
501
|
-
return { uri: res.data.uri };
|
|
502
|
-
},
|
|
503
|
-
{ projectId },
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
async getNeonAuth(
|
|
508
|
-
projectId: string,
|
|
509
|
-
branchId: string,
|
|
510
|
-
): Promise<NeonAuthSnapshot | null> {
|
|
511
|
-
// `GET /projects/:pid/branches/:bid/auth` returns 404 when no integration exists.
|
|
512
|
-
// Surface that as `null` so callers can branch cleanly instead of try/catch.
|
|
513
|
-
try {
|
|
514
|
-
return await this.call(
|
|
515
|
-
`getNeonAuth(${projectId}/${branchId})`,
|
|
516
|
-
async () => {
|
|
517
|
-
const res = await this.client.getNeonAuth(
|
|
518
|
-
projectId,
|
|
519
|
-
branchId,
|
|
520
|
-
);
|
|
521
|
-
return neonAuthResponseToSnapshot(res.data);
|
|
522
|
-
},
|
|
523
|
-
{ projectId },
|
|
524
|
-
);
|
|
525
|
-
} catch (err) {
|
|
526
|
-
if (err instanceof PlatformError && err.code === ErrorCode.NotFound)
|
|
527
|
-
return null;
|
|
528
|
-
throw err;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
async enableNeonAuth(
|
|
533
|
-
projectId: string,
|
|
534
|
-
branchId: string,
|
|
535
|
-
input: { databaseName?: string } = {},
|
|
536
|
-
): Promise<NeonAuthSnapshot> {
|
|
537
|
-
// Idempotent: if an integration already exists on the branch, the POST returns 409
|
|
538
|
-
// (`Conflict`). We swallow that and re-fetch the existing snapshot so callers can
|
|
539
|
-
// rely on `enableNeonAuth` to be safe to invoke from any push, including no-ops.
|
|
540
|
-
try {
|
|
541
|
-
return await this.call(
|
|
542
|
-
`enableNeonAuth(${projectId}/${branchId})`,
|
|
543
|
-
async () => {
|
|
544
|
-
// TODO: switch back to `this.client.createNeonAuth` once
|
|
545
|
-
// @neondatabase/api-client narrows this branch endpoint to `better_auth`.
|
|
546
|
-
const data = await this.postJson(
|
|
547
|
-
`/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/auth`,
|
|
548
|
-
createNeonAuthRestInput(input),
|
|
549
|
-
);
|
|
550
|
-
const parsed = neonAuthResponseSchema.parse(data);
|
|
551
|
-
return neonAuthResponseToSnapshot(parsed);
|
|
552
|
-
},
|
|
553
|
-
{ projectId, mutating: true },
|
|
554
|
-
);
|
|
555
|
-
} catch (err) {
|
|
556
|
-
if (
|
|
557
|
-
err instanceof PlatformError &&
|
|
558
|
-
err.code === ErrorCode.Conflict
|
|
559
|
-
) {
|
|
560
|
-
const existing = await this.getNeonAuth(projectId, branchId);
|
|
561
|
-
if (existing) return existing;
|
|
562
|
-
}
|
|
563
|
-
throw err;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
private async postJson(path: string, body: unknown): Promise<unknown> {
|
|
568
|
-
return this.request("POST", path, {
|
|
569
|
-
headers: { "Content-Type": "application/json" },
|
|
570
|
-
body: JSON.stringify(body),
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
private async getJson(path: string): Promise<unknown> {
|
|
575
|
-
return this.request("GET", path);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
private async deleteJson(path: string): Promise<unknown> {
|
|
579
|
-
return this.request("DELETE", path);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Upload a built function bundle via `multipart/form-data` to the deploy endpoint.
|
|
584
|
-
* Sends the bundle as the `file` field plus the deploy params Neon requires.
|
|
585
|
-
*/
|
|
586
|
-
private async postMultipart(
|
|
587
|
-
path: string,
|
|
588
|
-
input: DeployFunctionInput,
|
|
589
|
-
): Promise<unknown> {
|
|
590
|
-
const form = new FormData();
|
|
591
|
-
form.set(
|
|
592
|
-
"file",
|
|
593
|
-
new Blob([input.bundle as BlobPart], {
|
|
594
|
-
type: "application/zip",
|
|
595
|
-
}),
|
|
596
|
-
"bundle.zip",
|
|
597
|
-
);
|
|
598
|
-
form.set("memory_mib", String(input.memoryMib));
|
|
599
|
-
// Keep concurrency internal for now. The API requires it, but the public
|
|
600
|
-
// neon.ts config surface intentionally does not expose it yet.
|
|
601
|
-
form.set("concurrency", "1");
|
|
602
|
-
form.set("runtime", input.runtime);
|
|
603
|
-
for (const [key, value] of Object.entries(input.environment)) {
|
|
604
|
-
form.set(`environment[${key}]`, value);
|
|
605
|
-
}
|
|
606
|
-
return this.request("POST", path, { body: form });
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
private async request(
|
|
610
|
-
method: "GET" | "POST" | "DELETE",
|
|
611
|
-
path: string,
|
|
612
|
-
init: { headers?: Record<string, string>; body?: BodyInit } = {},
|
|
613
|
-
): Promise<unknown> {
|
|
614
|
-
const url = `${this.restConfig.baseUrl.replace(/\/+$/, "")}${path}`;
|
|
615
|
-
const res = await fetch(url, {
|
|
616
|
-
method,
|
|
617
|
-
headers: {
|
|
618
|
-
Authorization: `Bearer ${this.restConfig.apiKey}`,
|
|
619
|
-
...(init.headers ?? {}),
|
|
620
|
-
},
|
|
621
|
-
...(init.body !== undefined ? { body: init.body } : {}),
|
|
622
|
-
});
|
|
623
|
-
const data = await readJsonBody(res);
|
|
624
|
-
if (!res.ok) {
|
|
625
|
-
throw {
|
|
626
|
-
response: {
|
|
627
|
-
status: res.status,
|
|
628
|
-
data,
|
|
629
|
-
},
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
return data;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
async getNeonDataApi(
|
|
636
|
-
projectId: string,
|
|
637
|
-
branchId: string,
|
|
638
|
-
databaseName: string,
|
|
639
|
-
): Promise<NeonDataApiSnapshot | null> {
|
|
640
|
-
// Same shape as getNeonAuth — 404 means "no integration on this branch/db", which
|
|
641
|
-
// we translate to `null` for the caller.
|
|
642
|
-
try {
|
|
643
|
-
return await this.call(
|
|
644
|
-
`getNeonDataApi(${projectId}/${branchId}/${databaseName})`,
|
|
645
|
-
async () => {
|
|
646
|
-
const res = await this.client.getProjectBranchDataApi(
|
|
647
|
-
projectId,
|
|
648
|
-
branchId,
|
|
649
|
-
databaseName,
|
|
650
|
-
);
|
|
651
|
-
return { url: res.data.url };
|
|
652
|
-
},
|
|
653
|
-
{ projectId },
|
|
654
|
-
);
|
|
655
|
-
} catch (err) {
|
|
656
|
-
if (err instanceof PlatformError && err.code === ErrorCode.NotFound)
|
|
657
|
-
return null;
|
|
658
|
-
throw err;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
async enableProjectBranchDataApi(
|
|
663
|
-
projectId: string,
|
|
664
|
-
branchId: string,
|
|
665
|
-
databaseName: string,
|
|
666
|
-
): Promise<NeonDataApiSnapshot> {
|
|
667
|
-
// Idempotent in the same shape as `enableNeonAuth`: if an integration already
|
|
668
|
-
// exists, the POST returns 409 and we re-fetch the existing snapshot.
|
|
669
|
-
try {
|
|
670
|
-
return await this.call(
|
|
671
|
-
`enableProjectBranchDataApi(${projectId}/${branchId}/${databaseName})`,
|
|
672
|
-
async () => {
|
|
673
|
-
const res = await this.client.createProjectBranchDataApi(
|
|
674
|
-
projectId,
|
|
675
|
-
branchId,
|
|
676
|
-
databaseName,
|
|
677
|
-
// Empty body — pick up Neon defaults (auth_provider inferred from
|
|
678
|
-
// whether Neon Auth is also enabled; default schemas/grants).
|
|
679
|
-
{},
|
|
680
|
-
);
|
|
681
|
-
return { url: res.data.url };
|
|
682
|
-
},
|
|
683
|
-
{ projectId, mutating: true },
|
|
684
|
-
);
|
|
685
|
-
} catch (err) {
|
|
686
|
-
if (
|
|
687
|
-
err instanceof PlatformError &&
|
|
688
|
-
err.code === ErrorCode.Conflict
|
|
689
|
-
) {
|
|
690
|
-
const existing = await this.getNeonDataApi(
|
|
691
|
-
projectId,
|
|
692
|
-
branchId,
|
|
693
|
-
databaseName,
|
|
694
|
-
);
|
|
695
|
-
if (existing) return existing;
|
|
696
|
-
}
|
|
697
|
-
throw err;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// ─── Preview: buckets ──────────────────────────────────────────────────────
|
|
702
|
-
|
|
703
|
-
async listBranchBuckets(
|
|
704
|
-
projectId: string,
|
|
705
|
-
branchId: string,
|
|
706
|
-
): Promise<NeonBucketSnapshot[]> {
|
|
707
|
-
return this.call(
|
|
708
|
-
`listBranchBuckets(${projectId}/${branchId})`,
|
|
709
|
-
async () => {
|
|
710
|
-
const data = await this.getJson(
|
|
711
|
-
branchPreviewPath(projectId, branchId, "buckets"),
|
|
712
|
-
);
|
|
713
|
-
const parsed = bucketsListResponseSchema.parse(data);
|
|
714
|
-
return parsed.buckets.map(bucketToSnapshot);
|
|
715
|
-
},
|
|
716
|
-
{ projectId },
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
async createBranchBucket(
|
|
721
|
-
projectId: string,
|
|
722
|
-
branchId: string,
|
|
723
|
-
input: CreateBucketInput,
|
|
724
|
-
): Promise<NeonBucketSnapshot> {
|
|
725
|
-
return this.call(
|
|
726
|
-
`createBranchBucket(${projectId}/${branchId}/${input.name})`,
|
|
727
|
-
async () => {
|
|
728
|
-
const data = await this.postJson(
|
|
729
|
-
branchPreviewPath(projectId, branchId, "buckets"),
|
|
730
|
-
{
|
|
731
|
-
name: input.name,
|
|
732
|
-
...(input.accessLevel
|
|
733
|
-
? { access_level: input.accessLevel }
|
|
734
|
-
: {}),
|
|
735
|
-
},
|
|
736
|
-
);
|
|
737
|
-
const parsed = bucketResponseSchema.parse(data);
|
|
738
|
-
return bucketToSnapshot(parsed.bucket);
|
|
739
|
-
},
|
|
740
|
-
{ projectId, mutating: true },
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
async deleteBranchBucket(
|
|
745
|
-
projectId: string,
|
|
746
|
-
branchId: string,
|
|
747
|
-
bucketName: string,
|
|
748
|
-
): Promise<void> {
|
|
749
|
-
await this.call(
|
|
750
|
-
`deleteBranchBucket(${projectId}/${branchId}/${bucketName})`,
|
|
751
|
-
async () => {
|
|
752
|
-
await this.deleteJson(
|
|
753
|
-
`${branchPreviewPath(projectId, branchId, "buckets")}/${encodeURIComponent(bucketName)}`,
|
|
754
|
-
);
|
|
755
|
-
},
|
|
756
|
-
{ projectId, mutating: true },
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// ─── Preview: functions ────────────────────────────────────────────────────
|
|
761
|
-
|
|
762
|
-
async listBranchFunctions(
|
|
763
|
-
projectId: string,
|
|
764
|
-
branchId: string,
|
|
765
|
-
): Promise<NeonFunctionSnapshot[]> {
|
|
766
|
-
return this.call(
|
|
767
|
-
`listBranchFunctions(${projectId}/${branchId})`,
|
|
768
|
-
async () => {
|
|
769
|
-
const data = await this.getJson(
|
|
770
|
-
branchPreviewPath(projectId, branchId, "functions"),
|
|
771
|
-
);
|
|
772
|
-
const parsed = functionsListResponseSchema.parse(data);
|
|
773
|
-
return parsed.functions.map(functionToSnapshot);
|
|
774
|
-
},
|
|
775
|
-
{ projectId },
|
|
776
|
-
);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
async createBranchFunction(
|
|
780
|
-
projectId: string,
|
|
781
|
-
branchId: string,
|
|
782
|
-
input: { slug: string; name: string },
|
|
783
|
-
): Promise<NeonFunctionSnapshot> {
|
|
784
|
-
return this.call(
|
|
785
|
-
`createBranchFunction(${projectId}/${branchId}/${input.slug})`,
|
|
786
|
-
async () => {
|
|
787
|
-
const data = await this.postJson(
|
|
788
|
-
branchPreviewPath(projectId, branchId, "functions"),
|
|
789
|
-
{ slug: input.slug, name: input.name },
|
|
790
|
-
);
|
|
791
|
-
const parsed = functionResponseSchema.parse(data);
|
|
792
|
-
return functionToSnapshot(parsed.function);
|
|
793
|
-
},
|
|
794
|
-
{ projectId, mutating: true },
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
async deleteBranchFunction(
|
|
799
|
-
projectId: string,
|
|
800
|
-
branchId: string,
|
|
801
|
-
slug: string,
|
|
802
|
-
): Promise<void> {
|
|
803
|
-
await this.call(
|
|
804
|
-
`deleteBranchFunction(${projectId}/${branchId}/${slug})`,
|
|
805
|
-
async () => {
|
|
806
|
-
await this.deleteJson(
|
|
807
|
-
`${branchPreviewPath(projectId, branchId, "functions")}/${encodeURIComponent(slug)}`,
|
|
808
|
-
);
|
|
809
|
-
},
|
|
810
|
-
{ projectId, mutating: true },
|
|
811
|
-
);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
async deployBranchFunction(
|
|
815
|
-
projectId: string,
|
|
816
|
-
branchId: string,
|
|
817
|
-
slug: string,
|
|
818
|
-
input: DeployFunctionInput,
|
|
819
|
-
): Promise<NeonFunctionDeploymentSnapshot> {
|
|
820
|
-
return this.call(
|
|
821
|
-
`deployBranchFunction(${projectId}/${branchId}/${slug})`,
|
|
822
|
-
async () => {
|
|
823
|
-
const data = await this.postMultipart(
|
|
824
|
-
`${branchPreviewPath(projectId, branchId, "functions")}/${encodeURIComponent(slug)}/deployments`,
|
|
825
|
-
input,
|
|
826
|
-
);
|
|
827
|
-
const parsed = functionDeploymentResponseSchema.parse(data);
|
|
828
|
-
return deploymentToSnapshot(parsed.deployment);
|
|
829
|
-
},
|
|
830
|
-
{ projectId, mutating: true },
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// ─── Preview: AI Gateway ───────────────────────────────────────────────────
|
|
835
|
-
//
|
|
836
|
-
// TODO(neon-deploy): the AI Gateway routes are not yet in the public API spec we wired
|
|
837
|
-
// the rest of this adapter against. The paths below follow the established branch-scoped
|
|
838
|
-
// convention (`/projects/{p}/branches/{b}/ai-gateway`); confirm them against the real
|
|
839
|
-
// API (and the exact enable/disable verb + response shape) before relying on this in
|
|
840
|
-
// production, and swap to the typed `@neondatabase/api-client` method once it exists.
|
|
841
|
-
|
|
842
|
-
async getAiGatewayEnabled(
|
|
843
|
-
projectId: string,
|
|
844
|
-
branchId: string,
|
|
845
|
-
): Promise<boolean> {
|
|
846
|
-
try {
|
|
847
|
-
return await this.call(
|
|
848
|
-
`getAiGatewayEnabled(${projectId}/${branchId})`,
|
|
849
|
-
async () => {
|
|
850
|
-
const data = await this.getJson(
|
|
851
|
-
aiGatewayPath(projectId, branchId),
|
|
852
|
-
);
|
|
853
|
-
return aiGatewayEnabledFromResponse(data);
|
|
854
|
-
},
|
|
855
|
-
{ projectId },
|
|
856
|
-
);
|
|
857
|
-
} catch (err) {
|
|
858
|
-
if (err instanceof PlatformError && err.code === ErrorCode.NotFound)
|
|
859
|
-
return false;
|
|
860
|
-
throw err;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
async enableAiGateway(projectId: string, branchId: string): Promise<void> {
|
|
865
|
-
await this.call(
|
|
866
|
-
`enableAiGateway(${projectId}/${branchId})`,
|
|
867
|
-
async () => {
|
|
868
|
-
await this.postJson(aiGatewayPath(projectId, branchId), {
|
|
869
|
-
enabled: true,
|
|
870
|
-
});
|
|
871
|
-
},
|
|
872
|
-
{ projectId, mutating: true },
|
|
873
|
-
);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
async disableAiGateway(projectId: string, branchId: string): Promise<void> {
|
|
877
|
-
await this.call(
|
|
878
|
-
`disableAiGateway(${projectId}/${branchId})`,
|
|
879
|
-
async () => {
|
|
880
|
-
await this.deleteJson(aiGatewayPath(projectId, branchId));
|
|
881
|
-
},
|
|
882
|
-
{ projectId, mutating: true },
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function branchPreviewPath(
|
|
888
|
-
projectId: string,
|
|
889
|
-
branchId: string,
|
|
890
|
-
resource: "buckets" | "functions",
|
|
891
|
-
): string {
|
|
892
|
-
return `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/${resource}`;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function aiGatewayPath(projectId: string, branchId: string): string {
|
|
896
|
-
return `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/ai-gateway`;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function bucketToSnapshot(
|
|
900
|
-
bucket: z.infer<typeof bucketSchema>,
|
|
901
|
-
): NeonBucketSnapshot {
|
|
902
|
-
return {
|
|
903
|
-
name: bucket.name,
|
|
904
|
-
accessLevel: normalizeBucketAccessLevel(bucket.access_level),
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
/**
|
|
909
|
-
* The Neon API returns `access_level` as a free-form string (per the API guidelines:
|
|
910
|
-
* responses use plain strings, not enums). Map the known values onto our union and treat
|
|
911
|
-
* anything else as `private` — the safe default for an unrecognised access level.
|
|
912
|
-
*/
|
|
913
|
-
function normalizeBucketAccessLevel(
|
|
914
|
-
value: string | undefined,
|
|
915
|
-
): BucketAccessLevel {
|
|
916
|
-
return value === "public_read" ? "public_read" : "private";
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function functionToSnapshot(
|
|
920
|
-
fn: z.infer<typeof neonFunctionSchema>,
|
|
921
|
-
): NeonFunctionSnapshot {
|
|
922
|
-
const snapshot: NeonFunctionSnapshot = {
|
|
923
|
-
id: fn.id,
|
|
924
|
-
slug: fn.slug,
|
|
925
|
-
name: fn.name,
|
|
926
|
-
invocationUrl: fn.invocation_url,
|
|
927
|
-
};
|
|
928
|
-
if (fn.active_deployment) {
|
|
929
|
-
snapshot.activeDeploymentId = fn.active_deployment.id;
|
|
930
|
-
}
|
|
931
|
-
return snapshot;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function deploymentToSnapshot(
|
|
935
|
-
deployment: z.infer<typeof functionDeploymentSchema>,
|
|
936
|
-
): NeonFunctionDeploymentSnapshot {
|
|
937
|
-
return {
|
|
938
|
-
id: deployment.id,
|
|
939
|
-
status: normalizeDeploymentStatus(deployment.status),
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
function normalizeDeploymentStatus(
|
|
944
|
-
value: string,
|
|
945
|
-
): NeonFunctionDeploymentSnapshot["status"] {
|
|
946
|
-
switch (value) {
|
|
947
|
-
case "pending":
|
|
948
|
-
case "building":
|
|
949
|
-
case "completed":
|
|
950
|
-
case "failed":
|
|
951
|
-
return value;
|
|
952
|
-
default:
|
|
953
|
-
// Unknown status from a newer server — surface as `pending` rather than throwing,
|
|
954
|
-
// matching the API guideline that clients treat undocumented enum values leniently.
|
|
955
|
-
return "pending";
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
function aiGatewayEnabledFromResponse(data: unknown): boolean {
|
|
960
|
-
if (data !== null && typeof data === "object" && "enabled" in data) {
|
|
961
|
-
return (data as { enabled?: unknown }).enabled === true;
|
|
962
|
-
}
|
|
963
|
-
return false;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
function neonAuthResponseToSnapshot(
|
|
967
|
-
data: z.infer<typeof neonAuthResponseSchema>,
|
|
968
|
-
): NeonAuthSnapshot {
|
|
969
|
-
const snapshot: NeonAuthSnapshot = {
|
|
970
|
-
projectId: data.auth_provider_project_id,
|
|
971
|
-
jwksUrl: data.jwks_url,
|
|
972
|
-
};
|
|
973
|
-
if (data.pub_client_key !== undefined) {
|
|
974
|
-
snapshot.publishableClientKey = data.pub_client_key;
|
|
975
|
-
}
|
|
976
|
-
if (data.secret_server_key !== undefined) {
|
|
977
|
-
snapshot.secretServerKey = data.secret_server_key;
|
|
978
|
-
}
|
|
979
|
-
if (data.base_url) snapshot.baseUrl = data.base_url;
|
|
980
|
-
return snapshot;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
export function createNeonAuthRestInput(input: {
|
|
984
|
-
databaseName?: string;
|
|
985
|
-
}): CreateNeonAuthRestInput {
|
|
986
|
-
return {
|
|
987
|
-
auth_provider: "better_auth",
|
|
988
|
-
...(input.databaseName ? { database_name: input.databaseName } : {}),
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
async function readJsonBody(res: Response): Promise<unknown> {
|
|
993
|
-
const text = await res.text();
|
|
994
|
-
if (text.trim() === "") return {};
|
|
995
|
-
return JSON.parse(text);
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
function projectToSnapshot(
|
|
999
|
-
project: Project | ProjectListItem,
|
|
1000
|
-
): NeonProjectSnapshot {
|
|
1001
|
-
const defaults = project.default_endpoint_settings;
|
|
1002
|
-
const snapshot: NeonProjectSnapshot = {
|
|
1003
|
-
id: project.id,
|
|
1004
|
-
name: project.name,
|
|
1005
|
-
regionId: project.region_id,
|
|
1006
|
-
pgVersion: project.pg_version,
|
|
1007
|
-
};
|
|
1008
|
-
if (project.org_id) snapshot.orgId = project.org_id;
|
|
1009
|
-
if (defaults) {
|
|
1010
|
-
const compute = defaultsToComputeSettings(defaults);
|
|
1011
|
-
if (compute) snapshot.defaultEndpointSettings = compute;
|
|
1012
|
-
}
|
|
1013
|
-
return snapshot;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
function branchToSnapshot(branch: Branch): NeonBranchSnapshot {
|
|
1017
|
-
const snapshot: NeonBranchSnapshot = {
|
|
1018
|
-
id: branch.id,
|
|
1019
|
-
name: branch.name,
|
|
1020
|
-
isDefault: branch.default,
|
|
1021
|
-
protected: branch.protected === true,
|
|
1022
|
-
};
|
|
1023
|
-
if (branch.parent_id) snapshot.parentId = branch.parent_id;
|
|
1024
|
-
if (branch.expires_at) snapshot.expiresAt = branch.expires_at;
|
|
1025
|
-
return snapshot;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function endpointToSnapshot(endpoint: Endpoint): NeonEndpointSnapshot {
|
|
1029
|
-
return {
|
|
1030
|
-
id: endpoint.id,
|
|
1031
|
-
branchId: endpoint.branch_id,
|
|
1032
|
-
type:
|
|
1033
|
-
endpoint.type === EndpointType.ReadOnly
|
|
1034
|
-
? "read_only"
|
|
1035
|
-
: "read_write",
|
|
1036
|
-
autoscalingLimitMinCu:
|
|
1037
|
-
endpoint.autoscaling_limit_min_cu as ComputeSettings["autoscalingLimitMinCu"],
|
|
1038
|
-
autoscalingLimitMaxCu:
|
|
1039
|
-
endpoint.autoscaling_limit_max_cu as ComputeSettings["autoscalingLimitMaxCu"],
|
|
1040
|
-
suspendTimeout: formatSuspendTimeout(endpoint.suspend_timeout_seconds),
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
function roleToSnapshot(role: Role): NeonRoleSnapshot {
|
|
1045
|
-
return {
|
|
1046
|
-
name: role.name,
|
|
1047
|
-
branchId: role.branch_id,
|
|
1048
|
-
protected: role.protected ?? false,
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
function databaseToSnapshot(database: Database): NeonDatabaseSnapshot {
|
|
1053
|
-
return {
|
|
1054
|
-
name: database.name,
|
|
1055
|
-
branchId: database.branch_id,
|
|
1056
|
-
ownerName: database.owner_name,
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
function computeSettingsToDefaults(
|
|
1061
|
-
settings: ComputeSettings,
|
|
1062
|
-
): DefaultEndpointSettings {
|
|
1063
|
-
const out: DefaultEndpointSettings = {};
|
|
1064
|
-
if (settings.autoscalingLimitMinCu !== undefined)
|
|
1065
|
-
out.autoscaling_limit_min_cu = settings.autoscalingLimitMinCu;
|
|
1066
|
-
if (settings.autoscalingLimitMaxCu !== undefined)
|
|
1067
|
-
out.autoscaling_limit_max_cu = settings.autoscalingLimitMaxCu;
|
|
1068
|
-
if (settings.suspendTimeout !== undefined) {
|
|
1069
|
-
const parsed = parseSuspendTimeout(settings.suspendTimeout);
|
|
1070
|
-
if ("error" in parsed) {
|
|
1071
|
-
throw new PlatformError(
|
|
1072
|
-
ErrorCode.InvalidConfig,
|
|
1073
|
-
`Invalid suspendTimeout: ${parsed.error}`,
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
out.suspend_timeout_seconds = parsed.seconds;
|
|
1077
|
-
}
|
|
1078
|
-
return out;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
function computeSettingsToEndpointOptions(settings: ComputeSettings): {
|
|
1082
|
-
autoscaling_limit_min_cu?: number;
|
|
1083
|
-
autoscaling_limit_max_cu?: number;
|
|
1084
|
-
suspend_timeout_seconds?: number;
|
|
1085
|
-
} {
|
|
1086
|
-
const out: {
|
|
1087
|
-
autoscaling_limit_min_cu?: number;
|
|
1088
|
-
autoscaling_limit_max_cu?: number;
|
|
1089
|
-
suspend_timeout_seconds?: number;
|
|
1090
|
-
} = {};
|
|
1091
|
-
if (settings.autoscalingLimitMinCu !== undefined)
|
|
1092
|
-
out.autoscaling_limit_min_cu = settings.autoscalingLimitMinCu;
|
|
1093
|
-
if (settings.autoscalingLimitMaxCu !== undefined)
|
|
1094
|
-
out.autoscaling_limit_max_cu = settings.autoscalingLimitMaxCu;
|
|
1095
|
-
if (settings.suspendTimeout !== undefined) {
|
|
1096
|
-
const parsed = parseSuspendTimeout(settings.suspendTimeout);
|
|
1097
|
-
if ("error" in parsed) {
|
|
1098
|
-
throw new PlatformError(
|
|
1099
|
-
ErrorCode.InvalidConfig,
|
|
1100
|
-
`Invalid suspendTimeout: ${parsed.error}`,
|
|
1101
|
-
);
|
|
1102
|
-
}
|
|
1103
|
-
out.suspend_timeout_seconds = parsed.seconds;
|
|
1104
|
-
}
|
|
1105
|
-
return out;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
function defaultsToComputeSettings(
|
|
1109
|
-
defaults: DefaultEndpointSettings,
|
|
1110
|
-
): ComputeSettings | undefined {
|
|
1111
|
-
const out: ComputeSettings = {};
|
|
1112
|
-
if (defaults.autoscaling_limit_min_cu !== undefined)
|
|
1113
|
-
out.autoscalingLimitMinCu =
|
|
1114
|
-
defaults.autoscaling_limit_min_cu as ComputeSettings["autoscalingLimitMinCu"];
|
|
1115
|
-
if (defaults.autoscaling_limit_max_cu !== undefined)
|
|
1116
|
-
out.autoscalingLimitMaxCu =
|
|
1117
|
-
defaults.autoscaling_limit_max_cu as ComputeSettings["autoscalingLimitMaxCu"];
|
|
1118
|
-
if (defaults.suspend_timeout_seconds !== undefined)
|
|
1119
|
-
out.suspendTimeout = formatSuspendTimeout(
|
|
1120
|
-
defaults.suspend_timeout_seconds,
|
|
1121
|
-
);
|
|
1122
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
1123
|
-
}
|