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.
Files changed (80) hide show
  1. package/lib/cloudflare/ai-gateway.d.ts.map +1 -1
  2. package/lib/cloudflare/ai-gateway.js +1 -2
  3. package/lib/cloudflare/ai-gateway.js.map +1 -1
  4. package/lib/cloudflare/bindings.d.ts +2 -1
  5. package/lib/cloudflare/bindings.d.ts.map +1 -1
  6. package/lib/cloudflare/bound.d.ts +2 -1
  7. package/lib/cloudflare/bound.d.ts.map +1 -1
  8. package/lib/cloudflare/bucket.d.ts +8 -2
  9. package/lib/cloudflare/bucket.d.ts.map +1 -1
  10. package/lib/cloudflare/bucket.js +1 -1
  11. package/lib/cloudflare/bucket.js.map +1 -1
  12. package/lib/cloudflare/d1-database.d.ts +8 -2
  13. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  14. package/lib/cloudflare/d1-database.js +2 -2
  15. package/lib/cloudflare/d1-database.js.map +1 -1
  16. package/lib/cloudflare/event-source.d.ts +26 -0
  17. package/lib/cloudflare/event-source.d.ts.map +1 -0
  18. package/lib/cloudflare/event-source.js +9 -0
  19. package/lib/cloudflare/event-source.js.map +1 -0
  20. package/lib/cloudflare/hyperdrive.d.ts +216 -0
  21. package/lib/cloudflare/hyperdrive.d.ts.map +1 -0
  22. package/lib/cloudflare/hyperdrive.js +184 -0
  23. package/lib/cloudflare/hyperdrive.js.map +1 -0
  24. package/lib/cloudflare/index.d.ts +2 -0
  25. package/lib/cloudflare/index.d.ts.map +1 -1
  26. package/lib/cloudflare/index.js +2 -0
  27. package/lib/cloudflare/index.js.map +1 -1
  28. package/lib/cloudflare/queue-consumer.d.ts +162 -0
  29. package/lib/cloudflare/queue-consumer.d.ts.map +1 -0
  30. package/lib/cloudflare/queue-consumer.js +197 -0
  31. package/lib/cloudflare/queue-consumer.js.map +1 -0
  32. package/lib/cloudflare/queue.d.ts +15 -3
  33. package/lib/cloudflare/queue.d.ts.map +1 -1
  34. package/lib/cloudflare/queue.js +8 -2
  35. package/lib/cloudflare/queue.js.map +1 -1
  36. package/lib/cloudflare/website.d.ts.map +1 -1
  37. package/lib/cloudflare/website.js +8 -10
  38. package/lib/cloudflare/website.js.map +1 -1
  39. package/lib/cloudflare/worker.d.ts +13 -2
  40. package/lib/cloudflare/worker.d.ts.map +1 -1
  41. package/lib/cloudflare/worker.js +75 -3
  42. package/lib/cloudflare/worker.js.map +1 -1
  43. package/lib/cloudflare/wrangler.json.d.ts +13 -3
  44. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  45. package/lib/cloudflare/wrangler.json.js +27 -5
  46. package/lib/cloudflare/wrangler.json.js.map +1 -1
  47. package/lib/neon/api-error.d.ts +31 -0
  48. package/lib/neon/api-error.d.ts.map +1 -0
  49. package/lib/neon/api-error.js +73 -0
  50. package/lib/neon/api-error.js.map +1 -0
  51. package/lib/neon/api.d.ts +70 -0
  52. package/lib/neon/api.d.ts.map +1 -0
  53. package/lib/neon/api.js +128 -0
  54. package/lib/neon/api.js.map +1 -0
  55. package/lib/neon/index.d.ts +4 -0
  56. package/lib/neon/index.d.ts.map +1 -0
  57. package/lib/neon/index.js +4 -0
  58. package/lib/neon/index.js.map +1 -0
  59. package/lib/neon/project.d.ts +345 -0
  60. package/lib/neon/project.d.ts.map +1 -0
  61. package/lib/neon/project.js +305 -0
  62. package/lib/neon/project.js.map +1 -0
  63. package/package.json +2 -1
  64. package/src/cloudflare/ai-gateway.ts +0 -1
  65. package/src/cloudflare/bindings.ts +4 -2
  66. package/src/cloudflare/bound.ts +20 -17
  67. package/src/cloudflare/bucket.ts +9 -2
  68. package/src/cloudflare/d1-database.ts +10 -3
  69. package/src/cloudflare/event-source.ts +33 -0
  70. package/src/cloudflare/hyperdrive.ts +375 -0
  71. package/src/cloudflare/index.ts +2 -0
  72. package/src/cloudflare/queue-consumer.ts +433 -0
  73. package/src/cloudflare/queue.ts +83 -63
  74. package/src/cloudflare/website.ts +9 -13
  75. package/src/cloudflare/worker.ts +109 -5
  76. package/src/cloudflare/wrangler.json.ts +52 -8
  77. package/src/neon/api-error.ts +81 -0
  78. package/src/neon/api.ts +178 -0
  79. package/src/neon/index.ts +3 -0
  80. package/src/neon/project.ts +870 -0
@@ -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
- * This is mandatory - must be explicitly specified
108
+ *
109
+ * @default id
102
110
  */
103
- name: string;
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 ?? "2024-09-09";
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
- queue: string;
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(spec: WranglerJsonSpec, bindings: Bindings): void {
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: { binding: string; queue: string }[] = [];
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
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./api-error.js";
2
+ export * from "./api.js";
3
+ export * from "./project.js";