alchemy 0.6.0 → 0.7.1

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,354 @@
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
+ /**
55
+ * Output returned after D1 Database creation/update
56
+ */
57
+ export interface D1Database
58
+ extends Resource<"cloudflare::D1Database">,
59
+ D1DatabaseProps {
60
+ type: "d1";
61
+ /**
62
+ * The unique ID of the database (UUID)
63
+ */
64
+ id: string;
65
+
66
+ /**
67
+ * File size of the database
68
+ */
69
+ fileSize: number;
70
+
71
+ /**
72
+ * Number of tables in the database
73
+ */
74
+ numTables: number;
75
+
76
+ /**
77
+ * Version of the database
78
+ */
79
+ version: string;
80
+
81
+ /**
82
+ * Read replication configuration
83
+ */
84
+ readReplication?: {
85
+ /**
86
+ * Read replication mode
87
+ */
88
+ mode: "auto" | "disabled";
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Creates and manages Cloudflare D1 Databases.
94
+ *
95
+ * D1 Databases provide serverless SQL databases built on SQLite with
96
+ * automatic data replication for high availability.
97
+ *
98
+ * @example
99
+ * // Create a basic D1 database with default settings
100
+ * const basicDatabase = await D1Database("my-app-db", {
101
+ * name: "my-app-db"
102
+ * });
103
+ *
104
+ * @example
105
+ * // Create a database with location hint for optimal performance
106
+ * const westUsDatabase = await D1Database("west-us-db", {
107
+ * name: "west-us-db",
108
+ * primaryLocationHint: "wnam"
109
+ * });
110
+ *
111
+ * @see https://developers.cloudflare.com/d1/
112
+ */
113
+ export const D1Database = Resource(
114
+ "cloudflare::D1Database",
115
+ async function (
116
+ this: Context<D1Database>,
117
+ id: string,
118
+ props: D1DatabaseProps
119
+ ): Promise<D1Database> {
120
+ const api = await createCloudflareApi(props);
121
+ const databaseName = props.name || id;
122
+
123
+ if (this.phase === "delete") {
124
+ console.log("Deleting D1 database:", databaseName);
125
+ if (props.delete !== false) {
126
+ // Delete D1 database
127
+ console.log("Deleting D1 database:", databaseName);
128
+ await deleteDatabase(api, this.output?.id);
129
+ }
130
+
131
+ // Return void (a deleted database has no content)
132
+ return this.destroy();
133
+ } else {
134
+ let dbData: CloudflareD1Response;
135
+
136
+ if (this.phase === "create") {
137
+ console.log("Creating D1 database:", databaseName);
138
+ dbData = await createDatabase(api, databaseName, props);
139
+ } else {
140
+ // Update operation
141
+ if (this.output?.id) {
142
+ console.log("Updating D1 database:", databaseName);
143
+ // Update the database with new properties
144
+ dbData = await updateDatabase(api, this.output.id, props);
145
+ } else {
146
+ // If no ID exists, fall back to creating a new database
147
+ console.log(
148
+ "No existing database ID found, creating new D1 database:",
149
+ databaseName
150
+ );
151
+ dbData = await createDatabase(api, databaseName, props);
152
+ }
153
+ }
154
+
155
+ return this({
156
+ type: "d1",
157
+ id: dbData.result.uuid || "",
158
+ name: databaseName,
159
+ fileSize: dbData.result.file_size,
160
+ numTables: dbData.result.num_tables,
161
+ version: dbData.result.version,
162
+ readReplication: dbData.result.read_replication,
163
+ primaryLocationHint: props.primaryLocationHint,
164
+ accountId: api.accountId,
165
+ });
166
+ }
167
+ }
168
+ );
169
+
170
+ interface CloudflareD1Response {
171
+ result: {
172
+ uuid?: string;
173
+ name: string;
174
+ file_size: number;
175
+ num_tables: number;
176
+ version: string;
177
+ primary_location_hint?: string;
178
+ read_replication?: {
179
+ mode: "auto" | "disabled";
180
+ };
181
+ };
182
+ success: boolean;
183
+ errors: Array<{ code: number; message: string }>;
184
+ messages: string[];
185
+ }
186
+
187
+ /**
188
+ * Create a new D1 database
189
+ */
190
+ export async function createDatabase(
191
+ api: CloudflareApi,
192
+ databaseName: string,
193
+ props: D1DatabaseProps
194
+ ): Promise<CloudflareD1Response> {
195
+ // Create new D1 database
196
+ const createPayload: any = {
197
+ name: databaseName,
198
+ };
199
+
200
+ if (props.primaryLocationHint) {
201
+ createPayload.primary_location_hint = props.primaryLocationHint;
202
+ }
203
+
204
+ const createResponse = await api.post(
205
+ `/accounts/${api.accountId}/d1/database`,
206
+ createPayload
207
+ );
208
+
209
+ if (!createResponse.ok) {
210
+ return await handleApiError(
211
+ createResponse,
212
+ "creating",
213
+ "D1 database",
214
+ databaseName
215
+ );
216
+ }
217
+
218
+ return (await createResponse.json()) as CloudflareD1Response;
219
+ }
220
+
221
+ /**
222
+ * Get a D1 database
223
+ */
224
+ export async function getDatabase(
225
+ api: CloudflareApi,
226
+ databaseId?: string
227
+ ): Promise<CloudflareD1Response> {
228
+ if (!databaseId) {
229
+ throw new Error("Database ID is required");
230
+ }
231
+
232
+ const response = await api.get(
233
+ `/accounts/${api.accountId}/d1/database/${databaseId}`
234
+ );
235
+
236
+ if (!response.ok) {
237
+ return await handleApiError(response, "getting", "D1 database", databaseId);
238
+ }
239
+
240
+ return (await response.json()) as CloudflareD1Response;
241
+ }
242
+
243
+ /**
244
+ * Delete a D1 database
245
+ */
246
+ export async function deleteDatabase(
247
+ api: CloudflareApi,
248
+ databaseId?: string
249
+ ): Promise<void> {
250
+ if (!databaseId) {
251
+ console.log("No database ID provided, skipping delete");
252
+ return;
253
+ }
254
+
255
+ // Delete D1 database
256
+ const deleteResponse = await api.delete(
257
+ `/accounts/${api.accountId}/d1/database/${databaseId}`
258
+ );
259
+
260
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
261
+ const errorData: any = await deleteResponse.json().catch(() => ({
262
+ errors: [{ message: deleteResponse.statusText }],
263
+ }));
264
+ throw new CloudflareApiError(
265
+ `Error deleting D1 database '${databaseId}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
266
+ deleteResponse
267
+ );
268
+ }
269
+ }
270
+
271
+ /**
272
+ * List all D1 databases in an account
273
+ */
274
+ export async function listDatabases(
275
+ api: CloudflareApi
276
+ ): Promise<{ name: string; id: string }[]> {
277
+ const response = await api.get(`/accounts/${api.accountId}/d1/database`);
278
+
279
+ if (!response.ok) {
280
+ throw new CloudflareApiError(
281
+ `Failed to list databases: ${response.statusText}`,
282
+ response
283
+ );
284
+ }
285
+
286
+ const data = (await response.json()) as {
287
+ success: boolean;
288
+ errors?: Array<{ code: number; message: string }>;
289
+ result?: Array<{
290
+ name: string;
291
+ uuid: string;
292
+ }>;
293
+ };
294
+
295
+ if (!data.success) {
296
+ const errorMessage = data.errors?.[0]?.message || "Unknown error";
297
+ throw new Error(`Failed to list databases: ${errorMessage}`);
298
+ }
299
+
300
+ // Transform API response
301
+ return (data.result || []).map((db) => ({
302
+ name: db.name,
303
+ id: db.uuid,
304
+ }));
305
+ }
306
+
307
+ /**
308
+ * Update a D1 database
309
+ *
310
+ * Note: According to Cloudflare API, only read_replication.mode can be modified during updates.
311
+ */
312
+ export async function updateDatabase(
313
+ api: CloudflareApi,
314
+ databaseId: string,
315
+ props: D1DatabaseProps
316
+ ): Promise<CloudflareD1Response> {
317
+ // Get current database state to check for non-mutable changes
318
+ const currentDB = await getDatabase(api, databaseId);
319
+
320
+ // Only read_replication can be modified in update
321
+ if (
322
+ props.primaryLocationHint &&
323
+ props.primaryLocationHint !== currentDB.result.primary_location_hint
324
+ ) {
325
+ throw new Error(
326
+ "Cannot update primaryLocationHint after database creation. Only readReplication.mode can be modified."
327
+ );
328
+ }
329
+
330
+ const updatePayload: any = {};
331
+
332
+ // Only include read_replication in update payload
333
+ if (props.readReplication) {
334
+ updatePayload.read_replication = {
335
+ mode: props.readReplication.mode,
336
+ };
337
+ }
338
+
339
+ const updateResponse = await api.patch(
340
+ `/accounts/${api.accountId}/d1/database/${databaseId}`,
341
+ updatePayload
342
+ );
343
+
344
+ if (!updateResponse.ok) {
345
+ return await handleApiError(
346
+ updateResponse,
347
+ "updating",
348
+ "D1 database",
349
+ databaseId
350
+ );
351
+ }
352
+
353
+ return (await updateResponse.json()) as CloudflareD1Response;
354
+ }
@@ -42,7 +42,7 @@ export class DurableObjectNamespace implements DurableObjectNamespaceInput {
42
42
 
43
43
  constructor(
44
44
  public readonly id: string,
45
- input: DurableObjectNamespaceInput,
45
+ input: DurableObjectNamespaceInput
46
46
  ) {
47
47
  this.className = input.className;
48
48
  this.scriptName = input.scriptName;
@@ -1,14 +1,18 @@
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 "./queue";
11
14
  export * from "./r2-rest-state-store";
12
15
  export * from "./worker";
16
+ export { Workflow } from "./workflow";
13
17
  export * from "./wrangler.json";
14
18
  export * from "./zone";
@@ -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
  */