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,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";
@@ -100,6 +99,17 @@ export interface WorkerProps<B extends Bindings = Bindings>
100
99
  * Whether to adopt the Worker if it already exists when creating
101
100
  */
102
101
  adopt?: boolean;
102
+
103
+ /**
104
+ * The compatibility date for the worker
105
+ * @default "2024-09-09"
106
+ */
107
+ compatibilityDate?: string;
108
+
109
+ /**
110
+ * The compatibility flags for the worker
111
+ */
112
+ compatibilityFlags?: string[];
103
113
  }
104
114
 
105
115
  /**
@@ -319,8 +329,10 @@ export const Worker = Resource(
319
329
 
320
330
  // Construct the output
321
331
  return this({
332
+ ...props,
322
333
  type: "service",
323
334
  id,
335
+ entrypoint: props.entrypoint,
324
336
  name: workerName,
325
337
  script: scriptContent,
326
338
  format: props.format || "esm", // Include format in the output
@@ -455,6 +467,8 @@ class NotFoundError extends Error {
455
467
  }
456
468
 
457
469
  interface WorkerMetadata {
470
+ compatibility_date: string;
471
+ compatibility_flags?: string[];
458
472
  bindings: WorkerBindingSpec[];
459
473
  observability: {
460
474
  enabled: boolean;
@@ -489,6 +503,8 @@ async function prepareWorkerMetadata<B extends Bindings>(
489
503
  ): Promise<WorkerMetadata> {
490
504
  // Prepare metadata with bindings
491
505
  const meta: WorkerMetadata = {
506
+ compatibility_date: props.compatibilityDate ?? "2024-09-09",
507
+ compatibility_flags: props.compatibilityFlags,
492
508
  bindings: [],
493
509
  observability: {
494
510
  enabled: props.observability?.enabled !== false,
@@ -521,17 +537,23 @@ async function prepareWorkerMetadata<B extends Bindings>(
521
537
  for (const [bindingName, binding] of Object.entries(bindings)) {
522
538
  // Create a copy of the binding to avoid modifying the original
523
539
 
524
- if (isKVNamespace(binding)) {
540
+ if (typeof binding === "string") {
525
541
  meta.bindings.push({
526
- type: "kv_namespace",
542
+ type: "plain_text",
527
543
  name: bindingName,
528
- namespace_id: binding.namespaceId,
544
+ text: binding,
529
545
  });
530
- } else if (typeof binding === "string") {
546
+ } else if (binding.type === "d1") {
531
547
  meta.bindings.push({
532
- type: "plain_text",
548
+ type: "d1",
533
549
  name: bindingName,
534
- text: binding,
550
+ id: binding.id,
551
+ });
552
+ } else if (binding.type === "kv_namespace") {
553
+ meta.bindings.push({
554
+ type: "kv_namespace",
555
+ name: bindingName,
556
+ namespace_id: binding.namespaceId,
535
557
  });
536
558
  } else if (binding.type === "service") {
537
559
  meta.bindings.push({
@@ -577,6 +599,18 @@ async function prepareWorkerMetadata<B extends Bindings>(
577
599
  });
578
600
  // it's unclear whether this is needed, but it works both ways
579
601
  configureClassMigration(binding, binding.id, binding.className);
602
+ } else if (binding.type === "queue") {
603
+ meta.bindings.push({
604
+ type: "queue",
605
+ name: bindingName,
606
+ queue_name: binding.name,
607
+ });
608
+ } else if (binding.type === "pipeline") {
609
+ meta.bindings.push({
610
+ type: "pipelines",
611
+ name: bindingName,
612
+ pipeline: binding.name,
613
+ });
580
614
  } else {
581
615
  // @ts-expect-error - we should never reach here
582
616
  throw new Error(`Unsupported binding type: ${binding.type}`);