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.
Files changed (44) 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.js +8 -0
  17. package/lib/cloudflare/worker.js.map +1 -1
  18. package/lib/neon/api-error.d.ts +31 -0
  19. package/lib/neon/api-error.d.ts.map +1 -0
  20. package/lib/neon/api-error.js +73 -0
  21. package/lib/neon/api-error.js.map +1 -0
  22. package/lib/neon/api.d.ts +70 -0
  23. package/lib/neon/api.d.ts.map +1 -0
  24. package/lib/neon/api.js +128 -0
  25. package/lib/neon/api.js.map +1 -0
  26. package/lib/neon/index.d.ts +4 -0
  27. package/lib/neon/index.d.ts.map +1 -0
  28. package/lib/neon/index.js +4 -0
  29. package/lib/neon/index.js.map +1 -0
  30. package/lib/neon/project.d.ts +345 -0
  31. package/lib/neon/project.d.ts.map +1 -0
  32. package/lib/neon/project.js +305 -0
  33. package/lib/neon/project.js.map +1 -0
  34. package/package.json +2 -1
  35. package/src/cloudflare/ai-gateway.ts +0 -1
  36. package/src/cloudflare/bindings.ts +4 -2
  37. package/src/cloudflare/bound.ts +20 -17
  38. package/src/cloudflare/hyperdrive.ts +375 -0
  39. package/src/cloudflare/index.ts +1 -0
  40. package/src/cloudflare/worker.ts +7 -0
  41. package/src/neon/api-error.ts +81 -0
  42. package/src/neon/api.ts +178 -0
  43. package/src/neon/index.ts +3 -0
  44. 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";
@@ -784,6 +784,13 @@ async function prepareWorkerMetadata<B extends Bindings>(
784
784
  type: "ai",
785
785
  name: bindingName,
786
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
+ });
787
794
  } else {
788
795
  // @ts-expect-error - we should never reach here
789
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
+ }