alchemy 0.12.6 → 0.12.8
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/bucket.d.ts +8 -2
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +1 -1
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/d1-database.d.ts +8 -2
- package/lib/cloudflare/d1-database.d.ts.map +1 -1
- package/lib/cloudflare/d1-database.js +2 -2
- package/lib/cloudflare/d1-database.js.map +1 -1
- package/lib/cloudflare/event-source.d.ts +26 -0
- package/lib/cloudflare/event-source.d.ts.map +1 -0
- package/lib/cloudflare/event-source.js +9 -0
- package/lib/cloudflare/event-source.js.map +1 -0
- 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 +2 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +2 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/queue-consumer.d.ts +162 -0
- package/lib/cloudflare/queue-consumer.d.ts.map +1 -0
- package/lib/cloudflare/queue-consumer.js +197 -0
- package/lib/cloudflare/queue-consumer.js.map +1 -0
- package/lib/cloudflare/queue.d.ts +15 -3
- package/lib/cloudflare/queue.d.ts.map +1 -1
- package/lib/cloudflare/queue.js +8 -2
- package/lib/cloudflare/queue.js.map +1 -1
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +8 -10
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +13 -2
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +75 -3
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +13 -3
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +27 -5
- package/lib/cloudflare/wrangler.json.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/bucket.ts +9 -2
- package/src/cloudflare/d1-database.ts +10 -3
- package/src/cloudflare/event-source.ts +33 -0
- package/src/cloudflare/hyperdrive.ts +375 -0
- package/src/cloudflare/index.ts +2 -0
- package/src/cloudflare/queue-consumer.ts +433 -0
- package/src/cloudflare/queue.ts +83 -63
- package/src/cloudflare/website.ts +9 -13
- package/src/cloudflare/worker.ts +109 -5
- package/src/cloudflare/wrangler.json.ts +52 -8
- 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
package/src/cloudflare/worker.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Resource } from "../resource.js";
|
|
|
6
6
|
import { getContentType } from "../util/content-type.js";
|
|
7
7
|
import { withExponentialBackoff } from "../util/retry.js";
|
|
8
8
|
import { slugify } from "../util/slugify.js";
|
|
9
|
-
import { handleApiError } from "./api-error.js";
|
|
9
|
+
import { CloudflareApiError, handleApiError } from "./api-error.js";
|
|
10
10
|
import {
|
|
11
11
|
type CloudflareApi,
|
|
12
12
|
type CloudflareApiOptions,
|
|
@@ -16,6 +16,13 @@ import type { Assets } from "./assets.js";
|
|
|
16
16
|
import type { Bindings, WorkerBindingSpec } from "./bindings.js";
|
|
17
17
|
import type { Bound } from "./bound.js";
|
|
18
18
|
import type { DurableObjectNamespace } from "./durable-object-namespace.js";
|
|
19
|
+
import { type EventSource, isQueueEventSource } from "./event-source.js";
|
|
20
|
+
import {
|
|
21
|
+
QueueConsumer,
|
|
22
|
+
deleteQueueConsumer,
|
|
23
|
+
listQueueConsumers,
|
|
24
|
+
} from "./queue-consumer.js";
|
|
25
|
+
import { isQueue } from "./queue.js";
|
|
19
26
|
import type { WorkerScriptMetadata } from "./worker-metadata.js";
|
|
20
27
|
import type { SingleStepMigration } from "./worker-migration.js";
|
|
21
28
|
import { type Workflow, upsertWorkflow } from "./workflow.js";
|
|
@@ -98,9 +105,10 @@ export interface WorkerProps<B extends Bindings = Bindings>
|
|
|
98
105
|
|
|
99
106
|
/**
|
|
100
107
|
* Name for the worker
|
|
101
|
-
*
|
|
108
|
+
*
|
|
109
|
+
* @default id
|
|
102
110
|
*/
|
|
103
|
-
name
|
|
111
|
+
name?: string;
|
|
104
112
|
|
|
105
113
|
/**
|
|
106
114
|
* Bindings to attach to the worker
|
|
@@ -169,6 +177,12 @@ export interface WorkerProps<B extends Bindings = Bindings>
|
|
|
169
177
|
* @see https://developers.cloudflare.com/workers/configuration/cron-triggers/#examples
|
|
170
178
|
*/
|
|
171
179
|
crons?: string[];
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Event sources that this worker will consume.
|
|
183
|
+
* Can include queues, streams, or other event sources.
|
|
184
|
+
*/
|
|
185
|
+
eventSources?: EventSource[];
|
|
172
186
|
}
|
|
173
187
|
|
|
174
188
|
/**
|
|
@@ -184,6 +198,11 @@ export interface Worker<B extends Bindings = Bindings>
|
|
|
184
198
|
*/
|
|
185
199
|
id: string;
|
|
186
200
|
|
|
201
|
+
/**
|
|
202
|
+
* The name of the worker
|
|
203
|
+
*/
|
|
204
|
+
name: string;
|
|
205
|
+
|
|
187
206
|
/**
|
|
188
207
|
* Time at which the worker was created
|
|
189
208
|
*/
|
|
@@ -326,7 +345,7 @@ export const Worker = Resource(
|
|
|
326
345
|
const api = await createCloudflareApi(props);
|
|
327
346
|
|
|
328
347
|
// Use the provided name
|
|
329
|
-
const workerName = props.name;
|
|
348
|
+
const workerName = props.name ?? id;
|
|
330
349
|
|
|
331
350
|
// Validate input - we need either script, entryPoint, or bundle
|
|
332
351
|
if (!props.script && !props.entrypoint) {
|
|
@@ -379,7 +398,7 @@ export const Worker = Resource(
|
|
|
379
398
|
);
|
|
380
399
|
}
|
|
381
400
|
|
|
382
|
-
const compatibilityDate = props.compatibilityDate ?? "
|
|
401
|
+
const compatibilityDate = props.compatibilityDate ?? "2025-03-01";
|
|
383
402
|
const compatibilityFlags = props.compatibilityFlags ?? [];
|
|
384
403
|
|
|
385
404
|
// Prepare metadata with bindings
|
|
@@ -404,6 +423,24 @@ export const Worker = Resource(
|
|
|
404
423
|
});
|
|
405
424
|
}
|
|
406
425
|
|
|
426
|
+
await Promise.all(
|
|
427
|
+
props.eventSources?.map((eventSource) => {
|
|
428
|
+
if (isQueue(eventSource) || isQueueEventSource(eventSource)) {
|
|
429
|
+
const queue = isQueue(eventSource) ? eventSource : eventSource.queue;
|
|
430
|
+
return QueueConsumer(`${queue.id}-consumer`, {
|
|
431
|
+
queue,
|
|
432
|
+
scriptName: workerName,
|
|
433
|
+
accountId: api.accountId,
|
|
434
|
+
settings: isQueueEventSource(eventSource)
|
|
435
|
+
? eventSource.settings
|
|
436
|
+
: undefined,
|
|
437
|
+
});
|
|
438
|
+
} else {
|
|
439
|
+
throw new Error(`Unsupported event source type: ${eventSource}`);
|
|
440
|
+
}
|
|
441
|
+
}) ?? []
|
|
442
|
+
);
|
|
443
|
+
|
|
407
444
|
// TODO: it is less than ideal that this can fail, resulting in state problem
|
|
408
445
|
await this.set("bindings", props.bindings);
|
|
409
446
|
|
|
@@ -451,6 +488,7 @@ export const Worker = Resource(
|
|
|
451
488
|
observability: scriptMetadata.observability,
|
|
452
489
|
createdAt: now,
|
|
453
490
|
updatedAt: now,
|
|
491
|
+
eventSources: props.eventSources,
|
|
454
492
|
url: workerUrl,
|
|
455
493
|
// Include assets configuration in the output
|
|
456
494
|
assets: props.assets,
|
|
@@ -467,6 +505,9 @@ async function deleteWorker<B extends Bindings>(
|
|
|
467
505
|
api: CloudflareApi,
|
|
468
506
|
workerName: string
|
|
469
507
|
) {
|
|
508
|
+
// Delete any queue consumers attached to this worker first
|
|
509
|
+
await deleteQueueConsumers(ctx, api, workerName);
|
|
510
|
+
|
|
470
511
|
// Delete worker
|
|
471
512
|
const deleteResponse = await api.delete(
|
|
472
513
|
`/accounts/${api.accountId}/workers/scripts/${workerName}`
|
|
@@ -784,6 +825,13 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
784
825
|
type: "ai",
|
|
785
826
|
name: bindingName,
|
|
786
827
|
});
|
|
828
|
+
} else if (binding.type === "hyperdrive") {
|
|
829
|
+
// Hyperdrive binding
|
|
830
|
+
meta.bindings.push({
|
|
831
|
+
type: "hyperdrive",
|
|
832
|
+
name: bindingName,
|
|
833
|
+
id: binding.hyperdriveId,
|
|
834
|
+
});
|
|
787
835
|
} else {
|
|
788
836
|
// @ts-expect-error - we should never reach here
|
|
789
837
|
throw new Error(`Unsupported binding type: ${binding.type}`);
|
|
@@ -1215,3 +1263,59 @@ async function calculateFileMetadata(
|
|
|
1215
1263
|
size: fileContent.length,
|
|
1216
1264
|
};
|
|
1217
1265
|
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Lists and deletes all queue consumers for a specific worker
|
|
1269
|
+
* @param ctx Worker context containing eventSources
|
|
1270
|
+
* @param api CloudflareApi instance
|
|
1271
|
+
* @param workerName Name of the worker script
|
|
1272
|
+
*/
|
|
1273
|
+
async function deleteQueueConsumers<B extends Bindings>(
|
|
1274
|
+
ctx: Context<Worker<B>>,
|
|
1275
|
+
api: CloudflareApi,
|
|
1276
|
+
workerName: string
|
|
1277
|
+
): Promise<void> {
|
|
1278
|
+
const eventSources = ctx.output?.eventSources || [];
|
|
1279
|
+
|
|
1280
|
+
// Extract queue IDs from event sources
|
|
1281
|
+
const queueIds = eventSources.flatMap((eventSource) => {
|
|
1282
|
+
if (isQueue(eventSource)) {
|
|
1283
|
+
return [eventSource.id];
|
|
1284
|
+
} else if (isQueueEventSource(eventSource)) {
|
|
1285
|
+
return [eventSource.queue.id];
|
|
1286
|
+
}
|
|
1287
|
+
return [];
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
// Process each queue associated with this worker
|
|
1291
|
+
await Promise.all(
|
|
1292
|
+
queueIds.map(async (queueId) => {
|
|
1293
|
+
try {
|
|
1294
|
+
// List all consumers for this queue
|
|
1295
|
+
const consumers = await listQueueConsumers(api, queueId);
|
|
1296
|
+
|
|
1297
|
+
// Filter consumers by worker name
|
|
1298
|
+
const workerConsumers = consumers.filter(
|
|
1299
|
+
(consumer) => consumer.scriptName === workerName
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
// Delete all consumers for this worker in parallel
|
|
1303
|
+
await Promise.all(
|
|
1304
|
+
workerConsumers.map(async (consumer) => {
|
|
1305
|
+
console.log(
|
|
1306
|
+
`Deleting queue consumer ${consumer.id} for worker ${workerName}`
|
|
1307
|
+
);
|
|
1308
|
+
// Use the deleteQueueConsumer function from queue-consumer.ts
|
|
1309
|
+
await deleteQueueConsumer(api, consumer.queueId, consumer.id);
|
|
1310
|
+
})
|
|
1311
|
+
);
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
if (err instanceof CloudflareApiError && err.status === 404) {
|
|
1314
|
+
// this is OK
|
|
1315
|
+
} else {
|
|
1316
|
+
throw err;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
})
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
@@ -3,6 +3,9 @@ import { StaticJsonFile } from "../fs/static-json-file.js";
|
|
|
3
3
|
import { Resource } from "../resource.js";
|
|
4
4
|
import type { Bindings } from "./bindings.js";
|
|
5
5
|
import type { DurableObjectNamespace } from "./durable-object-namespace.js";
|
|
6
|
+
import type { EventSource } from "./event-source.js";
|
|
7
|
+
import { isQueueEventSource } from "./event-source.js";
|
|
8
|
+
import { isQueue } from "./queue.js";
|
|
6
9
|
import type { Worker } from "./worker.js";
|
|
7
10
|
|
|
8
11
|
/**
|
|
@@ -80,7 +83,7 @@ export const WranglerJson = Resource(
|
|
|
80
83
|
|
|
81
84
|
// Process bindings if they exist
|
|
82
85
|
if (worker.bindings) {
|
|
83
|
-
processBindings(spec, worker.bindings);
|
|
86
|
+
processBindings(spec, worker.bindings, worker.eventSources);
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
// Add environment variables as vars
|
|
@@ -166,9 +169,16 @@ export interface WranglerJsonSpec {
|
|
|
166
169
|
* Queue bindings
|
|
167
170
|
*/
|
|
168
171
|
queues?: {
|
|
169
|
-
binding: string;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
producers: { queue: string; binding: string }[];
|
|
173
|
+
consumers: {
|
|
174
|
+
queue: string;
|
|
175
|
+
max_batch_size?: number;
|
|
176
|
+
max_concurrency?: number;
|
|
177
|
+
max_retries?: number;
|
|
178
|
+
max_wait_time_ms?: number;
|
|
179
|
+
retry_delay?: number;
|
|
180
|
+
}[];
|
|
181
|
+
};
|
|
172
182
|
|
|
173
183
|
/**
|
|
174
184
|
* Service bindings
|
|
@@ -238,7 +248,11 @@ export interface WranglerJsonSpec {
|
|
|
238
248
|
/**
|
|
239
249
|
* Process worker bindings into wrangler.json format
|
|
240
250
|
*/
|
|
241
|
-
function processBindings(
|
|
251
|
+
function processBindings(
|
|
252
|
+
spec: WranglerJsonSpec,
|
|
253
|
+
bindings: Bindings,
|
|
254
|
+
eventSources: EventSource[] | undefined
|
|
255
|
+
): void {
|
|
242
256
|
// Arrays to collect different binding types
|
|
243
257
|
const kvNamespaces: { binding: string; id: string }[] = [];
|
|
244
258
|
const durableObjects: {
|
|
@@ -258,9 +272,39 @@ function processBindings(spec: WranglerJsonSpec, bindings: Bindings): void {
|
|
|
258
272
|
database_name: string;
|
|
259
273
|
migrations_dir?: string;
|
|
260
274
|
}[] = [];
|
|
261
|
-
const queues: {
|
|
275
|
+
const queues: {
|
|
276
|
+
producers: { queue: string; binding: string }[];
|
|
277
|
+
consumers: {
|
|
278
|
+
queue: string;
|
|
279
|
+
max_batch_size?: number;
|
|
280
|
+
max_concurrency?: number;
|
|
281
|
+
max_retries?: number;
|
|
282
|
+
max_wait_time_ms?: number;
|
|
283
|
+
retry_delay?: number;
|
|
284
|
+
}[];
|
|
285
|
+
} = {
|
|
286
|
+
producers: [],
|
|
287
|
+
consumers: [],
|
|
288
|
+
};
|
|
289
|
+
|
|
262
290
|
const vectorizeIndexes: { binding: string; index_name: string }[] = [];
|
|
263
291
|
|
|
292
|
+
for (const eventSource of eventSources ?? []) {
|
|
293
|
+
if (isQueueEventSource(eventSource)) {
|
|
294
|
+
queues.consumers.push({
|
|
295
|
+
queue: eventSource.queue.id,
|
|
296
|
+
max_batch_size: eventSource.settings?.batchSize,
|
|
297
|
+
max_concurrency: eventSource.settings?.maxConcurrency,
|
|
298
|
+
max_retries: eventSource.settings?.maxRetries,
|
|
299
|
+
max_wait_time_ms: eventSource.settings?.maxWaitTimeMs,
|
|
300
|
+
retry_delay: eventSource.settings?.retryDelay,
|
|
301
|
+
});
|
|
302
|
+
} else if (isQueue(eventSource)) {
|
|
303
|
+
queues.consumers.push({
|
|
304
|
+
queue: eventSource.id,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
264
308
|
// Process each binding
|
|
265
309
|
for (const [bindingName, binding] of Object.entries(bindings)) {
|
|
266
310
|
if (typeof binding === "string") {
|
|
@@ -320,7 +364,7 @@ function processBindings(spec: WranglerJsonSpec, bindings: Bindings): void {
|
|
|
320
364
|
migrations_dir: binding.migrationsDir,
|
|
321
365
|
});
|
|
322
366
|
} else if (binding.type === "queue") {
|
|
323
|
-
queues.push({
|
|
367
|
+
queues.producers.push({
|
|
324
368
|
binding: bindingName,
|
|
325
369
|
queue: binding.name,
|
|
326
370
|
});
|
|
@@ -355,7 +399,7 @@ function processBindings(spec: WranglerJsonSpec, bindings: Bindings): void {
|
|
|
355
399
|
spec.d1_databases = d1Databases;
|
|
356
400
|
}
|
|
357
401
|
|
|
358
|
-
if (queues.length > 0) {
|
|
402
|
+
if (queues.consumers.length > 0) {
|
|
359
403
|
spec.queues = queues;
|
|
360
404
|
}
|
|
361
405
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error class for Neon API errors
|
|
3
|
+
*/
|
|
4
|
+
export class NeonApiError extends Error {
|
|
5
|
+
/**
|
|
6
|
+
* HTTP status code
|
|
7
|
+
*/
|
|
8
|
+
public readonly status: number;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* HTTP status text
|
|
12
|
+
*/
|
|
13
|
+
public readonly statusText: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Error data from the API response
|
|
17
|
+
*/
|
|
18
|
+
public readonly errorData?: any;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a new NeonApiError
|
|
22
|
+
*/
|
|
23
|
+
constructor(message: string, response: Response, errorData?: any) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "NeonApiError";
|
|
26
|
+
this.status = response.status;
|
|
27
|
+
this.statusText = response.statusText;
|
|
28
|
+
this.errorData = errorData;
|
|
29
|
+
|
|
30
|
+
// Capture stack trace
|
|
31
|
+
if (Error.captureStackTrace) {
|
|
32
|
+
Error.captureStackTrace(this, NeonApiError);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handle API errors and throw NeonApiError with context
|
|
39
|
+
*
|
|
40
|
+
* @param response Response object from fetch
|
|
41
|
+
* @param action Action that was being performed
|
|
42
|
+
* @param resourceType Type of resource
|
|
43
|
+
* @param resourceId ID of the resource
|
|
44
|
+
*/
|
|
45
|
+
export async function handleApiError(
|
|
46
|
+
response: Response,
|
|
47
|
+
action: "get" | "create" | "update" | "delete" | "list",
|
|
48
|
+
resourceType: string,
|
|
49
|
+
resourceId?: string
|
|
50
|
+
): Promise<never> {
|
|
51
|
+
const resourceDisplay = resourceId ? `'${resourceId}'` : "";
|
|
52
|
+
let errorData: any;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
errorData = await response.json();
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// If we can't parse JSON, just use the response text
|
|
58
|
+
try {
|
|
59
|
+
errorData = { message: await response.text() };
|
|
60
|
+
} catch (textError) {
|
|
61
|
+
errorData = { message: response.statusText };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let message = `Error ${action} ${resourceType} ${resourceDisplay}: `;
|
|
66
|
+
|
|
67
|
+
if (errorData?.error?.message) {
|
|
68
|
+
message += errorData.error.message;
|
|
69
|
+
} else if (errorData?.message) {
|
|
70
|
+
message += errorData.message;
|
|
71
|
+
} else if (errorData?.error) {
|
|
72
|
+
message +=
|
|
73
|
+
typeof errorData.error === "string"
|
|
74
|
+
? errorData.error
|
|
75
|
+
: JSON.stringify(errorData.error);
|
|
76
|
+
} else {
|
|
77
|
+
message += response.statusText;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw new NeonApiError(message, response, errorData);
|
|
81
|
+
}
|
package/src/neon/api.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { Secret } from "../secret.js";
|
|
2
|
+
import { withExponentialBackoff } from "../util/retry.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for Neon API requests
|
|
6
|
+
*/
|
|
7
|
+
export interface NeonApiOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Base URL for Neon API
|
|
10
|
+
* @default https://console.neon.tech/api/v2
|
|
11
|
+
*/
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* API Key to use (overrides NEON_API_KEY env var)
|
|
16
|
+
*/
|
|
17
|
+
apiKey?: Secret;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a NeonApi instance with environment variable fallback
|
|
22
|
+
* @param options API options
|
|
23
|
+
* @returns NeonApi instance
|
|
24
|
+
*/
|
|
25
|
+
export function createNeonApi(options: Partial<NeonApiOptions> = {}): NeonApi {
|
|
26
|
+
return new NeonApi({
|
|
27
|
+
baseUrl: options.baseUrl,
|
|
28
|
+
apiKey: options.apiKey,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get authentication headers for Neon API
|
|
34
|
+
* @param options NeonApiOptions
|
|
35
|
+
* @returns Headers for authentication
|
|
36
|
+
*/
|
|
37
|
+
export async function getNeonAuthHeaders(
|
|
38
|
+
options: Partial<NeonApiOptions>
|
|
39
|
+
): Promise<Record<string, string>> {
|
|
40
|
+
const apiKey = options.apiKey?.unencrypted ?? process.env.NEON_API_KEY;
|
|
41
|
+
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Neon API key is required. Set NEON_API_KEY environment variable or provide apiKey option."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
Accept: "application/json",
|
|
51
|
+
Authorization: `Bearer ${apiKey}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Neon API client using raw fetch
|
|
57
|
+
*/
|
|
58
|
+
export class NeonApi {
|
|
59
|
+
public readonly baseUrl: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a new Neon API client
|
|
63
|
+
* Use createNeonApi factory function instead of direct constructor
|
|
64
|
+
*
|
|
65
|
+
* @param options API options
|
|
66
|
+
*/
|
|
67
|
+
constructor(private readonly options: NeonApiOptions) {
|
|
68
|
+
this.baseUrl = options.baseUrl ?? "https://console.neon.tech/api/v2";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Make a fetch request to the Neon API
|
|
73
|
+
*
|
|
74
|
+
* @param path API path (without base URL)
|
|
75
|
+
* @param init Fetch init options
|
|
76
|
+
* @returns Raw Response object from fetch
|
|
77
|
+
*/
|
|
78
|
+
async fetch(path: string, init: RequestInit = {}): Promise<Response> {
|
|
79
|
+
let headers: Record<string, string> = {};
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(init.headers)) {
|
|
82
|
+
init.headers.forEach(([key, value]) => {
|
|
83
|
+
headers[key] = value;
|
|
84
|
+
});
|
|
85
|
+
} else if (init.headers instanceof Headers) {
|
|
86
|
+
init.headers.forEach((value, key) => {
|
|
87
|
+
headers[key] = value;
|
|
88
|
+
});
|
|
89
|
+
} else if (init.headers) {
|
|
90
|
+
headers = init.headers as Record<string, string>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
headers = {
|
|
94
|
+
...(await getNeonAuthHeaders(this.options)),
|
|
95
|
+
...headers,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Use withExponentialBackoff for automatic retry on network errors
|
|
99
|
+
return withExponentialBackoff(
|
|
100
|
+
() =>
|
|
101
|
+
fetch(`${this.baseUrl}${path}`, {
|
|
102
|
+
...init,
|
|
103
|
+
headers,
|
|
104
|
+
}),
|
|
105
|
+
(error) => {
|
|
106
|
+
// Only retry on network-related errors
|
|
107
|
+
const errorMsg = (error as Error).message || "";
|
|
108
|
+
const isNetworkError =
|
|
109
|
+
errorMsg.includes("socket connection was closed") ||
|
|
110
|
+
errorMsg.includes("ECONNRESET") ||
|
|
111
|
+
errorMsg.includes("ETIMEDOUT") ||
|
|
112
|
+
errorMsg.includes("ECONNREFUSED");
|
|
113
|
+
|
|
114
|
+
return isNetworkError || error?.status?.toString().startsWith("5");
|
|
115
|
+
},
|
|
116
|
+
5, // Maximum 5 attempts (1 initial + 4 retries)
|
|
117
|
+
1000 // Start with 1s delay, will exponentially increase
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Helper for GET requests
|
|
123
|
+
*/
|
|
124
|
+
async get(path: string, init: RequestInit = {}): Promise<Response> {
|
|
125
|
+
return this.fetch(path, { ...init, method: "GET" });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Helper for POST requests
|
|
130
|
+
*/
|
|
131
|
+
async post(
|
|
132
|
+
path: string,
|
|
133
|
+
body: any,
|
|
134
|
+
init: RequestInit = {}
|
|
135
|
+
): Promise<Response> {
|
|
136
|
+
const requestBody =
|
|
137
|
+
body instanceof FormData
|
|
138
|
+
? body
|
|
139
|
+
: typeof body === "string"
|
|
140
|
+
? body
|
|
141
|
+
: JSON.stringify(body);
|
|
142
|
+
return this.fetch(path, { ...init, method: "POST", body: requestBody });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Helper for PUT requests
|
|
147
|
+
*/
|
|
148
|
+
async put(
|
|
149
|
+
path: string,
|
|
150
|
+
body: any,
|
|
151
|
+
init: RequestInit = {}
|
|
152
|
+
): Promise<Response> {
|
|
153
|
+
const requestBody = body instanceof FormData ? body : JSON.stringify(body);
|
|
154
|
+
return this.fetch(path, { ...init, method: "PUT", body: requestBody });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Helper for PATCH requests
|
|
159
|
+
*/
|
|
160
|
+
async patch(
|
|
161
|
+
path: string,
|
|
162
|
+
body: any,
|
|
163
|
+
init: RequestInit = {}
|
|
164
|
+
): Promise<Response> {
|
|
165
|
+
return this.fetch(path, {
|
|
166
|
+
...init,
|
|
167
|
+
method: "PATCH",
|
|
168
|
+
body: JSON.stringify(body),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Helper for DELETE requests
|
|
174
|
+
*/
|
|
175
|
+
async delete(path: string, init: RequestInit = {}): Promise<Response> {
|
|
176
|
+
return this.fetch(path, { ...init, method: "DELETE" });
|
|
177
|
+
}
|
|
178
|
+
}
|