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.
- package/lib/cloudflare/bindings.d.ts +21 -7
- package/lib/cloudflare/bindings.js +0 -9
- package/lib/cloudflare/bound.d.ts +4 -1
- package/lib/cloudflare/d1-database.d.ts +134 -0
- package/lib/cloudflare/d1-database.js +161 -0
- package/lib/cloudflare/index.d.ts +4 -0
- package/lib/cloudflare/index.js +4 -0
- package/lib/cloudflare/kv-namespace.d.ts +0 -1
- package/lib/cloudflare/kv-namespace.js +0 -3
- package/lib/cloudflare/queue.d.ts +147 -0
- package/lib/cloudflare/queue.js +192 -0
- package/lib/cloudflare/worker.js +78 -34
- package/lib/cloudflare/workflow.d.ts +46 -0
- package/lib/cloudflare/workflow.js +29 -0
- package/lib/cloudflare/wrangler.json.d.ts +82 -185
- package/lib/cloudflare/wrangler.json.js +124 -17
- package/lib/internal/docs/providers.js +1 -1
- package/package.json +1 -1
- package/src/cloudflare/bindings.ts +27 -18
- package/src/cloudflare/bound.ts +10 -1
- package/src/cloudflare/d1-database.ts +354 -0
- package/src/cloudflare/durable-object-namespace.ts +1 -1
- package/src/cloudflare/index.ts +4 -0
- package/src/cloudflare/kv-namespace.ts +0 -6
- package/src/cloudflare/queue.ts +371 -0
- package/src/cloudflare/worker.ts +85 -45
- package/src/cloudflare/workflow.ts +79 -0
- package/src/cloudflare/wrangler.json.ts +239 -218
- package/src/internal/docs/providers.ts +1 -1
|
@@ -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
|
+
}
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -3,27 +3,21 @@ import * as fs from "fs/promises";
|
|
|
3
3
|
import type { Context } from "../context";
|
|
4
4
|
import { Bundle, type BundleProps } from "../esbuild/bundle";
|
|
5
5
|
import { Resource } from "../resource";
|
|
6
|
-
import { isSecret } from "../secret";
|
|
7
6
|
import { getContentType } from "../util/content-type";
|
|
8
7
|
import { withExponentialBackoff } from "../util/retry";
|
|
9
8
|
import { slugify } from "../util/slugify";
|
|
10
9
|
import {
|
|
10
|
+
createCloudflareApi,
|
|
11
11
|
type CloudflareApi,
|
|
12
12
|
type CloudflareApiOptions,
|
|
13
|
-
createCloudflareApi,
|
|
14
13
|
} from "./api";
|
|
15
|
-
import type
|
|
16
|
-
import {
|
|
17
|
-
type Bindings,
|
|
18
|
-
type WorkerBindingSpec,
|
|
19
|
-
isAssets,
|
|
20
|
-
isDurableObjectNamespace,
|
|
21
|
-
} from "./bindings";
|
|
14
|
+
import { type Assets } from "./assets";
|
|
15
|
+
import { type Bindings, type WorkerBindingSpec } from "./bindings";
|
|
22
16
|
import type { Bound } from "./bound";
|
|
23
|
-
import type
|
|
24
|
-
import { isKVNamespace } from "./kv-namespace";
|
|
17
|
+
import { type DurableObjectNamespace } from "./durable-object-namespace";
|
|
25
18
|
import type { WorkerScriptMetadata } from "./worker-metadata";
|
|
26
19
|
import type { SingleStepMigration } from "./worker-migration";
|
|
20
|
+
import { upsertWorkflow, type Workflow } from "./workflow";
|
|
27
21
|
|
|
28
22
|
/**
|
|
29
23
|
* Properties for creating or updating a Worker
|
|
@@ -261,10 +255,16 @@ export const Worker = Resource(
|
|
|
261
255
|
|
|
262
256
|
// Find any assets bindings
|
|
263
257
|
const assetsBindings: { name: string; assets: Assets }[] = [];
|
|
258
|
+
const workflowsBindings: Workflow[] = [];
|
|
259
|
+
|
|
264
260
|
if (props.bindings) {
|
|
265
261
|
for (const [bindingName, binding] of Object.entries(props.bindings)) {
|
|
266
|
-
if (
|
|
267
|
-
|
|
262
|
+
if (typeof binding === "object") {
|
|
263
|
+
if (binding.type === "assets") {
|
|
264
|
+
assetsBindings.push({ name: bindingName, assets: binding });
|
|
265
|
+
} else if (binding.type === "workflow") {
|
|
266
|
+
workflowsBindings.push(binding);
|
|
267
|
+
}
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
}
|
|
@@ -292,9 +292,16 @@ export const Worker = Resource(
|
|
|
292
292
|
assetUploadResult
|
|
293
293
|
);
|
|
294
294
|
|
|
295
|
-
// Upload the worker script
|
|
296
295
|
await putWorker(api, workerName, scriptContent, scriptMetadata);
|
|
297
296
|
|
|
297
|
+
for (const workflow of workflowsBindings) {
|
|
298
|
+
await upsertWorkflow(api, {
|
|
299
|
+
workflowName: workflow.workflowName,
|
|
300
|
+
className: workflow.className,
|
|
301
|
+
scriptName: workerName,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
298
305
|
// TODO: it is less than ideal that this can fail, resulting in state problem
|
|
299
306
|
await this.set("bindings", props.bindings);
|
|
300
307
|
|
|
@@ -311,8 +318,10 @@ export const Worker = Resource(
|
|
|
311
318
|
|
|
312
319
|
// Construct the output
|
|
313
320
|
return this({
|
|
321
|
+
...props,
|
|
314
322
|
type: "service",
|
|
315
323
|
id,
|
|
324
|
+
entrypoint: props.entrypoint,
|
|
316
325
|
name: workerName,
|
|
317
326
|
script: scriptContent,
|
|
318
327
|
format: props.format || "esm", // Include format in the output
|
|
@@ -513,17 +522,23 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
513
522
|
for (const [bindingName, binding] of Object.entries(bindings)) {
|
|
514
523
|
// Create a copy of the binding to avoid modifying the original
|
|
515
524
|
|
|
516
|
-
if (
|
|
525
|
+
if (typeof binding === "string") {
|
|
517
526
|
meta.bindings.push({
|
|
518
|
-
type: "
|
|
527
|
+
type: "plain_text",
|
|
519
528
|
name: bindingName,
|
|
520
|
-
|
|
529
|
+
text: binding,
|
|
521
530
|
});
|
|
522
|
-
} else if (
|
|
531
|
+
} else if (binding.type === "d1") {
|
|
523
532
|
meta.bindings.push({
|
|
524
|
-
type: "
|
|
533
|
+
type: "d1",
|
|
525
534
|
name: bindingName,
|
|
526
|
-
|
|
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,
|
|
527
542
|
});
|
|
528
543
|
} else if (binding.type === "service") {
|
|
529
544
|
meta.bindings.push({
|
|
@@ -532,60 +547,85 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
532
547
|
service: binding.id,
|
|
533
548
|
});
|
|
534
549
|
} else if (binding.type === "durable_object_namespace") {
|
|
535
|
-
const stableId = binding.id;
|
|
536
|
-
const className = binding.className;
|
|
537
|
-
|
|
538
550
|
meta.bindings.push({
|
|
539
551
|
type: "durable_object_namespace",
|
|
540
552
|
name: bindingName,
|
|
541
|
-
class_name: className,
|
|
553
|
+
class_name: binding.className,
|
|
542
554
|
script_name: binding.scriptName,
|
|
543
555
|
environment: binding.environment,
|
|
544
556
|
namespace_id: binding.namespaceId,
|
|
545
557
|
});
|
|
546
|
-
|
|
547
|
-
const oldBinding: DurableObjectNamespace | undefined = Object.values(
|
|
548
|
-
oldBindings ?? {}
|
|
549
|
-
)
|
|
550
|
-
?.filter(isDurableObjectNamespace)
|
|
551
|
-
?.find((b) => b.id === stableId);
|
|
552
|
-
|
|
553
|
-
if (!oldBinding) {
|
|
554
|
-
if (binding.sqlite) {
|
|
555
|
-
meta.migrations!.new_sqlite_classes!.push(className);
|
|
556
|
-
} else {
|
|
557
|
-
meta.migrations!.new_classes!.push(className);
|
|
558
|
-
}
|
|
559
|
-
} else if (oldBinding.className !== className) {
|
|
560
|
-
meta.migrations!.renamed_classes!.push({
|
|
561
|
-
from: oldBinding.className,
|
|
562
|
-
to: className,
|
|
563
|
-
});
|
|
564
|
-
}
|
|
558
|
+
configureClassMigration(binding, binding.id, binding.className);
|
|
565
559
|
} else if (binding.type === "r2_bucket") {
|
|
566
560
|
meta.bindings.push({
|
|
567
561
|
type: "r2_bucket",
|
|
568
562
|
name: bindingName,
|
|
569
563
|
bucket_name: binding.name,
|
|
570
564
|
});
|
|
571
|
-
} else if (
|
|
565
|
+
} else if (binding.type === "assets") {
|
|
572
566
|
meta.bindings.push({
|
|
573
567
|
type: "assets",
|
|
574
568
|
name: bindingName,
|
|
575
569
|
});
|
|
576
|
-
} else if (
|
|
570
|
+
} else if (binding.type === "secret") {
|
|
577
571
|
meta.bindings.push({
|
|
578
572
|
type: "secret_text",
|
|
579
573
|
name: bindingName,
|
|
580
574
|
text: binding.unencrypted,
|
|
581
575
|
});
|
|
576
|
+
} else if (binding.type === "workflow") {
|
|
577
|
+
meta.bindings.push({
|
|
578
|
+
type: "workflow",
|
|
579
|
+
name: bindingName,
|
|
580
|
+
workflow_name: binding.workflowName,
|
|
581
|
+
class_name: binding.className,
|
|
582
|
+
// this should be set if the Workflow is in another script ...
|
|
583
|
+
// script_name: ??,
|
|
584
|
+
});
|
|
585
|
+
// it's unclear whether this is needed, but it works both ways
|
|
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
|
+
});
|
|
582
593
|
} else {
|
|
583
594
|
// @ts-expect-error - we should never reach here
|
|
584
595
|
throw new Error(`Unsupported binding type: ${binding.type}`);
|
|
585
596
|
}
|
|
586
597
|
}
|
|
587
598
|
|
|
599
|
+
function configureClassMigration(
|
|
600
|
+
binding: DurableObjectNamespace | Workflow,
|
|
601
|
+
stableId: string,
|
|
602
|
+
className: string
|
|
603
|
+
) {
|
|
604
|
+
const oldBinding: DurableObjectNamespace | Workflow | undefined =
|
|
605
|
+
Object.values(oldBindings ?? {})
|
|
606
|
+
?.filter(
|
|
607
|
+
(b) =>
|
|
608
|
+
typeof b === "object" &&
|
|
609
|
+
(b.type === "durable_object_namespace" || b.type === "workflow")
|
|
610
|
+
)
|
|
611
|
+
?.find((b) => b.id === stableId);
|
|
612
|
+
|
|
613
|
+
if (!oldBinding) {
|
|
614
|
+
if (binding.type === "durable_object_namespace" && binding.sqlite) {
|
|
615
|
+
meta.migrations!.new_sqlite_classes!.push(className);
|
|
616
|
+
} else {
|
|
617
|
+
meta.migrations!.new_classes!.push(className);
|
|
618
|
+
}
|
|
619
|
+
} else if (oldBinding.className !== className) {
|
|
620
|
+
meta.migrations!.renamed_classes!.push({
|
|
621
|
+
from: oldBinding.className,
|
|
622
|
+
to: className,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
588
627
|
// Convert env variables to plain_text bindings
|
|
628
|
+
// TODO(sam): remove Worker.env in favor of always bindings
|
|
589
629
|
if (props.env) {
|
|
590
630
|
for (const [key, value] of Object.entries(props.env)) {
|
|
591
631
|
meta.bindings.push({
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CloudflareApi } from "./api";
|
|
2
|
+
import { handleApiError } from "./api-error";
|
|
3
|
+
|
|
4
|
+
export interface WorkflowProps {
|
|
5
|
+
/**
|
|
6
|
+
* Name of the workflow
|
|
7
|
+
*
|
|
8
|
+
* @maxLength 64
|
|
9
|
+
* @minLength 1
|
|
10
|
+
* @default - className if provided, otherwise id
|
|
11
|
+
*/
|
|
12
|
+
workflowName?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Name of the class that implements the workflow
|
|
15
|
+
*
|
|
16
|
+
* @maxLength 255
|
|
17
|
+
* @minLength 1
|
|
18
|
+
* @default - workflowName if provided, otherwise id
|
|
19
|
+
*/
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class Workflow<PARAMS = unknown> {
|
|
24
|
+
public readonly type: "workflow" = "workflow";
|
|
25
|
+
/**
|
|
26
|
+
* Phantom property to preserve workflow params at the type level.
|
|
27
|
+
*
|
|
28
|
+
* No value exists.
|
|
29
|
+
*/
|
|
30
|
+
public readonly _PARAMS: PARAMS = undefined!;
|
|
31
|
+
|
|
32
|
+
public readonly workflowName: string;
|
|
33
|
+
public readonly className: string;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
public readonly id: string,
|
|
37
|
+
props: WorkflowProps = {}
|
|
38
|
+
) {
|
|
39
|
+
this.workflowName = props.workflowName ?? props.className ?? id;
|
|
40
|
+
this.className = props.className ?? this.workflowName;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkflowMetadata {
|
|
45
|
+
id: string; // uuid
|
|
46
|
+
class_name: string;
|
|
47
|
+
created_on: string; // date-time
|
|
48
|
+
modified_on: string; // date-time
|
|
49
|
+
name: string; // maxLength: 64, minLength: 1
|
|
50
|
+
script_name: string;
|
|
51
|
+
triggered_on: string; // date-time
|
|
52
|
+
version_id: string; // uuid
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function upsertWorkflow(
|
|
56
|
+
api: CloudflareApi,
|
|
57
|
+
props: WorkflowProps & {
|
|
58
|
+
workflowName: string;
|
|
59
|
+
scriptName: string;
|
|
60
|
+
}
|
|
61
|
+
) {
|
|
62
|
+
const response = await api.put(
|
|
63
|
+
`/accounts/${api.accountId}/workflows/${props.workflowName}`,
|
|
64
|
+
{
|
|
65
|
+
class_name: props.className,
|
|
66
|
+
script_name: props.scriptName,
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
await handleApiError(response, "create", "workflow", props.workflowName);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const body = (await response.json()) as {
|
|
75
|
+
result: WorkflowMetadata;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return body.result;
|
|
79
|
+
}
|