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
|
@@ -5,7 +5,11 @@ import type {
|
|
|
5
5
|
DnsRecordWithMetadata,
|
|
6
6
|
} from "../dns/record";
|
|
7
7
|
import { Resource } from "../resource";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type CloudflareApi,
|
|
10
|
+
type CloudflareApiOptions,
|
|
11
|
+
createCloudflareApi,
|
|
12
|
+
} from "./api";
|
|
9
13
|
import type { CloudflareResponse } from "./response";
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -48,7 +52,7 @@ export interface DnsRecord extends DnsRecordWithMetadata {}
|
|
|
48
52
|
/**
|
|
49
53
|
* Properties for managing multiple DNS records
|
|
50
54
|
*/
|
|
51
|
-
export interface DnsRecordsProps {
|
|
55
|
+
export interface DnsRecordsProps extends CloudflareApiOptions {
|
|
52
56
|
/**
|
|
53
57
|
* Zone ID or domain name where records will be created
|
|
54
58
|
*/
|
|
@@ -127,7 +131,7 @@ export const DnsRecords = Resource(
|
|
|
127
131
|
props: DnsRecordsProps
|
|
128
132
|
): Promise<DnsRecords> {
|
|
129
133
|
// Create Cloudflare API client
|
|
130
|
-
const api = await createCloudflareApi();
|
|
134
|
+
const api = await createCloudflareApi(props);
|
|
131
135
|
|
|
132
136
|
// Get zone ID if domain name was provided
|
|
133
137
|
const zoneId = props.zoneId;
|
|
@@ -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
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { Context } from "../context";
|
|
2
2
|
import { Resource } from "../resource";
|
|
3
3
|
import { withExponentialBackoff } from "../util/retry";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CloudflareApi,
|
|
6
|
+
createCloudflareApi,
|
|
7
|
+
type CloudflareApiOptions,
|
|
8
|
+
} from "./api";
|
|
9
|
+
import { handleApiError } from "./api-error";
|
|
5
10
|
|
|
6
11
|
export function isKVNamespace(resource: any): resource is KVNamespace {
|
|
7
12
|
return (
|
|
@@ -12,7 +17,7 @@ export function isKVNamespace(resource: any): resource is KVNamespace {
|
|
|
12
17
|
/**
|
|
13
18
|
* Properties for creating or updating a KV Namespace
|
|
14
19
|
*/
|
|
15
|
-
export interface KVNamespaceProps {
|
|
20
|
+
export interface KVNamespaceProps extends CloudflareApiOptions {
|
|
16
21
|
/**
|
|
17
22
|
* Title of the namespace
|
|
18
23
|
*/
|
|
@@ -119,28 +124,16 @@ export const KVNamespace = Resource(
|
|
|
119
124
|
async function (
|
|
120
125
|
this: Context<KVNamespace>,
|
|
121
126
|
id: string,
|
|
122
|
-
props: KVNamespaceProps
|
|
127
|
+
props: KVNamespaceProps
|
|
123
128
|
) {
|
|
124
129
|
// Create Cloudflare API client with automatic account discovery
|
|
125
|
-
const api = await createCloudflareApi();
|
|
130
|
+
const api = await createCloudflareApi(props);
|
|
126
131
|
|
|
127
132
|
if (this.phase === "delete") {
|
|
128
133
|
// For delete operations, we need to check if the namespace ID exists in the output
|
|
129
134
|
const namespaceId = this.output?.namespaceId;
|
|
130
135
|
if (namespaceId) {
|
|
131
|
-
|
|
132
|
-
const deleteResponse = await api.delete(
|
|
133
|
-
`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}`,
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
if (!deleteResponse.ok && deleteResponse.status !== 404) {
|
|
137
|
-
const errorData: any = await deleteResponse.json().catch(() => ({
|
|
138
|
-
errors: [{ message: deleteResponse.statusText }],
|
|
139
|
-
}));
|
|
140
|
-
throw new Error(
|
|
141
|
-
`Error deleting KV namespace '${props.title}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
|
|
142
|
-
);
|
|
143
|
-
}
|
|
136
|
+
await deleteKVNamespace(api, namespaceId);
|
|
144
137
|
}
|
|
145
138
|
|
|
146
139
|
// Return minimal output for deleted state
|
|
@@ -159,96 +152,12 @@ export const KVNamespace = Resource(
|
|
|
159
152
|
// Can't update a KV namespace title directly, just work with existing ID
|
|
160
153
|
} else {
|
|
161
154
|
// TODO: if it already exists, then check the tags to see if we own it and continue
|
|
162
|
-
|
|
163
|
-
// Create new KV namespace
|
|
164
|
-
const createResponse = await api.post(
|
|
165
|
-
`/accounts/${api.accountId}/storage/kv/namespaces`,
|
|
166
|
-
{
|
|
167
|
-
title: props.title,
|
|
168
|
-
},
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
if (!createResponse.ok) {
|
|
172
|
-
const errorData: any = await createResponse.json().catch(() => ({
|
|
173
|
-
errors: [{ message: createResponse.statusText }],
|
|
174
|
-
}));
|
|
175
|
-
throw new Error(
|
|
176
|
-
`Error creating KV namespace '${props.title}': ${errorData.errors?.[0]?.message || createResponse.statusText}`,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const createResult: any = await createResponse.json();
|
|
181
|
-
namespaceId = createResult.result.id;
|
|
155
|
+
const { id } = await createKVNamespace(api, props);
|
|
182
156
|
createdAt = Date.now();
|
|
157
|
+
namespaceId = id;
|
|
183
158
|
}
|
|
184
159
|
|
|
185
|
-
|
|
186
|
-
if (props.values && props.values.length > 0 && namespaceId) {
|
|
187
|
-
// Process KV pairs in batches of 10000 (API limit)
|
|
188
|
-
const BATCH_SIZE = 10000;
|
|
189
|
-
|
|
190
|
-
for (let i = 0; i < props.values.length; i += BATCH_SIZE) {
|
|
191
|
-
const batch = props.values.slice(i, i + BATCH_SIZE);
|
|
192
|
-
|
|
193
|
-
const bulkPayload = batch.map((entry) => {
|
|
194
|
-
const item: any = {
|
|
195
|
-
key: entry.key,
|
|
196
|
-
value:
|
|
197
|
-
typeof entry.value === "string"
|
|
198
|
-
? entry.value
|
|
199
|
-
: JSON.stringify(entry.value),
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
if (entry.expiration) {
|
|
203
|
-
item.expiration = entry.expiration;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (entry.expirationTtl) {
|
|
207
|
-
item.expiration_ttl = entry.expirationTtl;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (entry.metadata) {
|
|
211
|
-
item.metadata = entry.metadata;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return item;
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
await withExponentialBackoff(
|
|
219
|
-
async () => {
|
|
220
|
-
const bulkResponse = await api.put(
|
|
221
|
-
`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/bulk`,
|
|
222
|
-
bulkPayload,
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
if (!bulkResponse.ok) {
|
|
226
|
-
const errorData: any = await bulkResponse
|
|
227
|
-
.json()
|
|
228
|
-
.catch(() => ({
|
|
229
|
-
errors: [{ message: bulkResponse.statusText }],
|
|
230
|
-
}));
|
|
231
|
-
const errorMessage =
|
|
232
|
-
errorData.errors?.[0]?.message || bulkResponse.statusText;
|
|
233
|
-
|
|
234
|
-
// Throw error to trigger retry
|
|
235
|
-
throw new Error(`Error writing KV batch: ${errorMessage}`);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return bulkResponse;
|
|
239
|
-
},
|
|
240
|
-
(error) => {
|
|
241
|
-
// Retry on "namespace not found" errors as they're likely propagation delays
|
|
242
|
-
return error.message?.includes("not found");
|
|
243
|
-
},
|
|
244
|
-
5, // 5 retry attempts
|
|
245
|
-
1000, // Start with 1 second delay
|
|
246
|
-
);
|
|
247
|
-
} catch (error: any) {
|
|
248
|
-
console.warn(error.message);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
160
|
+
await insertKVRecords(api, namespaceId, props);
|
|
252
161
|
|
|
253
162
|
return this({
|
|
254
163
|
type: "kv_namespace",
|
|
@@ -259,5 +168,108 @@ export const KVNamespace = Resource(
|
|
|
259
168
|
modifiedAt: Date.now(),
|
|
260
169
|
});
|
|
261
170
|
}
|
|
262
|
-
}
|
|
171
|
+
}
|
|
263
172
|
);
|
|
173
|
+
|
|
174
|
+
export async function createKVNamespace(
|
|
175
|
+
api: CloudflareApi,
|
|
176
|
+
props: KVNamespaceProps
|
|
177
|
+
): Promise<{ id: string }> {
|
|
178
|
+
const createResponse = await api.post(
|
|
179
|
+
`/accounts/${api.accountId}/storage/kv/namespaces`,
|
|
180
|
+
{
|
|
181
|
+
title: props.title,
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (!createResponse.ok) {
|
|
186
|
+
await handleApiError(createResponse, "create", "kv_namespace", props.title);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { id: ((await createResponse.json()) as any).result.id };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function deleteKVNamespace(
|
|
193
|
+
api: CloudflareApi,
|
|
194
|
+
namespaceId: string
|
|
195
|
+
) {
|
|
196
|
+
// Delete KV namespace
|
|
197
|
+
const deleteResponse = await api.delete(
|
|
198
|
+
`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}`
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (!deleteResponse.ok && deleteResponse.status !== 404) {
|
|
202
|
+
await handleApiError(deleteResponse, "delete", "kv_namespace", namespaceId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function insertKVRecords(
|
|
207
|
+
api: CloudflareApi,
|
|
208
|
+
namespaceId: string,
|
|
209
|
+
props: KVNamespaceProps
|
|
210
|
+
) {
|
|
211
|
+
if (props.values && props.values.length > 0) {
|
|
212
|
+
// Process KV pairs in batches of 10000 (API limit)
|
|
213
|
+
const BATCH_SIZE = 10000;
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < props.values.length; i += BATCH_SIZE) {
|
|
216
|
+
const batch = props.values.slice(i, i + BATCH_SIZE);
|
|
217
|
+
|
|
218
|
+
const bulkPayload = batch.map((entry) => {
|
|
219
|
+
const item: any = {
|
|
220
|
+
key: entry.key,
|
|
221
|
+
value:
|
|
222
|
+
typeof entry.value === "string"
|
|
223
|
+
? entry.value
|
|
224
|
+
: JSON.stringify(entry.value),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (entry.expiration) {
|
|
228
|
+
item.expiration = entry.expiration;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (entry.expirationTtl) {
|
|
232
|
+
item.expiration_ttl = entry.expirationTtl;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (entry.metadata) {
|
|
236
|
+
item.metadata = entry.metadata;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return item;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await withExponentialBackoff(
|
|
244
|
+
async () => {
|
|
245
|
+
const bulkResponse = await api.put(
|
|
246
|
+
`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/bulk`,
|
|
247
|
+
bulkPayload
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!bulkResponse.ok) {
|
|
251
|
+
const errorData: any = await bulkResponse.json().catch(() => ({
|
|
252
|
+
errors: [{ message: bulkResponse.statusText }],
|
|
253
|
+
}));
|
|
254
|
+
const errorMessage =
|
|
255
|
+
errorData.errors?.[0]?.message || bulkResponse.statusText;
|
|
256
|
+
|
|
257
|
+
// Throw error to trigger retry
|
|
258
|
+
throw new Error(`Error writing KV batch: ${errorMessage}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return bulkResponse;
|
|
262
|
+
},
|
|
263
|
+
(error) => {
|
|
264
|
+
// Retry on "namespace not found" errors as they're likely propagation delays
|
|
265
|
+
return error.message?.includes("not found");
|
|
266
|
+
},
|
|
267
|
+
5, // 5 retry attempts
|
|
268
|
+
1000 // Start with 1 second delay
|
|
269
|
+
);
|
|
270
|
+
} catch (error: any) {
|
|
271
|
+
console.warn(error.message);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -72,6 +72,10 @@ export type PermissionGroups = Resource<"cloudflare::PermissionGroups"> & {
|
|
|
72
72
|
*
|
|
73
73
|
* This is primarily used when creating API tokens for Cloudflare services like R2.
|
|
74
74
|
*
|
|
75
|
+
* Note: Requires a Cloudflare API Key or Token with account read access.
|
|
76
|
+
* The API token must have permission to read token permission groups.
|
|
77
|
+
* The OAuth token from `wrangler login` is NOT sufficient for this operation.
|
|
78
|
+
*
|
|
75
79
|
* @example
|
|
76
80
|
* // Get all permission groups including those for R2
|
|
77
81
|
* const permissions = await PermissionGroups("cloudflare-permissions");
|
|
@@ -99,7 +103,7 @@ export const PermissionGroups = Resource(
|
|
|
99
103
|
async function (
|
|
100
104
|
this: Context<PermissionGroups>,
|
|
101
105
|
id: string,
|
|
102
|
-
options
|
|
106
|
+
options: CloudflareApiOptions = {}
|
|
103
107
|
): Promise<PermissionGroups> {
|
|
104
108
|
// Only create and update phases are supported
|
|
105
109
|
if (this.phase === "delete") {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { Scope } from "../scope";
|
|
2
|
-
import type { Secret } from "../secret";
|
|
3
2
|
import { deserialize, serialize } from "../serde";
|
|
4
3
|
import type { State, StateStore } from "../state";
|
|
5
4
|
import { withExponentialBackoff } from "../util/retry";
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type CloudflareApi,
|
|
7
|
+
type CloudflareApiOptions,
|
|
8
|
+
createCloudflareApi,
|
|
9
|
+
} from "./api";
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Options for CloudflareR2StateStore
|
|
10
13
|
*/
|
|
11
|
-
export interface CloudflareR2StateStoreOptions {
|
|
14
|
+
export interface CloudflareR2StateStoreOptions extends CloudflareApiOptions {
|
|
12
15
|
/**
|
|
13
16
|
* The prefix to use for object keys in the R2 bucket
|
|
14
17
|
* This allows multiple state stores to use the same R2 bucket
|
|
@@ -20,21 +23,6 @@ export interface CloudflareR2StateStoreOptions {
|
|
|
20
23
|
* Required - the bucket must already exist
|
|
21
24
|
*/
|
|
22
25
|
bucketName: string;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* API key to use (overrides CLOUDFLARE_API_KEY env var)
|
|
26
|
-
*/
|
|
27
|
-
apiKey?: Secret;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
|
|
31
|
-
*/
|
|
32
|
-
accountId?: string;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Email to use with API Key authentication (overrides CLOUDFLARE_EMAIL env var)
|
|
36
|
-
*/
|
|
37
|
-
email?: string;
|
|
38
26
|
}
|
|
39
27
|
|
|
40
28
|
/**
|
|
@@ -45,9 +33,6 @@ export class R2RestStateStore implements StateStore {
|
|
|
45
33
|
private api: CloudflareApi;
|
|
46
34
|
private prefix: string;
|
|
47
35
|
private bucketName: string;
|
|
48
|
-
private apiKey: Secret | undefined;
|
|
49
|
-
private accountId: string | undefined;
|
|
50
|
-
private email: string | undefined;
|
|
51
36
|
private initialized = false;
|
|
52
37
|
|
|
53
38
|
/**
|
|
@@ -58,7 +43,7 @@ export class R2RestStateStore implements StateStore {
|
|
|
58
43
|
*/
|
|
59
44
|
constructor(
|
|
60
45
|
public readonly scope: Scope,
|
|
61
|
-
options: CloudflareR2StateStoreOptions
|
|
46
|
+
private readonly options: CloudflareR2StateStoreOptions
|
|
62
47
|
) {
|
|
63
48
|
// Use the scope's chain to build the prefix, similar to how FileSystemStateStore builds its directory
|
|
64
49
|
const scopePath = scope.chain.join("/");
|
|
@@ -71,10 +56,6 @@ export class R2RestStateStore implements StateStore {
|
|
|
71
56
|
}
|
|
72
57
|
this.bucketName = options.bucketName;
|
|
73
58
|
|
|
74
|
-
this.apiKey = options.apiKey;
|
|
75
|
-
this.accountId = options.accountId;
|
|
76
|
-
this.email = options.email;
|
|
77
|
-
|
|
78
59
|
// We'll initialize the API in init() to allow for async creation
|
|
79
60
|
this.api = null as any;
|
|
80
61
|
}
|
|
@@ -86,11 +67,7 @@ export class R2RestStateStore implements StateStore {
|
|
|
86
67
|
if (this.initialized) return;
|
|
87
68
|
|
|
88
69
|
// Create Cloudflare API client with automatic account discovery
|
|
89
|
-
this.api = await createCloudflareApi(
|
|
90
|
-
apiKey: this.apiKey,
|
|
91
|
-
accountId: this.accountId,
|
|
92
|
-
email: this.email,
|
|
93
|
-
});
|
|
70
|
+
this.api = await createCloudflareApi(this.options);
|
|
94
71
|
|
|
95
72
|
this.initialized = true;
|
|
96
73
|
}
|