alchemy 0.7.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,371 @@
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
+ * Settings for a Cloudflare Queue
12
+ */
13
+ export interface QueueSettings {
14
+ /**
15
+ * Delay in seconds before message delivery
16
+ * Queue will not deliver messages until this time has elapsed
17
+ */
18
+ deliveryDelay?: number;
19
+
20
+ /**
21
+ * Whether delivery is paused
22
+ * If true, the queue will not deliver messages to consumers
23
+ */
24
+ deliveryPaused?: boolean;
25
+
26
+ /**
27
+ * Period in seconds to retain messages
28
+ * Messages will be automatically deleted after this time
29
+ */
30
+ messageRetentionPeriod?: number;
31
+ }
32
+
33
+ /**
34
+ * Properties for creating or updating a Cloudflare Queue
35
+ */
36
+ export interface QueueProps extends CloudflareApiOptions {
37
+ /**
38
+ * Name of the queue
39
+ * Required during creation
40
+ * Cannot be changed after creation
41
+ */
42
+ name: string;
43
+
44
+ /**
45
+ * Settings for the queue
46
+ * These can be updated after queue creation
47
+ */
48
+ settings?: QueueSettings;
49
+
50
+ /**
51
+ * Whether to delete the queue.
52
+ * If set to false, the queue will remain but the resource will be removed from state
53
+ *
54
+ * @default true
55
+ */
56
+ delete?: boolean;
57
+ }
58
+
59
+ /**
60
+ * Output returned after Cloudflare Queue creation/update
61
+ */
62
+ export interface Queue extends Resource<"cloudflare::Queue">, QueueProps {
63
+ /**
64
+ * Type identifier for Cloudflare Queue
65
+ */
66
+ type: "queue";
67
+
68
+ /**
69
+ * The unique ID of the queue
70
+ */
71
+ id: string;
72
+
73
+ /**
74
+ * Time when the queue was created
75
+ */
76
+ createdOn: string;
77
+
78
+ /**
79
+ * Modified timestamp
80
+ */
81
+ modifiedOn: string;
82
+ }
83
+
84
+ interface CloudflareQueueResponse {
85
+ result: {
86
+ queue_id?: string;
87
+ queue_name: string;
88
+ created_on?: string;
89
+ modified_on?: string;
90
+ settings?: {
91
+ delivery_delay?: number;
92
+ delivery_paused?: boolean;
93
+ message_retention_period?: number;
94
+ };
95
+ };
96
+ success: boolean;
97
+ errors: Array<{ code: number; message: string }>;
98
+ messages: string[];
99
+ }
100
+
101
+ /**
102
+ * Creates and manages Cloudflare Queues.
103
+ *
104
+ * Queues provide a managed queue system for reliable message delivery
105
+ * between workers and other systems.
106
+ *
107
+ * @example
108
+ * // Create a basic queue with default settings
109
+ * const basicQueue = await Queue("my-app-queue", {
110
+ * name: "my-app-queue"
111
+ * });
112
+ *
113
+ * @example
114
+ * // Create a queue with custom settings
115
+ * const customQueue = await Queue("delayed-queue", {
116
+ * name: "delayed-queue",
117
+ * settings: {
118
+ * deliveryDelay: 30, // 30 second delay before message delivery
119
+ * messageRetentionPeriod: 86400 // Store messages for 1 day
120
+ * }
121
+ * });
122
+ *
123
+ * @example
124
+ * // Create a paused queue for later activation
125
+ * const pausedQueue = await Queue("paused-queue", {
126
+ * name: "paused-queue",
127
+ * settings: {
128
+ * deliveryPaused: true
129
+ * }
130
+ * });
131
+ *
132
+ * @see https://developers.cloudflare.com/queues/
133
+ */
134
+ export const Queue = Resource(
135
+ "cloudflare::Queue",
136
+ async function (
137
+ this: Context<Queue>,
138
+ id: string,
139
+ props: QueueProps
140
+ ): Promise<Queue> {
141
+ const api = await createCloudflareApi(props);
142
+ const queueName = props.name || id;
143
+
144
+ if (this.phase === "delete") {
145
+ console.log("Deleting Cloudflare Queue:", queueName);
146
+ if (props.delete !== false) {
147
+ // Delete Queue
148
+ await deleteQueue(api, this.output?.id);
149
+ }
150
+
151
+ // Return void (a deleted queue has no content)
152
+ return this.destroy();
153
+ } else {
154
+ let queueData: CloudflareQueueResponse;
155
+
156
+ if (this.phase === "create") {
157
+ console.log("Creating Cloudflare Queue:", queueName);
158
+ queueData = await createQueue(api, queueName, props);
159
+ } else {
160
+ // Update operation
161
+ if (this.output?.id) {
162
+ console.log("Updating Cloudflare Queue:", queueName);
163
+
164
+ // Check if name is being changed, which is not allowed
165
+ if (props.name !== this.output.name) {
166
+ throw new Error(
167
+ "Cannot update Queue name after creation. Queue name is immutable."
168
+ );
169
+ }
170
+
171
+ // Update the queue with new settings
172
+ queueData = await updateQueue(api, this.output.id, props);
173
+ } else {
174
+ // If no ID exists, fall back to creating a new queue
175
+ console.log(
176
+ "No existing Queue ID found, creating new Cloudflare Queue:",
177
+ queueName
178
+ );
179
+ queueData = await createQueue(api, queueName, props);
180
+ }
181
+ }
182
+
183
+ return this({
184
+ type: "queue",
185
+ id: queueData.result.queue_id || "",
186
+ name: queueName,
187
+ settings: queueData.result.settings
188
+ ? {
189
+ deliveryDelay: queueData.result.settings.delivery_delay,
190
+ deliveryPaused: queueData.result.settings.delivery_paused,
191
+ messageRetentionPeriod:
192
+ queueData.result.settings.message_retention_period,
193
+ }
194
+ : undefined,
195
+ createdOn: queueData.result.created_on || new Date().toISOString(),
196
+ modifiedOn: queueData.result.modified_on || new Date().toISOString(),
197
+ accountId: api.accountId,
198
+ });
199
+ }
200
+ }
201
+ );
202
+
203
+ /**
204
+ * Create a new Cloudflare Queue
205
+ */
206
+ export async function createQueue(
207
+ api: CloudflareApi,
208
+ queueName: string,
209
+ props: QueueProps
210
+ ): Promise<CloudflareQueueResponse> {
211
+ // Prepare the create payload
212
+ const createPayload: any = {
213
+ queue_name: queueName,
214
+ };
215
+
216
+ // Add settings if provided
217
+ if (props.settings) {
218
+ createPayload.settings = {};
219
+
220
+ if (props.settings.deliveryDelay !== undefined) {
221
+ createPayload.settings.delivery_delay = props.settings.deliveryDelay;
222
+ }
223
+
224
+ if (props.settings.deliveryPaused !== undefined) {
225
+ createPayload.settings.delivery_paused = props.settings.deliveryPaused;
226
+ }
227
+
228
+ if (props.settings.messageRetentionPeriod !== undefined) {
229
+ createPayload.settings.message_retention_period =
230
+ props.settings.messageRetentionPeriod;
231
+ }
232
+ }
233
+
234
+ const createResponse = await api.post(
235
+ `/accounts/${api.accountId}/queues`,
236
+ createPayload
237
+ );
238
+
239
+ if (!createResponse.ok) {
240
+ return await handleApiError(createResponse, "creating", "Queue", queueName);
241
+ }
242
+
243
+ return (await createResponse.json()) as CloudflareQueueResponse;
244
+ }
245
+
246
+ /**
247
+ * Get a Cloudflare Queue
248
+ */
249
+ export async function getQueue(
250
+ api: CloudflareApi,
251
+ queueId: string
252
+ ): Promise<CloudflareQueueResponse> {
253
+ const response = await api.get(
254
+ `/accounts/${api.accountId}/queues/${queueId}`
255
+ );
256
+
257
+ if (!response.ok) {
258
+ return await handleApiError(response, "getting", "Queue", queueId);
259
+ }
260
+
261
+ return (await response.json()) as CloudflareQueueResponse;
262
+ }
263
+
264
+ /**
265
+ * Delete a Cloudflare Queue
266
+ */
267
+ export async function deleteQueue(
268
+ api: CloudflareApi,
269
+ queueId?: string
270
+ ): Promise<void> {
271
+ if (!queueId) {
272
+ console.log("No Queue ID provided, skipping delete");
273
+ return;
274
+ }
275
+
276
+ // Delete Queue
277
+ const deleteResponse = await api.delete(
278
+ `/accounts/${api.accountId}/queues/${queueId}`
279
+ );
280
+
281
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
282
+ const errorData: any = await deleteResponse.json().catch(() => ({
283
+ errors: [{ message: deleteResponse.statusText }],
284
+ }));
285
+ throw new CloudflareApiError(
286
+ `Error deleting Cloudflare Queue '${queueId}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
287
+ deleteResponse
288
+ );
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Update a Cloudflare Queue
294
+ *
295
+ * Note: According to Cloudflare API, the queue name cannot be changed after creation.
296
+ * Only the settings can be updated.
297
+ */
298
+ export async function updateQueue(
299
+ api: CloudflareApi,
300
+ queueId: string,
301
+ props: QueueProps
302
+ ): Promise<CloudflareQueueResponse> {
303
+ // Prepare the update payload - only include settings
304
+ const updatePayload: any = {};
305
+
306
+ // Add settings if provided
307
+ if (props.settings) {
308
+ updatePayload.settings = {};
309
+
310
+ if (props.settings.deliveryDelay !== undefined) {
311
+ updatePayload.settings.delivery_delay = props.settings.deliveryDelay;
312
+ }
313
+
314
+ if (props.settings.deliveryPaused !== undefined) {
315
+ updatePayload.settings.delivery_paused = props.settings.deliveryPaused;
316
+ }
317
+
318
+ if (props.settings.messageRetentionPeriod !== undefined) {
319
+ updatePayload.settings.message_retention_period =
320
+ props.settings.messageRetentionPeriod;
321
+ }
322
+ }
323
+
324
+ // Use PATCH for partial updates (only settings can be updated)
325
+ const updateResponse = await api.patch(
326
+ `/accounts/${api.accountId}/queues/${queueId}`,
327
+ updatePayload
328
+ );
329
+
330
+ if (!updateResponse.ok) {
331
+ return await handleApiError(updateResponse, "updating", "Queue", queueId);
332
+ }
333
+
334
+ return (await updateResponse.json()) as CloudflareQueueResponse;
335
+ }
336
+
337
+ /**
338
+ * List all Cloudflare Queues in an account
339
+ */
340
+ export async function listQueues(
341
+ api: CloudflareApi
342
+ ): Promise<{ name: string; id: string }[]> {
343
+ const response = await api.get(`/accounts/${api.accountId}/queues`);
344
+
345
+ if (!response.ok) {
346
+ throw new CloudflareApiError(
347
+ `Failed to list queues: ${response.statusText}`,
348
+ response
349
+ );
350
+ }
351
+
352
+ const data = (await response.json()) as {
353
+ success: boolean;
354
+ errors?: Array<{ code: number; message: string }>;
355
+ result?: Array<{
356
+ queue_name: string;
357
+ queue_id: string;
358
+ }>;
359
+ };
360
+
361
+ if (!data.success) {
362
+ const errorMessage = data.errors?.[0]?.message || "Unknown error";
363
+ throw new Error(`Failed to list queues: ${errorMessage}`);
364
+ }
365
+
366
+ // Transform API response
367
+ return (data.result || []).map((queue) => ({
368
+ name: queue.queue_name,
369
+ id: queue.queue_id,
370
+ }));
371
+ }
@@ -15,7 +15,6 @@ import { type Assets } from "./assets";
15
15
  import { type Bindings, type WorkerBindingSpec } from "./bindings";
16
16
  import type { Bound } from "./bound";
17
17
  import { type DurableObjectNamespace } from "./durable-object-namespace";
18
- import { isKVNamespace } from "./kv-namespace";
19
18
  import type { WorkerScriptMetadata } from "./worker-metadata";
20
19
  import type { SingleStepMigration } from "./worker-migration";
21
20
  import { upsertWorkflow, type Workflow } from "./workflow";
@@ -319,8 +318,10 @@ export const Worker = Resource(
319
318
 
320
319
  // Construct the output
321
320
  return this({
321
+ ...props,
322
322
  type: "service",
323
323
  id,
324
+ entrypoint: props.entrypoint,
324
325
  name: workerName,
325
326
  script: scriptContent,
326
327
  format: props.format || "esm", // Include format in the output
@@ -521,17 +522,23 @@ async function prepareWorkerMetadata<B extends Bindings>(
521
522
  for (const [bindingName, binding] of Object.entries(bindings)) {
522
523
  // Create a copy of the binding to avoid modifying the original
523
524
 
524
- if (isKVNamespace(binding)) {
525
+ if (typeof binding === "string") {
525
526
  meta.bindings.push({
526
- type: "kv_namespace",
527
+ type: "plain_text",
527
528
  name: bindingName,
528
- namespace_id: binding.namespaceId,
529
+ text: binding,
529
530
  });
530
- } else if (typeof binding === "string") {
531
+ } else if (binding.type === "d1") {
531
532
  meta.bindings.push({
532
- type: "plain_text",
533
+ type: "d1",
533
534
  name: bindingName,
534
- text: binding,
535
+ id: binding.id,
536
+ });
537
+ } else if (binding.type === "kv_namespace") {
538
+ meta.bindings.push({
539
+ type: "kv_namespace",
540
+ name: bindingName,
541
+ namespace_id: binding.namespaceId,
535
542
  });
536
543
  } else if (binding.type === "service") {
537
544
  meta.bindings.push({
@@ -577,6 +584,12 @@ async function prepareWorkerMetadata<B extends Bindings>(
577
584
  });
578
585
  // it's unclear whether this is needed, but it works both ways
579
586
  configureClassMigration(binding, binding.id, binding.className);
587
+ } else if (binding.type === "queue") {
588
+ meta.bindings.push({
589
+ type: "queue",
590
+ name: bindingName,
591
+ queue_name: binding.name,
592
+ });
580
593
  } else {
581
594
  // @ts-expect-error - we should never reach here
582
595
  throw new Error(`Unsupported binding type: ${binding.type}`);