alchemy 0.5.2 → 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.
Files changed (54) hide show
  1. package/lib/cloudflare/account-api-token.d.ts +6 -1
  2. package/lib/cloudflare/account-api-token.js +5 -1
  3. package/lib/cloudflare/account-id.d.ts +5 -1
  4. package/lib/cloudflare/account-id.js +5 -0
  5. package/lib/cloudflare/api-error.d.ts +32 -0
  6. package/lib/cloudflare/api-error.js +47 -0
  7. package/lib/cloudflare/api.d.ts +16 -49
  8. package/lib/cloudflare/api.js +37 -149
  9. package/lib/cloudflare/auth.d.ts +55 -38
  10. package/lib/cloudflare/auth.js +148 -77
  11. package/lib/cloudflare/bindings.d.ts +19 -7
  12. package/lib/cloudflare/bindings.js +0 -9
  13. package/lib/cloudflare/bound.d.ts +2 -1
  14. package/lib/cloudflare/bucket.d.ts +30 -5
  15. package/lib/cloudflare/bucket.js +12 -11
  16. package/lib/cloudflare/custom-domain.d.ts +2 -1
  17. package/lib/cloudflare/custom-domain.js +3 -2
  18. package/lib/cloudflare/dns-records.d.ts +2 -1
  19. package/lib/cloudflare/dns-records.js +2 -2
  20. package/lib/cloudflare/index.d.ts +1 -0
  21. package/lib/cloudflare/index.js +1 -0
  22. package/lib/cloudflare/kv-namespace.d.ts +7 -1
  23. package/lib/cloudflare/kv-namespace.js +72 -71
  24. package/lib/cloudflare/permission-groups.d.ts +4 -0
  25. package/lib/cloudflare/permission-groups.js +5 -1
  26. package/lib/cloudflare/r2-rest-state-store.d.ts +3 -17
  27. package/lib/cloudflare/r2-rest-state-store.js +4 -12
  28. package/lib/cloudflare/worker.d.ts +2 -6
  29. package/lib/cloudflare/worker.js +58 -90
  30. package/lib/cloudflare/workflow.d.ts +46 -0
  31. package/lib/cloudflare/workflow.js +29 -0
  32. package/lib/cloudflare/zone.d.ts +2 -6
  33. package/lib/cloudflare/zone.js +1 -3
  34. package/lib/internal/docs/providers.js +1 -8
  35. package/package.json +5 -1
  36. package/src/cloudflare/account-api-token.ts +7 -3
  37. package/src/cloudflare/account-id.ts +12 -0
  38. package/src/cloudflare/api-error.ts +58 -0
  39. package/src/cloudflare/api.ts +50 -216
  40. package/src/cloudflare/auth.ts +262 -119
  41. package/src/cloudflare/bindings.ts +21 -16
  42. package/src/cloudflare/bound.ts +4 -1
  43. package/src/cloudflare/bucket.ts +52 -29
  44. package/src/cloudflare/custom-domain.ts +8 -3
  45. package/src/cloudflare/dns-records.ts +7 -3
  46. package/src/cloudflare/durable-object-namespace.ts +1 -1
  47. package/src/cloudflare/index.ts +1 -0
  48. package/src/cloudflare/kv-namespace.ts +117 -105
  49. package/src/cloudflare/permission-groups.ts +5 -1
  50. package/src/cloudflare/r2-rest-state-store.ts +8 -31
  51. package/src/cloudflare/worker.ts +71 -141
  52. package/src/cloudflare/workflow.ts +79 -0
  53. package/src/cloudflare/zone.ts +17 -25
  54. package/src/internal/docs/providers.ts +1 -8
@@ -1,6 +1,7 @@
1
1
  import { Resource } from "../resource";
2
2
  import { withExponentialBackoff } from "../util/retry";
3
- import { createCloudflareApi } from "./api";
3
+ import { CloudflareApi, createCloudflareApi, } from "./api";
4
+ import { handleApiError } from "./api-error";
4
5
  export function isKVNamespace(resource) {
5
6
  return (resource && typeof resource === "object" && resource.type === "kv_namespace");
6
7
  }
@@ -42,19 +43,12 @@ export function isKVNamespace(resource) {
42
43
  */
43
44
  export const KVNamespace = Resource("cloudflare::KVNamespace", async function (id, props) {
44
45
  // Create Cloudflare API client with automatic account discovery
45
- const api = await createCloudflareApi();
46
+ const api = await createCloudflareApi(props);
46
47
  if (this.phase === "delete") {
47
48
  // For delete operations, we need to check if the namespace ID exists in the output
48
49
  const namespaceId = this.output?.namespaceId;
49
50
  if (namespaceId) {
50
- // Delete KV namespace
51
- const deleteResponse = await api.delete(`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}`);
52
- if (!deleteResponse.ok && deleteResponse.status !== 404) {
53
- const errorData = await deleteResponse.json().catch(() => ({
54
- errors: [{ message: deleteResponse.statusText }],
55
- }));
56
- throw new Error(`Error deleting KV namespace '${props.title}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`);
57
- }
51
+ await deleteKVNamespace(api, namespaceId);
58
52
  }
59
53
  // Return minimal output for deleted state
60
54
  return this.destroy();
@@ -71,69 +65,11 @@ export const KVNamespace = Resource("cloudflare::KVNamespace", async function (i
71
65
  }
72
66
  else {
73
67
  // TODO: if it already exists, then check the tags to see if we own it and continue
74
- // Create new KV namespace
75
- const createResponse = await api.post(`/accounts/${api.accountId}/storage/kv/namespaces`, {
76
- title: props.title,
77
- });
78
- if (!createResponse.ok) {
79
- const errorData = await createResponse.json().catch(() => ({
80
- errors: [{ message: createResponse.statusText }],
81
- }));
82
- throw new Error(`Error creating KV namespace '${props.title}': ${errorData.errors?.[0]?.message || createResponse.statusText}`);
83
- }
84
- const createResult = await createResponse.json();
85
- namespaceId = createResult.result.id;
68
+ const { id } = await createKVNamespace(api, props);
86
69
  createdAt = Date.now();
70
+ namespaceId = id;
87
71
  }
88
- // Handle KV pairs if provided
89
- if (props.values && props.values.length > 0 && namespaceId) {
90
- // Process KV pairs in batches of 10000 (API limit)
91
- const BATCH_SIZE = 10000;
92
- for (let i = 0; i < props.values.length; i += BATCH_SIZE) {
93
- const batch = props.values.slice(i, i + BATCH_SIZE);
94
- const bulkPayload = batch.map((entry) => {
95
- const item = {
96
- key: entry.key,
97
- value: typeof entry.value === "string"
98
- ? entry.value
99
- : JSON.stringify(entry.value),
100
- };
101
- if (entry.expiration) {
102
- item.expiration = entry.expiration;
103
- }
104
- if (entry.expirationTtl) {
105
- item.expiration_ttl = entry.expirationTtl;
106
- }
107
- if (entry.metadata) {
108
- item.metadata = entry.metadata;
109
- }
110
- return item;
111
- });
112
- try {
113
- await withExponentialBackoff(async () => {
114
- const bulkResponse = await api.put(`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/bulk`, bulkPayload);
115
- if (!bulkResponse.ok) {
116
- const errorData = await bulkResponse
117
- .json()
118
- .catch(() => ({
119
- errors: [{ message: bulkResponse.statusText }],
120
- }));
121
- const errorMessage = errorData.errors?.[0]?.message || bulkResponse.statusText;
122
- // Throw error to trigger retry
123
- throw new Error(`Error writing KV batch: ${errorMessage}`);
124
- }
125
- return bulkResponse;
126
- }, (error) => {
127
- // Retry on "namespace not found" errors as they're likely propagation delays
128
- return error.message?.includes("not found");
129
- }, 5, // 5 retry attempts
130
- 1000);
131
- }
132
- catch (error) {
133
- console.warn(error.message);
134
- }
135
- }
136
- }
72
+ await insertKVRecords(api, namespaceId, props);
137
73
  return this({
138
74
  type: "kv_namespace",
139
75
  namespaceId: namespaceId,
@@ -144,3 +80,68 @@ export const KVNamespace = Resource("cloudflare::KVNamespace", async function (i
144
80
  });
145
81
  }
146
82
  });
83
+ export async function createKVNamespace(api, props) {
84
+ const createResponse = await api.post(`/accounts/${api.accountId}/storage/kv/namespaces`, {
85
+ title: props.title,
86
+ });
87
+ if (!createResponse.ok) {
88
+ await handleApiError(createResponse, "create", "kv_namespace", props.title);
89
+ }
90
+ return { id: (await createResponse.json()).result.id };
91
+ }
92
+ export async function deleteKVNamespace(api, namespaceId) {
93
+ // Delete KV namespace
94
+ const deleteResponse = await api.delete(`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}`);
95
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
96
+ await handleApiError(deleteResponse, "delete", "kv_namespace", namespaceId);
97
+ }
98
+ }
99
+ export async function insertKVRecords(api, namespaceId, props) {
100
+ if (props.values && props.values.length > 0) {
101
+ // Process KV pairs in batches of 10000 (API limit)
102
+ const BATCH_SIZE = 10000;
103
+ for (let i = 0; i < props.values.length; i += BATCH_SIZE) {
104
+ const batch = props.values.slice(i, i + BATCH_SIZE);
105
+ const bulkPayload = batch.map((entry) => {
106
+ const item = {
107
+ key: entry.key,
108
+ value: typeof entry.value === "string"
109
+ ? entry.value
110
+ : JSON.stringify(entry.value),
111
+ };
112
+ if (entry.expiration) {
113
+ item.expiration = entry.expiration;
114
+ }
115
+ if (entry.expirationTtl) {
116
+ item.expiration_ttl = entry.expirationTtl;
117
+ }
118
+ if (entry.metadata) {
119
+ item.metadata = entry.metadata;
120
+ }
121
+ return item;
122
+ });
123
+ try {
124
+ await withExponentialBackoff(async () => {
125
+ const bulkResponse = await api.put(`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/bulk`, bulkPayload);
126
+ if (!bulkResponse.ok) {
127
+ const errorData = await bulkResponse.json().catch(() => ({
128
+ errors: [{ message: bulkResponse.statusText }],
129
+ }));
130
+ const errorMessage = errorData.errors?.[0]?.message || bulkResponse.statusText;
131
+ // Throw error to trigger retry
132
+ throw new Error(`Error writing KV batch: ${errorMessage}`);
133
+ }
134
+ return bulkResponse;
135
+ }, (error) => {
136
+ // Retry on "namespace not found" errors as they're likely propagation delays
137
+ return error.message?.includes("not found");
138
+ }, 5, // 5 retry attempts
139
+ 1000 // Start with 1 second delay
140
+ );
141
+ }
142
+ catch (error) {
143
+ console.warn(error.message);
144
+ }
145
+ }
146
+ }
147
+ }
@@ -53,6 +53,10 @@ export type PermissionGroups = Resource<"cloudflare::PermissionGroups"> & {
53
53
  *
54
54
  * This is primarily used when creating API tokens for Cloudflare services like R2.
55
55
  *
56
+ * Note: Requires a Cloudflare API Key or Token with account read access.
57
+ * The API token must have permission to read token permission groups.
58
+ * The OAuth token from `wrangler login` is NOT sufficient for this operation.
59
+ *
56
60
  * @example
57
61
  * // Get all permission groups including those for R2
58
62
  * const permissions = await PermissionGroups("cloudflare-permissions");
@@ -6,6 +6,10 @@ import { createCloudflareApi } from "./api";
6
6
  *
7
7
  * This is primarily used when creating API tokens for Cloudflare services like R2.
8
8
  *
9
+ * Note: Requires a Cloudflare API Key or Token with account read access.
10
+ * The API token must have permission to read token permission groups.
11
+ * The OAuth token from `wrangler login` is NOT sufficient for this operation.
12
+ *
9
13
  * @example
10
14
  * // Get all permission groups including those for R2
11
15
  * const permissions = await PermissionGroups("cloudflare-permissions");
@@ -28,7 +32,7 @@ import { createCloudflareApi } from "./api";
28
32
  * ]
29
33
  * });
30
34
  */
31
- export const PermissionGroups = Resource("cloudflare::PermissionGroups", async function (id, options) {
35
+ export const PermissionGroups = Resource("cloudflare::PermissionGroups", async function (id, options = {}) {
32
36
  // Only create and update phases are supported
33
37
  if (this.phase === "delete") {
34
38
  return this.destroy();
@@ -1,10 +1,10 @@
1
1
  import type { Scope } from "../scope";
2
- import type { Secret } from "../secret";
3
2
  import type { State, StateStore } from "../state";
3
+ import { type CloudflareApiOptions } from "./api";
4
4
  /**
5
5
  * Options for CloudflareR2StateStore
6
6
  */
7
- export interface CloudflareR2StateStoreOptions {
7
+ export interface CloudflareR2StateStoreOptions extends CloudflareApiOptions {
8
8
  /**
9
9
  * The prefix to use for object keys in the R2 bucket
10
10
  * This allows multiple state stores to use the same R2 bucket
@@ -15,18 +15,6 @@ export interface CloudflareR2StateStoreOptions {
15
15
  * Required - the bucket must already exist
16
16
  */
17
17
  bucketName: string;
18
- /**
19
- * API key to use (overrides CLOUDFLARE_API_KEY env var)
20
- */
21
- apiKey?: Secret;
22
- /**
23
- * Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
24
- */
25
- accountId?: string;
26
- /**
27
- * Email to use with API Key authentication (overrides CLOUDFLARE_EMAIL env var)
28
- */
29
- email?: string;
30
18
  }
31
19
  /**
32
20
  * State store implementation using Cloudflare R2 API
@@ -34,12 +22,10 @@ export interface CloudflareR2StateStoreOptions {
34
22
  */
35
23
  export declare class R2RestStateStore implements StateStore {
36
24
  readonly scope: Scope;
25
+ private readonly options;
37
26
  private api;
38
27
  private prefix;
39
28
  private bucketName;
40
- private apiKey;
41
- private accountId;
42
- private email;
43
29
  private initialized;
44
30
  /**
45
31
  * Create a new CloudflareR2StateStore
@@ -1,18 +1,16 @@
1
1
  import { deserialize, serialize } from "../serde";
2
2
  import { withExponentialBackoff } from "../util/retry";
3
- import { createCloudflareApi } from "./api";
3
+ import { createCloudflareApi, } from "./api";
4
4
  /**
5
5
  * State store implementation using Cloudflare R2 API
6
6
  * Uses R2 for immediate consistency compared to KV's eventual consistency
7
7
  */
8
8
  export class R2RestStateStore {
9
9
  scope;
10
+ options;
10
11
  api;
11
12
  prefix;
12
13
  bucketName;
13
- apiKey;
14
- accountId;
15
- email;
16
14
  initialized = false;
17
15
  /**
18
16
  * Create a new CloudflareR2StateStore
@@ -22,6 +20,7 @@ export class R2RestStateStore {
22
20
  */
23
21
  constructor(scope, options) {
24
22
  this.scope = scope;
23
+ this.options = options;
25
24
  // Use the scope's chain to build the prefix, similar to how FileSystemStateStore builds its directory
26
25
  const scopePath = scope.chain.join("/");
27
26
  this.prefix = options.prefix
@@ -31,9 +30,6 @@ export class R2RestStateStore {
31
30
  throw new Error("bucketName is required for CloudflareR2StateStore");
32
31
  }
33
32
  this.bucketName = options.bucketName;
34
- this.apiKey = options.apiKey;
35
- this.accountId = options.accountId;
36
- this.email = options.email;
37
33
  // We'll initialize the API in init() to allow for async creation
38
34
  this.api = null;
39
35
  }
@@ -44,11 +40,7 @@ export class R2RestStateStore {
44
40
  if (this.initialized)
45
41
  return;
46
42
  // Create Cloudflare API client with automatic account discovery
47
- this.api = await createCloudflareApi({
48
- apiKey: this.apiKey,
49
- accountId: this.accountId,
50
- email: this.email,
51
- });
43
+ this.api = await createCloudflareApi(this.options);
52
44
  this.initialized = true;
53
45
  }
54
46
  /**
@@ -1,13 +1,14 @@
1
1
  import type { Context } from "../context";
2
2
  import { type BundleProps } from "../esbuild/bundle";
3
3
  import { Resource } from "../resource";
4
+ import { type CloudflareApiOptions } from "./api";
4
5
  import { type Bindings } from "./bindings";
5
6
  import type { Bound } from "./bound";
6
7
  import type { SingleStepMigration } from "./worker-migration";
7
8
  /**
8
9
  * Properties for creating or updating a Worker
9
10
  */
10
- export interface WorkerProps<B extends Bindings = Bindings> {
11
+ export interface WorkerProps<B extends Bindings = Bindings> extends CloudflareApiOptions {
11
12
  /**
12
13
  * The worker script content (JavaScript or WASM)
13
14
  * One of script, entryPoint, or bundle must be provided
@@ -36,11 +37,6 @@ export interface WorkerProps<B extends Bindings = Bindings> {
36
37
  * This is mandatory - must be explicitly specified
37
38
  */
38
39
  name: string;
39
- /**
40
- * Routes to associate with this worker
41
- * Format: example.com/* or *.example.com/*
42
- */
43
- routes?: string[];
44
40
  /**
45
41
  * Bindings to attach to the worker
46
42
  */
@@ -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
- import { createCloudflareApi } from "./api";
10
- import { isAssets, isDurableObjectNamespace, } from "./bindings";
8
+ import { createCloudflareApi, } from "./api";
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
  *
@@ -90,7 +92,7 @@ export const Worker = Resource("cloudflare::Worker", {
90
92
  alwaysUpdate: true,
91
93
  }, async function (id, props) {
92
94
  // Create Cloudflare API client with automatic account discovery
93
- const api = await createCloudflareApi();
95
+ const api = await createCloudflareApi(props);
94
96
  // Use the provided name
95
97
  const workerName = props.name;
96
98
  // Validate input - we need either script, entryPoint, or bundle
@@ -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,12 +137,16 @@ 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
- // Handle routes if requested
137
- await setupRoutes(api, workerName, props.routes || []);
138
150
  // Handle worker URL if requested
139
151
  const workerUrl = await configureURL(this, api, workerName, props.url ?? false);
140
152
  // Get current timestamp
@@ -146,7 +158,6 @@ export const Worker = Resource("cloudflare::Worker", {
146
158
  name: workerName,
147
159
  script: scriptContent,
148
160
  format: props.format || "esm", // Include format in the output
149
- routes: props.routes || [],
150
161
  bindings: props.bindings ?? {},
151
162
  env: props.env,
152
163
  observability: scriptMetadata.observability,
@@ -167,25 +178,6 @@ async function deleteWorker(ctx, api, workerName) {
167
178
  .catch(() => ({ errors: [{ message: deleteResponse.statusText }] }));
168
179
  console.error("Error deleting worker:", errorData.errors?.[0]?.message || deleteResponse.statusText);
169
180
  }
170
- // Also remove any associated routes if they exist
171
- if (ctx.output?.routes && ctx.output.routes.length > 0 && api.zoneId) {
172
- // First get existing routes to find their IDs
173
- const routesResponse = await api.get(`/zones/${api.zoneId}/workers/routes`);
174
- if (!routesResponse.ok) {
175
- throw new Error(`Could not fetch routes for cleanup: ${routesResponse.status} ${routesResponse.statusText}`);
176
- }
177
- const routesData = await routesResponse.json();
178
- const existingRoutes = routesData.result || [];
179
- for (const route of existingRoutes) {
180
- if (ctx.output.routes.includes(route.pattern)) {
181
- // Delete the route
182
- const routeDeleteResponse = await api.delete(`/zones/${api.zoneId}/workers/routes/${route.id}`);
183
- if (!routeDeleteResponse.ok) {
184
- console.warn(`Failed to delete route ${route.pattern}: ${routeDeleteResponse.status} ${routeDeleteResponse.statusText}`);
185
- }
186
- }
187
- }
188
- }
189
181
  // Disable the URL if it was enabled
190
182
  if (ctx.output?.url) {
191
183
  try {
@@ -295,33 +287,15 @@ async function prepareWorkerMetadata(ctx, oldBindings, props, assetUploadResult)
295
287
  });
296
288
  }
297
289
  else if (binding.type === "durable_object_namespace") {
298
- const stableId = binding.id;
299
- const className = binding.className;
300
290
  meta.bindings.push({
301
291
  type: "durable_object_namespace",
302
292
  name: bindingName,
303
- class_name: className,
293
+ class_name: binding.className,
304
294
  script_name: binding.scriptName,
305
295
  environment: binding.environment,
306
296
  namespace_id: binding.namespaceId,
307
297
  });
308
- const oldBinding = Object.values(oldBindings ?? {})
309
- ?.filter(isDurableObjectNamespace)
310
- ?.find((b) => b.id === stableId);
311
- if (!oldBinding) {
312
- if (binding.sqlite) {
313
- meta.migrations.new_sqlite_classes.push(className);
314
- }
315
- else {
316
- meta.migrations.new_classes.push(className);
317
- }
318
- }
319
- else if (oldBinding.className !== className) {
320
- meta.migrations.renamed_classes.push({
321
- from: oldBinding.className,
322
- to: className,
323
- });
324
- }
298
+ configureClassMigration(binding, binding.id, binding.className);
325
299
  }
326
300
  else if (binding.type === "r2_bucket") {
327
301
  meta.bindings.push({
@@ -330,25 +304,58 @@ async function prepareWorkerMetadata(ctx, oldBindings, props, assetUploadResult)
330
304
  bucket_name: binding.name,
331
305
  });
332
306
  }
333
- else if (isAssets(binding)) {
307
+ else if (binding.type === "assets") {
334
308
  meta.bindings.push({
335
309
  type: "assets",
336
310
  name: bindingName,
337
311
  });
338
312
  }
339
- else if (isSecret(binding)) {
313
+ else if (binding.type === "secret") {
340
314
  meta.bindings.push({
341
315
  type: "secret_text",
342
316
  name: bindingName,
343
317
  text: binding.unencrypted,
344
318
  });
345
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
+ }
346
332
  else {
347
333
  // @ts-expect-error - we should never reach here
348
334
  throw new Error(`Unsupported binding type: ${binding.type}`);
349
335
  }
350
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
+ }
351
357
  // Convert env variables to plain_text bindings
358
+ // TODO(sam): remove Worker.env in favor of always bindings
352
359
  if (props.env) {
353
360
  for (const [key, value] of Object.entries(props.env)) {
354
361
  meta.bindings.push({
@@ -421,44 +428,6 @@ async function bundleWorkerScript(props) {
421
428
  throw new Error("Error reading bundle");
422
429
  }
423
430
  }
424
- async function setupRoutes(api, workerName, routes) {
425
- // Set up routes if provided
426
- if (routes && routes.length > 0 && api.zoneId) {
427
- // First get existing routes
428
- const routesResponse = await api.get(`/zones/${api.zoneId}/workers/routes`);
429
- if (!routesResponse.ok) {
430
- throw new Error(`Could not fetch routes: ${routesResponse.status} ${routesResponse.statusText}`);
431
- }
432
- const routesData = await routesResponse.json();
433
- const existingRoutes = routesData.result || [];
434
- // For each desired route
435
- for (const pattern of routes) {
436
- const existingRoute = existingRoutes.find((r) => r.pattern === pattern);
437
- if (existingRoute) {
438
- // Update if script name doesn't match
439
- if (existingRoute.script !== workerName) {
440
- const updateRouteResponse = await api.put(`/zones/${api.zoneId}/workers/routes/${existingRoute.id}`, {
441
- pattern,
442
- script: workerName,
443
- });
444
- if (!updateRouteResponse.ok) {
445
- console.warn(`Failed to update route ${pattern}: ${updateRouteResponse.status} ${updateRouteResponse.statusText}`);
446
- }
447
- }
448
- }
449
- else {
450
- // Create new route
451
- const createRouteResponse = await api.post(`/zones/${api.zoneId}/workers/routes`, {
452
- pattern,
453
- script: workerName,
454
- });
455
- if (!createRouteResponse.ok) {
456
- throw new Error(`Failed to create route ${pattern}: ${createRouteResponse.status} ${createRouteResponse.statusText}`);
457
- }
458
- }
459
- }
460
- }
461
- }
462
431
  async function configureURL(ctx, api, workerName, url) {
463
432
  let workerUrl;
464
433
  if (url) {
@@ -579,7 +548,6 @@ async function uploadAssets(api, workerName, assets) {
579
548
  totalBytes += blob.size;
580
549
  formData.append(fileHash, blob, fileHash);
581
550
  }
582
- console.log(`Uploading ${totalBytes} bytes of assets`);
583
551
  // Upload this batch of files
584
552
  const uploadResponse = await api.post(`/accounts/${api.accountId}/workers/assets/upload?base64=true`, formData, {
585
553
  headers: {
@@ -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
+ }
@@ -1,19 +1,15 @@
1
1
  import type { Context } from "../context";
2
2
  import { Resource } from "../resource";
3
+ import { type CloudflareApiOptions } from "./api";
3
4
  import type { AlwaysUseHTTPSValue, AutomaticHTTPSRewritesValue, BrotliValue, DevelopmentModeValue, EarlyHintsValue, EmailObfuscationValue, HTTP2Value, HTTP3Value, HotlinkProtectionValue, IPv6Value, MinTLSVersionValue, SSLValue, TLS13Value, WebSocketsValue, ZeroRTTValue } from "./zone-settings";
4
5
  /**
5
6
  * Properties for creating or updating a Zone
6
7
  */
7
- export interface ZoneProps {
8
+ export interface ZoneProps extends CloudflareApiOptions {
8
9
  /**
9
10
  * The domain name for the zone
10
11
  */
11
12
  name: string;
12
- /**
13
- * Account ID to use for the zone
14
- * If not provided, will use the default account
15
- */
16
- accountId?: string;
17
13
  /**
18
14
  * The type of zone to create
19
15
  * "full" - Full zone implies that DNS is hosted with Cloudflare