@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
|
@@ -0,0 +1,957 @@
|
|
|
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 { createNeonClient } from "@neon/sdk";
|
|
6
|
+
import { createProject, createProjectBranch, createProjectBranchDataApi, getConnectionUri, getNeonAuth, getProject, getProjectBranchDataApi, listProjectBranchDatabases, listProjectBranchRoles, listProjectBranches, listProjectEndpoints, listProjects, updateProject, updateProjectBranch, updateProjectBranchDataApi, updateProjectEndpoint } from "@neon/sdk/raw";
|
|
7
|
+
//#region src/lib/neon-api-real.ts
|
|
8
|
+
const DEFAULT_NEON_API_BASE_URL = "https://console.neon.tech/api/v2";
|
|
9
|
+
/**
|
|
10
|
+
* Unwrap a `@neon/sdk` raw `{ data, error, response }` result into the bare body, throwing
|
|
11
|
+
* on a non-2xx response. The thrown shape (`{ response: { status, data } }`) deliberately
|
|
12
|
+
* matches what the package's REST fallbacks already throw, so {@link wrapNeonError} and the
|
|
13
|
+
* 423 {@link retryOnLocked} retry keep working unchanged across both transports.
|
|
14
|
+
*/
|
|
15
|
+
function unwrap(result) {
|
|
16
|
+
const response = result.response;
|
|
17
|
+
if (!response || !response.ok) throw { response: {
|
|
18
|
+
status: response?.status,
|
|
19
|
+
data: result.error
|
|
20
|
+
} };
|
|
21
|
+
return result.data;
|
|
22
|
+
}
|
|
23
|
+
const neonAuthResponseSchema = z.object({
|
|
24
|
+
auth_provider_project_id: z.string(),
|
|
25
|
+
pub_client_key: z.string().optional(),
|
|
26
|
+
secret_server_key: z.string().optional(),
|
|
27
|
+
jwks_url: z.string(),
|
|
28
|
+
base_url: z.string().optional()
|
|
29
|
+
});
|
|
30
|
+
/** Map our camelCase {@link DataApiSettings} onto the Neon API's snake_case `DataAPISettings`. */
|
|
31
|
+
function dataApiSettingsToApi(settings) {
|
|
32
|
+
const out = {};
|
|
33
|
+
if (settings.dbAggregatesEnabled !== void 0) out.db_aggregates_enabled = settings.dbAggregatesEnabled;
|
|
34
|
+
if (settings.dbAnonRole !== void 0) out.db_anon_role = settings.dbAnonRole;
|
|
35
|
+
if (settings.dbExtraSearchPath !== void 0) out.db_extra_search_path = settings.dbExtraSearchPath;
|
|
36
|
+
if (settings.dbMaxRows !== void 0) out.db_max_rows = settings.dbMaxRows;
|
|
37
|
+
if (settings.dbSchemas !== void 0) out.db_schemas = settings.dbSchemas;
|
|
38
|
+
if (settings.jwtRoleClaimKey !== void 0) out.jwt_role_claim_key = settings.jwtRoleClaimKey;
|
|
39
|
+
if (settings.jwtCacheMaxLifetime !== void 0) out.jwt_cache_max_lifetime = settings.jwtCacheMaxLifetime;
|
|
40
|
+
if (settings.openapiMode !== void 0) out.openapi_mode = settings.openapiMode;
|
|
41
|
+
if (settings.serverCorsAllowedOrigins !== void 0) out.server_cors_allowed_origins = settings.serverCorsAllowedOrigins;
|
|
42
|
+
if (settings.serverTimingEnabled !== void 0) out.server_timing_enabled = settings.serverTimingEnabled;
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/** Narrow the API's free-form `openapi_mode` string to our literal union (else drop it). */
|
|
46
|
+
function normalizeOpenapiMode(value) {
|
|
47
|
+
return value === "ignore-privileges" || value === "disabled" ? value : void 0;
|
|
48
|
+
}
|
|
49
|
+
/** Map the Neon API's snake_case `DataAPISettings` back to our camelCase {@link DataApiSettings}. */
|
|
50
|
+
function dataApiSettingsFromApi(settings) {
|
|
51
|
+
if (!settings) return void 0;
|
|
52
|
+
const out = {};
|
|
53
|
+
if (settings.db_aggregates_enabled !== void 0) out.dbAggregatesEnabled = settings.db_aggregates_enabled;
|
|
54
|
+
if (settings.db_anon_role !== void 0) out.dbAnonRole = settings.db_anon_role;
|
|
55
|
+
if (settings.db_extra_search_path !== void 0) out.dbExtraSearchPath = settings.db_extra_search_path;
|
|
56
|
+
if (settings.db_max_rows !== void 0) out.dbMaxRows = settings.db_max_rows;
|
|
57
|
+
if (settings.db_schemas !== void 0) out.dbSchemas = settings.db_schemas;
|
|
58
|
+
if (settings.jwt_role_claim_key !== void 0) out.jwtRoleClaimKey = settings.jwt_role_claim_key;
|
|
59
|
+
if (settings.jwt_cache_max_lifetime !== void 0) out.jwtCacheMaxLifetime = settings.jwt_cache_max_lifetime;
|
|
60
|
+
if (settings.openapi_mode !== void 0) {
|
|
61
|
+
const mode = normalizeOpenapiMode(settings.openapi_mode);
|
|
62
|
+
if (mode !== void 0) out.openapiMode = mode;
|
|
63
|
+
}
|
|
64
|
+
if (settings.server_cors_allowed_origins !== void 0) out.serverCorsAllowedOrigins = settings.server_cors_allowed_origins;
|
|
65
|
+
if (settings.server_timing_enabled !== void 0) out.serverTimingEnabled = settings.server_timing_enabled;
|
|
66
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
67
|
+
}
|
|
68
|
+
/** Build the Neon API `DataAPICreateRequest` from our {@link EnableDataApiInput}. */
|
|
69
|
+
function dataApiCreateRequest(input) {
|
|
70
|
+
const req = {};
|
|
71
|
+
if (!input) return req;
|
|
72
|
+
if (input.authProvider !== void 0) req.auth_provider = input.authProvider === "neon" ? "neon_auth" : "external";
|
|
73
|
+
if (input.jwksUrl !== void 0) req.jwks_url = input.jwksUrl;
|
|
74
|
+
if (input.providerName !== void 0) req.provider_name = input.providerName;
|
|
75
|
+
if (input.jwtAudience !== void 0) req.jwt_audience = input.jwtAudience;
|
|
76
|
+
if (input.settings) {
|
|
77
|
+
const settings = dataApiSettingsToApi(input.settings);
|
|
78
|
+
if (Object.keys(settings).length > 0) req.settings = settings;
|
|
79
|
+
}
|
|
80
|
+
return req;
|
|
81
|
+
}
|
|
82
|
+
/** Map a `DataAPIReponse` (GET) onto our {@link NeonDataApiSnapshot}. */
|
|
83
|
+
function dataApiSnapshotFromResponse(data) {
|
|
84
|
+
const snapshot = { url: data.url };
|
|
85
|
+
if (data.status !== void 0) snapshot.status = data.status;
|
|
86
|
+
const settings = dataApiSettingsFromApi(data.settings);
|
|
87
|
+
if (settings) snapshot.settings = settings;
|
|
88
|
+
return snapshot;
|
|
89
|
+
}
|
|
90
|
+
const bucketSchema = z.object({
|
|
91
|
+
name: z.string(),
|
|
92
|
+
access_level: z.string().optional()
|
|
93
|
+
});
|
|
94
|
+
const bucketResponseSchema = z.object({ bucket: bucketSchema });
|
|
95
|
+
const bucketsListResponseSchema = z.object({ buckets: z.array(bucketSchema) });
|
|
96
|
+
const branchStorageSchema = z.object({
|
|
97
|
+
enabled: z.boolean().optional(),
|
|
98
|
+
s3_endpoint: z.string(),
|
|
99
|
+
region: z.string(),
|
|
100
|
+
force_path_style: z.boolean()
|
|
101
|
+
});
|
|
102
|
+
const functionDeploymentSchema = z.object({
|
|
103
|
+
id: z.number(),
|
|
104
|
+
status: z.string()
|
|
105
|
+
});
|
|
106
|
+
const neonFunctionSchema = z.object({
|
|
107
|
+
id: z.string(),
|
|
108
|
+
slug: z.string(),
|
|
109
|
+
name: z.string(),
|
|
110
|
+
invocation_url: z.string(),
|
|
111
|
+
active_deployment: functionDeploymentSchema.optional()
|
|
112
|
+
});
|
|
113
|
+
const functionsListResponseSchema = z.object({ functions: z.array(neonFunctionSchema) });
|
|
114
|
+
const functionDeploymentResponseSchema = z.object({ deployment: functionDeploymentSchema });
|
|
115
|
+
const credentialScopeSchema = z.enum([
|
|
116
|
+
"storage:read",
|
|
117
|
+
"storage:write",
|
|
118
|
+
"ai_gateway:invoke",
|
|
119
|
+
"functions:invoke"
|
|
120
|
+
]);
|
|
121
|
+
const createCredentialResponseSchema = z.object({
|
|
122
|
+
token_id: z.string(),
|
|
123
|
+
token_id_short: z.string(),
|
|
124
|
+
name: z.string().optional(),
|
|
125
|
+
api_token: z.string(),
|
|
126
|
+
s3_secret_access_key: z.string(),
|
|
127
|
+
scopes: z.array(credentialScopeSchema),
|
|
128
|
+
branch_id: z.string(),
|
|
129
|
+
created_at: z.string(),
|
|
130
|
+
expires_at: z.string().optional()
|
|
131
|
+
});
|
|
132
|
+
const credentialMetaSchema = z.object({
|
|
133
|
+
token_id: z.string(),
|
|
134
|
+
token_id_short: z.string(),
|
|
135
|
+
name: z.string().optional(),
|
|
136
|
+
scopes: z.array(credentialScopeSchema),
|
|
137
|
+
principal_type: z.enum(["user", "function"]),
|
|
138
|
+
function_id: z.string().optional(),
|
|
139
|
+
branch_id: z.string().optional(),
|
|
140
|
+
created_at: z.string(),
|
|
141
|
+
last_used_at: z.string().optional(),
|
|
142
|
+
revoked_at: z.string().optional(),
|
|
143
|
+
expires_at: z.string().optional()
|
|
144
|
+
});
|
|
145
|
+
const listCredentialsResponseSchema = z.object({ credentials: z.array(credentialMetaSchema) });
|
|
146
|
+
/**
|
|
147
|
+
* Adapt `@neon/sdk` (raw layer) to the narrow {@link NeonApi} façade used by the rest of
|
|
148
|
+
* this package. Constructs are restricted to whole-object read/write of just the fields we
|
|
149
|
+
* model in {@link Config}; anything else stays untouched on the remote.
|
|
150
|
+
*/
|
|
151
|
+
function createRealNeonApi(options) {
|
|
152
|
+
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(" "));
|
|
153
|
+
const client = createNeonClient({
|
|
154
|
+
apiKey: options.apiKey,
|
|
155
|
+
retries: 0,
|
|
156
|
+
...options.baseUrl ? { baseUrl: options.baseUrl } : {}
|
|
157
|
+
}).client;
|
|
158
|
+
return new RealNeonApi(client, {
|
|
159
|
+
maxAttempts: options.retryOnLocked?.maxAttempts ?? 12,
|
|
160
|
+
initialDelayMs: options.retryOnLocked?.initialDelayMs ?? 250,
|
|
161
|
+
maxDelayMs: options.retryOnLocked?.maxDelayMs ?? 5e3
|
|
162
|
+
}, {
|
|
163
|
+
apiKey: options.apiKey,
|
|
164
|
+
baseUrl: options.baseUrl ?? DEFAULT_NEON_API_BASE_URL
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Retry a function whenever it throws an HTTP 423 (Locked) — Neon's signal that a prior
|
|
169
|
+
* mutation on the same resource is still in flight. Uses exponential backoff capped at
|
|
170
|
+
* `maxDelayMs`. Any other error (and the last attempt) propagates.
|
|
171
|
+
*
|
|
172
|
+
* Exported only for tests; production callers go through the wrapped {@link NeonApi}.
|
|
173
|
+
*/
|
|
174
|
+
async function retryOnLocked(fn, config) {
|
|
175
|
+
let delay = config.initialDelayMs;
|
|
176
|
+
let lastError;
|
|
177
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) try {
|
|
178
|
+
return await fn();
|
|
179
|
+
} catch (err) {
|
|
180
|
+
lastError = err;
|
|
181
|
+
if (readHttpStatusFromError(err) !== 423 || attempt === config.maxAttempts) throw err;
|
|
182
|
+
await sleep(delay);
|
|
183
|
+
delay = Math.min(delay * 2, config.maxDelayMs);
|
|
184
|
+
}
|
|
185
|
+
throw lastError;
|
|
186
|
+
}
|
|
187
|
+
function readHttpStatusFromError(err) {
|
|
188
|
+
if (err === null || typeof err !== "object") return void 0;
|
|
189
|
+
const response = err.response;
|
|
190
|
+
if (response === null || typeof response !== "object") return void 0;
|
|
191
|
+
const status = response.status;
|
|
192
|
+
return typeof status === "number" ? status : void 0;
|
|
193
|
+
}
|
|
194
|
+
function sleep(ms) {
|
|
195
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
|
+
}
|
|
197
|
+
var RealNeonApi = class {
|
|
198
|
+
client;
|
|
199
|
+
retryConfig;
|
|
200
|
+
restConfig;
|
|
201
|
+
constructor(client, retryConfig, restConfig) {
|
|
202
|
+
this.client = client;
|
|
203
|
+
this.retryConfig = retryConfig;
|
|
204
|
+
this.restConfig = restConfig;
|
|
205
|
+
}
|
|
206
|
+
retry(fn) {
|
|
207
|
+
return retryOnLocked(fn, this.retryConfig);
|
|
208
|
+
}
|
|
209
|
+
async call(op, fn, options = {}) {
|
|
210
|
+
try {
|
|
211
|
+
return options.mutating ? await this.retry(fn) : await fn();
|
|
212
|
+
} catch (err) {
|
|
213
|
+
throw wrapNeonError(err, options.projectId ? {
|
|
214
|
+
op,
|
|
215
|
+
projectId: options.projectId
|
|
216
|
+
} : { op });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async listProjects(filter) {
|
|
220
|
+
return this.call(filter.orgId ? `listProjects(org=${filter.orgId})` : "listProjects", async () => {
|
|
221
|
+
const projects = [];
|
|
222
|
+
let cursor;
|
|
223
|
+
while (true) {
|
|
224
|
+
const body = unwrap(await listProjects({
|
|
225
|
+
client: this.client,
|
|
226
|
+
query: {
|
|
227
|
+
...filter.orgId ? { org_id: filter.orgId } : {},
|
|
228
|
+
...cursor ? { cursor } : {},
|
|
229
|
+
limit: 100
|
|
230
|
+
}
|
|
231
|
+
}));
|
|
232
|
+
projects.push(...body.projects);
|
|
233
|
+
const next = body.pagination?.next;
|
|
234
|
+
if (!next || next === cursor) break;
|
|
235
|
+
cursor = next;
|
|
236
|
+
}
|
|
237
|
+
return projects.map(projectToSnapshot);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async getProject(projectId) {
|
|
241
|
+
return this.call(`getProject(${projectId})`, async () => {
|
|
242
|
+
return projectToSnapshot(unwrap(await getProject({
|
|
243
|
+
client: this.client,
|
|
244
|
+
path: { project_id: projectId }
|
|
245
|
+
})).project);
|
|
246
|
+
}, { projectId });
|
|
247
|
+
}
|
|
248
|
+
async createProject(input) {
|
|
249
|
+
const body = { project: {
|
|
250
|
+
name: input.name,
|
|
251
|
+
region_id: input.regionId,
|
|
252
|
+
...input.pgVersion !== void 0 ? { pg_version: input.pgVersion } : {},
|
|
253
|
+
...input.orgId ? { org_id: input.orgId } : {},
|
|
254
|
+
...input.defaultEndpointSettings ? { default_endpoint_settings: computeSettingsToDefaults(input.defaultEndpointSettings) } : {},
|
|
255
|
+
...input.defaultBranchName ? { branch: { name: input.defaultBranchName } } : {}
|
|
256
|
+
} };
|
|
257
|
+
return this.call(`createProject(${input.name})`, async () => {
|
|
258
|
+
return projectToSnapshot(unwrap(await createProject({
|
|
259
|
+
client: this.client,
|
|
260
|
+
body
|
|
261
|
+
})).project);
|
|
262
|
+
}, { mutating: true });
|
|
263
|
+
}
|
|
264
|
+
async updateProject(projectId, input) {
|
|
265
|
+
const body = { project: {
|
|
266
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
267
|
+
...input.defaultEndpointSettings ? { default_endpoint_settings: computeSettingsToDefaults(input.defaultEndpointSettings) } : {}
|
|
268
|
+
} };
|
|
269
|
+
return this.call(`updateProject(${projectId})`, async () => {
|
|
270
|
+
return projectToSnapshot(unwrap(await updateProject({
|
|
271
|
+
client: this.client,
|
|
272
|
+
path: { project_id: projectId },
|
|
273
|
+
body
|
|
274
|
+
})).project);
|
|
275
|
+
}, {
|
|
276
|
+
projectId,
|
|
277
|
+
mutating: true
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async listBranches(projectId) {
|
|
281
|
+
return this.call(`listBranches(${projectId})`, async () => {
|
|
282
|
+
const branches = [];
|
|
283
|
+
let cursor;
|
|
284
|
+
while (true) {
|
|
285
|
+
const body = unwrap(await listProjectBranches({
|
|
286
|
+
client: this.client,
|
|
287
|
+
path: { project_id: projectId },
|
|
288
|
+
query: {
|
|
289
|
+
limit: 100,
|
|
290
|
+
...cursor ? { cursor } : {}
|
|
291
|
+
}
|
|
292
|
+
}));
|
|
293
|
+
branches.push(...body.branches);
|
|
294
|
+
const next = body.pagination?.next;
|
|
295
|
+
if (!next || next === cursor) break;
|
|
296
|
+
cursor = next;
|
|
297
|
+
}
|
|
298
|
+
return branches.map(branchToSnapshot);
|
|
299
|
+
}, { projectId });
|
|
300
|
+
}
|
|
301
|
+
async createBranch(projectId, input) {
|
|
302
|
+
const endpointOptions = input.computeSettings ? {
|
|
303
|
+
type: "read_write",
|
|
304
|
+
...computeSettingsToEndpointOptions(input.computeSettings)
|
|
305
|
+
} : { type: "read_write" };
|
|
306
|
+
const body = {
|
|
307
|
+
branch: {
|
|
308
|
+
name: input.name,
|
|
309
|
+
...input.parentId ? { parent_id: input.parentId } : {},
|
|
310
|
+
...input.expiresAt ? { expires_at: input.expiresAt } : {},
|
|
311
|
+
...input.protected !== void 0 ? { protected: input.protected } : {}
|
|
312
|
+
},
|
|
313
|
+
endpoints: [endpointOptions]
|
|
314
|
+
};
|
|
315
|
+
return this.call(`createBranch(${projectId}/${input.name})`, async () => {
|
|
316
|
+
const data = unwrap(await createProjectBranch({
|
|
317
|
+
client: this.client,
|
|
318
|
+
path: { project_id: projectId },
|
|
319
|
+
body
|
|
320
|
+
}));
|
|
321
|
+
return {
|
|
322
|
+
branch: branchToSnapshot(data.branch),
|
|
323
|
+
endpoints: (data.endpoints ?? []).map(endpointToSnapshot)
|
|
324
|
+
};
|
|
325
|
+
}, {
|
|
326
|
+
projectId,
|
|
327
|
+
mutating: true
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
async updateBranch(projectId, branchId, input) {
|
|
331
|
+
const branch = {};
|
|
332
|
+
if (input.name !== void 0) branch.name = input.name;
|
|
333
|
+
if (input.expiresAt !== void 0) branch.expires_at = input.expiresAt;
|
|
334
|
+
if (input.protected !== void 0) branch.protected = input.protected;
|
|
335
|
+
return this.call(`updateBranch(${projectId}/${branchId})`, async () => {
|
|
336
|
+
return branchToSnapshot(unwrap(await updateProjectBranch({
|
|
337
|
+
client: this.client,
|
|
338
|
+
path: {
|
|
339
|
+
project_id: projectId,
|
|
340
|
+
branch_id: branchId
|
|
341
|
+
},
|
|
342
|
+
body: { branch }
|
|
343
|
+
})).branch);
|
|
344
|
+
}, {
|
|
345
|
+
projectId,
|
|
346
|
+
mutating: true
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async listEndpoints(projectId) {
|
|
350
|
+
return this.call(`listEndpoints(${projectId})`, async () => {
|
|
351
|
+
return unwrap(await listProjectEndpoints({
|
|
352
|
+
client: this.client,
|
|
353
|
+
path: { project_id: projectId }
|
|
354
|
+
})).endpoints.map(endpointToSnapshot);
|
|
355
|
+
}, { projectId });
|
|
356
|
+
}
|
|
357
|
+
async updateEndpoint(projectId, endpointId, settings) {
|
|
358
|
+
const endpoint = computeSettingsToEndpointOptions(settings);
|
|
359
|
+
return this.call(`updateEndpoint(${projectId}/${endpointId})`, async () => {
|
|
360
|
+
return endpointToSnapshot(unwrap(await updateProjectEndpoint({
|
|
361
|
+
client: this.client,
|
|
362
|
+
path: {
|
|
363
|
+
project_id: projectId,
|
|
364
|
+
endpoint_id: endpointId
|
|
365
|
+
},
|
|
366
|
+
body: { endpoint }
|
|
367
|
+
})).endpoint);
|
|
368
|
+
}, {
|
|
369
|
+
projectId,
|
|
370
|
+
mutating: true
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
async listBranchRoles(projectId, branchId) {
|
|
374
|
+
return this.call(`listBranchRoles(${projectId}/${branchId})`, async () => {
|
|
375
|
+
return unwrap(await listProjectBranchRoles({
|
|
376
|
+
client: this.client,
|
|
377
|
+
path: {
|
|
378
|
+
project_id: projectId,
|
|
379
|
+
branch_id: branchId
|
|
380
|
+
}
|
|
381
|
+
})).roles.map(roleToSnapshot);
|
|
382
|
+
}, { projectId });
|
|
383
|
+
}
|
|
384
|
+
async listBranchDatabases(projectId, branchId) {
|
|
385
|
+
return this.call(`listBranchDatabases(${projectId}/${branchId})`, async () => {
|
|
386
|
+
return unwrap(await listProjectBranchDatabases({
|
|
387
|
+
client: this.client,
|
|
388
|
+
path: {
|
|
389
|
+
project_id: projectId,
|
|
390
|
+
branch_id: branchId
|
|
391
|
+
}
|
|
392
|
+
})).databases.map(databaseToSnapshot);
|
|
393
|
+
}, { projectId });
|
|
394
|
+
}
|
|
395
|
+
async getConnectionUri(projectId, input) {
|
|
396
|
+
const op = `getConnectionUri(${projectId}/${input.databaseName}@${input.roleName}${input.pooled ? " pooled" : ""})`;
|
|
397
|
+
const pooled = input.pooled === true;
|
|
398
|
+
return this.call(op, async () => {
|
|
399
|
+
return { uri: unwrap(await getConnectionUri({
|
|
400
|
+
client: this.client,
|
|
401
|
+
path: { project_id: projectId },
|
|
402
|
+
query: {
|
|
403
|
+
database_name: input.databaseName,
|
|
404
|
+
role_name: input.roleName,
|
|
405
|
+
...input.branchId ? { branch_id: input.branchId } : {},
|
|
406
|
+
...input.endpointId ? { endpoint_id: input.endpointId } : {},
|
|
407
|
+
pooled
|
|
408
|
+
}
|
|
409
|
+
})).uri };
|
|
410
|
+
}, { projectId });
|
|
411
|
+
}
|
|
412
|
+
async getNeonAuth(projectId, branchId) {
|
|
413
|
+
try {
|
|
414
|
+
return await this.call(`getNeonAuth(${projectId}/${branchId})`, async () => {
|
|
415
|
+
const data = unwrap(await getNeonAuth({
|
|
416
|
+
client: this.client,
|
|
417
|
+
path: {
|
|
418
|
+
project_id: projectId,
|
|
419
|
+
branch_id: branchId
|
|
420
|
+
}
|
|
421
|
+
}));
|
|
422
|
+
return neonAuthResponseToSnapshot(neonAuthResponseSchema.parse(data));
|
|
423
|
+
}, { projectId });
|
|
424
|
+
} catch (err) {
|
|
425
|
+
if (err instanceof PlatformError && err.code === ErrorCode.NotFound) return null;
|
|
426
|
+
throw err;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async enableNeonAuth(projectId, branchId, input = {}) {
|
|
430
|
+
try {
|
|
431
|
+
return await this.call(`enableNeonAuth(${projectId}/${branchId})`, async () => {
|
|
432
|
+
const data = await this.postJson(`/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/auth`, createNeonAuthRestInput(input));
|
|
433
|
+
return neonAuthResponseToSnapshot(neonAuthResponseSchema.parse(data));
|
|
434
|
+
}, {
|
|
435
|
+
projectId,
|
|
436
|
+
mutating: true
|
|
437
|
+
});
|
|
438
|
+
} catch (err) {
|
|
439
|
+
if (err instanceof PlatformError && err.code === ErrorCode.Conflict) {
|
|
440
|
+
const existing = await this.getNeonAuth(projectId, branchId);
|
|
441
|
+
if (existing) return existing;
|
|
442
|
+
}
|
|
443
|
+
throw err;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async postJson(path, body) {
|
|
447
|
+
return this.request("POST", path, {
|
|
448
|
+
headers: { "Content-Type": "application/json" },
|
|
449
|
+
body: JSON.stringify(body)
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async getJson(path) {
|
|
453
|
+
return this.request("GET", path);
|
|
454
|
+
}
|
|
455
|
+
async deleteJson(path) {
|
|
456
|
+
return this.request("DELETE", path);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Upload a built function bundle via `multipart/form-data` to the deploy endpoint
|
|
460
|
+
* (`POST .../functions/{slug}/deployments`). Body shape lives in the pure
|
|
461
|
+
* {@link buildFunctionDeployForm} helper so it can be unit-tested against the spec.
|
|
462
|
+
*/
|
|
463
|
+
async postMultipart(path, input) {
|
|
464
|
+
return this.request("POST", path, { body: buildFunctionDeployForm(input) });
|
|
465
|
+
}
|
|
466
|
+
async request(method, path, init = {}) {
|
|
467
|
+
const url = `${this.restConfig.baseUrl.replace(/\/+$/, "")}${path}`;
|
|
468
|
+
const res = await fetch(url, {
|
|
469
|
+
method,
|
|
470
|
+
headers: {
|
|
471
|
+
Authorization: `Bearer ${this.restConfig.apiKey}`,
|
|
472
|
+
...init.headers ?? {}
|
|
473
|
+
},
|
|
474
|
+
...init.body !== void 0 ? { body: init.body } : {}
|
|
475
|
+
});
|
|
476
|
+
const data = await readJsonBody(res);
|
|
477
|
+
if (!res.ok) throw { response: {
|
|
478
|
+
status: res.status,
|
|
479
|
+
data
|
|
480
|
+
} };
|
|
481
|
+
return data;
|
|
482
|
+
}
|
|
483
|
+
async getNeonDataApi(projectId, branchId, databaseName) {
|
|
484
|
+
try {
|
|
485
|
+
return await this.call(`getNeonDataApi(${projectId}/${branchId}/${databaseName})`, async () => dataApiSnapshotFromResponse(unwrap(await getProjectBranchDataApi({
|
|
486
|
+
client: this.client,
|
|
487
|
+
path: {
|
|
488
|
+
project_id: projectId,
|
|
489
|
+
branch_id: branchId,
|
|
490
|
+
database_name: databaseName
|
|
491
|
+
}
|
|
492
|
+
}))), { projectId });
|
|
493
|
+
} catch (err) {
|
|
494
|
+
if (err instanceof PlatformError && err.code === ErrorCode.NotFound) return null;
|
|
495
|
+
throw err;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async enableProjectBranchDataApi(projectId, branchId, databaseName, input) {
|
|
499
|
+
try {
|
|
500
|
+
return await this.call(`enableProjectBranchDataApi(${projectId}/${branchId}/${databaseName})`, async () => {
|
|
501
|
+
return { url: unwrap(await createProjectBranchDataApi({
|
|
502
|
+
client: this.client,
|
|
503
|
+
path: {
|
|
504
|
+
project_id: projectId,
|
|
505
|
+
branch_id: branchId,
|
|
506
|
+
database_name: databaseName
|
|
507
|
+
},
|
|
508
|
+
body: dataApiCreateRequest(input)
|
|
509
|
+
})).url };
|
|
510
|
+
}, {
|
|
511
|
+
projectId,
|
|
512
|
+
mutating: true
|
|
513
|
+
});
|
|
514
|
+
} catch (err) {
|
|
515
|
+
if (err instanceof PlatformError && err.code === ErrorCode.Conflict) {
|
|
516
|
+
const existing = await this.getNeonDataApi(projectId, branchId, databaseName);
|
|
517
|
+
if (existing) return existing;
|
|
518
|
+
}
|
|
519
|
+
throw err;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async updateProjectBranchDataApi(projectId, branchId, databaseName, settings) {
|
|
523
|
+
return await this.call(`updateProjectBranchDataApi(${projectId}/${branchId}/${databaseName})`, async () => {
|
|
524
|
+
unwrap(await updateProjectBranchDataApi({
|
|
525
|
+
client: this.client,
|
|
526
|
+
path: {
|
|
527
|
+
project_id: projectId,
|
|
528
|
+
branch_id: branchId,
|
|
529
|
+
database_name: databaseName
|
|
530
|
+
},
|
|
531
|
+
body: { settings: dataApiSettingsToApi(settings) }
|
|
532
|
+
}));
|
|
533
|
+
return dataApiSnapshotFromResponse(unwrap(await getProjectBranchDataApi({
|
|
534
|
+
client: this.client,
|
|
535
|
+
path: {
|
|
536
|
+
project_id: projectId,
|
|
537
|
+
branch_id: branchId,
|
|
538
|
+
database_name: databaseName
|
|
539
|
+
}
|
|
540
|
+
})));
|
|
541
|
+
}, {
|
|
542
|
+
projectId,
|
|
543
|
+
mutating: true
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async listBranchBuckets(projectId, branchId) {
|
|
547
|
+
try {
|
|
548
|
+
return await this.call(`listBranchBuckets(${projectId}/${branchId})`, async () => {
|
|
549
|
+
const data = await this.getJson(branchPreviewPath(projectId, branchId, "buckets"));
|
|
550
|
+
return bucketsListResponseSchema.parse(data).buckets.map(bucketToSnapshot);
|
|
551
|
+
}, { projectId });
|
|
552
|
+
} catch (err) {
|
|
553
|
+
throw previewUnavailableError(err, "Object storage (buckets)");
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async createBranchBucket(projectId, branchId, input) {
|
|
557
|
+
return this.call(`createBranchBucket(${projectId}/${branchId}/${input.name})`, async () => {
|
|
558
|
+
const data = await this.postJson(branchPreviewPath(projectId, branchId, "buckets"), {
|
|
559
|
+
name: input.name,
|
|
560
|
+
...input.accessLevel ? { access_level: input.accessLevel } : {}
|
|
561
|
+
});
|
|
562
|
+
return bucketToSnapshot(bucketResponseSchema.parse(data).bucket);
|
|
563
|
+
}, {
|
|
564
|
+
projectId,
|
|
565
|
+
mutating: true
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
async deleteBranchBucket(projectId, branchId, bucketName) {
|
|
569
|
+
await this.call(`deleteBranchBucket(${projectId}/${branchId}/${bucketName})`, async () => {
|
|
570
|
+
await this.deleteJson(`${branchPreviewPath(projectId, branchId, "buckets")}/${encodeURIComponent(bucketName)}`);
|
|
571
|
+
}, {
|
|
572
|
+
projectId,
|
|
573
|
+
mutating: true
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
async getProjectBranchStorage(projectId, branchId) {
|
|
577
|
+
try {
|
|
578
|
+
return await this.call(`getProjectBranchStorage(${projectId}/${branchId})`, async () => {
|
|
579
|
+
const data = await this.getJson(`/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/storage`);
|
|
580
|
+
const parsed = branchStorageSchema.parse(data);
|
|
581
|
+
return {
|
|
582
|
+
s3Endpoint: parsed.s3_endpoint,
|
|
583
|
+
region: parsed.region,
|
|
584
|
+
forcePathStyle: parsed.force_path_style
|
|
585
|
+
};
|
|
586
|
+
}, { projectId });
|
|
587
|
+
} catch (err) {
|
|
588
|
+
if (err instanceof PlatformError && err.code === ErrorCode.NotFound) return null;
|
|
589
|
+
throw previewUnavailableError(err, "Object storage");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async listBranchFunctions(projectId, branchId) {
|
|
593
|
+
try {
|
|
594
|
+
return await this.call(`listBranchFunctions(${projectId}/${branchId})`, async () => {
|
|
595
|
+
const data = await this.getJson(branchPreviewPath(projectId, branchId, "functions"));
|
|
596
|
+
return functionsListResponseSchema.parse(data).functions.map(functionToSnapshot);
|
|
597
|
+
}, { projectId });
|
|
598
|
+
} catch (err) {
|
|
599
|
+
throw previewUnavailableError(err, "Functions");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async deleteBranchFunction(projectId, branchId, slug) {
|
|
603
|
+
await this.call(`deleteBranchFunction(${projectId}/${branchId}/${slug})`, async () => {
|
|
604
|
+
await this.deleteJson(`${branchPreviewPath(projectId, branchId, "functions")}/${encodeURIComponent(slug)}`);
|
|
605
|
+
}, {
|
|
606
|
+
projectId,
|
|
607
|
+
mutating: true
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
async deployBranchFunction(projectId, branchId, slug, input) {
|
|
611
|
+
return this.call(`deployBranchFunction(${projectId}/${branchId}/${slug})`, async () => {
|
|
612
|
+
const data = await this.postMultipart(`${branchPreviewPath(projectId, branchId, "functions")}/${encodeURIComponent(slug)}/deployments`, input);
|
|
613
|
+
return deploymentToSnapshot(functionDeploymentResponseSchema.parse(data).deployment);
|
|
614
|
+
}, {
|
|
615
|
+
projectId,
|
|
616
|
+
mutating: true
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
async createCredential(projectId, branchId, input) {
|
|
620
|
+
try {
|
|
621
|
+
return await this.call(`createCredential(${projectId}/${branchId})`, async () => {
|
|
622
|
+
const data = await this.postJson(credentialsPath(projectId, branchId), {
|
|
623
|
+
scopes: input.scopes,
|
|
624
|
+
principal_type: input.principalType,
|
|
625
|
+
...input.functionId ? { function_id: input.functionId } : {},
|
|
626
|
+
...input.name ? { name: input.name } : {}
|
|
627
|
+
});
|
|
628
|
+
return createCredentialToSnapshot(createCredentialResponseSchema.parse(data));
|
|
629
|
+
}, {
|
|
630
|
+
projectId,
|
|
631
|
+
mutating: true
|
|
632
|
+
});
|
|
633
|
+
} catch (err) {
|
|
634
|
+
throw previewUnavailableError(err, "Branch credentials");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async listCredentials(projectId, branchId) {
|
|
638
|
+
try {
|
|
639
|
+
return await this.call(`listCredentials(${projectId}/${branchId})`, async () => {
|
|
640
|
+
const data = await this.getJson(credentialsPath(projectId, branchId));
|
|
641
|
+
return listCredentialsResponseSchema.parse(data).credentials.map(credentialMetaToSnapshot);
|
|
642
|
+
}, { projectId });
|
|
643
|
+
} catch (err) {
|
|
644
|
+
throw previewUnavailableError(err, "Branch credentials");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async revokeCredential(projectId, branchId, tokenId) {
|
|
648
|
+
await this.call(`revokeCredential(${projectId}/${branchId}/${tokenId})`, async () => {
|
|
649
|
+
await this.deleteJson(`${credentialsPath(projectId, branchId)}/${encodeURIComponent(tokenId)}`);
|
|
650
|
+
}, {
|
|
651
|
+
projectId,
|
|
652
|
+
mutating: true
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
function branchPreviewPath(projectId, branchId, resource) {
|
|
657
|
+
return `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/${resource}`;
|
|
658
|
+
}
|
|
659
|
+
function credentialsPath(projectId, branchId) {
|
|
660
|
+
return `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/credentials`;
|
|
661
|
+
}
|
|
662
|
+
function createCredentialToSnapshot(data) {
|
|
663
|
+
const snapshot = {
|
|
664
|
+
tokenId: data.token_id,
|
|
665
|
+
tokenIdShort: data.token_id_short,
|
|
666
|
+
apiToken: data.api_token,
|
|
667
|
+
s3SecretAccessKey: data.s3_secret_access_key,
|
|
668
|
+
scopes: data.scopes,
|
|
669
|
+
branchId: data.branch_id,
|
|
670
|
+
createdAt: data.created_at
|
|
671
|
+
};
|
|
672
|
+
if (data.name !== void 0) snapshot.name = data.name;
|
|
673
|
+
if (data.expires_at !== void 0) snapshot.expiresAt = data.expires_at;
|
|
674
|
+
return snapshot;
|
|
675
|
+
}
|
|
676
|
+
function credentialMetaToSnapshot(data) {
|
|
677
|
+
const snapshot = {
|
|
678
|
+
tokenId: data.token_id,
|
|
679
|
+
tokenIdShort: data.token_id_short,
|
|
680
|
+
scopes: data.scopes,
|
|
681
|
+
principalType: data.principal_type,
|
|
682
|
+
createdAt: data.created_at
|
|
683
|
+
};
|
|
684
|
+
if (data.name !== void 0) snapshot.name = data.name;
|
|
685
|
+
if (data.function_id !== void 0) snapshot.functionId = data.function_id;
|
|
686
|
+
if (data.branch_id !== void 0) snapshot.branchId = data.branch_id;
|
|
687
|
+
if (data.last_used_at !== void 0) snapshot.lastUsedAt = data.last_used_at;
|
|
688
|
+
if (data.revoked_at !== void 0) snapshot.revokedAt = data.revoked_at;
|
|
689
|
+
if (data.expires_at !== void 0) snapshot.expiresAt = data.expires_at;
|
|
690
|
+
return snapshot;
|
|
691
|
+
}
|
|
692
|
+
function bucketToSnapshot(bucket) {
|
|
693
|
+
return {
|
|
694
|
+
name: bucket.name,
|
|
695
|
+
accessLevel: normalizeBucketAccessLevel(bucket.access_level)
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* The Neon API returns `access_level` as a free-form string (per the API guidelines:
|
|
700
|
+
* responses use plain strings, not enums). Map the known values onto our union and treat
|
|
701
|
+
* anything else as `private` — the safe default for an unrecognised access level.
|
|
702
|
+
*/
|
|
703
|
+
function normalizeBucketAccessLevel(value) {
|
|
704
|
+
return value === "public_read" ? "public_read" : "private";
|
|
705
|
+
}
|
|
706
|
+
function functionToSnapshot(fn) {
|
|
707
|
+
const snapshot = {
|
|
708
|
+
id: fn.id,
|
|
709
|
+
slug: fn.slug,
|
|
710
|
+
name: fn.name,
|
|
711
|
+
invocationUrl: fn.invocation_url
|
|
712
|
+
};
|
|
713
|
+
if (fn.active_deployment) snapshot.activeDeploymentId = fn.active_deployment.id;
|
|
714
|
+
return snapshot;
|
|
715
|
+
}
|
|
716
|
+
function deploymentToSnapshot(deployment) {
|
|
717
|
+
return {
|
|
718
|
+
id: deployment.id,
|
|
719
|
+
status: normalizeDeploymentStatus(deployment.status)
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function normalizeDeploymentStatus(value) {
|
|
723
|
+
switch (value) {
|
|
724
|
+
case "pending":
|
|
725
|
+
case "building":
|
|
726
|
+
case "completed":
|
|
727
|
+
case "failed": return value;
|
|
728
|
+
default: return "pending";
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Whether an error from a Preview-feature read means the feature simply isn't available
|
|
733
|
+
* for this project/branch/region (as opposed to a real, transient failure). Neon signals
|
|
734
|
+
* this a few ways: a 404 "this route does not exist" (the route isn't deployed at all), or
|
|
735
|
+
* a 503/4xx whose message says the platform feature is "not available" / "not enabled".
|
|
736
|
+
*
|
|
737
|
+
* Callers do **not** swallow this into an empty result — touching a Preview feature that
|
|
738
|
+
* isn't available is surfaced as a {@link previewUnavailableError} so `plan` / `status` /
|
|
739
|
+
* `pull` (and `neon dev`) fail clearly instead of, say, planning to create resources the
|
|
740
|
+
* API will refuse to create.
|
|
741
|
+
*/
|
|
742
|
+
function isPreviewFeatureUnavailable(err) {
|
|
743
|
+
if (!(err instanceof PlatformError)) return false;
|
|
744
|
+
const status = err.details.status;
|
|
745
|
+
const message = typeof err.details.neonMessage === "string" ? err.details.neonMessage.toLowerCase() : "";
|
|
746
|
+
return (message.includes("not available") || message.includes("does not exist") || message.includes("not enabled")) && (status === 503 || status === 404 || status === 501);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Reason phrase for the handful of HTTP statuses a Preview-feature read can surface as
|
|
750
|
+
* "unavailable". Used to print a short `HTTP <status> <reason>` line (not a stack trace),
|
|
751
|
+
* so the message reads like the API response the user would see in a tool like curl.
|
|
752
|
+
*/
|
|
753
|
+
const HTTP_STATUS_TEXT = {
|
|
754
|
+
401: "Unauthorized",
|
|
755
|
+
403: "Forbidden",
|
|
756
|
+
404: "Not Found",
|
|
757
|
+
500: "Internal Server Error",
|
|
758
|
+
501: "Not Implemented",
|
|
759
|
+
503: "Service Unavailable"
|
|
760
|
+
};
|
|
761
|
+
/**
|
|
762
|
+
* Per-status guidance for a Preview feature that came back "unavailable". A preview can be
|
|
763
|
+
* gated several different ways and the HTTP status is the best signal for which, so we tailor
|
|
764
|
+
* the next step instead of emitting one catch-all — valuable while these features are in
|
|
765
|
+
* preview and rolling out region by region:
|
|
766
|
+
*
|
|
767
|
+
* - 404 / 501 — the route isn't deployed for this project's region (or the account isn't in
|
|
768
|
+
* the private preview): create a project in a region where the preview is enabled, and
|
|
769
|
+
* confirm your account has preview access.
|
|
770
|
+
* - 503 — the route exists but is refusing right now: either the preview is still coming up,
|
|
771
|
+
* or Neon is having a transient incident. Retry; if it persists it's likely an incident.
|
|
772
|
+
* - anything else — generic "not enabled for your account/region; request access".
|
|
773
|
+
*
|
|
774
|
+
* Only statuses {@link isPreviewFeatureUnavailable} accepts (404/501/503) actually reach
|
|
775
|
+
* this, so there is intentionally no 401/403 branch — those never classify as "unavailable".
|
|
776
|
+
*/
|
|
777
|
+
function previewUnavailableHint(status) {
|
|
778
|
+
switch (status) {
|
|
779
|
+
case 404:
|
|
780
|
+
case 501: return "This usually means the preview isn't available in your project's region yet, or your Neon account isn't in the private preview: create a project in a region where the preview is enabled, and make sure your account has access to the preview.";
|
|
781
|
+
case 503: return "The endpoint is reachable but refused the request — the preview may still be coming up, or Neon may be having a transient incident. Retry shortly; if it keeps failing, check https://neonstatus.com and report it to Neon support.";
|
|
782
|
+
default: return "This usually means the preview isn't enabled for your Neon account or the project's region. Request access to the preview, or use a project in a region where it's available.";
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Convert a Preview-feature error into a clear {@link PlatformError} when the feature is
|
|
787
|
+
* unavailable for the project; otherwise pass the original error through unchanged so a
|
|
788
|
+
* genuine failure (auth, transient 5xx, …) keeps its specific code and message.
|
|
789
|
+
*
|
|
790
|
+
* The message names the failing feature, summarizes the response in one short
|
|
791
|
+
* `HTTP <status> <reason>` line, includes the raw Neon API message + request id (valuable
|
|
792
|
+
* signal while the feature is in preview), gives status-specific guidance (see
|
|
793
|
+
* {@link previewUnavailableHint}), and offers removing the feature from the policy as an
|
|
794
|
+
* escape hatch. `status`/`requestId` are also kept on `details` for programmatic consumers.
|
|
795
|
+
*/
|
|
796
|
+
function previewUnavailableError(err, featureLabel) {
|
|
797
|
+
if (!isPreviewFeatureUnavailable(err)) return err;
|
|
798
|
+
const details = err instanceof PlatformError ? err.details : {};
|
|
799
|
+
const status = typeof details.status === "number" ? details.status : void 0;
|
|
800
|
+
const neonMessage = typeof details.neonMessage === "string" ? details.neonMessage : void 0;
|
|
801
|
+
const requestId = typeof details.requestId === "string" ? details.requestId : void 0;
|
|
802
|
+
const statusText = status ? HTTP_STATUS_TEXT[status] : void 0;
|
|
803
|
+
const apiParts = [
|
|
804
|
+
status ? `HTTP ${status}${statusText ? ` ${statusText}` : ""}` : void 0,
|
|
805
|
+
neonMessage ? `Neon API said: "${neonMessage}"` : void 0,
|
|
806
|
+
requestId ? `request id ${requestId}` : void 0
|
|
807
|
+
].filter((part) => part !== void 0);
|
|
808
|
+
const apiContext = apiParts.length > 0 ? ` (${apiParts.join("; ")})` : "";
|
|
809
|
+
return new PlatformError(ErrorCode.FeatureUnavailable, [
|
|
810
|
+
`${featureLabel} is a Preview feature and isn't available for this Neon project${apiContext}.`,
|
|
811
|
+
previewUnavailableHint(status),
|
|
812
|
+
"If you don't need it, remove the corresponding feature from the `preview` block of your neon.ts and re-run."
|
|
813
|
+
].join(" "), {
|
|
814
|
+
cause: err,
|
|
815
|
+
details: {
|
|
816
|
+
feature: featureLabel,
|
|
817
|
+
...status !== void 0 ? { status } : {},
|
|
818
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
function neonAuthResponseToSnapshot(data) {
|
|
823
|
+
const snapshot = {
|
|
824
|
+
projectId: data.auth_provider_project_id,
|
|
825
|
+
jwksUrl: data.jwks_url
|
|
826
|
+
};
|
|
827
|
+
if (data.pub_client_key !== void 0) snapshot.publishableClientKey = data.pub_client_key;
|
|
828
|
+
if (data.secret_server_key !== void 0) snapshot.secretServerKey = data.secret_server_key;
|
|
829
|
+
if (data.base_url) snapshot.baseUrl = data.base_url;
|
|
830
|
+
return snapshot;
|
|
831
|
+
}
|
|
832
|
+
function createNeonAuthRestInput(input) {
|
|
833
|
+
return {
|
|
834
|
+
auth_provider: "better_auth",
|
|
835
|
+
...input.databaseName ? { database_name: input.databaseName } : {}
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Build the `multipart/form-data` body for a function deployment, matching the public
|
|
840
|
+
* `FunctionDeployRequest` schema (`POST .../functions/{slug}/deployments`):
|
|
841
|
+
*
|
|
842
|
+
* - `zip` — the bundle as a binary part (named `bundle.zip`).
|
|
843
|
+
* - `runtime` — the function runtime.
|
|
844
|
+
* - `environment` — a single JSON-encoded string→string map (multipart can't carry a typed
|
|
845
|
+
* object part), omitted entirely when there are no env vars.
|
|
846
|
+
*
|
|
847
|
+
* Pure (no I/O) so it can be unit-tested against the spec without stubbing `fetch`.
|
|
848
|
+
*/
|
|
849
|
+
function buildFunctionDeployForm(input) {
|
|
850
|
+
const form = new FormData();
|
|
851
|
+
form.set("zip", new Blob([input.bundle], { type: "application/zip" }), "bundle.zip");
|
|
852
|
+
form.set("runtime", input.runtime);
|
|
853
|
+
if (Object.keys(input.environment).length > 0) form.set("environment", JSON.stringify(input.environment));
|
|
854
|
+
return form;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Read a response body as JSON, tolerating non-JSON. Some Neon routes return a plain-text
|
|
858
|
+
* body (e.g. a 404 `"this route does not exist"` for a Preview feature not enabled in the
|
|
859
|
+
* project/region). Parsing that with `JSON.parse` used to throw a cryptic
|
|
860
|
+
* `SyntaxError: Unexpected token …`, which — because parsing happens before the `res.ok`
|
|
861
|
+
* check in {@link request} — masked the real HTTP status. We instead return the raw text
|
|
862
|
+
* wrapped as `{ message }` so the status-based error path in `request` / `wrapNeonError`
|
|
863
|
+
* runs and produces a proper {@link PlatformError} (e.g. `NotFound`), and a non-error body
|
|
864
|
+
* that simply isn't JSON degrades to text rather than crashing.
|
|
865
|
+
*/
|
|
866
|
+
async function readJsonBody(res) {
|
|
867
|
+
const text = await res.text();
|
|
868
|
+
if (text.trim() === "") return {};
|
|
869
|
+
try {
|
|
870
|
+
return JSON.parse(text);
|
|
871
|
+
} catch {
|
|
872
|
+
return { message: text.trim() };
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function projectToSnapshot(project) {
|
|
876
|
+
const defaults = project.default_endpoint_settings;
|
|
877
|
+
const snapshot = {
|
|
878
|
+
id: project.id,
|
|
879
|
+
name: project.name,
|
|
880
|
+
regionId: project.region_id,
|
|
881
|
+
pgVersion: project.pg_version
|
|
882
|
+
};
|
|
883
|
+
if (project.org_id) snapshot.orgId = project.org_id;
|
|
884
|
+
if (defaults) {
|
|
885
|
+
const compute = defaultsToComputeSettings(defaults);
|
|
886
|
+
if (compute) snapshot.defaultEndpointSettings = compute;
|
|
887
|
+
}
|
|
888
|
+
return snapshot;
|
|
889
|
+
}
|
|
890
|
+
function branchToSnapshot(branch) {
|
|
891
|
+
const snapshot = {
|
|
892
|
+
id: branch.id,
|
|
893
|
+
name: branch.name,
|
|
894
|
+
isDefault: branch.default,
|
|
895
|
+
protected: branch.protected === true
|
|
896
|
+
};
|
|
897
|
+
if (branch.parent_id) snapshot.parentId = branch.parent_id;
|
|
898
|
+
if (branch.expires_at) snapshot.expiresAt = branch.expires_at;
|
|
899
|
+
return snapshot;
|
|
900
|
+
}
|
|
901
|
+
function endpointToSnapshot(endpoint) {
|
|
902
|
+
return {
|
|
903
|
+
id: endpoint.id,
|
|
904
|
+
branchId: endpoint.branch_id,
|
|
905
|
+
type: endpoint.type === "read_only" ? "read_only" : "read_write",
|
|
906
|
+
autoscalingLimitMinCu: endpoint.autoscaling_limit_min_cu,
|
|
907
|
+
autoscalingLimitMaxCu: endpoint.autoscaling_limit_max_cu,
|
|
908
|
+
suspendTimeout: formatSuspendTimeout(endpoint.suspend_timeout_seconds)
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function roleToSnapshot(role) {
|
|
912
|
+
return {
|
|
913
|
+
name: role.name,
|
|
914
|
+
branchId: role.branch_id,
|
|
915
|
+
protected: role.protected ?? false
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function databaseToSnapshot(database) {
|
|
919
|
+
return {
|
|
920
|
+
name: database.name,
|
|
921
|
+
branchId: database.branch_id,
|
|
922
|
+
ownerName: database.owner_name
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function computeSettingsToDefaults(settings) {
|
|
926
|
+
const out = {};
|
|
927
|
+
if (settings.autoscalingLimitMinCu !== void 0) out.autoscaling_limit_min_cu = settings.autoscalingLimitMinCu;
|
|
928
|
+
if (settings.autoscalingLimitMaxCu !== void 0) out.autoscaling_limit_max_cu = settings.autoscalingLimitMaxCu;
|
|
929
|
+
if (settings.suspendTimeout !== void 0) {
|
|
930
|
+
const parsed = parseSuspendTimeout(settings.suspendTimeout);
|
|
931
|
+
if ("error" in parsed) throw new PlatformError(ErrorCode.InvalidConfig, `Invalid suspendTimeout: ${parsed.error}`);
|
|
932
|
+
out.suspend_timeout_seconds = parsed.seconds;
|
|
933
|
+
}
|
|
934
|
+
return out;
|
|
935
|
+
}
|
|
936
|
+
function computeSettingsToEndpointOptions(settings) {
|
|
937
|
+
const out = {};
|
|
938
|
+
if (settings.autoscalingLimitMinCu !== void 0) out.autoscaling_limit_min_cu = settings.autoscalingLimitMinCu;
|
|
939
|
+
if (settings.autoscalingLimitMaxCu !== void 0) out.autoscaling_limit_max_cu = settings.autoscalingLimitMaxCu;
|
|
940
|
+
if (settings.suspendTimeout !== void 0) {
|
|
941
|
+
const parsed = parseSuspendTimeout(settings.suspendTimeout);
|
|
942
|
+
if ("error" in parsed) throw new PlatformError(ErrorCode.InvalidConfig, `Invalid suspendTimeout: ${parsed.error}`);
|
|
943
|
+
out.suspend_timeout_seconds = parsed.seconds;
|
|
944
|
+
}
|
|
945
|
+
return out;
|
|
946
|
+
}
|
|
947
|
+
function defaultsToComputeSettings(defaults) {
|
|
948
|
+
const out = {};
|
|
949
|
+
if (defaults.autoscaling_limit_min_cu !== void 0) out.autoscalingLimitMinCu = defaults.autoscaling_limit_min_cu;
|
|
950
|
+
if (defaults.autoscaling_limit_max_cu !== void 0) out.autoscalingLimitMaxCu = defaults.autoscaling_limit_max_cu;
|
|
951
|
+
if (defaults.suspend_timeout_seconds !== void 0) out.suspendTimeout = formatSuspendTimeout(defaults.suspend_timeout_seconds);
|
|
952
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
953
|
+
}
|
|
954
|
+
//#endregion
|
|
955
|
+
export { buildFunctionDeployForm, createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
|
|
956
|
+
|
|
957
|
+
//# sourceMappingURL=neon-api-real.js.map
|