alchemy 0.12.6 → 0.12.7
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/lib/cloudflare/ai-gateway.d.ts.map +1 -1
- package/lib/cloudflare/ai-gateway.js +1 -2
- package/lib/cloudflare/ai-gateway.js.map +1 -1
- package/lib/cloudflare/bindings.d.ts +2 -1
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bound.d.ts +2 -1
- package/lib/cloudflare/bound.d.ts.map +1 -1
- package/lib/cloudflare/hyperdrive.d.ts +216 -0
- package/lib/cloudflare/hyperdrive.d.ts.map +1 -0
- package/lib/cloudflare/hyperdrive.js +184 -0
- package/lib/cloudflare/hyperdrive.js.map +1 -0
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/worker.js +8 -0
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/neon/api-error.d.ts +31 -0
- package/lib/neon/api-error.d.ts.map +1 -0
- package/lib/neon/api-error.js +73 -0
- package/lib/neon/api-error.js.map +1 -0
- package/lib/neon/api.d.ts +70 -0
- package/lib/neon/api.d.ts.map +1 -0
- package/lib/neon/api.js +128 -0
- package/lib/neon/api.js.map +1 -0
- package/lib/neon/index.d.ts +4 -0
- package/lib/neon/index.d.ts.map +1 -0
- package/lib/neon/index.js +4 -0
- package/lib/neon/index.js.map +1 -0
- package/lib/neon/project.d.ts +345 -0
- package/lib/neon/project.d.ts.map +1 -0
- package/lib/neon/project.js +305 -0
- package/lib/neon/project.js.map +1 -0
- package/package.json +2 -1
- package/src/cloudflare/ai-gateway.ts +0 -1
- package/src/cloudflare/bindings.ts +4 -2
- package/src/cloudflare/bound.ts +20 -17
- package/src/cloudflare/hyperdrive.ts +375 -0
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/worker.ts +7 -0
- package/src/neon/api-error.ts +81 -0
- package/src/neon/api.ts +178 -0
- package/src/neon/index.ts +3 -0
- package/src/neon/project.ts +870 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
import { alchemy } from "../alchemy.js";
|
|
2
|
+
import type { Context } from "../context.js";
|
|
3
|
+
import { Resource } from "../resource.js";
|
|
4
|
+
import type { Secret } from "../secret.js";
|
|
5
|
+
import { handleApiError } from "./api-error.js";
|
|
6
|
+
import { createNeonApi, type NeonApiOptions } from "./api.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A Neon region where projects can be provisioned
|
|
10
|
+
*/
|
|
11
|
+
export type NeonRegion =
|
|
12
|
+
| "aws-us-east-1"
|
|
13
|
+
| "aws-us-east-2"
|
|
14
|
+
| "aws-us-west-2"
|
|
15
|
+
| "aws-eu-central-1"
|
|
16
|
+
| "aws-eu-west-2"
|
|
17
|
+
| "aws-ap-southeast-1"
|
|
18
|
+
| "aws-ap-southeast-2"
|
|
19
|
+
| "aws-sa-east-1"
|
|
20
|
+
| "azure-eastus2"
|
|
21
|
+
| "azure-westus3"
|
|
22
|
+
| "azure-gwc";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Properties for creating or updating a Neon project
|
|
26
|
+
*/
|
|
27
|
+
export interface NeonProjectProps extends NeonApiOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Name of the project
|
|
30
|
+
*/
|
|
31
|
+
name: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Region where the project will be provisioned
|
|
35
|
+
* @default "aws-us-east-1"
|
|
36
|
+
*/
|
|
37
|
+
region_id?: NeonRegion;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* PostgreSQL version to use
|
|
41
|
+
* @default 15
|
|
42
|
+
*/
|
|
43
|
+
pg_version?: 14 | 15 | 16;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether to create a default branch and endpoint
|
|
47
|
+
* @default true
|
|
48
|
+
*/
|
|
49
|
+
default_endpoint?: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default branch name
|
|
53
|
+
* @default "main"
|
|
54
|
+
*/
|
|
55
|
+
default_branch_name?: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Existing project ID to update
|
|
59
|
+
* Used internally during update operations
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
existing_project_id?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A Neon database
|
|
67
|
+
*/
|
|
68
|
+
export interface NeonDatabase {
|
|
69
|
+
/**
|
|
70
|
+
* Database ID
|
|
71
|
+
*/
|
|
72
|
+
id: number;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ID of the branch this database belongs to
|
|
76
|
+
*/
|
|
77
|
+
branch_id: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Database name
|
|
81
|
+
*/
|
|
82
|
+
name: string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Name of the database owner role
|
|
86
|
+
*/
|
|
87
|
+
owner_name: string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Time at which the database was created
|
|
91
|
+
*/
|
|
92
|
+
created_at: string;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Time at which the database was last updated
|
|
96
|
+
*/
|
|
97
|
+
updated_at: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A Neon database role
|
|
102
|
+
*/
|
|
103
|
+
export interface NeonRole {
|
|
104
|
+
/**
|
|
105
|
+
* ID of the branch this role belongs to
|
|
106
|
+
*/
|
|
107
|
+
branch_id: string;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Role name
|
|
111
|
+
*/
|
|
112
|
+
name: string;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Role password (only included during creation)
|
|
116
|
+
*/
|
|
117
|
+
password?: string;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether this role is protected from deletion
|
|
121
|
+
*/
|
|
122
|
+
protected: boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Time at which the role was created
|
|
126
|
+
*/
|
|
127
|
+
created_at: string;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Time at which the role was last updated
|
|
131
|
+
*/
|
|
132
|
+
updated_at: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A Neon branch
|
|
137
|
+
*/
|
|
138
|
+
export interface NeonBranch {
|
|
139
|
+
/**
|
|
140
|
+
* Branch ID
|
|
141
|
+
*/
|
|
142
|
+
id: string;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* ID of the project this branch belongs to
|
|
146
|
+
*/
|
|
147
|
+
project_id: string;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Branch name
|
|
151
|
+
*/
|
|
152
|
+
name: string;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Current state of the branch
|
|
156
|
+
*/
|
|
157
|
+
current_state: string;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Pending state of the branch
|
|
161
|
+
*/
|
|
162
|
+
pending_state: string;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Time at which the branch was created
|
|
166
|
+
*/
|
|
167
|
+
created_at: string;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Time at which the branch was last updated
|
|
171
|
+
*/
|
|
172
|
+
updated_at: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A Neon compute endpoint
|
|
177
|
+
*/
|
|
178
|
+
export interface NeonEndpoint {
|
|
179
|
+
/**
|
|
180
|
+
* Endpoint ID
|
|
181
|
+
*/
|
|
182
|
+
id: string;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Host for connecting to this endpoint
|
|
186
|
+
*/
|
|
187
|
+
host: string;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* ID of the project this endpoint belongs to
|
|
191
|
+
*/
|
|
192
|
+
project_id: string;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* ID of the branch this endpoint belongs to
|
|
196
|
+
*/
|
|
197
|
+
branch_id: string;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Endpoint type (read_write, read_only)
|
|
201
|
+
*/
|
|
202
|
+
type: string;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Current state of the endpoint
|
|
206
|
+
*/
|
|
207
|
+
current_state: string;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Pending state of the endpoint
|
|
211
|
+
*/
|
|
212
|
+
pending_state: string;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Region ID where this endpoint is provisioned
|
|
216
|
+
*/
|
|
217
|
+
region_id: string;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Minimum compute units for autoscaling
|
|
221
|
+
*/
|
|
222
|
+
autoscaling_limit_min_cu: number;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Maximum compute units for autoscaling
|
|
226
|
+
*/
|
|
227
|
+
autoscaling_limit_max_cu: number;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Whether connection pooler is enabled
|
|
231
|
+
*/
|
|
232
|
+
pooler_enabled: boolean;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Connection pooler mode
|
|
236
|
+
*/
|
|
237
|
+
pooler_mode: string;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Whether this endpoint is disabled
|
|
241
|
+
*/
|
|
242
|
+
disabled: boolean;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Whether passwordless access is enabled
|
|
246
|
+
*/
|
|
247
|
+
passwordless_access: boolean;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Time at which the endpoint was created
|
|
251
|
+
*/
|
|
252
|
+
created_at: string;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Time at which the endpoint was last updated
|
|
256
|
+
*/
|
|
257
|
+
updated_at: string;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Proxy host for this endpoint
|
|
261
|
+
*/
|
|
262
|
+
proxy_host: string;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Endpoint settings
|
|
266
|
+
*/
|
|
267
|
+
settings: {
|
|
268
|
+
/**
|
|
269
|
+
* PostgreSQL settings
|
|
270
|
+
*/
|
|
271
|
+
pg_settings: Record<string, any>;
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A Neon connection URI
|
|
277
|
+
*/
|
|
278
|
+
export interface NeonConnectionUri {
|
|
279
|
+
/**
|
|
280
|
+
* Connection URI string
|
|
281
|
+
*/
|
|
282
|
+
connection_uri: Secret;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Connection parameters
|
|
286
|
+
*/
|
|
287
|
+
connection_parameters: {
|
|
288
|
+
database: string;
|
|
289
|
+
host: string;
|
|
290
|
+
port: number;
|
|
291
|
+
user: string;
|
|
292
|
+
password: Secret;
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A Neon operation
|
|
298
|
+
*/
|
|
299
|
+
export interface NeonOperation {
|
|
300
|
+
/**
|
|
301
|
+
* Operation ID
|
|
302
|
+
*/
|
|
303
|
+
id: string;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* ID of the project this operation belongs to
|
|
307
|
+
*/
|
|
308
|
+
project_id: string;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* ID of the branch this operation affects, if applicable
|
|
312
|
+
*/
|
|
313
|
+
branch_id?: string;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* ID of the endpoint this operation affects, if applicable
|
|
317
|
+
*/
|
|
318
|
+
endpoint_id?: string;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Action being performed
|
|
322
|
+
*/
|
|
323
|
+
action: string;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Current status of the operation
|
|
327
|
+
*/
|
|
328
|
+
status: string;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Number of failures encountered
|
|
332
|
+
*/
|
|
333
|
+
failures_count: number;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Time at which the operation was created
|
|
337
|
+
*/
|
|
338
|
+
created_at: string;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Time at which the operation was last updated
|
|
342
|
+
*/
|
|
343
|
+
updated_at: string;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* API response structure for Neon projects
|
|
348
|
+
*/
|
|
349
|
+
interface NeonApiResponse {
|
|
350
|
+
project: {
|
|
351
|
+
id: string;
|
|
352
|
+
name: string;
|
|
353
|
+
region_id: string;
|
|
354
|
+
pg_version: number;
|
|
355
|
+
created_at: string;
|
|
356
|
+
updated_at: string;
|
|
357
|
+
proxy_host?: string;
|
|
358
|
+
[key: string]: any;
|
|
359
|
+
};
|
|
360
|
+
connection_uris?: Array<{
|
|
361
|
+
connection_uri: string;
|
|
362
|
+
connection_parameters: {
|
|
363
|
+
database: string;
|
|
364
|
+
host: string;
|
|
365
|
+
port: number;
|
|
366
|
+
user: string;
|
|
367
|
+
password: string;
|
|
368
|
+
};
|
|
369
|
+
}>;
|
|
370
|
+
roles?: Array<{
|
|
371
|
+
branch_id: string;
|
|
372
|
+
name: string;
|
|
373
|
+
password?: string;
|
|
374
|
+
protected: boolean;
|
|
375
|
+
created_at: string;
|
|
376
|
+
updated_at: string;
|
|
377
|
+
}>;
|
|
378
|
+
databases?: Array<{
|
|
379
|
+
id: number;
|
|
380
|
+
branch_id: string;
|
|
381
|
+
name: string;
|
|
382
|
+
owner_name: string;
|
|
383
|
+
created_at: string;
|
|
384
|
+
updated_at: string;
|
|
385
|
+
}>;
|
|
386
|
+
operations?: Array<{
|
|
387
|
+
id: string;
|
|
388
|
+
project_id: string;
|
|
389
|
+
branch_id?: string;
|
|
390
|
+
endpoint_id?: string;
|
|
391
|
+
action: string;
|
|
392
|
+
status: string;
|
|
393
|
+
failures_count: number;
|
|
394
|
+
created_at: string;
|
|
395
|
+
updated_at: string;
|
|
396
|
+
}>;
|
|
397
|
+
branch?: {
|
|
398
|
+
id: string;
|
|
399
|
+
project_id: string;
|
|
400
|
+
name: string;
|
|
401
|
+
current_state: string;
|
|
402
|
+
pending_state: string;
|
|
403
|
+
created_at: string;
|
|
404
|
+
updated_at: string;
|
|
405
|
+
};
|
|
406
|
+
endpoints?: Array<{
|
|
407
|
+
id: string;
|
|
408
|
+
host: string;
|
|
409
|
+
project_id: string;
|
|
410
|
+
branch_id: string;
|
|
411
|
+
type: string;
|
|
412
|
+
current_state: string;
|
|
413
|
+
pending_state: string;
|
|
414
|
+
region_id: string;
|
|
415
|
+
autoscaling_limit_min_cu: number;
|
|
416
|
+
autoscaling_limit_max_cu: number;
|
|
417
|
+
pooler_enabled: boolean;
|
|
418
|
+
pooler_mode: string;
|
|
419
|
+
disabled: boolean;
|
|
420
|
+
passwordless_access: boolean;
|
|
421
|
+
created_at: string;
|
|
422
|
+
updated_at: string;
|
|
423
|
+
proxy_host: string;
|
|
424
|
+
settings: {
|
|
425
|
+
pg_settings: Record<string, any>;
|
|
426
|
+
};
|
|
427
|
+
}>;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Output returned after Neon project creation/update
|
|
432
|
+
* IMPORTANT: The interface name MUST match the exported resource name
|
|
433
|
+
*/
|
|
434
|
+
export interface NeonProject
|
|
435
|
+
extends Resource<"neon::Project">,
|
|
436
|
+
Omit<NeonProjectProps, "apiKey" | "existing_project_id"> {
|
|
437
|
+
/**
|
|
438
|
+
* The ID of the project
|
|
439
|
+
*/
|
|
440
|
+
id: string;
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Time at which the project was created
|
|
444
|
+
*/
|
|
445
|
+
created_at: string;
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Time at which the project was last updated
|
|
449
|
+
*/
|
|
450
|
+
updated_at: string;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Hostname for proxy access
|
|
454
|
+
*/
|
|
455
|
+
proxy_host?: string;
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Connection URIs for the databases
|
|
459
|
+
*/
|
|
460
|
+
connection_uris: [NeonConnectionUri, ...NeonConnectionUri[]];
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Database roles created with the project
|
|
464
|
+
*/
|
|
465
|
+
roles: [NeonRole, ...NeonRole[]];
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Databases created with the project
|
|
469
|
+
*/
|
|
470
|
+
databases?: [NeonDatabase, ...NeonDatabase[]];
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Default branch information
|
|
474
|
+
*/
|
|
475
|
+
branch?: NeonBranch;
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Compute endpoints for the project
|
|
479
|
+
*/
|
|
480
|
+
endpoints: [NeonEndpoint, ...NeonEndpoint[]];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Creates a Neon serverless PostgreSQL project.
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* // Create a basic Neon project with default settings:
|
|
488
|
+
* const project = await NeonProject("my-project", {
|
|
489
|
+
* name: "My Project"
|
|
490
|
+
* });
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* // Create a Neon project in a specific region with a specific PostgreSQL version:
|
|
494
|
+
* const euProject = await NeonProject("my-eu-project", {
|
|
495
|
+
* name: "My EU Project",
|
|
496
|
+
* region_id: "aws-eu-west-1",
|
|
497
|
+
* pg_version: 16,
|
|
498
|
+
* apiKey: alchemy.secret(process.env.NEON_API_KEY)
|
|
499
|
+
* });
|
|
500
|
+
*
|
|
501
|
+
* @example
|
|
502
|
+
* // Create a Neon project with a custom default branch name:
|
|
503
|
+
* const devProject = await NeonProject("dev-project", {
|
|
504
|
+
* name: "Development Project",
|
|
505
|
+
* default_branch_name: "development"
|
|
506
|
+
* });
|
|
507
|
+
*/
|
|
508
|
+
export const NeonProject = Resource(
|
|
509
|
+
"neon::Project",
|
|
510
|
+
async function (
|
|
511
|
+
this: Context<NeonProject>,
|
|
512
|
+
id: string,
|
|
513
|
+
props: NeonProjectProps
|
|
514
|
+
): Promise<NeonProject> {
|
|
515
|
+
const api = createNeonApi(props);
|
|
516
|
+
const projectId = props.existing_project_id || this.output?.id;
|
|
517
|
+
|
|
518
|
+
if (this.phase === "delete") {
|
|
519
|
+
try {
|
|
520
|
+
// Check if the project exists before attempting to delete
|
|
521
|
+
if (projectId) {
|
|
522
|
+
const deleteResponse = await api.delete(`/projects/${projectId}`);
|
|
523
|
+
if (!deleteResponse.ok && deleteResponse.status !== 404) {
|
|
524
|
+
await handleApiError(deleteResponse, "delete", "project", id);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} catch (error) {
|
|
528
|
+
console.error(`Error deleting Neon project ${id}:`, error);
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
return this.destroy();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
let response: NeonApiResponse;
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
if (this.phase === "update" && projectId) {
|
|
538
|
+
// Update existing project
|
|
539
|
+
// Neon only allows updating the project name
|
|
540
|
+
const projectResponse = await api.patch(`/projects/${projectId}`, {
|
|
541
|
+
project: {
|
|
542
|
+
name: props.name,
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
if (!projectResponse.ok) {
|
|
547
|
+
await handleApiError(projectResponse, "update", "project", id);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const initialData = await projectResponse.json();
|
|
551
|
+
|
|
552
|
+
// Reify project properties to get complete data
|
|
553
|
+
response = await getProject(
|
|
554
|
+
api,
|
|
555
|
+
projectId,
|
|
556
|
+
initialData as Partial<NeonApiResponse>
|
|
557
|
+
);
|
|
558
|
+
} else {
|
|
559
|
+
// Check if a project with this ID already exists
|
|
560
|
+
if (projectId) {
|
|
561
|
+
const getResponse = await api.get(`/projects/${projectId}`);
|
|
562
|
+
if (getResponse.ok) {
|
|
563
|
+
// Project exists, update it
|
|
564
|
+
const projectResponse = await api.patch(`/projects/${projectId}`, {
|
|
565
|
+
project: {
|
|
566
|
+
name: props.name,
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
if (!projectResponse.ok) {
|
|
571
|
+
await handleApiError(projectResponse, "update", "project", id);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const initialData = await projectResponse.json();
|
|
575
|
+
// Reify project properties to get complete data
|
|
576
|
+
response = await getProject(
|
|
577
|
+
api,
|
|
578
|
+
projectId,
|
|
579
|
+
initialData as Partial<NeonApiResponse>
|
|
580
|
+
);
|
|
581
|
+
} else if (getResponse.status !== 404) {
|
|
582
|
+
// Unexpected error during GET check
|
|
583
|
+
await handleApiError(getResponse, "get", "project", id);
|
|
584
|
+
throw new Error("Failed to check if project exists");
|
|
585
|
+
} else {
|
|
586
|
+
// Project doesn't exist, create new
|
|
587
|
+
response = await createNewProject(api, props);
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
// No output ID, create new project
|
|
591
|
+
response = await createNewProject(api, props);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Wait for any pending operations to complete
|
|
596
|
+
if (response.operations && response.operations.length > 0) {
|
|
597
|
+
await waitForOperations(api, response.operations);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Get the latest project state after operations complete
|
|
601
|
+
if (response.project?.id) {
|
|
602
|
+
// Reify project properties to get complete data
|
|
603
|
+
response = await getProject(api, response.project.id, response);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return this({
|
|
607
|
+
id: response.project.id,
|
|
608
|
+
name: response.project.name,
|
|
609
|
+
region_id: response.project.region_id as NeonRegion,
|
|
610
|
+
pg_version: response.project.pg_version as 14 | 15 | 16,
|
|
611
|
+
created_at: response.project.created_at,
|
|
612
|
+
updated_at: response.project.updated_at,
|
|
613
|
+
proxy_host: response.project.proxy_host,
|
|
614
|
+
// Pass through the provided props except apiKey (which is sensitive)
|
|
615
|
+
default_endpoint: props.default_endpoint,
|
|
616
|
+
default_branch_name: props.default_branch_name,
|
|
617
|
+
baseUrl: props.baseUrl,
|
|
618
|
+
// Add all available data
|
|
619
|
+
// @ts-ignore - api ensures they're non-empty
|
|
620
|
+
connection_uris: response.connection_uris,
|
|
621
|
+
// @ts-ignore
|
|
622
|
+
roles: response.roles,
|
|
623
|
+
// @ts-ignore
|
|
624
|
+
databases: response.databases,
|
|
625
|
+
// @ts-ignore
|
|
626
|
+
branch: response.branch,
|
|
627
|
+
// @ts-ignore
|
|
628
|
+
endpoints: response.endpoints,
|
|
629
|
+
});
|
|
630
|
+
} catch (error) {
|
|
631
|
+
console.error(`Error ${this.phase} Neon project '${id}':`, error);
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Helper function to create a new Neon project
|
|
639
|
+
*/
|
|
640
|
+
async function createNewProject(
|
|
641
|
+
api: any,
|
|
642
|
+
props: NeonProjectProps
|
|
643
|
+
): Promise<NeonApiResponse> {
|
|
644
|
+
const defaultEndpoint = props.default_endpoint ?? true;
|
|
645
|
+
const projectResponse = await api.post("/projects", {
|
|
646
|
+
project: {
|
|
647
|
+
name: props.name,
|
|
648
|
+
region_id: props.region_id || "aws-us-east-1",
|
|
649
|
+
pg_version: props.pg_version || 15,
|
|
650
|
+
default_endpoint: defaultEndpoint,
|
|
651
|
+
branch: defaultEndpoint
|
|
652
|
+
? { name: props.default_branch_name || "main" }
|
|
653
|
+
: undefined,
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
if (!projectResponse.ok) {
|
|
658
|
+
await handleApiError(projectResponse, "create", "project");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return (await projectResponse.json()) as NeonApiResponse;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Helper function to get complete project details by fetching all related data
|
|
666
|
+
*
|
|
667
|
+
* @param api The Neon API client
|
|
668
|
+
* @param projectId The project ID
|
|
669
|
+
* @param initialData Initial project data (optional)
|
|
670
|
+
* @returns Complete project data with all related resources
|
|
671
|
+
*/
|
|
672
|
+
async function getProject(
|
|
673
|
+
api: any,
|
|
674
|
+
projectId: string,
|
|
675
|
+
initialData: Partial<NeonApiResponse> = {}
|
|
676
|
+
): Promise<NeonApiResponse> {
|
|
677
|
+
// Get the latest project details
|
|
678
|
+
const updatedData = await getProjectDetails(api, projectId);
|
|
679
|
+
|
|
680
|
+
// Start with a copy of the initial data
|
|
681
|
+
let responseData = { ...initialData };
|
|
682
|
+
|
|
683
|
+
// Check if we have a branch ID from the initial data
|
|
684
|
+
const branchId = initialData.branch?.id;
|
|
685
|
+
|
|
686
|
+
if (branchId) {
|
|
687
|
+
// Get the branch details
|
|
688
|
+
const branchData = await getBranchDetails(api, projectId, branchId);
|
|
689
|
+
|
|
690
|
+
// Update with the latest branch data
|
|
691
|
+
responseData.branch = branchData.branch;
|
|
692
|
+
|
|
693
|
+
// Also fetch the latest endpoint details for this branch
|
|
694
|
+
const endpointData = await getEndpointDetails(api, projectId, branchId);
|
|
695
|
+
|
|
696
|
+
// Update with the latest endpoint data if available
|
|
697
|
+
if (endpointData.endpoints && endpointData.endpoints.length > 0) {
|
|
698
|
+
responseData.endpoints = endpointData.endpoints;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Preserve all data from the original response
|
|
703
|
+
// Only update properties that might have changed during operations
|
|
704
|
+
return {
|
|
705
|
+
...responseData,
|
|
706
|
+
connection_uris: (
|
|
707
|
+
updatedData.connection_uris || responseData.connection_uris
|
|
708
|
+
)?.map((uri) => ({
|
|
709
|
+
connection_uri: alchemy.secret(uri.connection_uri),
|
|
710
|
+
connection_parameters: {
|
|
711
|
+
database: uri.connection_parameters.database,
|
|
712
|
+
host: uri.connection_parameters.host,
|
|
713
|
+
port: uri.connection_parameters.port ?? 5432,
|
|
714
|
+
user: uri.connection_parameters.user ?? "neondb_owner",
|
|
715
|
+
password: alchemy.secret(uri.connection_parameters.password),
|
|
716
|
+
},
|
|
717
|
+
})),
|
|
718
|
+
project: updatedData.project,
|
|
719
|
+
branch: updatedData.branch || responseData.branch,
|
|
720
|
+
endpoints: updatedData.endpoints || responseData.endpoints,
|
|
721
|
+
} as NeonApiResponse;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Wait for operations to complete
|
|
726
|
+
*
|
|
727
|
+
* @param api The Neon API client
|
|
728
|
+
* @param operations Operations to wait for
|
|
729
|
+
* @throws Error if an operation fails or times out
|
|
730
|
+
* @returns Promise that resolves when all operations complete
|
|
731
|
+
*/
|
|
732
|
+
async function waitForOperations(
|
|
733
|
+
api: any,
|
|
734
|
+
operations: Array<{
|
|
735
|
+
id: string;
|
|
736
|
+
project_id: string;
|
|
737
|
+
status: string;
|
|
738
|
+
action: string;
|
|
739
|
+
}>
|
|
740
|
+
): Promise<void> {
|
|
741
|
+
const pendingOperations = operations.filter(
|
|
742
|
+
(op) => op.status !== "finished" && op.status !== "failed"
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
if (pendingOperations.length === 0) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Maximum wait time in milliseconds (5 minutes)
|
|
750
|
+
const maxWaitTime = 5 * 60 * 1000;
|
|
751
|
+
// Initial delay between retries in milliseconds
|
|
752
|
+
const initialRetryDelay = 500;
|
|
753
|
+
// Maximum delay between retries
|
|
754
|
+
const maxRetryDelay = 10000;
|
|
755
|
+
// Backoff factor for exponential backoff
|
|
756
|
+
const backoffFactor = 1.5;
|
|
757
|
+
|
|
758
|
+
for (const operation of pendingOperations) {
|
|
759
|
+
let totalWaitTime = 0;
|
|
760
|
+
let retryDelay = initialRetryDelay;
|
|
761
|
+
let operationStatus = operation.status;
|
|
762
|
+
|
|
763
|
+
while (
|
|
764
|
+
operationStatus !== "finished" &&
|
|
765
|
+
operationStatus !== "failed" &&
|
|
766
|
+
totalWaitTime < maxWaitTime
|
|
767
|
+
) {
|
|
768
|
+
// Wait before checking again with exponential backoff
|
|
769
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
770
|
+
totalWaitTime += retryDelay;
|
|
771
|
+
|
|
772
|
+
// Increase delay for next retry with exponential backoff, up to max
|
|
773
|
+
retryDelay = Math.min(retryDelay * backoffFactor, maxRetryDelay);
|
|
774
|
+
|
|
775
|
+
// Check operation status
|
|
776
|
+
const operationResponse = await api.get(
|
|
777
|
+
`/projects/${operation.project_id}/operations/${operation.id}`
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
if (operationResponse.ok) {
|
|
781
|
+
const operationData = await operationResponse.json();
|
|
782
|
+
operationStatus = operationData.operation?.status;
|
|
783
|
+
} else {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`Failed to check operation ${operation.id} status: HTTP ${operationResponse.status}`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (operationStatus === "failed") {
|
|
791
|
+
throw new Error(`Operation ${operation.id} (${operation.action}) failed`);
|
|
792
|
+
} else if (totalWaitTime >= maxWaitTime) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`Timeout waiting for operation ${operation.id} (${operation.action}) to complete`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Explicitly return when all operations are complete
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Get the latest project details
|
|
805
|
+
*
|
|
806
|
+
* @param api The Neon API client
|
|
807
|
+
* @param projectId The project ID
|
|
808
|
+
* @returns Project details including branch and endpoints
|
|
809
|
+
* @throws Error if project details cannot be retrieved
|
|
810
|
+
*/
|
|
811
|
+
async function getProjectDetails(
|
|
812
|
+
api: any,
|
|
813
|
+
projectId: string
|
|
814
|
+
): Promise<NeonApiResponse> {
|
|
815
|
+
const response = await api.get(`/projects/${projectId}`);
|
|
816
|
+
|
|
817
|
+
if (!response.ok) {
|
|
818
|
+
throw new Error(`Failed to get project details: HTTP ${response.status}`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return (await response.json()) as NeonApiResponse;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Get the latest branch details
|
|
826
|
+
*
|
|
827
|
+
* @param api The Neon API client
|
|
828
|
+
* @param projectId The project ID
|
|
829
|
+
* @param branchId The branch ID
|
|
830
|
+
* @returns Branch details
|
|
831
|
+
* @throws Error if branch details cannot be retrieved
|
|
832
|
+
*/
|
|
833
|
+
async function getBranchDetails(
|
|
834
|
+
api: any,
|
|
835
|
+
projectId: string,
|
|
836
|
+
branchId: string
|
|
837
|
+
): Promise<{ branch: NeonBranch }> {
|
|
838
|
+
const response = await api.get(`/projects/${projectId}/branches/${branchId}`);
|
|
839
|
+
|
|
840
|
+
if (!response.ok) {
|
|
841
|
+
throw new Error(`Failed to get branch details: HTTP ${response.status}`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return (await response.json()) as { branch: NeonBranch };
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Get the latest endpoint details for a branch
|
|
849
|
+
*
|
|
850
|
+
* @param api The Neon API client
|
|
851
|
+
* @param projectId The project ID
|
|
852
|
+
* @param branchId The branch ID
|
|
853
|
+
* @returns Endpoint details for the branch
|
|
854
|
+
* @throws Error if endpoint details cannot be retrieved
|
|
855
|
+
*/
|
|
856
|
+
async function getEndpointDetails(
|
|
857
|
+
api: any,
|
|
858
|
+
projectId: string,
|
|
859
|
+
branchId: string
|
|
860
|
+
): Promise<{ endpoints: NeonEndpoint[] }> {
|
|
861
|
+
const response = await api.get(
|
|
862
|
+
`/projects/${projectId}/branches/${branchId}/endpoints`
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
if (!response.ok) {
|
|
866
|
+
throw new Error(`Failed to get endpoint details: HTTP ${response.status}`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return (await response.json()) as { endpoints: NeonEndpoint[] };
|
|
870
|
+
}
|