alchemy 0.12.5 → 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.
Files changed (46) 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/hyperdrive.d.ts +216 -0
  9. package/lib/cloudflare/hyperdrive.d.ts.map +1 -0
  10. package/lib/cloudflare/hyperdrive.js +184 -0
  11. package/lib/cloudflare/hyperdrive.js.map +1 -0
  12. package/lib/cloudflare/index.d.ts +1 -0
  13. package/lib/cloudflare/index.d.ts.map +1 -1
  14. package/lib/cloudflare/index.js +1 -0
  15. package/lib/cloudflare/index.js.map +1 -1
  16. package/lib/cloudflare/worker.d.ts +19 -2
  17. package/lib/cloudflare/worker.d.ts.map +1 -1
  18. package/lib/cloudflare/worker.js +28 -4
  19. package/lib/cloudflare/worker.js.map +1 -1
  20. package/lib/neon/api-error.d.ts +31 -0
  21. package/lib/neon/api-error.d.ts.map +1 -0
  22. package/lib/neon/api-error.js +73 -0
  23. package/lib/neon/api-error.js.map +1 -0
  24. package/lib/neon/api.d.ts +70 -0
  25. package/lib/neon/api.d.ts.map +1 -0
  26. package/lib/neon/api.js +128 -0
  27. package/lib/neon/api.js.map +1 -0
  28. package/lib/neon/index.d.ts +4 -0
  29. package/lib/neon/index.d.ts.map +1 -0
  30. package/lib/neon/index.js +4 -0
  31. package/lib/neon/index.js.map +1 -0
  32. package/lib/neon/project.d.ts +345 -0
  33. package/lib/neon/project.d.ts.map +1 -0
  34. package/lib/neon/project.js +305 -0
  35. package/lib/neon/project.js.map +1 -0
  36. package/package.json +2 -1
  37. package/src/cloudflare/ai-gateway.ts +0 -1
  38. package/src/cloudflare/bindings.ts +4 -2
  39. package/src/cloudflare/bound.ts +20 -17
  40. package/src/cloudflare/hyperdrive.ts +375 -0
  41. package/src/cloudflare/index.ts +1 -0
  42. package/src/cloudflare/worker.ts +55 -6
  43. package/src/neon/api-error.ts +81 -0
  44. package/src/neon/api.ts +178 -0
  45. package/src/neon/index.ts +3 -0
  46. package/src/neon/project.ts +870 -0
@@ -151,7 +151,6 @@ export interface AiGateway
151
151
  */
152
152
  export const AiGateway = Resource(
153
153
  "cloudflare::AiGateway",
154
- { alwaysUpdate: true }, // AI Gateway PUT updates, so always run update logic
155
154
  async function (
156
155
  this: Context<AiGateway>,
157
156
  id: string,
@@ -9,6 +9,7 @@ import type { Assets } from "./assets.js";
9
9
  import type { R2Bucket } from "./bucket.js";
10
10
  import type { D1Database } from "./d1-database.js";
11
11
  import type { DurableObjectNamespace } from "./durable-object-namespace.js";
12
+ import type { Hyperdrive } from "./hyperdrive.js";
12
13
  import type { KVNamespace } from "./kv-namespace.js";
13
14
  import type { Pipeline } from "./pipeline.js";
14
15
  import type { Queue } from "./queue.js";
@@ -24,9 +25,11 @@ export type Bindings = {
24
25
  * L2 Binding Resources.
25
26
  */
26
27
  export type Binding =
28
+ | AiGateway
27
29
  | Assets
28
30
  | D1Database
29
31
  | DurableObjectNamespace
32
+ | Hyperdrive
30
33
  | KVNamespace
31
34
  | Pipeline
32
35
  | Queue
@@ -35,8 +38,7 @@ export type Binding =
35
38
  | string
36
39
  | VectorizeIndex
37
40
  | Worker
38
- | Workflow
39
- | AiGateway;
41
+ | Workflow;
40
42
 
41
43
  /**
42
44
  * Union type for all Worker binding types (API spec)
@@ -6,6 +6,7 @@ import type { Binding } from "./bindings.js";
6
6
  import type { R2Bucket as _R2Bucket } from "./bucket.js";
7
7
  import type { D1Database as _D1Database } from "./d1-database.js";
8
8
  import type { DurableObjectNamespace as _DurableObjectNamespace } from "./durable-object-namespace.js";
9
+ import type { Hyperdrive as _Hyperdrive } from "./hyperdrive.js";
9
10
  import type { KVNamespace as _KVNamespace } from "./kv-namespace.js";
10
11
  import type { Pipeline as _Pipeline } from "./pipeline.js";
11
12
  import type { Queue as _Queue } from "./queue.js";
@@ -23,20 +24,22 @@ export type Bound<T extends Binding> = T extends _DurableObjectNamespace
23
24
  ? R2Bucket
24
25
  : T extends _AiGateway
25
26
  ? AiGateway
26
- : T extends Secret
27
- ? string
28
- : T extends Assets
29
- ? Service
30
- : T extends _Workflow<infer P>
31
- ? Workflow<P>
32
- : T extends _D1Database
33
- ? D1Database
34
- : T extends _VectorizeIndex
35
- ? VectorizeIndex
36
- : T extends _Queue
37
- ? Queue
38
- : T extends _Pipeline<infer R>
39
- ? Pipeline<R>
40
- : T extends string
41
- ? string
42
- : Service;
27
+ : T extends _Hyperdrive
28
+ ? Hyperdrive
29
+ : T extends Secret
30
+ ? string
31
+ : T extends Assets
32
+ ? Service
33
+ : T extends _Workflow<infer P>
34
+ ? Workflow<P>
35
+ : T extends _D1Database
36
+ ? D1Database
37
+ : T extends _VectorizeIndex
38
+ ? VectorizeIndex
39
+ : T extends _Queue
40
+ ? Queue
41
+ : T extends _Pipeline<infer R>
42
+ ? Pipeline<R>
43
+ : T extends string
44
+ ? string
45
+ : Service;
@@ -0,0 +1,375 @@
1
+ import type { Context } from "../context.js";
2
+ import { Resource } from "../resource.js";
3
+ import type { Secret } from "../secret.js";
4
+ import { handleApiError } from "./api-error.js";
5
+ import { createCloudflareApi, type CloudflareApiOptions } from "./api.js";
6
+
7
+ /**
8
+ * Origin configuration for a PostgreSQL database connection
9
+ */
10
+ export interface HyperdriveOrigin {
11
+ /**
12
+ * Database name
13
+ */
14
+ database: string;
15
+
16
+ /**
17
+ * Database host
18
+ */
19
+ host: string;
20
+
21
+ /**
22
+ * Database password
23
+ * Use alchemy.secret() to securely store this value
24
+ */
25
+ password: Secret;
26
+
27
+ /**
28
+ * Database port
29
+ * @default 5432
30
+ */
31
+ port?: number;
32
+
33
+ /**
34
+ * Connection scheme
35
+ * @default "postgres"
36
+ */
37
+ scheme?: "postgres";
38
+
39
+ /**
40
+ * Database user
41
+ */
42
+ user: string;
43
+ }
44
+
45
+ /**
46
+ * Origin configuration for a database connection with access tokens
47
+ */
48
+ export interface HyperdriveOriginWithAccess {
49
+ /**
50
+ * Access client ID
51
+ */
52
+ access_client_id: string;
53
+
54
+ /**
55
+ * Access client secret
56
+ * Use alchemy.secret() to securely store this value
57
+ */
58
+ access_client_secret: Secret;
59
+
60
+ /**
61
+ * Database host
62
+ */
63
+ host: string;
64
+
65
+ /**
66
+ * Database name
67
+ */
68
+ database: string;
69
+
70
+ /**
71
+ * Database port
72
+ * @default 5432
73
+ */
74
+ port?: number;
75
+
76
+ /**
77
+ * Connection scheme
78
+ * @default "postgres"
79
+ */
80
+ scheme?: "postgres";
81
+
82
+ /**
83
+ * Database user
84
+ */
85
+ user: string;
86
+ }
87
+
88
+ /**
89
+ * Caching configuration for Hyperdrive
90
+ */
91
+ export interface HyperdriveCaching {
92
+ /**
93
+ * Whether caching is disabled
94
+ * @default false
95
+ */
96
+ disabled?: boolean;
97
+ }
98
+
99
+ /**
100
+ * mTLS configuration for Hyperdrive
101
+ */
102
+ export interface HyperdriveMtls {
103
+ /**
104
+ * CA certificate ID
105
+ */
106
+ ca_certificate_id?: string;
107
+
108
+ /**
109
+ * mTLS certificate ID
110
+ */
111
+ mtls_certificate_id?: string;
112
+
113
+ /**
114
+ * SSL mode
115
+ * @default "verify-full"
116
+ */
117
+ sslmode?: "verify-ca" | "verify-full";
118
+ }
119
+
120
+ /**
121
+ * Properties for creating or updating a Cloudflare Hyperdrive.
122
+ */
123
+ export interface HyperdriveProps extends CloudflareApiOptions {
124
+ /**
125
+ * Name of the Hyperdrive configuration
126
+ */
127
+ name: string;
128
+
129
+ /**
130
+ * Database connection origin configuration
131
+ */
132
+ origin: HyperdriveOrigin | HyperdriveOriginWithAccess;
133
+
134
+ /**
135
+ * Caching configuration
136
+ */
137
+ caching?: HyperdriveCaching;
138
+
139
+ /**
140
+ * mTLS configuration
141
+ */
142
+ mtls?: HyperdriveMtls;
143
+
144
+ /**
145
+ * UUID of the hyperdrive (only used for update/delete operations)
146
+ * This is provided by Cloudflare and is different from the resource ID
147
+ * @internal
148
+ */
149
+ hyperdriveId?: string;
150
+ }
151
+
152
+ /**
153
+ * Output returned after Cloudflare Hyperdrive creation/update.
154
+ * IMPORTANT: The interface name MUST match the exported resource name.
155
+ */
156
+ export interface Hyperdrive
157
+ extends Resource<"cloudflare::Hyperdrive">,
158
+ Omit<HyperdriveProps, "origin"> {
159
+ /**
160
+ * The ID of the resource
161
+ */
162
+ id: string;
163
+
164
+ /**
165
+ * The Cloudflare-generated UUID of the hyperdrive
166
+ */
167
+ hyperdriveId: string;
168
+
169
+ /**
170
+ * Database connection origin configuration
171
+ */
172
+ origin: HyperdriveOrigin | HyperdriveOriginWithAccess;
173
+
174
+ /**
175
+ * Resource type identifier for binding.
176
+ * @internal
177
+ */
178
+ type: "hyperdrive";
179
+ }
180
+
181
+ /**
182
+ * Represents a Cloudflare Hyperdrive configuration.
183
+ *
184
+ * @example
185
+ * // Create a basic Hyperdrive connection to a PostgreSQL database
186
+ * const basicHyperdrive = await Hyperdrive("my-postgres-db", {
187
+ * name: "my-postgres-db",
188
+ * origin: {
189
+ * database: "postgres",
190
+ * host: "database.example.com",
191
+ * password: alchemy.secret("your-password"),
192
+ * port: 5432,
193
+ * user: "postgres"
194
+ * }
195
+ * });
196
+ *
197
+ * @example
198
+ * // Create a Hyperdrive with caching disabled
199
+ * const noCacheHyperdrive = await Hyperdrive("no-cache-db", {
200
+ * name: "no-cache-db",
201
+ * origin: {
202
+ * database: "postgres",
203
+ * host: "database.example.com",
204
+ * password: alchemy.secret(process.env.DB_PASSWORD),
205
+ * port: 5432,
206
+ * user: "postgres"
207
+ * },
208
+ * caching: {
209
+ * disabled: true
210
+ * }
211
+ * });
212
+ *
213
+ * @example
214
+ * // Create a Hyperdrive with mTLS configuration
215
+ * const mtlsHyperdrive = await Hyperdrive("secure-db", {
216
+ * name: "secure-db",
217
+ * origin: {
218
+ * database: "postgres",
219
+ * host: "database.example.com",
220
+ * password: alchemy.secret(process.env.DB_PASSWORD),
221
+ * port: 5432,
222
+ * user: "postgres"
223
+ * },
224
+ * mtls: {
225
+ * ca_certificate_id: "00000000-0000-0000-0000-0000000000",
226
+ * mtls_certificate_id: "00000000-0000-0000-0000-0000000000",
227
+ * sslmode: "verify-full"
228
+ * }
229
+ * });
230
+ *
231
+ * @example
232
+ * // Create a Hyperdrive with access client credentials
233
+ * const accessHyperdrive = await Hyperdrive("access-db", {
234
+ * name: "access-db",
235
+ * origin: {
236
+ * database: "postgres",
237
+ * host: "database.example.com",
238
+ * access_client_id: "client-id",
239
+ * access_client_secret: alchemy.secret(process.env.ACCESS_CLIENT_SECRET),
240
+ * port: 5432,
241
+ * user: "postgres"
242
+ * }
243
+ * });
244
+ */
245
+ export const Hyperdrive = Resource(
246
+ "cloudflare::Hyperdrive",
247
+ async function (
248
+ this: Context<Hyperdrive>,
249
+ id: string,
250
+ props: HyperdriveProps
251
+ ): Promise<Hyperdrive> {
252
+ const api = await createCloudflareApi(props);
253
+ const configsPath = `/accounts/${api.accountId}/hyperdrive/configs`;
254
+
255
+ // For create operations, we don't have a hyperdriveId yet
256
+ // For update/delete operations, we need to use the hyperdriveId from props or output
257
+ const hyperdriveId = props.hyperdriveId || this.output?.hyperdriveId;
258
+ const configPath = hyperdriveId
259
+ ? `${configsPath}/${hyperdriveId}`
260
+ : `${configsPath}`;
261
+
262
+ if (this.phase === "delete") {
263
+ if (!hyperdriveId) {
264
+ console.warn(`No hyperdriveId found for ${id}, skipping delete`);
265
+ return this.destroy();
266
+ }
267
+
268
+ try {
269
+ const deleteResponse = await api.delete(configPath);
270
+ // Only swallow 404 Not Found errors, all other errors should be handled
271
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
272
+ await handleApiError(deleteResponse, "delete", "hyperdrive", id);
273
+ }
274
+ } catch (error) {
275
+ console.error(`Error deleting Hyperdrive ${id}:`, error);
276
+ throw error;
277
+ }
278
+ return this.destroy();
279
+ }
280
+
281
+ let response: Response | undefined;
282
+ let apiResource: any;
283
+
284
+ // Prepare request body with unwrapped secrets
285
+ const requestBody = prepareRequestBody(props);
286
+
287
+ try {
288
+ if (this.phase === "update" && hyperdriveId) {
289
+ // Update existing hyperdrive
290
+ response = await api.put(configPath, requestBody);
291
+ } else {
292
+ // Create new hyperdrive
293
+ if (hyperdriveId) {
294
+ // If we have a hyperdriveId but we're in create phase, it could be because
295
+ // the resource exists but wasn't in state. Do a GET to check.
296
+ const getResponse = await api.get(configPath);
297
+ if (getResponse.status === 200) {
298
+ // Hyperdrive exists, update it
299
+ console.log(
300
+ `Hyperdrive '${id}' already exists. Updating existing resource.`
301
+ );
302
+ response = await api.put(configPath, requestBody);
303
+ } else if (getResponse.status === 404) {
304
+ // Hyperdrive doesn't exist, create new
305
+ response = await api.post(configsPath, {
306
+ ...requestBody,
307
+ // Ensure name is set correctly if not already set
308
+ name: props.name || id,
309
+ });
310
+ } else {
311
+ // Unexpected error during GET check
312
+ await handleApiError(getResponse, "get", "hyperdrive", id);
313
+ }
314
+ } else {
315
+ // No hyperdriveId, create new
316
+ response = await api.post(configsPath, {
317
+ ...requestBody,
318
+ // Ensure name is set correctly if not already set
319
+ name: props.name || id,
320
+ });
321
+ }
322
+ }
323
+
324
+ if (!response?.ok) {
325
+ const action = this.phase === "update" ? "update" : "create";
326
+ await handleApiError(response!, action, "hyperdrive", id);
327
+ }
328
+
329
+ const data: { result: Record<string, any> } = await response!.json();
330
+ apiResource = data.result;
331
+ } catch (error) {
332
+ console.error(`Error ${this.phase} Hyperdrive '${id}':`, error);
333
+ throw error;
334
+ }
335
+
336
+ // Construct the output object from API response and props
337
+ return this({
338
+ id,
339
+ hyperdriveId: apiResource.id, // Store the Cloudflare-assigned UUID
340
+ name: apiResource.name,
341
+ origin: props.origin, // Keep the original origin with secrets
342
+ caching: apiResource.caching,
343
+ mtls: apiResource.mtls,
344
+ type: "hyperdrive",
345
+ });
346
+ }
347
+ );
348
+
349
+ /**
350
+ * Prepare the request body by unwrapping secret values
351
+ */
352
+ function prepareRequestBody(props: HyperdriveProps): any {
353
+ const requestBody: any = { ...props };
354
+
355
+ // Remove internal props
356
+ delete requestBody.hyperdriveId;
357
+
358
+ // Deep clone and unwrap secrets in the origin object
359
+ if ("password" in props.origin) {
360
+ // Regular origin with password
361
+ requestBody.origin = {
362
+ ...props.origin,
363
+ password: props.origin.password.unencrypted,
364
+ scheme: props.origin.scheme ?? "postgres",
365
+ };
366
+ } else if ("access_client_secret" in props.origin) {
367
+ // Origin with access client secret
368
+ requestBody.origin = {
369
+ ...props.origin,
370
+ access_client_secret: props.origin.access_client_secret.unencrypted,
371
+ };
372
+ }
373
+
374
+ return requestBody;
375
+ }
@@ -9,6 +9,7 @@ export * from "./custom-domain.js";
9
9
  export * from "./d1-database.js";
10
10
  export * from "./dns-records.js";
11
11
  export * from "./durable-object-namespace.js";
12
+ export * from "./hyperdrive.js";
12
13
  export * from "./kv-namespace.js";
13
14
  export * from "./local-dev-cloudflare-shim.js";
14
15
  export * from "./nuxt.js";
@@ -6,18 +6,19 @@ 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
10
  import {
10
- createCloudflareApi,
11
11
  type CloudflareApi,
12
12
  type CloudflareApiOptions,
13
+ createCloudflareApi,
13
14
  } from "./api.js";
14
- import { type Assets } from "./assets.js";
15
- import { type Bindings, type WorkerBindingSpec } from "./bindings.js";
15
+ import type { Assets } from "./assets.js";
16
+ import type { Bindings, WorkerBindingSpec } from "./bindings.js";
16
17
  import type { Bound } from "./bound.js";
17
- import { type DurableObjectNamespace } from "./durable-object-namespace.js";
18
+ import type { DurableObjectNamespace } from "./durable-object-namespace.js";
18
19
  import type { WorkerScriptMetadata } from "./worker-metadata.js";
19
20
  import type { SingleStepMigration } from "./worker-migration.js";
20
- import { upsertWorkflow, type Workflow } from "./workflow.js";
21
+ import { type Workflow, upsertWorkflow } from "./workflow.js";
21
22
 
22
23
  /**
23
24
  * Configuration options for static assets
@@ -159,6 +160,15 @@ export interface WorkerProps<B extends Bindings = Bindings>
159
160
  * Configuration for static assets
160
161
  */
161
162
  assets?: AssetsConfig;
163
+
164
+ /**
165
+ * Cron expressions for the trigger.
166
+ * Uses standard cron syntax (e.g. "0 0 * * *" for daily at midnight)
167
+ * To clear all cron triggers, pass an empty array.
168
+ *
169
+ * @see https://developers.cloudflare.com/workers/configuration/cron-triggers/#examples
170
+ */
171
+ crons?: string[];
162
172
  }
163
173
 
164
174
  /**
@@ -291,7 +301,16 @@ export interface Worker<B extends Bindings = Bindings>
291
301
  * }
292
302
  * });
293
303
  *
294
- * @see https://developers.cloudflare.com/workers/
304
+ * @example
305
+ * // Create a worker with scheduled cron triggers:
306
+ * const cronWorker = await Worker("scheduled-tasks", {
307
+ * name: "cron-worker",
308
+ * entrypoint: "./src/scheduled.ts",
309
+ * crons: ['* 15 * * *', '0 0 * * *', '0 12 * * MON']
310
+ * })
311
+ *
312
+ * @see
313
+ * https://developers.cloudflare.com/workers/
295
314
  */
296
315
  export const Worker = Resource(
297
316
  "cloudflare::Worker",
@@ -399,6 +418,23 @@ export const Worker = Resource(
399
418
  // Get current timestamp
400
419
  const now = Date.now();
401
420
 
421
+ // Update cron triggers
422
+ if (props.crons) {
423
+ const res = await api.put(
424
+ `/accounts/${api.accountId}/workers/scripts/${workerName}/schedules`,
425
+ props.crons.map((cron) => ({ cron }))
426
+ );
427
+
428
+ if (!res.ok) {
429
+ await handleApiError(
430
+ res,
431
+ "updating cron triggers",
432
+ "worker",
433
+ workerName
434
+ );
435
+ }
436
+ }
437
+
402
438
  // Construct the output
403
439
  return this({
404
440
  ...props,
@@ -418,6 +454,8 @@ export const Worker = Resource(
418
454
  url: workerUrl,
419
455
  // Include assets configuration in the output
420
456
  assets: props.assets,
457
+ // Include cron triggers in the output
458
+ crons: props.crons,
421
459
  // phantom property
422
460
  Env: undefined!,
423
461
  });
@@ -558,6 +596,10 @@ interface WorkerMetadata {
558
596
  keep_assets?: boolean;
559
597
  config?: AssetsConfig;
560
598
  };
599
+ cron_triggers?: {
600
+ cron: string;
601
+ suspended: boolean;
602
+ }[];
561
603
  }
562
604
 
563
605
  interface AssetUploadResult {
@@ -742,6 +784,13 @@ async function prepareWorkerMetadata<B extends Bindings>(
742
784
  type: "ai",
743
785
  name: bindingName,
744
786
  });
787
+ } else if (binding.type === "hyperdrive") {
788
+ // Hyperdrive binding
789
+ meta.bindings.push({
790
+ type: "hyperdrive",
791
+ name: bindingName,
792
+ id: binding.hyperdriveId,
793
+ });
745
794
  } else {
746
795
  // @ts-expect-error - we should never reach here
747
796
  throw new Error(`Unsupported binding type: ${binding.type}`);
@@ -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
+ }