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.
- package/lib/cloudflare/account-api-token.d.ts +6 -1
- package/lib/cloudflare/account-api-token.js +5 -1
- package/lib/cloudflare/account-id.d.ts +5 -1
- package/lib/cloudflare/account-id.js +5 -0
- package/lib/cloudflare/api-error.d.ts +32 -0
- package/lib/cloudflare/api-error.js +47 -0
- package/lib/cloudflare/api.d.ts +16 -49
- package/lib/cloudflare/api.js +37 -149
- package/lib/cloudflare/auth.d.ts +55 -38
- package/lib/cloudflare/auth.js +148 -77
- 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/bucket.d.ts +30 -5
- package/lib/cloudflare/bucket.js +12 -11
- package/lib/cloudflare/custom-domain.d.ts +2 -1
- package/lib/cloudflare/custom-domain.js +3 -2
- package/lib/cloudflare/dns-records.d.ts +2 -1
- package/lib/cloudflare/dns-records.js +2 -2
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/kv-namespace.d.ts +7 -1
- package/lib/cloudflare/kv-namespace.js +72 -71
- package/lib/cloudflare/permission-groups.d.ts +4 -0
- package/lib/cloudflare/permission-groups.js +5 -1
- package/lib/cloudflare/r2-rest-state-store.d.ts +3 -17
- package/lib/cloudflare/r2-rest-state-store.js +4 -12
- package/lib/cloudflare/worker.d.ts +2 -6
- package/lib/cloudflare/worker.js +58 -90
- package/lib/cloudflare/workflow.d.ts +46 -0
- package/lib/cloudflare/workflow.js +29 -0
- package/lib/cloudflare/zone.d.ts +2 -6
- package/lib/cloudflare/zone.js +1 -3
- package/lib/internal/docs/providers.js +1 -8
- package/package.json +5 -1
- package/src/cloudflare/account-api-token.ts +7 -3
- package/src/cloudflare/account-id.ts +12 -0
- package/src/cloudflare/api-error.ts +58 -0
- package/src/cloudflare/api.ts +50 -216
- package/src/cloudflare/auth.ts +262 -119
- package/src/cloudflare/bindings.ts +21 -16
- package/src/cloudflare/bound.ts +4 -1
- package/src/cloudflare/bucket.ts +52 -29
- package/src/cloudflare/custom-domain.ts +8 -3
- package/src/cloudflare/dns-records.ts +7 -3
- package/src/cloudflare/durable-object-namespace.ts +1 -1
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/kv-namespace.ts +117 -105
- package/src/cloudflare/permission-groups.ts +5 -1
- package/src/cloudflare/r2-rest-state-store.ts +8 -31
- package/src/cloudflare/worker.ts +71 -141
- package/src/cloudflare/workflow.ts +79 -0
- package/src/cloudflare/zone.ts +17 -25
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*/
|
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
|
-
import { createCloudflareApi } from "./api";
|
|
10
|
-
import {
|
|
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 (
|
|
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,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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
+
}
|
package/lib/cloudflare/zone.d.ts
CHANGED
|
@@ -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
|