alchemy 0.7.0 → 0.7.2

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.
@@ -0,0 +1,412 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ import {
4
+ CloudflareApi,
5
+ createCloudflareApi,
6
+ type CloudflareApiOptions,
7
+ } from "./api";
8
+ import { CloudflareApiError, handleApiError } from "./api-error";
9
+
10
+ /**
11
+ * Properties for creating or updating a D1 Database
12
+ */
13
+ export interface D1DatabaseProps extends CloudflareApiOptions {
14
+ /**
15
+ * Name of the database
16
+ */
17
+ name: string;
18
+
19
+ /**
20
+ * Optional primary location hint for the database
21
+ * Indicates the primary geographical location data will be stored
22
+ */
23
+ primaryLocationHint?:
24
+ | "wnam"
25
+ | "enam"
26
+ | "weur"
27
+ | "eeur"
28
+ | "apac"
29
+ | "auto"
30
+ | string;
31
+
32
+ /**
33
+ * Read replication configuration
34
+ * Only mutable property during updates
35
+ */
36
+ readReplication?: {
37
+ /**
38
+ * Read replication mode
39
+ * - auto: Automatic read replication
40
+ * - disabled: No read replication
41
+ */
42
+ mode: "auto" | "disabled";
43
+ };
44
+
45
+ /**
46
+ * Whether to delete the database.
47
+ * If set to false, the database will remain but the resource will be removed from state
48
+ *
49
+ * @default true
50
+ */
51
+ delete?: boolean;
52
+
53
+ /**
54
+ * Whether to adopt an existing database with the same name if it exists
55
+ * If true and a database with the same name exists, it will be adopted rather than creating a new one
56
+ *
57
+ * @default false
58
+ */
59
+ adopt?: boolean;
60
+ }
61
+
62
+ /**
63
+ * Output returned after D1 Database creation/update
64
+ */
65
+ export interface D1Database
66
+ extends Resource<"cloudflare::D1Database">,
67
+ D1DatabaseProps {
68
+ type: "d1";
69
+ /**
70
+ * The unique ID of the database (UUID)
71
+ */
72
+ id: string;
73
+
74
+ /**
75
+ * File size of the database
76
+ */
77
+ fileSize: number;
78
+
79
+ /**
80
+ * Number of tables in the database
81
+ */
82
+ numTables: number;
83
+
84
+ /**
85
+ * Version of the database
86
+ */
87
+ version: string;
88
+
89
+ /**
90
+ * Read replication configuration
91
+ */
92
+ readReplication?: {
93
+ /**
94
+ * Read replication mode
95
+ */
96
+ mode: "auto" | "disabled";
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Creates and manages Cloudflare D1 Databases.
102
+ *
103
+ * D1 Databases provide serverless SQL databases built on SQLite with
104
+ * automatic data replication for high availability.
105
+ *
106
+ * @example
107
+ * // Create a basic D1 database with default settings
108
+ * const basicDatabase = await D1Database("my-app-db", {
109
+ * name: "my-app-db"
110
+ * });
111
+ *
112
+ * @example
113
+ * // Create a database with location hint for optimal performance
114
+ * const westUsDatabase = await D1Database("west-us-db", {
115
+ * name: "west-us-db",
116
+ * primaryLocationHint: "wnam"
117
+ * });
118
+ *
119
+ * @example
120
+ * // Adopt an existing database if it already exists instead of failing
121
+ * const existingDb = await D1Database("existing-db", {
122
+ * name: "existing-db",
123
+ * adopt: true,
124
+ * readReplication: {
125
+ * mode: "auto"
126
+ * }
127
+ * });
128
+ *
129
+ * @see https://developers.cloudflare.com/d1/
130
+ */
131
+ export const D1Database = Resource(
132
+ "cloudflare::D1Database",
133
+ async function (
134
+ this: Context<D1Database>,
135
+ id: string,
136
+ props: D1DatabaseProps
137
+ ): Promise<D1Database> {
138
+ const api = await createCloudflareApi(props);
139
+ const databaseName = props.name || id;
140
+
141
+ if (this.phase === "delete") {
142
+ console.log("Deleting D1 database:", databaseName);
143
+ if (props.delete !== false) {
144
+ // Delete D1 database
145
+ console.log("Deleting D1 database:", databaseName);
146
+ await deleteDatabase(api, this.output?.id);
147
+ }
148
+
149
+ // Return void (a deleted database has no content)
150
+ return this.destroy();
151
+ } else {
152
+ let dbData: CloudflareD1Response;
153
+
154
+ if (this.phase === "create") {
155
+ console.log("Creating D1 database:", databaseName);
156
+ try {
157
+ dbData = await createDatabase(api, databaseName, props);
158
+ } catch (error) {
159
+ // Check if this is a "database already exists" error and adopt is enabled
160
+ if (
161
+ props.adopt &&
162
+ error instanceof CloudflareApiError &&
163
+ error.message.includes("already exists")
164
+ ) {
165
+ console.log(`Database ${databaseName} already exists, adopting it`);
166
+ // Find the existing database by name
167
+ const databases = await listDatabases(api, databaseName);
168
+ const existingDb = databases.find((db) => db.name === databaseName);
169
+
170
+ if (!existingDb) {
171
+ throw new Error(
172
+ `Failed to find existing database '${databaseName}' for adoption`
173
+ );
174
+ }
175
+
176
+ // Get the database details using its ID
177
+ dbData = await getDatabase(api, existingDb.id);
178
+
179
+ // Update the database with the provided properties
180
+ if (props.readReplication) {
181
+ console.log(
182
+ `Updating adopted database ${databaseName} with new properties`
183
+ );
184
+ dbData = await updateDatabase(api, existingDb.id, props);
185
+ }
186
+ } else {
187
+ // Re-throw the error if adopt is false or it's not a "database already exists" error
188
+ throw error;
189
+ }
190
+ }
191
+ } else {
192
+ // Update operation
193
+ if (this.output?.id) {
194
+ console.log("Updating D1 database:", databaseName);
195
+ // Update the database with new properties
196
+ dbData = await updateDatabase(api, this.output.id, props);
197
+ } else {
198
+ // If no ID exists, fall back to creating a new database
199
+ console.log(
200
+ "No existing database ID found, creating new D1 database:",
201
+ databaseName
202
+ );
203
+ dbData = await createDatabase(api, databaseName, props);
204
+ }
205
+ }
206
+
207
+ return this({
208
+ type: "d1",
209
+ id: dbData.result.uuid || "",
210
+ name: databaseName,
211
+ fileSize: dbData.result.file_size,
212
+ numTables: dbData.result.num_tables,
213
+ version: dbData.result.version,
214
+ readReplication: dbData.result.read_replication,
215
+ primaryLocationHint: props.primaryLocationHint,
216
+ accountId: api.accountId,
217
+ });
218
+ }
219
+ }
220
+ );
221
+
222
+ interface CloudflareD1Response {
223
+ result: {
224
+ uuid?: string;
225
+ name: string;
226
+ file_size: number;
227
+ num_tables: number;
228
+ version: string;
229
+ primary_location_hint?: string;
230
+ read_replication?: {
231
+ mode: "auto" | "disabled";
232
+ };
233
+ };
234
+ success: boolean;
235
+ errors: Array<{ code: number; message: string }>;
236
+ messages: string[];
237
+ }
238
+
239
+ /**
240
+ * Create a new D1 database
241
+ */
242
+ export async function createDatabase(
243
+ api: CloudflareApi,
244
+ databaseName: string,
245
+ props: D1DatabaseProps
246
+ ): Promise<CloudflareD1Response> {
247
+ // Create new D1 database
248
+ const createPayload: any = {
249
+ name: databaseName,
250
+ };
251
+
252
+ if (props.primaryLocationHint) {
253
+ createPayload.primary_location_hint = props.primaryLocationHint;
254
+ }
255
+
256
+ const createResponse = await api.post(
257
+ `/accounts/${api.accountId}/d1/database`,
258
+ createPayload
259
+ );
260
+
261
+ if (!createResponse.ok) {
262
+ return await handleApiError(
263
+ createResponse,
264
+ "creating",
265
+ "D1 database",
266
+ databaseName
267
+ );
268
+ }
269
+
270
+ return (await createResponse.json()) as CloudflareD1Response;
271
+ }
272
+
273
+ /**
274
+ * Get a D1 database
275
+ */
276
+ export async function getDatabase(
277
+ api: CloudflareApi,
278
+ databaseId?: string
279
+ ): Promise<CloudflareD1Response> {
280
+ if (!databaseId) {
281
+ throw new Error("Database ID is required");
282
+ }
283
+
284
+ const response = await api.get(
285
+ `/accounts/${api.accountId}/d1/database/${databaseId}`
286
+ );
287
+
288
+ if (!response.ok) {
289
+ return await handleApiError(response, "getting", "D1 database", databaseId);
290
+ }
291
+
292
+ return (await response.json()) as CloudflareD1Response;
293
+ }
294
+
295
+ /**
296
+ * Delete a D1 database
297
+ */
298
+ export async function deleteDatabase(
299
+ api: CloudflareApi,
300
+ databaseId?: string
301
+ ): Promise<void> {
302
+ if (!databaseId) {
303
+ console.log("No database ID provided, skipping delete");
304
+ return;
305
+ }
306
+
307
+ // Delete D1 database
308
+ const deleteResponse = await api.delete(
309
+ `/accounts/${api.accountId}/d1/database/${databaseId}`
310
+ );
311
+
312
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
313
+ const errorData: any = await deleteResponse.json().catch(() => ({
314
+ errors: [{ message: deleteResponse.statusText }],
315
+ }));
316
+ throw new CloudflareApiError(
317
+ `Error deleting D1 database '${databaseId}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
318
+ deleteResponse
319
+ );
320
+ }
321
+ }
322
+
323
+ /**
324
+ * List all D1 databases in an account
325
+ */
326
+ export async function listDatabases(
327
+ api: CloudflareApi,
328
+ name?: string
329
+ ): Promise<{ name: string; id: string }[]> {
330
+ // Construct query string if name is provided
331
+ const queryParams = name ? `?name=${encodeURIComponent(name)}` : "";
332
+
333
+ const response = await api.get(
334
+ `/accounts/${api.accountId}/d1/database${queryParams}`
335
+ );
336
+
337
+ if (!response.ok) {
338
+ throw new CloudflareApiError(
339
+ `Failed to list databases: ${response.statusText}`,
340
+ response
341
+ );
342
+ }
343
+
344
+ const data = (await response.json()) as {
345
+ success: boolean;
346
+ errors?: Array<{ code: number; message: string }>;
347
+ result?: Array<{
348
+ name: string;
349
+ uuid: string;
350
+ }>;
351
+ };
352
+
353
+ if (!data.success) {
354
+ const errorMessage = data.errors?.[0]?.message || "Unknown error";
355
+ throw new Error(`Failed to list databases: ${errorMessage}`);
356
+ }
357
+
358
+ // Transform API response
359
+ return (data.result || []).map((db) => ({
360
+ name: db.name,
361
+ id: db.uuid,
362
+ }));
363
+ }
364
+
365
+ /**
366
+ * Update a D1 database
367
+ *
368
+ * Note: According to Cloudflare API, only read_replication.mode can be modified during updates.
369
+ */
370
+ export async function updateDatabase(
371
+ api: CloudflareApi,
372
+ databaseId: string,
373
+ props: D1DatabaseProps
374
+ ): Promise<CloudflareD1Response> {
375
+ // Get current database state to check for non-mutable changes
376
+ const currentDB = await getDatabase(api, databaseId);
377
+
378
+ // Only read_replication can be modified in update
379
+ if (
380
+ props.primaryLocationHint &&
381
+ props.primaryLocationHint !== currentDB.result.primary_location_hint
382
+ ) {
383
+ throw new Error(
384
+ "Cannot update primaryLocationHint after database creation. Only readReplication.mode can be modified."
385
+ );
386
+ }
387
+
388
+ const updatePayload: any = {};
389
+
390
+ // Only include read_replication in update payload
391
+ if (props.readReplication) {
392
+ updatePayload.read_replication = {
393
+ mode: props.readReplication.mode,
394
+ };
395
+ }
396
+
397
+ const updateResponse = await api.patch(
398
+ `/accounts/${api.accountId}/d1/database/${databaseId}`,
399
+ updatePayload
400
+ );
401
+
402
+ if (!updateResponse.ok) {
403
+ return await handleApiError(
404
+ updateResponse,
405
+ "updating",
406
+ "D1 database",
407
+ databaseId
408
+ );
409
+ }
410
+
411
+ return (await updateResponse.json()) as CloudflareD1Response;
412
+ }
@@ -1,13 +1,17 @@
1
1
  export * from "./account-api-token";
2
2
  export * from "./api";
3
+ export * from "./api-error";
3
4
  export * from "./assets";
4
5
  export * from "./bindings";
5
6
  export * from "./bucket";
6
7
  export * from "./custom-domain";
8
+ export * from "./d1-database";
7
9
  export * from "./dns-records";
8
10
  export * from "./durable-object-namespace";
9
11
  export * from "./kv-namespace";
10
12
  export * from "./permission-groups";
13
+ export * from "./pipeline";
14
+ export * from "./queue";
11
15
  export * from "./r2-rest-state-store";
12
16
  export * from "./worker";
13
17
  export { Workflow } from "./workflow";
@@ -8,12 +8,6 @@ import {
8
8
  } from "./api";
9
9
  import { handleApiError } from "./api-error";
10
10
 
11
- export function isKVNamespace(resource: any): resource is KVNamespace {
12
- return (
13
- resource && typeof resource === "object" && resource.type === "kv_namespace"
14
- );
15
- }
16
-
17
11
  /**
18
12
  * Properties for creating or updating a KV Namespace
19
13
  */