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
@@ -5,7 +5,11 @@ import type {
5
5
  DnsRecordWithMetadata,
6
6
  } from "../dns/record";
7
7
  import { Resource } from "../resource";
8
- import { type CloudflareApi, createCloudflareApi } from "./api";
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;
@@ -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";
@@ -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 { createCloudflareApi } from "./api";
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
- // Delete KV namespace
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
- // Handle KV pairs if provided
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?: CloudflareApiOptions
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 { type CloudflareApi, createCloudflareApi } from "./api";
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
  }