@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
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { ErrorCode, PlatformError } from "./errors.js";
|
|
2
|
+
import { formatSuspendTimeout, parseSuspendTimeout } from "./duration.js";
|
|
3
|
+
import { wrapNeonError } from "./wrap-neon-error.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { EndpointType, createApiClient } from "@neondatabase/api-client";
|
|
6
|
+
//#region src/lib/neon-api-real.ts
|
|
7
|
+
const DEFAULT_NEON_API_BASE_URL = "https://console.neon.tech/api/v2";
|
|
8
|
+
const neonAuthResponseSchema = z.object({
|
|
9
|
+
auth_provider_project_id: z.string(),
|
|
10
|
+
pub_client_key: z.string().optional(),
|
|
11
|
+
secret_server_key: z.string().optional(),
|
|
12
|
+
jwks_url: z.string(),
|
|
13
|
+
base_url: z.string().optional()
|
|
14
|
+
});
|
|
15
|
+
const bucketSchema = z.object({
|
|
16
|
+
name: z.string(),
|
|
17
|
+
access_level: z.string().optional()
|
|
18
|
+
});
|
|
19
|
+
const bucketResponseSchema = z.object({ bucket: bucketSchema });
|
|
20
|
+
const bucketsListResponseSchema = z.object({ buckets: z.array(bucketSchema) });
|
|
21
|
+
const functionDeploymentSchema = z.object({
|
|
22
|
+
id: z.number(),
|
|
23
|
+
status: z.string()
|
|
24
|
+
});
|
|
25
|
+
const neonFunctionSchema = z.object({
|
|
26
|
+
id: z.string(),
|
|
27
|
+
slug: z.string(),
|
|
28
|
+
name: z.string(),
|
|
29
|
+
invocation_url: z.string(),
|
|
30
|
+
active_deployment: functionDeploymentSchema.optional()
|
|
31
|
+
});
|
|
32
|
+
const functionResponseSchema = z.object({ function: neonFunctionSchema });
|
|
33
|
+
const functionsListResponseSchema = z.object({ functions: z.array(neonFunctionSchema) });
|
|
34
|
+
const functionDeploymentResponseSchema = z.object({ deployment: functionDeploymentSchema });
|
|
35
|
+
/**
|
|
36
|
+
* Adapt `@neondatabase/api-client` to the narrow {@link NeonApi} façade used by the rest of
|
|
37
|
+
* this package. Constructs are restricted to whole-object read/write of just the fields we
|
|
38
|
+
* model in {@link Config}; anything else stays untouched on the remote.
|
|
39
|
+
*/
|
|
40
|
+
function createRealNeonApi(options) {
|
|
41
|
+
if (!options.apiKey || options.apiKey.trim() === "") throw new PlatformError(ErrorCode.MissingApiKey, ["createRealNeonApi requires a non-empty `apiKey`.", "Generate one at https://console.neon.tech/app/settings/api-keys and pass it as { apiKey: process.env.NEON_API_KEY }."].join(" "));
|
|
42
|
+
return new RealNeonApi(createApiClient({
|
|
43
|
+
apiKey: options.apiKey,
|
|
44
|
+
...options.baseUrl ? { baseURL: options.baseUrl } : {}
|
|
45
|
+
}), {
|
|
46
|
+
maxAttempts: options.retryOnLocked?.maxAttempts ?? 12,
|
|
47
|
+
initialDelayMs: options.retryOnLocked?.initialDelayMs ?? 250,
|
|
48
|
+
maxDelayMs: options.retryOnLocked?.maxDelayMs ?? 5e3
|
|
49
|
+
}, {
|
|
50
|
+
apiKey: options.apiKey,
|
|
51
|
+
baseUrl: options.baseUrl ?? DEFAULT_NEON_API_BASE_URL
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Retry a function whenever it throws an HTTP 423 (Locked) — Neon's signal that a prior
|
|
56
|
+
* mutation on the same resource is still in flight. Uses exponential backoff capped at
|
|
57
|
+
* `maxDelayMs`. Any other error (and the last attempt) propagates.
|
|
58
|
+
*
|
|
59
|
+
* Exported only for tests; production callers go through the wrapped {@link NeonApi}.
|
|
60
|
+
*/
|
|
61
|
+
async function retryOnLocked(fn, config) {
|
|
62
|
+
let delay = config.initialDelayMs;
|
|
63
|
+
let lastError;
|
|
64
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) try {
|
|
65
|
+
return await fn();
|
|
66
|
+
} catch (err) {
|
|
67
|
+
lastError = err;
|
|
68
|
+
if (readHttpStatusFromError(err) !== 423 || attempt === config.maxAttempts) throw err;
|
|
69
|
+
await sleep(delay);
|
|
70
|
+
delay = Math.min(delay * 2, config.maxDelayMs);
|
|
71
|
+
}
|
|
72
|
+
throw lastError;
|
|
73
|
+
}
|
|
74
|
+
function readHttpStatusFromError(err) {
|
|
75
|
+
if (err === null || typeof err !== "object") return void 0;
|
|
76
|
+
const response = err.response;
|
|
77
|
+
if (response === null || typeof response !== "object") return void 0;
|
|
78
|
+
const status = response.status;
|
|
79
|
+
return typeof status === "number" ? status : void 0;
|
|
80
|
+
}
|
|
81
|
+
function sleep(ms) {
|
|
82
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
83
|
+
}
|
|
84
|
+
var RealNeonApi = class {
|
|
85
|
+
client;
|
|
86
|
+
retryConfig;
|
|
87
|
+
restConfig;
|
|
88
|
+
constructor(client, retryConfig, restConfig) {
|
|
89
|
+
this.client = client;
|
|
90
|
+
this.retryConfig = retryConfig;
|
|
91
|
+
this.restConfig = restConfig;
|
|
92
|
+
}
|
|
93
|
+
retry(fn) {
|
|
94
|
+
return retryOnLocked(fn, this.retryConfig);
|
|
95
|
+
}
|
|
96
|
+
async call(op, fn, options = {}) {
|
|
97
|
+
try {
|
|
98
|
+
return options.mutating ? await this.retry(fn) : await fn();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw wrapNeonError(err, options.projectId ? {
|
|
101
|
+
op,
|
|
102
|
+
projectId: options.projectId
|
|
103
|
+
} : { op });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async listProjects(filter) {
|
|
107
|
+
return this.call(filter.orgId ? `listProjects(org=${filter.orgId})` : "listProjects", async () => {
|
|
108
|
+
const projects = [];
|
|
109
|
+
let cursor;
|
|
110
|
+
while (true) {
|
|
111
|
+
const res = await this.client.listProjects({
|
|
112
|
+
...filter.orgId ? { org_id: filter.orgId } : {},
|
|
113
|
+
...cursor ? { cursor } : {},
|
|
114
|
+
limit: 100
|
|
115
|
+
});
|
|
116
|
+
projects.push(...res.data.projects);
|
|
117
|
+
const next = res.data.pagination?.next;
|
|
118
|
+
if (!next || next === cursor) break;
|
|
119
|
+
cursor = next;
|
|
120
|
+
}
|
|
121
|
+
return projects.map(projectToSnapshot);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async getProject(projectId) {
|
|
125
|
+
return this.call(`getProject(${projectId})`, async () => {
|
|
126
|
+
return projectToSnapshot((await this.client.getProject(projectId)).data.project);
|
|
127
|
+
}, { projectId });
|
|
128
|
+
}
|
|
129
|
+
async createProject(input) {
|
|
130
|
+
const body = { project: {
|
|
131
|
+
name: input.name,
|
|
132
|
+
region_id: input.regionId,
|
|
133
|
+
...input.pgVersion !== void 0 ? { pg_version: input.pgVersion } : {},
|
|
134
|
+
...input.orgId ? { org_id: input.orgId } : {},
|
|
135
|
+
...input.defaultEndpointSettings ? { default_endpoint_settings: computeSettingsToDefaults(input.defaultEndpointSettings) } : {},
|
|
136
|
+
...input.defaultBranchName ? { branch: { name: input.defaultBranchName } } : {}
|
|
137
|
+
} };
|
|
138
|
+
return this.call(`createProject(${input.name})`, async () => {
|
|
139
|
+
return projectToSnapshot((await this.client.createProject(body)).data.project);
|
|
140
|
+
}, { mutating: true });
|
|
141
|
+
}
|
|
142
|
+
async updateProject(projectId, input) {
|
|
143
|
+
const body = { project: {
|
|
144
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
145
|
+
...input.defaultEndpointSettings ? { default_endpoint_settings: computeSettingsToDefaults(input.defaultEndpointSettings) } : {}
|
|
146
|
+
} };
|
|
147
|
+
return this.call(`updateProject(${projectId})`, async () => {
|
|
148
|
+
return projectToSnapshot((await this.client.updateProject(projectId, body)).data.project);
|
|
149
|
+
}, {
|
|
150
|
+
projectId,
|
|
151
|
+
mutating: true
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async listBranches(projectId) {
|
|
155
|
+
return this.call(`listBranches(${projectId})`, async () => {
|
|
156
|
+
const branches = [];
|
|
157
|
+
let cursor;
|
|
158
|
+
while (true) {
|
|
159
|
+
const res = await this.client.listProjectBranches({
|
|
160
|
+
projectId,
|
|
161
|
+
limit: 100,
|
|
162
|
+
...cursor ? { cursor } : {}
|
|
163
|
+
});
|
|
164
|
+
branches.push(...res.data.branches);
|
|
165
|
+
const next = res.data.pagination?.next;
|
|
166
|
+
if (!next || next === cursor) break;
|
|
167
|
+
cursor = next;
|
|
168
|
+
}
|
|
169
|
+
return branches.map(branchToSnapshot);
|
|
170
|
+
}, { projectId });
|
|
171
|
+
}
|
|
172
|
+
async createBranch(projectId, input) {
|
|
173
|
+
const endpointOptions = input.computeSettings ? {
|
|
174
|
+
type: EndpointType.ReadWrite,
|
|
175
|
+
...computeSettingsToEndpointOptions(input.computeSettings)
|
|
176
|
+
} : { type: EndpointType.ReadWrite };
|
|
177
|
+
const body = {
|
|
178
|
+
branch: {
|
|
179
|
+
name: input.name,
|
|
180
|
+
...input.parentId ? { parent_id: input.parentId } : {},
|
|
181
|
+
...input.expiresAt ? { expires_at: input.expiresAt } : {},
|
|
182
|
+
...input.protected !== void 0 ? { protected: input.protected } : {}
|
|
183
|
+
},
|
|
184
|
+
endpoints: [endpointOptions]
|
|
185
|
+
};
|
|
186
|
+
return this.call(`createBranch(${projectId}/${input.name})`, async () => {
|
|
187
|
+
const res = await this.client.createProjectBranch(projectId, body);
|
|
188
|
+
return {
|
|
189
|
+
branch: branchToSnapshot(res.data.branch),
|
|
190
|
+
endpoints: (res.data.endpoints ?? []).map(endpointToSnapshot)
|
|
191
|
+
};
|
|
192
|
+
}, {
|
|
193
|
+
projectId,
|
|
194
|
+
mutating: true
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async updateBranch(projectId, branchId, input) {
|
|
198
|
+
const branch = {};
|
|
199
|
+
if (input.name !== void 0) branch.name = input.name;
|
|
200
|
+
if (input.expiresAt !== void 0) branch.expires_at = input.expiresAt;
|
|
201
|
+
if (input.protected !== void 0) branch.protected = input.protected;
|
|
202
|
+
return this.call(`updateBranch(${projectId}/${branchId})`, async () => {
|
|
203
|
+
return branchToSnapshot((await this.client.updateProjectBranch(projectId, branchId, { branch })).data.branch);
|
|
204
|
+
}, {
|
|
205
|
+
projectId,
|
|
206
|
+
mutating: true
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async listEndpoints(projectId) {
|
|
210
|
+
return this.call(`listEndpoints(${projectId})`, async () => {
|
|
211
|
+
return (await this.client.listProjectEndpoints(projectId)).data.endpoints.map(endpointToSnapshot);
|
|
212
|
+
}, { projectId });
|
|
213
|
+
}
|
|
214
|
+
async updateEndpoint(projectId, endpointId, settings) {
|
|
215
|
+
const endpoint = computeSettingsToEndpointOptions(settings);
|
|
216
|
+
return this.call(`updateEndpoint(${projectId}/${endpointId})`, async () => {
|
|
217
|
+
return endpointToSnapshot((await this.client.updateProjectEndpoint(projectId, endpointId, { endpoint })).data.endpoint);
|
|
218
|
+
}, {
|
|
219
|
+
projectId,
|
|
220
|
+
mutating: true
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async listBranchRoles(projectId, branchId) {
|
|
224
|
+
return this.call(`listBranchRoles(${projectId}/${branchId})`, async () => {
|
|
225
|
+
return (await this.client.listProjectBranchRoles(projectId, branchId)).data.roles.map(roleToSnapshot);
|
|
226
|
+
}, { projectId });
|
|
227
|
+
}
|
|
228
|
+
async listBranchDatabases(projectId, branchId) {
|
|
229
|
+
return this.call(`listBranchDatabases(${projectId}/${branchId})`, async () => {
|
|
230
|
+
return (await this.client.listProjectBranchDatabases(projectId, branchId)).data.databases.map(databaseToSnapshot);
|
|
231
|
+
}, { projectId });
|
|
232
|
+
}
|
|
233
|
+
async getConnectionUri(projectId, input) {
|
|
234
|
+
const op = `getConnectionUri(${projectId}/${input.databaseName}@${input.roleName}${input.pooled ? " pooled" : ""})`;
|
|
235
|
+
const pooled = input.pooled === true;
|
|
236
|
+
return this.call(op, async () => {
|
|
237
|
+
return { uri: (await this.client.getConnectionUri({
|
|
238
|
+
projectId,
|
|
239
|
+
database_name: input.databaseName,
|
|
240
|
+
role_name: input.roleName,
|
|
241
|
+
...input.branchId ? { branch_id: input.branchId } : {},
|
|
242
|
+
...input.endpointId ? { endpoint_id: input.endpointId } : {},
|
|
243
|
+
pooled
|
|
244
|
+
})).data.uri };
|
|
245
|
+
}, { projectId });
|
|
246
|
+
}
|
|
247
|
+
async getNeonAuth(projectId, branchId) {
|
|
248
|
+
try {
|
|
249
|
+
return await this.call(`getNeonAuth(${projectId}/${branchId})`, async () => {
|
|
250
|
+
return neonAuthResponseToSnapshot((await this.client.getNeonAuth(projectId, branchId)).data);
|
|
251
|
+
}, { projectId });
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (err instanceof PlatformError && err.code === ErrorCode.NotFound) return null;
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async enableNeonAuth(projectId, branchId, input = {}) {
|
|
258
|
+
try {
|
|
259
|
+
return await this.call(`enableNeonAuth(${projectId}/${branchId})`, async () => {
|
|
260
|
+
const data = await this.postJson(`/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/auth`, createNeonAuthRestInput(input));
|
|
261
|
+
return neonAuthResponseToSnapshot(neonAuthResponseSchema.parse(data));
|
|
262
|
+
}, {
|
|
263
|
+
projectId,
|
|
264
|
+
mutating: true
|
|
265
|
+
});
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (err instanceof PlatformError && err.code === ErrorCode.Conflict) {
|
|
268
|
+
const existing = await this.getNeonAuth(projectId, branchId);
|
|
269
|
+
if (existing) return existing;
|
|
270
|
+
}
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async postJson(path, body) {
|
|
275
|
+
return this.request("POST", path, {
|
|
276
|
+
headers: { "Content-Type": "application/json" },
|
|
277
|
+
body: JSON.stringify(body)
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async getJson(path) {
|
|
281
|
+
return this.request("GET", path);
|
|
282
|
+
}
|
|
283
|
+
async deleteJson(path) {
|
|
284
|
+
return this.request("DELETE", path);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Upload a built function bundle via `multipart/form-data` to the deploy endpoint.
|
|
288
|
+
* Sends the bundle as the `file` field plus the deploy params Neon requires.
|
|
289
|
+
*/
|
|
290
|
+
async postMultipart(path, input) {
|
|
291
|
+
const form = new FormData();
|
|
292
|
+
form.set("file", new Blob([input.bundle], { type: "application/zip" }), "bundle.zip");
|
|
293
|
+
form.set("memory_mib", String(input.memoryMib));
|
|
294
|
+
form.set("concurrency", "1");
|
|
295
|
+
form.set("runtime", input.runtime);
|
|
296
|
+
for (const [key, value] of Object.entries(input.environment)) form.set(`environment[${key}]`, value);
|
|
297
|
+
return this.request("POST", path, { body: form });
|
|
298
|
+
}
|
|
299
|
+
async request(method, path, init = {}) {
|
|
300
|
+
const url = `${this.restConfig.baseUrl.replace(/\/+$/, "")}${path}`;
|
|
301
|
+
const res = await fetch(url, {
|
|
302
|
+
method,
|
|
303
|
+
headers: {
|
|
304
|
+
Authorization: `Bearer ${this.restConfig.apiKey}`,
|
|
305
|
+
...init.headers ?? {}
|
|
306
|
+
},
|
|
307
|
+
...init.body !== void 0 ? { body: init.body } : {}
|
|
308
|
+
});
|
|
309
|
+
const data = await readJsonBody(res);
|
|
310
|
+
if (!res.ok) throw { response: {
|
|
311
|
+
status: res.status,
|
|
312
|
+
data
|
|
313
|
+
} };
|
|
314
|
+
return data;
|
|
315
|
+
}
|
|
316
|
+
async getNeonDataApi(projectId, branchId, databaseName) {
|
|
317
|
+
try {
|
|
318
|
+
return await this.call(`getNeonDataApi(${projectId}/${branchId}/${databaseName})`, async () => {
|
|
319
|
+
return { url: (await this.client.getProjectBranchDataApi(projectId, branchId, databaseName)).data.url };
|
|
320
|
+
}, { projectId });
|
|
321
|
+
} catch (err) {
|
|
322
|
+
if (err instanceof PlatformError && err.code === ErrorCode.NotFound) return null;
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async enableProjectBranchDataApi(projectId, branchId, databaseName) {
|
|
327
|
+
try {
|
|
328
|
+
return await this.call(`enableProjectBranchDataApi(${projectId}/${branchId}/${databaseName})`, async () => {
|
|
329
|
+
return { url: (await this.client.createProjectBranchDataApi(projectId, branchId, databaseName, {})).data.url };
|
|
330
|
+
}, {
|
|
331
|
+
projectId,
|
|
332
|
+
mutating: true
|
|
333
|
+
});
|
|
334
|
+
} catch (err) {
|
|
335
|
+
if (err instanceof PlatformError && err.code === ErrorCode.Conflict) {
|
|
336
|
+
const existing = await this.getNeonDataApi(projectId, branchId, databaseName);
|
|
337
|
+
if (existing) return existing;
|
|
338
|
+
}
|
|
339
|
+
throw err;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async listBranchBuckets(projectId, branchId) {
|
|
343
|
+
return this.call(`listBranchBuckets(${projectId}/${branchId})`, async () => {
|
|
344
|
+
const data = await this.getJson(branchPreviewPath(projectId, branchId, "buckets"));
|
|
345
|
+
return bucketsListResponseSchema.parse(data).buckets.map(bucketToSnapshot);
|
|
346
|
+
}, { projectId });
|
|
347
|
+
}
|
|
348
|
+
async createBranchBucket(projectId, branchId, input) {
|
|
349
|
+
return this.call(`createBranchBucket(${projectId}/${branchId}/${input.name})`, async () => {
|
|
350
|
+
const data = await this.postJson(branchPreviewPath(projectId, branchId, "buckets"), {
|
|
351
|
+
name: input.name,
|
|
352
|
+
...input.accessLevel ? { access_level: input.accessLevel } : {}
|
|
353
|
+
});
|
|
354
|
+
return bucketToSnapshot(bucketResponseSchema.parse(data).bucket);
|
|
355
|
+
}, {
|
|
356
|
+
projectId,
|
|
357
|
+
mutating: true
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
async deleteBranchBucket(projectId, branchId, bucketName) {
|
|
361
|
+
await this.call(`deleteBranchBucket(${projectId}/${branchId}/${bucketName})`, async () => {
|
|
362
|
+
await this.deleteJson(`${branchPreviewPath(projectId, branchId, "buckets")}/${encodeURIComponent(bucketName)}`);
|
|
363
|
+
}, {
|
|
364
|
+
projectId,
|
|
365
|
+
mutating: true
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async listBranchFunctions(projectId, branchId) {
|
|
369
|
+
return this.call(`listBranchFunctions(${projectId}/${branchId})`, async () => {
|
|
370
|
+
const data = await this.getJson(branchPreviewPath(projectId, branchId, "functions"));
|
|
371
|
+
return functionsListResponseSchema.parse(data).functions.map(functionToSnapshot);
|
|
372
|
+
}, { projectId });
|
|
373
|
+
}
|
|
374
|
+
async createBranchFunction(projectId, branchId, input) {
|
|
375
|
+
return this.call(`createBranchFunction(${projectId}/${branchId}/${input.slug})`, async () => {
|
|
376
|
+
const data = await this.postJson(branchPreviewPath(projectId, branchId, "functions"), {
|
|
377
|
+
slug: input.slug,
|
|
378
|
+
name: input.name
|
|
379
|
+
});
|
|
380
|
+
return functionToSnapshot(functionResponseSchema.parse(data).function);
|
|
381
|
+
}, {
|
|
382
|
+
projectId,
|
|
383
|
+
mutating: true
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
async deleteBranchFunction(projectId, branchId, slug) {
|
|
387
|
+
await this.call(`deleteBranchFunction(${projectId}/${branchId}/${slug})`, async () => {
|
|
388
|
+
await this.deleteJson(`${branchPreviewPath(projectId, branchId, "functions")}/${encodeURIComponent(slug)}`);
|
|
389
|
+
}, {
|
|
390
|
+
projectId,
|
|
391
|
+
mutating: true
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
async deployBranchFunction(projectId, branchId, slug, input) {
|
|
395
|
+
return this.call(`deployBranchFunction(${projectId}/${branchId}/${slug})`, async () => {
|
|
396
|
+
const data = await this.postMultipart(`${branchPreviewPath(projectId, branchId, "functions")}/${encodeURIComponent(slug)}/deployments`, input);
|
|
397
|
+
return deploymentToSnapshot(functionDeploymentResponseSchema.parse(data).deployment);
|
|
398
|
+
}, {
|
|
399
|
+
projectId,
|
|
400
|
+
mutating: true
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
async getAiGatewayEnabled(projectId, branchId) {
|
|
404
|
+
try {
|
|
405
|
+
return await this.call(`getAiGatewayEnabled(${projectId}/${branchId})`, async () => {
|
|
406
|
+
return aiGatewayEnabledFromResponse(await this.getJson(aiGatewayPath(projectId, branchId)));
|
|
407
|
+
}, { projectId });
|
|
408
|
+
} catch (err) {
|
|
409
|
+
if (err instanceof PlatformError && err.code === ErrorCode.NotFound) return false;
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async enableAiGateway(projectId, branchId) {
|
|
414
|
+
await this.call(`enableAiGateway(${projectId}/${branchId})`, async () => {
|
|
415
|
+
await this.postJson(aiGatewayPath(projectId, branchId), { enabled: true });
|
|
416
|
+
}, {
|
|
417
|
+
projectId,
|
|
418
|
+
mutating: true
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
async disableAiGateway(projectId, branchId) {
|
|
422
|
+
await this.call(`disableAiGateway(${projectId}/${branchId})`, async () => {
|
|
423
|
+
await this.deleteJson(aiGatewayPath(projectId, branchId));
|
|
424
|
+
}, {
|
|
425
|
+
projectId,
|
|
426
|
+
mutating: true
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
function branchPreviewPath(projectId, branchId, resource) {
|
|
431
|
+
return `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/${resource}`;
|
|
432
|
+
}
|
|
433
|
+
function aiGatewayPath(projectId, branchId) {
|
|
434
|
+
return `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/ai-gateway`;
|
|
435
|
+
}
|
|
436
|
+
function bucketToSnapshot(bucket) {
|
|
437
|
+
return {
|
|
438
|
+
name: bucket.name,
|
|
439
|
+
accessLevel: normalizeBucketAccessLevel(bucket.access_level)
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* The Neon API returns `access_level` as a free-form string (per the API guidelines:
|
|
444
|
+
* responses use plain strings, not enums). Map the known values onto our union and treat
|
|
445
|
+
* anything else as `private` — the safe default for an unrecognised access level.
|
|
446
|
+
*/
|
|
447
|
+
function normalizeBucketAccessLevel(value) {
|
|
448
|
+
return value === "public_read" ? "public_read" : "private";
|
|
449
|
+
}
|
|
450
|
+
function functionToSnapshot(fn) {
|
|
451
|
+
const snapshot = {
|
|
452
|
+
id: fn.id,
|
|
453
|
+
slug: fn.slug,
|
|
454
|
+
name: fn.name,
|
|
455
|
+
invocationUrl: fn.invocation_url
|
|
456
|
+
};
|
|
457
|
+
if (fn.active_deployment) snapshot.activeDeploymentId = fn.active_deployment.id;
|
|
458
|
+
return snapshot;
|
|
459
|
+
}
|
|
460
|
+
function deploymentToSnapshot(deployment) {
|
|
461
|
+
return {
|
|
462
|
+
id: deployment.id,
|
|
463
|
+
status: normalizeDeploymentStatus(deployment.status)
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function normalizeDeploymentStatus(value) {
|
|
467
|
+
switch (value) {
|
|
468
|
+
case "pending":
|
|
469
|
+
case "building":
|
|
470
|
+
case "completed":
|
|
471
|
+
case "failed": return value;
|
|
472
|
+
default: return "pending";
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function aiGatewayEnabledFromResponse(data) {
|
|
476
|
+
if (data !== null && typeof data === "object" && "enabled" in data) return data.enabled === true;
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
function neonAuthResponseToSnapshot(data) {
|
|
480
|
+
const snapshot = {
|
|
481
|
+
projectId: data.auth_provider_project_id,
|
|
482
|
+
jwksUrl: data.jwks_url
|
|
483
|
+
};
|
|
484
|
+
if (data.pub_client_key !== void 0) snapshot.publishableClientKey = data.pub_client_key;
|
|
485
|
+
if (data.secret_server_key !== void 0) snapshot.secretServerKey = data.secret_server_key;
|
|
486
|
+
if (data.base_url) snapshot.baseUrl = data.base_url;
|
|
487
|
+
return snapshot;
|
|
488
|
+
}
|
|
489
|
+
function createNeonAuthRestInput(input) {
|
|
490
|
+
return {
|
|
491
|
+
auth_provider: "better_auth",
|
|
492
|
+
...input.databaseName ? { database_name: input.databaseName } : {}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async function readJsonBody(res) {
|
|
496
|
+
const text = await res.text();
|
|
497
|
+
if (text.trim() === "") return {};
|
|
498
|
+
return JSON.parse(text);
|
|
499
|
+
}
|
|
500
|
+
function projectToSnapshot(project) {
|
|
501
|
+
const defaults = project.default_endpoint_settings;
|
|
502
|
+
const snapshot = {
|
|
503
|
+
id: project.id,
|
|
504
|
+
name: project.name,
|
|
505
|
+
regionId: project.region_id,
|
|
506
|
+
pgVersion: project.pg_version
|
|
507
|
+
};
|
|
508
|
+
if (project.org_id) snapshot.orgId = project.org_id;
|
|
509
|
+
if (defaults) {
|
|
510
|
+
const compute = defaultsToComputeSettings(defaults);
|
|
511
|
+
if (compute) snapshot.defaultEndpointSettings = compute;
|
|
512
|
+
}
|
|
513
|
+
return snapshot;
|
|
514
|
+
}
|
|
515
|
+
function branchToSnapshot(branch) {
|
|
516
|
+
const snapshot = {
|
|
517
|
+
id: branch.id,
|
|
518
|
+
name: branch.name,
|
|
519
|
+
isDefault: branch.default,
|
|
520
|
+
protected: branch.protected === true
|
|
521
|
+
};
|
|
522
|
+
if (branch.parent_id) snapshot.parentId = branch.parent_id;
|
|
523
|
+
if (branch.expires_at) snapshot.expiresAt = branch.expires_at;
|
|
524
|
+
return snapshot;
|
|
525
|
+
}
|
|
526
|
+
function endpointToSnapshot(endpoint) {
|
|
527
|
+
return {
|
|
528
|
+
id: endpoint.id,
|
|
529
|
+
branchId: endpoint.branch_id,
|
|
530
|
+
type: endpoint.type === EndpointType.ReadOnly ? "read_only" : "read_write",
|
|
531
|
+
autoscalingLimitMinCu: endpoint.autoscaling_limit_min_cu,
|
|
532
|
+
autoscalingLimitMaxCu: endpoint.autoscaling_limit_max_cu,
|
|
533
|
+
suspendTimeout: formatSuspendTimeout(endpoint.suspend_timeout_seconds)
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function roleToSnapshot(role) {
|
|
537
|
+
return {
|
|
538
|
+
name: role.name,
|
|
539
|
+
branchId: role.branch_id,
|
|
540
|
+
protected: role.protected ?? false
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function databaseToSnapshot(database) {
|
|
544
|
+
return {
|
|
545
|
+
name: database.name,
|
|
546
|
+
branchId: database.branch_id,
|
|
547
|
+
ownerName: database.owner_name
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function computeSettingsToDefaults(settings) {
|
|
551
|
+
const out = {};
|
|
552
|
+
if (settings.autoscalingLimitMinCu !== void 0) out.autoscaling_limit_min_cu = settings.autoscalingLimitMinCu;
|
|
553
|
+
if (settings.autoscalingLimitMaxCu !== void 0) out.autoscaling_limit_max_cu = settings.autoscalingLimitMaxCu;
|
|
554
|
+
if (settings.suspendTimeout !== void 0) {
|
|
555
|
+
const parsed = parseSuspendTimeout(settings.suspendTimeout);
|
|
556
|
+
if ("error" in parsed) throw new PlatformError(ErrorCode.InvalidConfig, `Invalid suspendTimeout: ${parsed.error}`);
|
|
557
|
+
out.suspend_timeout_seconds = parsed.seconds;
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
function computeSettingsToEndpointOptions(settings) {
|
|
562
|
+
const out = {};
|
|
563
|
+
if (settings.autoscalingLimitMinCu !== void 0) out.autoscaling_limit_min_cu = settings.autoscalingLimitMinCu;
|
|
564
|
+
if (settings.autoscalingLimitMaxCu !== void 0) out.autoscaling_limit_max_cu = settings.autoscalingLimitMaxCu;
|
|
565
|
+
if (settings.suspendTimeout !== void 0) {
|
|
566
|
+
const parsed = parseSuspendTimeout(settings.suspendTimeout);
|
|
567
|
+
if ("error" in parsed) throw new PlatformError(ErrorCode.InvalidConfig, `Invalid suspendTimeout: ${parsed.error}`);
|
|
568
|
+
out.suspend_timeout_seconds = parsed.seconds;
|
|
569
|
+
}
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
function defaultsToComputeSettings(defaults) {
|
|
573
|
+
const out = {};
|
|
574
|
+
if (defaults.autoscaling_limit_min_cu !== void 0) out.autoscalingLimitMinCu = defaults.autoscaling_limit_min_cu;
|
|
575
|
+
if (defaults.autoscaling_limit_max_cu !== void 0) out.autoscalingLimitMaxCu = defaults.autoscaling_limit_max_cu;
|
|
576
|
+
if (defaults.suspend_timeout_seconds !== void 0) out.suspendTimeout = formatSuspendTimeout(defaults.suspend_timeout_seconds);
|
|
577
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
578
|
+
}
|
|
579
|
+
//#endregion
|
|
580
|
+
export { createNeonAuthRestInput, createRealNeonApi, retryOnLocked };
|
|
581
|
+
|
|
582
|
+
//# sourceMappingURL=neon-api-real.js.map
|