alchemy 0.87.0 → 0.90.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/bin/alchemy.js +485 -543
- package/lib/cloudflare/compatibility-date.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.d.ts.map +1 -1
- package/lib/cloudflare/dns-records.d.ts +6 -0
- package/lib/cloudflare/dns-records.d.ts.map +1 -1
- package/lib/cloudflare/dns-records.js +14 -12
- package/lib/cloudflare/dns-records.js.map +1 -1
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/r2-bucket-notification.d.ts +312 -0
- package/lib/cloudflare/r2-bucket-notification.d.ts.map +1 -0
- package/lib/cloudflare/r2-bucket-notification.js +387 -0
- package/lib/cloudflare/r2-bucket-notification.js.map +1 -0
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +8 -1
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/workflow.d.ts +17 -0
- package/lib/cloudflare/workflow.d.ts.map +1 -1
- package/lib/cloudflare/workflow.js +1 -0
- package/lib/cloudflare/workflow.js.map +1 -1
- package/lib/cloudflare/wrangler.json.js +1 -0
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/docker/api.d.ts +40 -0
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +30 -0
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/network.d.ts.map +1 -1
- package/lib/docker/network.js +24 -7
- package/lib/docker/network.js.map +1 -1
- package/lib/encrypt.d.ts.map +1 -1
- package/lib/encrypt.js +2 -2
- package/lib/encrypt.js.map +1 -1
- package/lib/planetscale/api.d.ts +1 -0
- package/lib/planetscale/api.d.ts.map +1 -1
- package/lib/planetscale/api.js +1 -1
- package/lib/planetscale/api.js.map +1 -1
- package/lib/planetscale/branch.d.ts +28 -0
- package/lib/planetscale/branch.d.ts.map +1 -1
- package/lib/planetscale/branch.js +12 -0
- package/lib/planetscale/branch.js.map +1 -1
- package/lib/planetscale/database-extensions.d.ts +515 -0
- package/lib/planetscale/database-extensions.d.ts.map +1 -0
- package/lib/planetscale/database-extensions.js +389 -0
- package/lib/planetscale/database-extensions.js.map +1 -0
- package/lib/planetscale/database.d.ts +40 -2
- package/lib/planetscale/database.d.ts.map +1 -1
- package/lib/planetscale/database.js +76 -1
- package/lib/planetscale/database.js.map +1 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +1 -2
- package/lib/scope.js.map +1 -1
- package/lib/util/telemetry.d.ts +1 -6
- package/lib/util/telemetry.d.ts.map +1 -1
- package/lib/util/telemetry.js.map +1 -1
- package/package.json +6 -6
- package/src/cloudflare/dns-records.ts +25 -16
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/r2-bucket-notification.ts +809 -0
- package/src/cloudflare/worker.ts +8 -1
- package/src/cloudflare/workflow.ts +18 -0
- package/src/cloudflare/wrangler.json.ts +1 -0
- package/src/docker/api.ts +61 -0
- package/src/docker/network.ts +27 -7
- package/src/encrypt.ts +2 -0
- package/src/planetscale/api.ts +1 -1
- package/src/planetscale/branch.ts +45 -0
- package/src/planetscale/database-extensions.ts +947 -0
- package/src/planetscale/database.ts +141 -2
- package/src/scope.ts +1 -2
- package/src/util/telemetry.ts +1 -20
- package/workers/cloudflare-state-store.js +57 -57
- package/workers/tunnel-proxy.js +1 -1
- package/lib/state/instrumented-state-store.d.ts +0 -19
- package/lib/state/instrumented-state-store.d.ts.map +0 -1
- package/lib/state/instrumented-state-store.js +0 -68
- package/lib/state/instrumented-state-store.js.map +0 -1
- package/src/state/instrumented-state-store.ts +0 -108
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
import type { Context } from "../context.ts";
|
|
2
|
+
import { Resource, ResourceKind } from "../resource.ts";
|
|
3
|
+
import { withExponentialBackoff } from "../util/retry.ts";
|
|
4
|
+
import { CloudflareApiError, handleApiError } from "./api-error.ts";
|
|
5
|
+
import {
|
|
6
|
+
createCloudflareApi,
|
|
7
|
+
type CloudflareApi,
|
|
8
|
+
type CloudflareApiOptions,
|
|
9
|
+
} from "./api.ts";
|
|
10
|
+
import { withJurisdiction, type R2Bucket } from "./bucket.ts";
|
|
11
|
+
import { findQueueByName, type Queue } from "./queue.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Event types that can trigger R2 bucket notifications
|
|
15
|
+
*/
|
|
16
|
+
export type R2BucketNotificationEventType = "object-create" | "object-delete";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Message payload sent to a Queue when an R2 bucket notification is triggered
|
|
20
|
+
*
|
|
21
|
+
* @see https://developers.cloudflare.com/r2/buckets/event-notifications/#message-format
|
|
22
|
+
*/
|
|
23
|
+
export interface R2BucketNotificationMessage {
|
|
24
|
+
/**
|
|
25
|
+
* The Cloudflare account ID that the event is associated with
|
|
26
|
+
*/
|
|
27
|
+
account: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The type of action that triggered the event notification
|
|
31
|
+
* Example actions include: PutObject, CopyObject, CompleteMultipartUpload, DeleteObject
|
|
32
|
+
*/
|
|
33
|
+
action: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The name of the bucket where the event occurred
|
|
37
|
+
*/
|
|
38
|
+
bucket: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Details about the object involved in the event
|
|
42
|
+
*/
|
|
43
|
+
object: {
|
|
44
|
+
/**
|
|
45
|
+
* The key (or name) of the object within the bucket
|
|
46
|
+
*/
|
|
47
|
+
key: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The size of the object in bytes (not present for object-delete events)
|
|
51
|
+
*/
|
|
52
|
+
size?: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The entity tag (eTag) of the object (not present for object-delete events)
|
|
56
|
+
*/
|
|
57
|
+
eTag?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The time when the action that triggered the event occurred (ISO 8601 format)
|
|
62
|
+
*/
|
|
63
|
+
eventTime: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Details about the source of a copied object (only present for CopyObject events)
|
|
67
|
+
*/
|
|
68
|
+
copySource?: {
|
|
69
|
+
/**
|
|
70
|
+
* The bucket that contained the source object
|
|
71
|
+
*/
|
|
72
|
+
bucket: string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The name of the source object
|
|
76
|
+
*/
|
|
77
|
+
object: string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Base properties shared by all R2 Bucket Notification configurations
|
|
83
|
+
*/
|
|
84
|
+
interface R2BucketNotificationBaseProps extends CloudflareApiOptions {
|
|
85
|
+
/**
|
|
86
|
+
* The R2 bucket to attach the notification rule to.
|
|
87
|
+
* Can be either a bucket name (string) or an R2Bucket resource.
|
|
88
|
+
*/
|
|
89
|
+
bucket: string | R2Bucket;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The queue that will receive notification messages.
|
|
93
|
+
* Can be either a queue name (string) or a Queue resource.
|
|
94
|
+
*/
|
|
95
|
+
queue: string | Queue;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The type of events that will trigger notifications.
|
|
99
|
+
* - "object-create": Triggered when objects are created or overwritten (PutObject, CompleteMultipartUpload)
|
|
100
|
+
* - "object-delete": Triggered when objects are deleted (DeleteObject, CopyObject, LifecycleDeletion)
|
|
101
|
+
*/
|
|
102
|
+
eventTypes: R2BucketNotificationEventType[];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Optional description for the notification rule to help identify it.
|
|
106
|
+
*/
|
|
107
|
+
description?: string;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The jurisdiction where the bucket exists.
|
|
111
|
+
* Required for EU or FedRAMP jurisdictions.
|
|
112
|
+
*/
|
|
113
|
+
jurisdiction?: "default" | "eu" | "fedramp";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether to delete the notification rule when the resource is removed from Alchemy.
|
|
117
|
+
* @default true
|
|
118
|
+
*/
|
|
119
|
+
delete?: boolean;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Whether to adopt an existing notification rule if one already exists with the same configuration.
|
|
123
|
+
* @default false
|
|
124
|
+
*/
|
|
125
|
+
adopt?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Props with single prefix and suffix (creates one rule)
|
|
130
|
+
*/
|
|
131
|
+
interface R2BucketNotificationSingleProps extends R2BucketNotificationBaseProps {
|
|
132
|
+
/**
|
|
133
|
+
* Optional prefix filter - only objects with keys starting with this prefix will trigger notifications.
|
|
134
|
+
*/
|
|
135
|
+
prefix?: string;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Optional suffix filter - only objects with keys ending with this suffix will trigger notifications.
|
|
139
|
+
*/
|
|
140
|
+
suffix?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Props with multiple prefixes (creates one rule per prefix)
|
|
145
|
+
*/
|
|
146
|
+
interface R2BucketNotificationMultiplePrefixProps extends R2BucketNotificationBaseProps {
|
|
147
|
+
/**
|
|
148
|
+
* Array of prefix filters - creates one notification rule per prefix.
|
|
149
|
+
*/
|
|
150
|
+
prefix: string[];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Optional suffix filter applied to all rules.
|
|
154
|
+
*/
|
|
155
|
+
suffix?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Props with multiple suffixes (creates one rule per suffix)
|
|
160
|
+
*/
|
|
161
|
+
interface R2BucketNotificationMultipleSuffixProps extends R2BucketNotificationBaseProps {
|
|
162
|
+
/**
|
|
163
|
+
* Optional prefix filter applied to all rules.
|
|
164
|
+
*/
|
|
165
|
+
prefix?: string;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Array of suffix filters - creates one notification rule per suffix.
|
|
169
|
+
*/
|
|
170
|
+
suffix: string[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Properties for creating or updating R2 Bucket Notification rules.
|
|
175
|
+
*
|
|
176
|
+
* Either prefix OR suffix can be an array (creating multiple rules), but not both.
|
|
177
|
+
*/
|
|
178
|
+
export type R2BucketNotificationProps =
|
|
179
|
+
| R2BucketNotificationSingleProps
|
|
180
|
+
| R2BucketNotificationMultiplePrefixProps
|
|
181
|
+
| R2BucketNotificationMultipleSuffixProps;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Output returned after R2 Bucket Notification rule creation/update
|
|
185
|
+
*/
|
|
186
|
+
export type R2BucketNotification = Omit<
|
|
187
|
+
R2BucketNotificationBaseProps,
|
|
188
|
+
"bucket" | "queue" | "delete" | "adopt"
|
|
189
|
+
> & {
|
|
190
|
+
/**
|
|
191
|
+
* Resource type identifier
|
|
192
|
+
*/
|
|
193
|
+
type: "r2_bucket_notification";
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The Cloudflare-assigned rule ID(s) for this notification.
|
|
197
|
+
* When prefix or suffix is an array, this contains multiple rule IDs.
|
|
198
|
+
*/
|
|
199
|
+
ruleId: string | string[];
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The name of the bucket this notification is attached to
|
|
203
|
+
*/
|
|
204
|
+
bucketName: string;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The name of the queue that receives notification messages
|
|
208
|
+
*/
|
|
209
|
+
queueName: string;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Prefix filter(s) - normalized to always be a string or array matching input
|
|
213
|
+
*/
|
|
214
|
+
prefix?: string | string[];
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Suffix filter(s) - normalized to always be a string or array matching input
|
|
218
|
+
*/
|
|
219
|
+
suffix?: string | string[];
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Time when the notification rule was created
|
|
223
|
+
*/
|
|
224
|
+
createdAt?: string;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Type guard to check if a resource is an R2BucketNotification
|
|
229
|
+
*/
|
|
230
|
+
export function isR2BucketNotification(
|
|
231
|
+
resource: any,
|
|
232
|
+
): resource is R2BucketNotification {
|
|
233
|
+
return resource?.[ResourceKind] === "cloudflare::R2BucketNotification";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function resolveQueueId(
|
|
237
|
+
api: CloudflareApi,
|
|
238
|
+
queue: string | Queue,
|
|
239
|
+
): Promise<string> {
|
|
240
|
+
if (typeof queue !== "string" && queue.id) {
|
|
241
|
+
return queue.id;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const queueName = typeof queue === "string" ? queue : queue.name;
|
|
245
|
+
|
|
246
|
+
return await withExponentialBackoff(
|
|
247
|
+
async () => {
|
|
248
|
+
const queueId = (await findQueueByName(api, queueName))?.result?.queue_id;
|
|
249
|
+
if (!queueId) {
|
|
250
|
+
throw new Error(`Queue "${queueName}" not found`);
|
|
251
|
+
}
|
|
252
|
+
return queueId;
|
|
253
|
+
},
|
|
254
|
+
(error) => error instanceof Error && error.message.includes("not found"),
|
|
255
|
+
5,
|
|
256
|
+
1000,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Creates an event notification rule for an R2 bucket that sends messages to a Queue.
|
|
262
|
+
*
|
|
263
|
+
* Event notifications allow you to trigger automated workflows when objects are created,
|
|
264
|
+
* modified, or deleted in your R2 bucket. Messages are delivered to a Cloudflare Queue
|
|
265
|
+
* where they can be processed by a consumer Worker.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ## Basic object-create notification
|
|
269
|
+
*
|
|
270
|
+
* Send notifications to a queue when objects are created in a bucket:
|
|
271
|
+
*
|
|
272
|
+
* ```ts
|
|
273
|
+
* import { R2Bucket, Queue, R2BucketNotification } from "alchemy/cloudflare";
|
|
274
|
+
*
|
|
275
|
+
* const bucket = await R2Bucket("uploads");
|
|
276
|
+
* const queue = await Queue("upload-events");
|
|
277
|
+
*
|
|
278
|
+
* await R2BucketNotification("upload-notifications", {
|
|
279
|
+
* bucket,
|
|
280
|
+
* queue,
|
|
281
|
+
* eventTypes: ["object-create"],
|
|
282
|
+
* });
|
|
283
|
+
* ```
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ## Filtered notifications with prefix and suffix
|
|
287
|
+
*
|
|
288
|
+
* Only trigger notifications for specific object patterns:
|
|
289
|
+
*
|
|
290
|
+
* ```ts
|
|
291
|
+
* import { R2Bucket, Queue, R2BucketNotification } from "alchemy/cloudflare";
|
|
292
|
+
*
|
|
293
|
+
* const bucket = await R2Bucket("documents");
|
|
294
|
+
* const queue = await Queue("pdf-processing");
|
|
295
|
+
*
|
|
296
|
+
* await R2BucketNotification("pdf-uploads", {
|
|
297
|
+
* bucket,
|
|
298
|
+
* queue,
|
|
299
|
+
* eventTypes: ["object-create"],
|
|
300
|
+
* prefix: "incoming/",
|
|
301
|
+
* suffix: ".pdf",
|
|
302
|
+
* description: "Process newly uploaded PDF files",
|
|
303
|
+
* });
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ## Multiple suffixes for different file types
|
|
308
|
+
*
|
|
309
|
+
* Create rules for multiple file extensions at once:
|
|
310
|
+
*
|
|
311
|
+
* ```ts
|
|
312
|
+
* import { R2Bucket, Queue, R2BucketNotification } from "alchemy/cloudflare";
|
|
313
|
+
*
|
|
314
|
+
* const bucket = await R2Bucket("media");
|
|
315
|
+
* const queue = await Queue("audio-processing");
|
|
316
|
+
*
|
|
317
|
+
* await R2BucketNotification("audio-uploads", {
|
|
318
|
+
* bucket,
|
|
319
|
+
* queue,
|
|
320
|
+
* eventTypes: ["object-create"],
|
|
321
|
+
* prefix: "audio/",
|
|
322
|
+
* suffix: [".mp3", ".wav", ".flac"],
|
|
323
|
+
* });
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ## Multiple event types with typed queue
|
|
328
|
+
*
|
|
329
|
+
* Listen for both create and delete events with type-safe message handling:
|
|
330
|
+
*
|
|
331
|
+
* ```ts
|
|
332
|
+
* import { R2Bucket, Queue, R2BucketNotification, R2BucketNotificationMessage } from "alchemy/cloudflare";
|
|
333
|
+
*
|
|
334
|
+
* const bucket = await R2Bucket("assets");
|
|
335
|
+
* const queue = await Queue<R2BucketNotificationMessage>("asset-events");
|
|
336
|
+
*
|
|
337
|
+
* await R2BucketNotification("asset-notifications", {
|
|
338
|
+
* bucket,
|
|
339
|
+
* queue,
|
|
340
|
+
* eventTypes: ["object-create", "object-delete"],
|
|
341
|
+
* });
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* ## Process notifications with a Worker
|
|
346
|
+
*
|
|
347
|
+
* Complete example showing bucket notifications processed by a Worker:
|
|
348
|
+
*
|
|
349
|
+
* ```ts
|
|
350
|
+
* import { R2Bucket, Queue, R2BucketNotification, Worker, R2BucketNotificationMessage } from "alchemy/cloudflare";
|
|
351
|
+
*
|
|
352
|
+
* const bucket = await R2Bucket("uploads");
|
|
353
|
+
* const queue = await Queue<R2BucketNotificationMessage>("upload-events");
|
|
354
|
+
*
|
|
355
|
+
* await R2BucketNotification("upload-notifications", {
|
|
356
|
+
* bucket,
|
|
357
|
+
* queue,
|
|
358
|
+
* eventTypes: ["object-create"],
|
|
359
|
+
* });
|
|
360
|
+
*
|
|
361
|
+
* await Worker("processor", {
|
|
362
|
+
* entrypoint: "./src/processor.ts",
|
|
363
|
+
* eventSources: [queue],
|
|
364
|
+
* });
|
|
365
|
+
* ```
|
|
366
|
+
*
|
|
367
|
+
* @see https://developers.cloudflare.com/r2/buckets/event-notifications/
|
|
368
|
+
*/
|
|
369
|
+
export const R2BucketNotification = Resource(
|
|
370
|
+
"cloudflare::R2BucketNotification",
|
|
371
|
+
async function (
|
|
372
|
+
this: Context<R2BucketNotification>,
|
|
373
|
+
_id: string,
|
|
374
|
+
props: R2BucketNotificationProps,
|
|
375
|
+
): Promise<R2BucketNotification> {
|
|
376
|
+
const prefixIsArray = Array.isArray(props.prefix);
|
|
377
|
+
const suffixIsArray = Array.isArray(props.suffix);
|
|
378
|
+
|
|
379
|
+
if (prefixIsArray && suffixIsArray) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
"Cannot specify both prefix and suffix as arrays. Only one can be an array at a time.",
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const bucketName =
|
|
386
|
+
typeof props.bucket === "string" ? props.bucket : props.bucket.name;
|
|
387
|
+
const queueName =
|
|
388
|
+
typeof props.queue === "string" ? props.queue : props.queue.name;
|
|
389
|
+
const jurisdiction =
|
|
390
|
+
typeof props.bucket === "string"
|
|
391
|
+
? props.jurisdiction
|
|
392
|
+
: (props.bucket.jurisdiction ?? props.jurisdiction);
|
|
393
|
+
const adopt = props.adopt ?? this.scope.adopt;
|
|
394
|
+
|
|
395
|
+
const api = await createCloudflareApi(props);
|
|
396
|
+
|
|
397
|
+
const queueId = await resolveQueueId(api, props.queue);
|
|
398
|
+
|
|
399
|
+
const ruleConfigs: Array<{ prefix?: string; suffix?: string }> = [];
|
|
400
|
+
|
|
401
|
+
if (prefixIsArray) {
|
|
402
|
+
const prefixArray = props.prefix as string[];
|
|
403
|
+
const singleSuffix =
|
|
404
|
+
typeof props.suffix === "string" ? props.suffix : undefined;
|
|
405
|
+
for (const prefix of prefixArray) {
|
|
406
|
+
ruleConfigs.push({ prefix, suffix: singleSuffix });
|
|
407
|
+
}
|
|
408
|
+
} else if (suffixIsArray) {
|
|
409
|
+
const suffixArray = props.suffix as string[];
|
|
410
|
+
const singlePrefix =
|
|
411
|
+
typeof props.prefix === "string" ? props.prefix : undefined;
|
|
412
|
+
for (const suffix of suffixArray) {
|
|
413
|
+
ruleConfigs.push({ prefix: singlePrefix, suffix });
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
const singlePrefix =
|
|
417
|
+
typeof props.prefix === "string" ? props.prefix : undefined;
|
|
418
|
+
const singleSuffix =
|
|
419
|
+
typeof props.suffix === "string" ? props.suffix : undefined;
|
|
420
|
+
ruleConfigs.push({ prefix: singlePrefix, suffix: singleSuffix });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (this.scope.local) {
|
|
424
|
+
const mockRuleIds = ruleConfigs.map((_, i) =>
|
|
425
|
+
Array.isArray(this.output?.ruleId)
|
|
426
|
+
? (this.output.ruleId[i] ?? "")
|
|
427
|
+
: (this.output?.ruleId ?? ""),
|
|
428
|
+
);
|
|
429
|
+
return {
|
|
430
|
+
type: "r2_bucket_notification",
|
|
431
|
+
ruleId: ruleConfigs.length === 1 ? mockRuleIds[0] : mockRuleIds,
|
|
432
|
+
bucketName,
|
|
433
|
+
queueName,
|
|
434
|
+
eventTypes: props.eventTypes,
|
|
435
|
+
prefix: props.prefix,
|
|
436
|
+
suffix: props.suffix,
|
|
437
|
+
description: props.description,
|
|
438
|
+
jurisdiction,
|
|
439
|
+
accountId: this.output?.accountId ?? "",
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (this.phase === "delete") {
|
|
444
|
+
if (props.delete !== false && this.output?.ruleId) {
|
|
445
|
+
const ruleIds = Array.isArray(this.output.ruleId)
|
|
446
|
+
? this.output.ruleId
|
|
447
|
+
: [this.output.ruleId];
|
|
448
|
+
await Promise.all(
|
|
449
|
+
ruleIds.map((ruleId) =>
|
|
450
|
+
deleteNotificationRule(api, bucketName, queueId, ruleId, {
|
|
451
|
+
jurisdiction,
|
|
452
|
+
}),
|
|
453
|
+
),
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return this.destroy();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const existingRuleIds: string[] = Array.isArray(this.output?.ruleId)
|
|
460
|
+
? this.output.ruleId
|
|
461
|
+
: this.output?.ruleId
|
|
462
|
+
? [this.output.ruleId]
|
|
463
|
+
: [];
|
|
464
|
+
|
|
465
|
+
if (this.phase === "update" && existingRuleIds.length > 0) {
|
|
466
|
+
const existingRules = await listNotificationRules(api, bucketName, {
|
|
467
|
+
jurisdiction,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
for (const existingRuleId of existingRuleIds) {
|
|
471
|
+
const existingRule = existingRules.find(
|
|
472
|
+
(r) => r.ruleId === existingRuleId,
|
|
473
|
+
);
|
|
474
|
+
if (existingRule) {
|
|
475
|
+
await deleteNotificationRule(
|
|
476
|
+
api,
|
|
477
|
+
bucketName,
|
|
478
|
+
existingRule.queueId,
|
|
479
|
+
existingRuleId,
|
|
480
|
+
{ jurisdiction },
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const createdRuleIds: string[] = [];
|
|
487
|
+
|
|
488
|
+
for (const config of ruleConfigs) {
|
|
489
|
+
try {
|
|
490
|
+
const result = await createNotificationRule(api, bucketName, {
|
|
491
|
+
queueId,
|
|
492
|
+
queueName,
|
|
493
|
+
eventTypes: props.eventTypes,
|
|
494
|
+
prefix: config.prefix,
|
|
495
|
+
suffix: config.suffix,
|
|
496
|
+
description: props.description,
|
|
497
|
+
jurisdiction,
|
|
498
|
+
});
|
|
499
|
+
createdRuleIds.push(result.ruleId);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
const isConflictError =
|
|
502
|
+
err instanceof CloudflareApiError &&
|
|
503
|
+
(err.status === 409 ||
|
|
504
|
+
(err.status === 400 &&
|
|
505
|
+
err.errorData?.some(
|
|
506
|
+
(e: { code: number; message: string }) => e.code === 11020,
|
|
507
|
+
)));
|
|
508
|
+
|
|
509
|
+
if (isConflictError && adopt) {
|
|
510
|
+
const matchingRule = await withExponentialBackoff(
|
|
511
|
+
async () => {
|
|
512
|
+
const existingRules = await listNotificationRules(
|
|
513
|
+
api,
|
|
514
|
+
bucketName,
|
|
515
|
+
{
|
|
516
|
+
jurisdiction,
|
|
517
|
+
},
|
|
518
|
+
);
|
|
519
|
+
const match = existingRules.find((r) =>
|
|
520
|
+
matchesNotificationRule(r, {
|
|
521
|
+
queueId,
|
|
522
|
+
queueName,
|
|
523
|
+
eventTypes: props.eventTypes,
|
|
524
|
+
prefix: config.prefix,
|
|
525
|
+
suffix: config.suffix,
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
if (!match) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`Failed to find existing notification rule for bucket ${bucketName} and queue ${queueName}`,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
return match;
|
|
534
|
+
},
|
|
535
|
+
(error) =>
|
|
536
|
+
error instanceof Error &&
|
|
537
|
+
error.message.includes(
|
|
538
|
+
"Failed to find existing notification rule",
|
|
539
|
+
),
|
|
540
|
+
5,
|
|
541
|
+
1000,
|
|
542
|
+
);
|
|
543
|
+
if (matchingRule) {
|
|
544
|
+
createdRuleIds.push(matchingRule.ruleId);
|
|
545
|
+
} else {
|
|
546
|
+
throw err;
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
throw err;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
type: "r2_bucket_notification",
|
|
556
|
+
ruleId: createdRuleIds.length === 1 ? createdRuleIds[0] : createdRuleIds,
|
|
557
|
+
bucketName,
|
|
558
|
+
queueName,
|
|
559
|
+
eventTypes: props.eventTypes,
|
|
560
|
+
prefix: props.prefix,
|
|
561
|
+
suffix: props.suffix,
|
|
562
|
+
description: props.description,
|
|
563
|
+
jurisdiction,
|
|
564
|
+
accountId: api.accountId,
|
|
565
|
+
};
|
|
566
|
+
},
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
interface NotificationRuleInfo {
|
|
570
|
+
ruleId: string;
|
|
571
|
+
queueId: string;
|
|
572
|
+
queueName: string;
|
|
573
|
+
actions: string[];
|
|
574
|
+
prefix: string;
|
|
575
|
+
suffix: string;
|
|
576
|
+
createdAt?: string;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
interface CreateNotificationRuleOptions {
|
|
580
|
+
queueId: string;
|
|
581
|
+
queueName: string;
|
|
582
|
+
eventTypes: R2BucketNotificationEventType[];
|
|
583
|
+
prefix?: string;
|
|
584
|
+
suffix?: string;
|
|
585
|
+
description?: string;
|
|
586
|
+
jurisdiction?: string;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function mapEventTypesToActions(
|
|
590
|
+
eventTypes: R2BucketNotificationEventType[],
|
|
591
|
+
): string[] {
|
|
592
|
+
const actions: string[] = [];
|
|
593
|
+
for (const eventType of eventTypes) {
|
|
594
|
+
if (eventType === "object-create") {
|
|
595
|
+
actions.push("PutObject", "CompleteMultipartUpload", "CopyObject");
|
|
596
|
+
} else if (eventType === "object-delete") {
|
|
597
|
+
actions.push("DeleteObject", "LifecycleDeletion");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return [...new Set(actions)].sort();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function arraysEqual(a: string[], b: string[]): boolean {
|
|
604
|
+
const sortedA = [...a].sort();
|
|
605
|
+
const sortedB = [...b].sort();
|
|
606
|
+
return (
|
|
607
|
+
sortedA.length === sortedB.length &&
|
|
608
|
+
sortedA.every((v, i) => v === sortedB[i])
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function normalizeRuleFilter(value?: string): string {
|
|
613
|
+
return value ?? "";
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function normalizeQueueId(queueId: string): string {
|
|
617
|
+
return queueId.replaceAll("-", "");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function matchesNotificationRule(
|
|
621
|
+
rule: NotificationRuleInfo,
|
|
622
|
+
options: {
|
|
623
|
+
queueId: string;
|
|
624
|
+
queueName?: string;
|
|
625
|
+
eventTypes: R2BucketNotificationEventType[];
|
|
626
|
+
prefix?: string;
|
|
627
|
+
suffix?: string;
|
|
628
|
+
},
|
|
629
|
+
): boolean {
|
|
630
|
+
return (
|
|
631
|
+
(normalizeQueueId(rule.queueId) === normalizeQueueId(options.queueId) ||
|
|
632
|
+
(options.queueName !== undefined &&
|
|
633
|
+
rule.queueName === options.queueName)) &&
|
|
634
|
+
arraysEqual(rule.actions, mapEventTypesToActions(options.eventTypes)) &&
|
|
635
|
+
rule.prefix === normalizeRuleFilter(options.prefix) &&
|
|
636
|
+
rule.suffix === normalizeRuleFilter(options.suffix)
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function createNotificationRule(
|
|
641
|
+
api: CloudflareApi,
|
|
642
|
+
bucketName: string,
|
|
643
|
+
options: CreateNotificationRuleOptions,
|
|
644
|
+
): Promise<{ ruleId: string }> {
|
|
645
|
+
const payload: any = {
|
|
646
|
+
rules: [
|
|
647
|
+
{
|
|
648
|
+
actions: mapEventTypesToActions(options.eventTypes),
|
|
649
|
+
prefix: options.prefix ?? "",
|
|
650
|
+
suffix: options.suffix ?? "",
|
|
651
|
+
},
|
|
652
|
+
],
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const response = await api.put(
|
|
656
|
+
`/accounts/${api.accountId}/event_notifications/r2/${bucketName}/configuration/queues/${options.queueId}`,
|
|
657
|
+
payload,
|
|
658
|
+
{
|
|
659
|
+
headers: withJurisdiction({ jurisdiction: options.jurisdiction }),
|
|
660
|
+
},
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
if (!response.ok) {
|
|
664
|
+
return await handleApiError(
|
|
665
|
+
response,
|
|
666
|
+
"creating",
|
|
667
|
+
"R2 Bucket Notification",
|
|
668
|
+
`for bucket ${bucketName}`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const data = (await response.json()) as {
|
|
673
|
+
success: boolean;
|
|
674
|
+
result?: { event_notification_detail_id?: string };
|
|
675
|
+
errors?: Array<{ code: number; message: string }>;
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
if (data.result?.event_notification_detail_id) {
|
|
679
|
+
return { ruleId: data.result.event_notification_detail_id };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const createdRule = await withExponentialBackoff(
|
|
683
|
+
async () => {
|
|
684
|
+
const rules = await listNotificationRules(api, bucketName, {
|
|
685
|
+
jurisdiction: options.jurisdiction,
|
|
686
|
+
});
|
|
687
|
+
const match = rules.find((r) => matchesNotificationRule(r, options));
|
|
688
|
+
if (!match) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`Failed to find created notification rule for bucket ${bucketName} and queue ${options.queueName}`,
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return match;
|
|
694
|
+
},
|
|
695
|
+
(error) =>
|
|
696
|
+
error instanceof Error && error.message.includes("Failed to find"),
|
|
697
|
+
5,
|
|
698
|
+
1000,
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
if (!createdRule) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
`Failed to find created notification rule for bucket ${bucketName} and queue ${options.queueName}`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return { ruleId: createdRule.ruleId };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function listNotificationRules(
|
|
711
|
+
api: CloudflareApi,
|
|
712
|
+
bucketName: string,
|
|
713
|
+
options: { jurisdiction?: string } = {},
|
|
714
|
+
): Promise<NotificationRuleInfo[]> {
|
|
715
|
+
const response = await api.get(
|
|
716
|
+
`/accounts/${api.accountId}/event_notifications/r2/${bucketName}/configuration`,
|
|
717
|
+
{
|
|
718
|
+
headers: withJurisdiction(options),
|
|
719
|
+
},
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
if (!response.ok) {
|
|
723
|
+
if (response.status === 404) {
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
return await handleApiError(
|
|
727
|
+
response,
|
|
728
|
+
"listing",
|
|
729
|
+
"R2 Bucket Notifications",
|
|
730
|
+
`for bucket ${bucketName}`,
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const data = (await response.json()) as {
|
|
735
|
+
success: boolean;
|
|
736
|
+
result?: {
|
|
737
|
+
bucketName?: string;
|
|
738
|
+
queues?: Array<{
|
|
739
|
+
queueId: string;
|
|
740
|
+
queueName: string;
|
|
741
|
+
rules: Array<{
|
|
742
|
+
ruleId: string;
|
|
743
|
+
prefix: string;
|
|
744
|
+
suffix: string;
|
|
745
|
+
actions: string[];
|
|
746
|
+
createdAt?: string;
|
|
747
|
+
}>;
|
|
748
|
+
}>;
|
|
749
|
+
};
|
|
750
|
+
errors?: Array<{ code: number; message: string }>;
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
if (!data.success || !data.result?.queues) {
|
|
754
|
+
return [];
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const rules: NotificationRuleInfo[] = [];
|
|
758
|
+
for (const queue of data.result.queues) {
|
|
759
|
+
for (const rule of queue.rules) {
|
|
760
|
+
rules.push({
|
|
761
|
+
ruleId: rule.ruleId,
|
|
762
|
+
queueId: queue.queueId,
|
|
763
|
+
queueName: queue.queueName,
|
|
764
|
+
actions: rule.actions,
|
|
765
|
+
prefix: rule.prefix,
|
|
766
|
+
suffix: rule.suffix,
|
|
767
|
+
createdAt: rule.createdAt,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return rules;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async function deleteNotificationRule(
|
|
776
|
+
api: CloudflareApi,
|
|
777
|
+
bucketName: string,
|
|
778
|
+
queueId: string,
|
|
779
|
+
ruleId: string,
|
|
780
|
+
options: { jurisdiction?: string } = {},
|
|
781
|
+
): Promise<void> {
|
|
782
|
+
const response = await api.delete(
|
|
783
|
+
`/accounts/${api.accountId}/event_notifications/r2/${bucketName}/configuration/queues/${queueId}`,
|
|
784
|
+
{
|
|
785
|
+
headers: withJurisdiction(options),
|
|
786
|
+
body: JSON.stringify({ ruleIds: [ruleId] }),
|
|
787
|
+
},
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
if (!response.ok && response.status !== 404) {
|
|
791
|
+
await handleApiError(
|
|
792
|
+
response,
|
|
793
|
+
"deleting",
|
|
794
|
+
"R2 Bucket Notification",
|
|
795
|
+
`rule ${ruleId} for bucket ${bucketName}`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* List all notification rules for an R2 bucket
|
|
802
|
+
*/
|
|
803
|
+
export async function listR2BucketNotifications(
|
|
804
|
+
api: CloudflareApi,
|
|
805
|
+
bucketName: string,
|
|
806
|
+
options: { jurisdiction?: string } = {},
|
|
807
|
+
): Promise<NotificationRuleInfo[]> {
|
|
808
|
+
return listNotificationRules(api, bucketName, options);
|
|
809
|
+
}
|