alchemy 0.6.0 → 0.7.0
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 +19 -7
- package/lib/cloudflare/bindings.js +0 -9
- package/lib/cloudflare/bound.d.ts +2 -1
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/worker.js +56 -27
- package/lib/cloudflare/workflow.d.ts +46 -0
- package/lib/cloudflare/workflow.js +29 -0
- package/lib/internal/docs/providers.js +1 -1
- package/package.json +1 -1
- package/src/cloudflare/bindings.ts +21 -16
- package/src/cloudflare/bound.ts +4 -1
- package/src/cloudflare/durable-object-namespace.ts +1 -1
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/worker.ts +65 -38
- package/src/cloudflare/workflow.ts +79 -0
- package/src/internal/docs/providers.ts +1 -1
|
@@ -9,22 +9,18 @@ import type { R2Bucket } from "./bucket";
|
|
|
9
9
|
import type { DurableObjectNamespace } from "./durable-object-namespace";
|
|
10
10
|
import type { KVNamespace } from "./kv-namespace";
|
|
11
11
|
import type { Worker } from "./worker";
|
|
12
|
+
import type { Workflow } from "./workflow";
|
|
12
13
|
export type Bindings = {
|
|
13
14
|
[bindingName: string]: Binding;
|
|
14
15
|
};
|
|
15
16
|
/**
|
|
16
17
|
* L2 Binding Resources.
|
|
17
18
|
*/
|
|
18
|
-
export type Binding = DurableObjectNamespace | KVNamespace | Worker | R2Bucket | Secret | string | Assets;
|
|
19
|
-
export declare function isDurableObjectNamespace(binding: Binding): binding is DurableObjectNamespace;
|
|
20
|
-
/**
|
|
21
|
-
* Check if a binding is an Assets resource
|
|
22
|
-
*/
|
|
23
|
-
export declare function isAssets(binding: Binding): binding is Assets;
|
|
19
|
+
export type Binding = DurableObjectNamespace | KVNamespace | Worker | Workflow | R2Bucket | Secret | string | Assets;
|
|
24
20
|
/**
|
|
25
21
|
* Union type for all Worker binding types (API spec)
|
|
26
22
|
*/
|
|
27
|
-
export type WorkerBindingSpec = WorkerBindingAI | WorkerBindingAnalyticsEngine | WorkerBindingAssets | WorkerBindingBrowserRendering | WorkerBindingD1 | WorkerBindingDispatchNamespace | WorkerBindingDurableObjectNamespace | WorkerBindingHyperdrive | WorkerBindingJson | WorkerBindingKVNamespace | WorkerBindingMTLSCertificate | WorkerBindingPlainText | WorkerBindingQueue | WorkerBindingR2Bucket | WorkerBindingSecretText | WorkerBindingService | WorkerBindingStaticContent | WorkerBindingTailConsumer | WorkerBindingVectorize | WorkerBindingVersionMetadata | WorkerBindingWasmModule;
|
|
23
|
+
export type WorkerBindingSpec = WorkerBindingAI | WorkerBindingAnalyticsEngine | WorkerBindingAssets | WorkerBindingBrowserRendering | WorkerBindingD1 | WorkerBindingDispatchNamespace | WorkerBindingDurableObjectNamespace | WorkerBindingHyperdrive | WorkerBindingJson | WorkerBindingKVNamespace | WorkerBindingMTLSCertificate | WorkerBindingPlainText | WorkerBindingQueue | WorkerBindingR2Bucket | WorkerBindingSecretText | WorkerBindingService | WorkerBindingStaticContent | WorkerBindingTailConsumer | WorkerBindingVectorize | WorkerBindingVersionMetadata | WorkerBindingWasmModule | WorkerBindingWorkflow;
|
|
28
24
|
/**
|
|
29
25
|
* AI binding type
|
|
30
26
|
*/
|
|
@@ -264,3 +260,19 @@ export interface WorkerBindingStaticContent {
|
|
|
264
260
|
/** Type identifier for Static Content binding */
|
|
265
261
|
type: "static_content";
|
|
266
262
|
}
|
|
263
|
+
export interface WorkerBindingWorkflow {
|
|
264
|
+
/** The name of the binding */
|
|
265
|
+
name: string;
|
|
266
|
+
/** Type identifier for Workflow binding */
|
|
267
|
+
type: "workflow";
|
|
268
|
+
/** Workflow name */
|
|
269
|
+
workflow_name: string;
|
|
270
|
+
/** Workflow class name */
|
|
271
|
+
class_name: string;
|
|
272
|
+
/**
|
|
273
|
+
* Workflow script name
|
|
274
|
+
*
|
|
275
|
+
* @default - the name of the script it is bound to
|
|
276
|
+
*/
|
|
277
|
+
script_name?: string;
|
|
278
|
+
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export function isDurableObjectNamespace(binding) {
|
|
2
|
-
return (typeof binding === "object" && binding.type === "durable_object_namespace");
|
|
3
|
-
}
|
|
4
|
-
/**
|
|
5
|
-
* Check if a binding is an Assets resource
|
|
6
|
-
*/
|
|
7
|
-
export function isAssets(binding) {
|
|
8
|
-
return typeof binding === "object" && binding.type === "assets";
|
|
9
|
-
}
|
|
@@ -5,4 +5,5 @@ import type { R2Bucket as _R2Bucket } from "./bucket";
|
|
|
5
5
|
import type { DurableObjectNamespace as _DurableObjectNamespace } from "./durable-object-namespace";
|
|
6
6
|
import type { KVNamespace as _KVNamespace } from "./kv-namespace";
|
|
7
7
|
import type { Worker as _Worker } from "./worker";
|
|
8
|
-
|
|
8
|
+
import type { Workflow as _Workflow } from "./workflow";
|
|
9
|
+
export type Bound<T extends Binding> = T extends _DurableObjectNamespace ? DurableObjectNamespace : T extends _KVNamespace ? KVNamespace : T extends _Worker ? Worker : T extends _R2Bucket ? R2Bucket : T extends Secret ? string : T extends Assets ? Service : T extends _Workflow<infer P> ? Workflow<P> : Service;
|
package/lib/cloudflare/index.js
CHANGED
package/lib/cloudflare/worker.js
CHANGED
|
@@ -2,13 +2,15 @@ import * as crypto from "crypto";
|
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
3
|
import { Bundle } from "../esbuild/bundle";
|
|
4
4
|
import { Resource } from "../resource";
|
|
5
|
-
import { isSecret } from "../secret";
|
|
6
5
|
import { getContentType } from "../util/content-type";
|
|
7
6
|
import { withExponentialBackoff } from "../util/retry";
|
|
8
7
|
import { slugify } from "../util/slugify";
|
|
9
8
|
import { createCloudflareApi, } from "./api";
|
|
10
|
-
import {
|
|
9
|
+
import {} from "./assets";
|
|
10
|
+
import {} from "./bindings";
|
|
11
|
+
import {} from "./durable-object-namespace";
|
|
11
12
|
import { isKVNamespace } from "./kv-namespace";
|
|
13
|
+
import { upsertWorkflow } from "./workflow";
|
|
12
14
|
/**
|
|
13
15
|
* A Cloudflare Worker is a serverless function that can be deployed to the Cloudflare network.
|
|
14
16
|
*
|
|
@@ -111,10 +113,16 @@ export const Worker = Resource("cloudflare::Worker", {
|
|
|
111
113
|
const scriptContent = props.script ?? (await bundleWorkerScript(props));
|
|
112
114
|
// Find any assets bindings
|
|
113
115
|
const assetsBindings = [];
|
|
116
|
+
const workflowsBindings = [];
|
|
114
117
|
if (props.bindings) {
|
|
115
118
|
for (const [bindingName, binding] of Object.entries(props.bindings)) {
|
|
116
|
-
if (
|
|
117
|
-
|
|
119
|
+
if (typeof binding === "object") {
|
|
120
|
+
if (binding.type === "assets") {
|
|
121
|
+
assetsBindings.push({ name: bindingName, assets: binding });
|
|
122
|
+
}
|
|
123
|
+
else if (binding.type === "workflow") {
|
|
124
|
+
workflowsBindings.push(binding);
|
|
125
|
+
}
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
}
|
|
@@ -129,8 +137,14 @@ export const Worker = Resource("cloudflare::Worker", {
|
|
|
129
137
|
}
|
|
130
138
|
// Prepare metadata with bindings
|
|
131
139
|
const scriptMetadata = await prepareWorkerMetadata(this, oldBindings, props, assetUploadResult);
|
|
132
|
-
// Upload the worker script
|
|
133
140
|
await putWorker(api, workerName, scriptContent, scriptMetadata);
|
|
141
|
+
for (const workflow of workflowsBindings) {
|
|
142
|
+
await upsertWorkflow(api, {
|
|
143
|
+
workflowName: workflow.workflowName,
|
|
144
|
+
className: workflow.className,
|
|
145
|
+
scriptName: workerName,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
134
148
|
// TODO: it is less than ideal that this can fail, resulting in state problem
|
|
135
149
|
await this.set("bindings", props.bindings);
|
|
136
150
|
// Handle worker URL if requested
|
|
@@ -273,33 +287,15 @@ async function prepareWorkerMetadata(ctx, oldBindings, props, assetUploadResult)
|
|
|
273
287
|
});
|
|
274
288
|
}
|
|
275
289
|
else if (binding.type === "durable_object_namespace") {
|
|
276
|
-
const stableId = binding.id;
|
|
277
|
-
const className = binding.className;
|
|
278
290
|
meta.bindings.push({
|
|
279
291
|
type: "durable_object_namespace",
|
|
280
292
|
name: bindingName,
|
|
281
|
-
class_name: className,
|
|
293
|
+
class_name: binding.className,
|
|
282
294
|
script_name: binding.scriptName,
|
|
283
295
|
environment: binding.environment,
|
|
284
296
|
namespace_id: binding.namespaceId,
|
|
285
297
|
});
|
|
286
|
-
|
|
287
|
-
?.filter(isDurableObjectNamespace)
|
|
288
|
-
?.find((b) => b.id === stableId);
|
|
289
|
-
if (!oldBinding) {
|
|
290
|
-
if (binding.sqlite) {
|
|
291
|
-
meta.migrations.new_sqlite_classes.push(className);
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
meta.migrations.new_classes.push(className);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
else if (oldBinding.className !== className) {
|
|
298
|
-
meta.migrations.renamed_classes.push({
|
|
299
|
-
from: oldBinding.className,
|
|
300
|
-
to: className,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
298
|
+
configureClassMigration(binding, binding.id, binding.className);
|
|
303
299
|
}
|
|
304
300
|
else if (binding.type === "r2_bucket") {
|
|
305
301
|
meta.bindings.push({
|
|
@@ -308,25 +304,58 @@ async function prepareWorkerMetadata(ctx, oldBindings, props, assetUploadResult)
|
|
|
308
304
|
bucket_name: binding.name,
|
|
309
305
|
});
|
|
310
306
|
}
|
|
311
|
-
else if (
|
|
307
|
+
else if (binding.type === "assets") {
|
|
312
308
|
meta.bindings.push({
|
|
313
309
|
type: "assets",
|
|
314
310
|
name: bindingName,
|
|
315
311
|
});
|
|
316
312
|
}
|
|
317
|
-
else if (
|
|
313
|
+
else if (binding.type === "secret") {
|
|
318
314
|
meta.bindings.push({
|
|
319
315
|
type: "secret_text",
|
|
320
316
|
name: bindingName,
|
|
321
317
|
text: binding.unencrypted,
|
|
322
318
|
});
|
|
323
319
|
}
|
|
320
|
+
else if (binding.type === "workflow") {
|
|
321
|
+
meta.bindings.push({
|
|
322
|
+
type: "workflow",
|
|
323
|
+
name: bindingName,
|
|
324
|
+
workflow_name: binding.workflowName,
|
|
325
|
+
class_name: binding.className,
|
|
326
|
+
// this should be set if the Workflow is in another script ...
|
|
327
|
+
// script_name: ??,
|
|
328
|
+
});
|
|
329
|
+
// it's unclear whether this is needed, but it works both ways
|
|
330
|
+
configureClassMigration(binding, binding.id, binding.className);
|
|
331
|
+
}
|
|
324
332
|
else {
|
|
325
333
|
// @ts-expect-error - we should never reach here
|
|
326
334
|
throw new Error(`Unsupported binding type: ${binding.type}`);
|
|
327
335
|
}
|
|
328
336
|
}
|
|
337
|
+
function configureClassMigration(binding, stableId, className) {
|
|
338
|
+
const oldBinding = Object.values(oldBindings ?? {})
|
|
339
|
+
?.filter((b) => typeof b === "object" &&
|
|
340
|
+
(b.type === "durable_object_namespace" || b.type === "workflow"))
|
|
341
|
+
?.find((b) => b.id === stableId);
|
|
342
|
+
if (!oldBinding) {
|
|
343
|
+
if (binding.type === "durable_object_namespace" && binding.sqlite) {
|
|
344
|
+
meta.migrations.new_sqlite_classes.push(className);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
meta.migrations.new_classes.push(className);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else if (oldBinding.className !== className) {
|
|
351
|
+
meta.migrations.renamed_classes.push({
|
|
352
|
+
from: oldBinding.className,
|
|
353
|
+
to: className,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
329
357
|
// Convert env variables to plain_text bindings
|
|
358
|
+
// TODO(sam): remove Worker.env in favor of always bindings
|
|
330
359
|
if (props.env) {
|
|
331
360
|
for (const [key, value] of Object.entries(props.env)) {
|
|
332
361
|
meta.bindings.push({
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CloudflareApi } from "./api";
|
|
2
|
+
export interface WorkflowProps {
|
|
3
|
+
/**
|
|
4
|
+
* Name of the workflow
|
|
5
|
+
*
|
|
6
|
+
* @maxLength 64
|
|
7
|
+
* @minLength 1
|
|
8
|
+
* @default - className if provided, otherwise id
|
|
9
|
+
*/
|
|
10
|
+
workflowName?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Name of the class that implements the workflow
|
|
13
|
+
*
|
|
14
|
+
* @maxLength 255
|
|
15
|
+
* @minLength 1
|
|
16
|
+
* @default - workflowName if provided, otherwise id
|
|
17
|
+
*/
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class Workflow<PARAMS = unknown> {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly type: "workflow";
|
|
23
|
+
/**
|
|
24
|
+
* Phantom property to preserve workflow params at the type level.
|
|
25
|
+
*
|
|
26
|
+
* No value exists.
|
|
27
|
+
*/
|
|
28
|
+
readonly _PARAMS: PARAMS;
|
|
29
|
+
readonly workflowName: string;
|
|
30
|
+
readonly className: string;
|
|
31
|
+
constructor(id: string, props?: WorkflowProps);
|
|
32
|
+
}
|
|
33
|
+
export interface WorkflowMetadata {
|
|
34
|
+
id: string;
|
|
35
|
+
class_name: string;
|
|
36
|
+
created_on: string;
|
|
37
|
+
modified_on: string;
|
|
38
|
+
name: string;
|
|
39
|
+
script_name: string;
|
|
40
|
+
triggered_on: string;
|
|
41
|
+
version_id: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function upsertWorkflow(api: CloudflareApi, props: WorkflowProps & {
|
|
44
|
+
workflowName: string;
|
|
45
|
+
scriptName: string;
|
|
46
|
+
}): Promise<WorkflowMetadata>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { handleApiError } from "./api-error";
|
|
2
|
+
export class Workflow {
|
|
3
|
+
id;
|
|
4
|
+
type = "workflow";
|
|
5
|
+
/**
|
|
6
|
+
* Phantom property to preserve workflow params at the type level.
|
|
7
|
+
*
|
|
8
|
+
* No value exists.
|
|
9
|
+
*/
|
|
10
|
+
_PARAMS = undefined;
|
|
11
|
+
workflowName;
|
|
12
|
+
className;
|
|
13
|
+
constructor(id, props = {}) {
|
|
14
|
+
this.id = id;
|
|
15
|
+
this.workflowName = props.workflowName ?? props.className ?? id;
|
|
16
|
+
this.className = props.className ?? this.workflowName;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function upsertWorkflow(api, props) {
|
|
20
|
+
const response = await api.put(`/accounts/${api.accountId}/workflows/${props.workflowName}`, {
|
|
21
|
+
class_name: props.className,
|
|
22
|
+
script_name: props.scriptName,
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
await handleApiError(response, "create", "workflow", props.workflowName);
|
|
26
|
+
}
|
|
27
|
+
const body = (await response.json());
|
|
28
|
+
return body.result;
|
|
29
|
+
}
|
|
@@ -70,7 +70,7 @@ async function generateProviderDocs({ provider, outDir, parallel, }) {
|
|
|
70
70
|
The ${provider} Service has the following resources:
|
|
71
71
|
${alchemy.files(files)}
|
|
72
72
|
|
|
73
|
-
A file is considered a "Resource" if it contains a const <ResourceName> = Resource(...) call or if it is a function that calls a Resource function, e.g. const TypeScriptFile = () => File(...).
|
|
73
|
+
A file is considered a "Resource" if it contains a const <ResourceName> = Resource(...) call or if it is a function that calls a Resource function, e.g. const TypeScriptFile = () => File(...), or it is the Workflow or DurableObject classes.
|
|
74
74
|
A file is considered a "Client" if it exposes a wrapper around creating a SDK client or fetch.
|
|
75
75
|
A file is considered a "Utility" if it contains utility functions that are not resources or clients.
|
|
76
76
|
A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import type { R2Bucket } from "./bucket";
|
|
|
9
9
|
import type { DurableObjectNamespace } from "./durable-object-namespace";
|
|
10
10
|
import type { KVNamespace } from "./kv-namespace";
|
|
11
11
|
import type { Worker } from "./worker";
|
|
12
|
+
import type { Workflow } from "./workflow";
|
|
12
13
|
|
|
13
14
|
export type Bindings = {
|
|
14
15
|
[bindingName: string]: Binding;
|
|
@@ -21,26 +22,12 @@ export type Binding =
|
|
|
21
22
|
| DurableObjectNamespace
|
|
22
23
|
| KVNamespace
|
|
23
24
|
| Worker
|
|
25
|
+
| Workflow
|
|
24
26
|
| R2Bucket
|
|
25
27
|
| Secret
|
|
26
28
|
| string
|
|
27
29
|
| Assets;
|
|
28
30
|
|
|
29
|
-
export function isDurableObjectNamespace(
|
|
30
|
-
binding: Binding
|
|
31
|
-
): binding is DurableObjectNamespace {
|
|
32
|
-
return (
|
|
33
|
-
typeof binding === "object" && binding.type === "durable_object_namespace"
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Check if a binding is an Assets resource
|
|
39
|
-
*/
|
|
40
|
-
export function isAssets(binding: Binding): binding is Assets {
|
|
41
|
-
return typeof binding === "object" && binding.type === "assets";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
31
|
/**
|
|
45
32
|
* Union type for all Worker binding types (API spec)
|
|
46
33
|
*/
|
|
@@ -65,7 +52,8 @@ export type WorkerBindingSpec =
|
|
|
65
52
|
| WorkerBindingTailConsumer
|
|
66
53
|
| WorkerBindingVectorize
|
|
67
54
|
| WorkerBindingVersionMetadata
|
|
68
|
-
| WorkerBindingWasmModule
|
|
55
|
+
| WorkerBindingWasmModule
|
|
56
|
+
| WorkerBindingWorkflow;
|
|
69
57
|
|
|
70
58
|
/**
|
|
71
59
|
* AI binding type
|
|
@@ -326,3 +314,20 @@ export interface WorkerBindingStaticContent {
|
|
|
326
314
|
/** Type identifier for Static Content binding */
|
|
327
315
|
type: "static_content";
|
|
328
316
|
}
|
|
317
|
+
|
|
318
|
+
export interface WorkerBindingWorkflow {
|
|
319
|
+
/** The name of the binding */
|
|
320
|
+
name: string;
|
|
321
|
+
/** Type identifier for Workflow binding */
|
|
322
|
+
type: "workflow";
|
|
323
|
+
/** Workflow name */
|
|
324
|
+
workflow_name: string;
|
|
325
|
+
/** Workflow class name */
|
|
326
|
+
class_name: string;
|
|
327
|
+
/**
|
|
328
|
+
* Workflow script name
|
|
329
|
+
*
|
|
330
|
+
* @default - the name of the script it is bound to
|
|
331
|
+
*/
|
|
332
|
+
script_name?: string;
|
|
333
|
+
}
|
package/src/cloudflare/bound.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { R2Bucket as _R2Bucket } from "./bucket";
|
|
|
5
5
|
import type { DurableObjectNamespace as _DurableObjectNamespace } from "./durable-object-namespace";
|
|
6
6
|
import type { KVNamespace as _KVNamespace } from "./kv-namespace";
|
|
7
7
|
import type { Worker as _Worker } from "./worker";
|
|
8
|
+
import type { Workflow as _Workflow } from "./workflow";
|
|
8
9
|
|
|
9
10
|
export type Bound<T extends Binding> = T extends _DurableObjectNamespace
|
|
10
11
|
? DurableObjectNamespace
|
|
@@ -18,4 +19,6 @@ export type Bound<T extends Binding> = T extends _DurableObjectNamespace
|
|
|
18
19
|
? string
|
|
19
20
|
: T extends Assets
|
|
20
21
|
? Service
|
|
21
|
-
:
|
|
22
|
+
: T extends _Workflow<infer P>
|
|
23
|
+
? Workflow<P>
|
|
24
|
+
: Service;
|
|
@@ -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;
|
package/src/cloudflare/index.ts
CHANGED
package/src/cloudflare/worker.ts
CHANGED
|
@@ -3,27 +3,22 @@ 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
|
|
17
|
+
import { type DurableObjectNamespace } from "./durable-object-namespace";
|
|
24
18
|
import { isKVNamespace } from "./kv-namespace";
|
|
25
19
|
import type { WorkerScriptMetadata } from "./worker-metadata";
|
|
26
20
|
import type { SingleStepMigration } from "./worker-migration";
|
|
21
|
+
import { upsertWorkflow, type Workflow } from "./workflow";
|
|
27
22
|
|
|
28
23
|
/**
|
|
29
24
|
* Properties for creating or updating a Worker
|
|
@@ -261,10 +256,16 @@ export const Worker = Resource(
|
|
|
261
256
|
|
|
262
257
|
// Find any assets bindings
|
|
263
258
|
const assetsBindings: { name: string; assets: Assets }[] = [];
|
|
259
|
+
const workflowsBindings: Workflow[] = [];
|
|
260
|
+
|
|
264
261
|
if (props.bindings) {
|
|
265
262
|
for (const [bindingName, binding] of Object.entries(props.bindings)) {
|
|
266
|
-
if (
|
|
267
|
-
|
|
263
|
+
if (typeof binding === "object") {
|
|
264
|
+
if (binding.type === "assets") {
|
|
265
|
+
assetsBindings.push({ name: bindingName, assets: binding });
|
|
266
|
+
} else if (binding.type === "workflow") {
|
|
267
|
+
workflowsBindings.push(binding);
|
|
268
|
+
}
|
|
268
269
|
}
|
|
269
270
|
}
|
|
270
271
|
}
|
|
@@ -292,9 +293,16 @@ export const Worker = Resource(
|
|
|
292
293
|
assetUploadResult
|
|
293
294
|
);
|
|
294
295
|
|
|
295
|
-
// Upload the worker script
|
|
296
296
|
await putWorker(api, workerName, scriptContent, scriptMetadata);
|
|
297
297
|
|
|
298
|
+
for (const workflow of workflowsBindings) {
|
|
299
|
+
await upsertWorkflow(api, {
|
|
300
|
+
workflowName: workflow.workflowName,
|
|
301
|
+
className: workflow.className,
|
|
302
|
+
scriptName: workerName,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
298
306
|
// TODO: it is less than ideal that this can fail, resulting in state problem
|
|
299
307
|
await this.set("bindings", props.bindings);
|
|
300
308
|
|
|
@@ -532,60 +540,79 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
532
540
|
service: binding.id,
|
|
533
541
|
});
|
|
534
542
|
} else if (binding.type === "durable_object_namespace") {
|
|
535
|
-
const stableId = binding.id;
|
|
536
|
-
const className = binding.className;
|
|
537
|
-
|
|
538
543
|
meta.bindings.push({
|
|
539
544
|
type: "durable_object_namespace",
|
|
540
545
|
name: bindingName,
|
|
541
|
-
class_name: className,
|
|
546
|
+
class_name: binding.className,
|
|
542
547
|
script_name: binding.scriptName,
|
|
543
548
|
environment: binding.environment,
|
|
544
549
|
namespace_id: binding.namespaceId,
|
|
545
550
|
});
|
|
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
|
-
}
|
|
551
|
+
configureClassMigration(binding, binding.id, binding.className);
|
|
565
552
|
} else if (binding.type === "r2_bucket") {
|
|
566
553
|
meta.bindings.push({
|
|
567
554
|
type: "r2_bucket",
|
|
568
555
|
name: bindingName,
|
|
569
556
|
bucket_name: binding.name,
|
|
570
557
|
});
|
|
571
|
-
} else if (
|
|
558
|
+
} else if (binding.type === "assets") {
|
|
572
559
|
meta.bindings.push({
|
|
573
560
|
type: "assets",
|
|
574
561
|
name: bindingName,
|
|
575
562
|
});
|
|
576
|
-
} else if (
|
|
563
|
+
} else if (binding.type === "secret") {
|
|
577
564
|
meta.bindings.push({
|
|
578
565
|
type: "secret_text",
|
|
579
566
|
name: bindingName,
|
|
580
567
|
text: binding.unencrypted,
|
|
581
568
|
});
|
|
569
|
+
} else if (binding.type === "workflow") {
|
|
570
|
+
meta.bindings.push({
|
|
571
|
+
type: "workflow",
|
|
572
|
+
name: bindingName,
|
|
573
|
+
workflow_name: binding.workflowName,
|
|
574
|
+
class_name: binding.className,
|
|
575
|
+
// this should be set if the Workflow is in another script ...
|
|
576
|
+
// script_name: ??,
|
|
577
|
+
});
|
|
578
|
+
// it's unclear whether this is needed, but it works both ways
|
|
579
|
+
configureClassMigration(binding, binding.id, binding.className);
|
|
582
580
|
} else {
|
|
583
581
|
// @ts-expect-error - we should never reach here
|
|
584
582
|
throw new Error(`Unsupported binding type: ${binding.type}`);
|
|
585
583
|
}
|
|
586
584
|
}
|
|
587
585
|
|
|
586
|
+
function configureClassMigration(
|
|
587
|
+
binding: DurableObjectNamespace | Workflow,
|
|
588
|
+
stableId: string,
|
|
589
|
+
className: string
|
|
590
|
+
) {
|
|
591
|
+
const oldBinding: DurableObjectNamespace | Workflow | undefined =
|
|
592
|
+
Object.values(oldBindings ?? {})
|
|
593
|
+
?.filter(
|
|
594
|
+
(b) =>
|
|
595
|
+
typeof b === "object" &&
|
|
596
|
+
(b.type === "durable_object_namespace" || b.type === "workflow")
|
|
597
|
+
)
|
|
598
|
+
?.find((b) => b.id === stableId);
|
|
599
|
+
|
|
600
|
+
if (!oldBinding) {
|
|
601
|
+
if (binding.type === "durable_object_namespace" && binding.sqlite) {
|
|
602
|
+
meta.migrations!.new_sqlite_classes!.push(className);
|
|
603
|
+
} else {
|
|
604
|
+
meta.migrations!.new_classes!.push(className);
|
|
605
|
+
}
|
|
606
|
+
} else if (oldBinding.className !== className) {
|
|
607
|
+
meta.migrations!.renamed_classes!.push({
|
|
608
|
+
from: oldBinding.className,
|
|
609
|
+
to: className,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
588
614
|
// Convert env variables to plain_text bindings
|
|
615
|
+
// TODO(sam): remove Worker.env in favor of always bindings
|
|
589
616
|
if (props.env) {
|
|
590
617
|
for (const [key, value] of Object.entries(props.env)) {
|
|
591
618
|
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
|
+
}
|
|
@@ -145,7 +145,7 @@ async function generateProviderDocs({
|
|
|
145
145
|
The ${provider} Service has the following resources:
|
|
146
146
|
${alchemy.files(files)}
|
|
147
147
|
|
|
148
|
-
A file is considered a "Resource" if it contains a const <ResourceName> = Resource(...) call or if it is a function that calls a Resource function, e.g. const TypeScriptFile = () => File(...).
|
|
148
|
+
A file is considered a "Resource" if it contains a const <ResourceName> = Resource(...) call or if it is a function that calls a Resource function, e.g. const TypeScriptFile = () => File(...), or it is the Workflow or DurableObject classes.
|
|
149
149
|
A file is considered a "Client" if it exposes a wrapper around creating a SDK client or fetch.
|
|
150
150
|
A file is considered a "Utility" if it contains utility functions that are not resources or clients.
|
|
151
151
|
A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
|