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.
@@ -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
- 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 : Service;
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;
@@ -10,5 +10,6 @@ export * from "./kv-namespace";
10
10
  export * from "./permission-groups";
11
11
  export * from "./r2-rest-state-store";
12
12
  export * from "./worker";
13
+ export { Workflow } from "./workflow";
13
14
  export * from "./wrangler.json";
14
15
  export * from "./zone";
@@ -10,5 +10,6 @@ export * from "./kv-namespace";
10
10
  export * from "./permission-groups";
11
11
  export * from "./r2-rest-state-store";
12
12
  export * from "./worker";
13
+ export { Workflow } from "./workflow";
13
14
  export * from "./wrangler.json";
14
15
  export * from "./zone";
@@ -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 { isAssets, isDurableObjectNamespace, } from "./bindings";
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 (isAssets(binding)) {
117
- assetsBindings.push({ name: bindingName, assets: binding });
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
- const oldBinding = Object.values(oldBindings ?? {})
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 (isAssets(binding)) {
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 (isSecret(binding)) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
@@ -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
+ }
@@ -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
- : Service;
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;
@@ -10,5 +10,6 @@ export * from "./kv-namespace";
10
10
  export * from "./permission-groups";
11
11
  export * from "./r2-rest-state-store";
12
12
  export * from "./worker";
13
+ export { Workflow } from "./workflow";
13
14
  export * from "./wrangler.json";
14
15
  export * from "./zone";
@@ -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 { Assets } from "./assets";
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 { DurableObjectNamespace } from "./durable-object-namespace";
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 (isAssets(binding)) {
267
- assetsBindings.push({ name: bindingName, assets: binding });
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 (isAssets(binding)) {
558
+ } else if (binding.type === "assets") {
572
559
  meta.bindings.push({
573
560
  type: "assets",
574
561
  name: bindingName,
575
562
  });
576
- } else if (isSecret(binding)) {
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.