alchemy 0.7.1 → 0.7.2
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/bindings.d.ts +14 -2
- package/lib/cloudflare/bound.d.ts +3 -1
- package/lib/cloudflare/bucket.js +1 -0
- package/lib/cloudflare/d1-database.d.ts +18 -1
- package/lib/cloudflare/d1-database.js +42 -3
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/pipeline.d.ts +335 -0
- package/lib/cloudflare/pipeline.js +250 -0
- package/lib/cloudflare/worker.d.ts +9 -0
- package/lib/cloudflare/worker.js +9 -0
- package/package.json +1 -1
- package/src/cloudflare/bindings.ts +15 -0
- package/src/cloudflare/bound.ts +5 -1
- package/src/cloudflare/bucket.ts +1 -0
- package/src/cloudflare/d1-database.ts +61 -3
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/pipeline.ts +642 -0
- package/src/cloudflare/worker.ts +21 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import type { Context } from "../context";
|
|
2
|
+
import { Resource } from "../resource";
|
|
3
|
+
import type { Secret } from "../secret";
|
|
4
|
+
import {
|
|
5
|
+
CloudflareApi,
|
|
6
|
+
createCloudflareApi,
|
|
7
|
+
type CloudflareApiOptions,
|
|
8
|
+
} from "./api";
|
|
9
|
+
import { CloudflareApiError, handleApiError } from "./api-error";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Settings for compression of pipeline output
|
|
13
|
+
*/
|
|
14
|
+
export interface CompressionSettings {
|
|
15
|
+
/**
|
|
16
|
+
* Type of compression to use for pipeline output
|
|
17
|
+
* @default "gzip"
|
|
18
|
+
*/
|
|
19
|
+
type: "gzip" | "none";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Settings for batching behavior of pipeline output
|
|
24
|
+
*/
|
|
25
|
+
export interface BatchSettings {
|
|
26
|
+
/**
|
|
27
|
+
* Maximum size of batch in megabytes before delivery (1-100 MB)
|
|
28
|
+
* @default 100
|
|
29
|
+
*/
|
|
30
|
+
maxMb?: number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Maximum number of rows in a batch before delivery (1-10,000,000 rows)
|
|
34
|
+
* @default 10000000
|
|
35
|
+
*/
|
|
36
|
+
maxRows?: number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Maximum duration of a batch in seconds before delivery (1-300 seconds)
|
|
40
|
+
* @default 300
|
|
41
|
+
*/
|
|
42
|
+
maxSeconds?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Configuration for a pipeline HTTP source
|
|
47
|
+
*/
|
|
48
|
+
export interface HttpSource {
|
|
49
|
+
/**
|
|
50
|
+
* Format of the source data
|
|
51
|
+
* @default "json"
|
|
52
|
+
*/
|
|
53
|
+
format: "json";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Type of source
|
|
57
|
+
*/
|
|
58
|
+
type: "http";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether authentication is required
|
|
62
|
+
* @default true
|
|
63
|
+
*/
|
|
64
|
+
authentication?: boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* CORS configuration for HTTP endpoint source
|
|
68
|
+
*/
|
|
69
|
+
cors?: {
|
|
70
|
+
/**
|
|
71
|
+
* Allowed origins for CORS requests
|
|
72
|
+
* @default ["*"]
|
|
73
|
+
*/
|
|
74
|
+
origins: string[];
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Configuration for a pipeline binding source
|
|
80
|
+
*/
|
|
81
|
+
export interface BindingSource {
|
|
82
|
+
/**
|
|
83
|
+
* Format of the source data
|
|
84
|
+
* @default "json"
|
|
85
|
+
*/
|
|
86
|
+
format: "json";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Type of source
|
|
90
|
+
*/
|
|
91
|
+
type: "binding";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Configuration for a pipeline source
|
|
96
|
+
*/
|
|
97
|
+
export type PipelineSource = HttpSource | BindingSource;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Configuration for an R2 destination
|
|
101
|
+
*/
|
|
102
|
+
export interface R2DestinationConfig {
|
|
103
|
+
/**
|
|
104
|
+
* Type of destination (R2)
|
|
105
|
+
*/
|
|
106
|
+
type: "r2";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Format of the output data
|
|
110
|
+
* @default "json"
|
|
111
|
+
*/
|
|
112
|
+
format: "json" | "ndjson";
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Path configuration for the R2 destination
|
|
116
|
+
*/
|
|
117
|
+
path: {
|
|
118
|
+
/**
|
|
119
|
+
* R2 bucket name
|
|
120
|
+
*/
|
|
121
|
+
bucket: string;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Optional prefix for files in the bucket
|
|
125
|
+
*/
|
|
126
|
+
prefix?: string;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Optional filename pattern
|
|
130
|
+
* @default "${slug}${extension}"
|
|
131
|
+
*/
|
|
132
|
+
filename?: string;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Optional filepath pattern
|
|
136
|
+
* @default "${date}/${hour}"
|
|
137
|
+
*/
|
|
138
|
+
filepath?: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compression settings
|
|
143
|
+
*/
|
|
144
|
+
compression?: CompressionSettings;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Batch settings
|
|
148
|
+
*/
|
|
149
|
+
batch?: BatchSettings;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Credentials for the R2 bucket
|
|
153
|
+
* Required for R2 destinations
|
|
154
|
+
*/
|
|
155
|
+
credentials: {
|
|
156
|
+
/**
|
|
157
|
+
* Access key ID for the R2 bucket
|
|
158
|
+
*/
|
|
159
|
+
accessKeyId: Secret;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Secret access key for the R2 bucket
|
|
163
|
+
*/
|
|
164
|
+
secretAccessKey: Secret;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Endpoint for the R2 bucket
|
|
168
|
+
*/
|
|
169
|
+
endpoint?: string;
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Allowed destination types
|
|
175
|
+
*/
|
|
176
|
+
export type PipelineDestination = R2DestinationConfig;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Properties for creating or updating a Pipeline
|
|
180
|
+
*/
|
|
181
|
+
export interface PipelineProps extends CloudflareApiOptions {
|
|
182
|
+
/**
|
|
183
|
+
* Name of the pipeline
|
|
184
|
+
*/
|
|
185
|
+
name: string;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Source configuration for the pipeline
|
|
189
|
+
*/
|
|
190
|
+
source: PipelineSource[];
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Destination configuration for the pipeline
|
|
194
|
+
*/
|
|
195
|
+
destination: PipelineDestination;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Compression settings for the pipeline
|
|
199
|
+
* @default { type: "gzip" }
|
|
200
|
+
*/
|
|
201
|
+
compression?: CompressionSettings;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whether to delete the pipeline.
|
|
205
|
+
* If set to false, the pipeline will remain but the resource will be removed from state
|
|
206
|
+
*
|
|
207
|
+
* @default true
|
|
208
|
+
*/
|
|
209
|
+
delete?: boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Base type for pipeline records
|
|
214
|
+
*/
|
|
215
|
+
export interface PipelineRecord {
|
|
216
|
+
[key: string]: any;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Output returned after Pipeline creation/update
|
|
221
|
+
*/
|
|
222
|
+
export interface Pipeline<T extends PipelineRecord = PipelineRecord>
|
|
223
|
+
extends Resource<"cloudflare::Pipeline">,
|
|
224
|
+
PipelineProps {
|
|
225
|
+
/**
|
|
226
|
+
* Type identifier for the Pipeline resource
|
|
227
|
+
*/
|
|
228
|
+
type: "pipeline";
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The unique ID of the pipeline
|
|
232
|
+
*/
|
|
233
|
+
id: string;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* HTTP endpoint URL for the pipeline
|
|
237
|
+
*/
|
|
238
|
+
endpoint: string;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Version of the pipeline
|
|
242
|
+
*/
|
|
243
|
+
version: number;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Creates and manages Cloudflare Pipelines.
|
|
248
|
+
*
|
|
249
|
+
* Pipelines provide a managed data pipeline service that lets you collect, transform,
|
|
250
|
+
* and route data to various destinations like R2 buckets.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* // Create a basic pipeline with an R2 bucket destination
|
|
254
|
+
* const bucket = await R2Bucket("logs-bucket", {
|
|
255
|
+
* name: "logs-bucket"
|
|
256
|
+
* });
|
|
257
|
+
*
|
|
258
|
+
* const accessKey = alchemy.secret(process.env.R2_ACCESS_KEY_ID!);
|
|
259
|
+
* const secretKey = alchemy.secret(process.env.R2_SECRET_ACCESS_KEY!);
|
|
260
|
+
*
|
|
261
|
+
* const pipeline = await Pipeline("logs-pipeline", {
|
|
262
|
+
* name: "logs-pipeline",
|
|
263
|
+
* destination: {
|
|
264
|
+
* type: "r2",
|
|
265
|
+
* format: "json",
|
|
266
|
+
* path: {
|
|
267
|
+
* bucket: bucket.name,
|
|
268
|
+
* prefix: "app-logs",
|
|
269
|
+
* },
|
|
270
|
+
* credentials: {
|
|
271
|
+
* accessKeyId: accessKey,
|
|
272
|
+
* secretAccessKey: secretKey
|
|
273
|
+
* }
|
|
274
|
+
* },
|
|
275
|
+
* batch: {
|
|
276
|
+
* maxMb: 50,
|
|
277
|
+
* maxSeconds: 60
|
|
278
|
+
* }
|
|
279
|
+
* });
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* // Create a pipeline with custom source configuration
|
|
283
|
+
* const customPipeline = await Pipeline("custom-pipeline", {
|
|
284
|
+
* name: "custom-pipeline",
|
|
285
|
+
* source: [{
|
|
286
|
+
* type: "http",
|
|
287
|
+
* format: "json",
|
|
288
|
+
* authentication: true,
|
|
289
|
+
* cors: {
|
|
290
|
+
* origins: ["https://example.com"]
|
|
291
|
+
* }
|
|
292
|
+
* }],
|
|
293
|
+
* destination: {
|
|
294
|
+
* type: "r2",
|
|
295
|
+
* format: "json",
|
|
296
|
+
* path: {
|
|
297
|
+
* bucket: "my-bucket",
|
|
298
|
+
* prefix: "data"
|
|
299
|
+
* },
|
|
300
|
+
* credentials: {
|
|
301
|
+
* accessKeyId: alchemy.secret(process.env.R2_ACCESS_KEY_ID!),
|
|
302
|
+
* secretAccessKey: alchemy.secret(process.env.R2_SECRET_ACCESS_KEY!)
|
|
303
|
+
* },
|
|
304
|
+
* compression: {
|
|
305
|
+
* type: "gzip"
|
|
306
|
+
* }
|
|
307
|
+
* }
|
|
308
|
+
* });
|
|
309
|
+
*
|
|
310
|
+
* @see https://developers.cloudflare.com/pipelines/
|
|
311
|
+
*/
|
|
312
|
+
export const Pipeline = Resource("cloudflare::Pipeline", async function <
|
|
313
|
+
T extends PipelineRecord = PipelineRecord,
|
|
314
|
+
>(this: Context<Pipeline<T>>, id: string, props: PipelineProps): Promise<
|
|
315
|
+
Pipeline<T>
|
|
316
|
+
> {
|
|
317
|
+
const api = await createCloudflareApi(props);
|
|
318
|
+
const pipelineName = props.name || id;
|
|
319
|
+
|
|
320
|
+
if (this.phase === "delete") {
|
|
321
|
+
console.log("Deleting Cloudflare Pipeline:", pipelineName);
|
|
322
|
+
if (props.delete !== false) {
|
|
323
|
+
// Delete Pipeline
|
|
324
|
+
await deletePipeline(api, pipelineName);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Return void (a deleted pipeline has no content)
|
|
328
|
+
return this.destroy();
|
|
329
|
+
} else {
|
|
330
|
+
let pipelineData: CloudflarePipelineResponse;
|
|
331
|
+
|
|
332
|
+
if (this.phase === "create") {
|
|
333
|
+
console.log("Creating Cloudflare Pipeline:", pipelineName);
|
|
334
|
+
pipelineData = await createPipeline(api, pipelineName, props);
|
|
335
|
+
} else {
|
|
336
|
+
// Update operation
|
|
337
|
+
if (this.output?.id) {
|
|
338
|
+
console.log("Updating Cloudflare Pipeline:", pipelineName);
|
|
339
|
+
|
|
340
|
+
// Check if name is being changed, which is not allowed
|
|
341
|
+
if (props.name !== this.output.name) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
"Cannot update Pipeline name after creation. Pipeline name is immutable."
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Update the pipeline with new settings
|
|
348
|
+
pipelineData = await updatePipeline(api, pipelineName, props);
|
|
349
|
+
} else {
|
|
350
|
+
// If no ID exists, fall back to creating a new pipeline
|
|
351
|
+
console.log(
|
|
352
|
+
"No existing Pipeline ID found, creating new Cloudflare Pipeline:",
|
|
353
|
+
pipelineName
|
|
354
|
+
);
|
|
355
|
+
pipelineData = await createPipeline(api, pipelineName, props);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return this({
|
|
360
|
+
type: "pipeline",
|
|
361
|
+
id: pipelineData.result.id,
|
|
362
|
+
name: pipelineName,
|
|
363
|
+
endpoint: pipelineData.result.endpoint,
|
|
364
|
+
version: pipelineData.result.version,
|
|
365
|
+
source: pipelineData.result.source!.map((s) => ({
|
|
366
|
+
type: s.type as "http" | "binding",
|
|
367
|
+
format: s.format as "json",
|
|
368
|
+
authentication: s.authentication,
|
|
369
|
+
cors: s.cors,
|
|
370
|
+
})),
|
|
371
|
+
destination: props.destination, // Use the input destination, not the API response
|
|
372
|
+
compression: props.compression,
|
|
373
|
+
accountId: api.accountId,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
interface CloudflarePipelineResponse {
|
|
379
|
+
result: {
|
|
380
|
+
id: string;
|
|
381
|
+
name: string;
|
|
382
|
+
endpoint: string;
|
|
383
|
+
version: number;
|
|
384
|
+
source: Array<{
|
|
385
|
+
type: "http" | "binding";
|
|
386
|
+
format: string;
|
|
387
|
+
authentication?: boolean;
|
|
388
|
+
cors?: {
|
|
389
|
+
origins: string[];
|
|
390
|
+
};
|
|
391
|
+
}>;
|
|
392
|
+
destination: {
|
|
393
|
+
type: string;
|
|
394
|
+
format: string;
|
|
395
|
+
path?: {
|
|
396
|
+
bucket: string;
|
|
397
|
+
prefix?: string;
|
|
398
|
+
filename?: string;
|
|
399
|
+
filepath?: string;
|
|
400
|
+
};
|
|
401
|
+
compression?: {
|
|
402
|
+
type: string;
|
|
403
|
+
};
|
|
404
|
+
batch: {
|
|
405
|
+
max_bytes?: number;
|
|
406
|
+
max_rows?: number;
|
|
407
|
+
max_duration_s?: number;
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
success: boolean;
|
|
412
|
+
errors: Array<{ code: number; message: string }>;
|
|
413
|
+
messages: string[];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Get a pipeline
|
|
418
|
+
*/
|
|
419
|
+
export async function getPipeline(
|
|
420
|
+
api: CloudflareApi,
|
|
421
|
+
pipelineName: string
|
|
422
|
+
): Promise<CloudflarePipelineResponse> {
|
|
423
|
+
const response = await api.get(
|
|
424
|
+
`/accounts/${api.accountId}/pipelines/${pipelineName}`
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
if (!response.ok) {
|
|
428
|
+
return await handleApiError(response, "getting", "Pipeline", pipelineName);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return (await response.json()) as CloudflarePipelineResponse;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Delete a pipeline
|
|
436
|
+
*/
|
|
437
|
+
export async function deletePipeline(
|
|
438
|
+
api: CloudflareApi,
|
|
439
|
+
pipelineName: string
|
|
440
|
+
): Promise<void> {
|
|
441
|
+
// Delete Pipeline
|
|
442
|
+
const deleteResponse = await api.delete(
|
|
443
|
+
`/accounts/${api.accountId}/pipelines/${pipelineName}`
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
if (!deleteResponse.ok && deleteResponse.status !== 404) {
|
|
447
|
+
const errorData: any = await deleteResponse.json().catch(() => ({
|
|
448
|
+
errors: [{ message: deleteResponse.statusText }],
|
|
449
|
+
}));
|
|
450
|
+
throw new CloudflareApiError(
|
|
451
|
+
`Error deleting Cloudflare Pipeline '${pipelineName}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
|
|
452
|
+
deleteResponse
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Create a new pipeline
|
|
459
|
+
*/
|
|
460
|
+
export async function createPipeline(
|
|
461
|
+
api: CloudflareApi,
|
|
462
|
+
pipelineName: string,
|
|
463
|
+
props: PipelineProps
|
|
464
|
+
): Promise<CloudflarePipelineResponse> {
|
|
465
|
+
// Prepare the create payload
|
|
466
|
+
const createPayload = preparePipelinePayload(api, pipelineName, props);
|
|
467
|
+
|
|
468
|
+
const createResponse = await api.post(
|
|
469
|
+
`/accounts/${api.accountId}/pipelines`,
|
|
470
|
+
createPayload
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
if (!createResponse.ok) {
|
|
474
|
+
return await handleApiError(
|
|
475
|
+
createResponse,
|
|
476
|
+
"creating",
|
|
477
|
+
"Pipeline",
|
|
478
|
+
pipelineName
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return (await createResponse.json()) as CloudflarePipelineResponse;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Update a pipeline
|
|
487
|
+
*/
|
|
488
|
+
export async function updatePipeline(
|
|
489
|
+
api: CloudflareApi,
|
|
490
|
+
pipelineName: string,
|
|
491
|
+
props: PipelineProps
|
|
492
|
+
): Promise<CloudflarePipelineResponse> {
|
|
493
|
+
// Get current pipeline to build update payload
|
|
494
|
+
const currentPipeline = await getPipeline(api, pipelineName);
|
|
495
|
+
|
|
496
|
+
// Prepare the update payload
|
|
497
|
+
const updatePayload = preparePipelinePayload(
|
|
498
|
+
api,
|
|
499
|
+
pipelineName,
|
|
500
|
+
props,
|
|
501
|
+
currentPipeline
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const updateResponse = await api.put(
|
|
505
|
+
`/accounts/${api.accountId}/pipelines/${pipelineName}`,
|
|
506
|
+
updatePayload
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
if (!updateResponse.ok) {
|
|
510
|
+
return await handleApiError(
|
|
511
|
+
updateResponse,
|
|
512
|
+
"updating",
|
|
513
|
+
"Pipeline",
|
|
514
|
+
pipelineName
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return (await updateResponse.json()) as CloudflarePipelineResponse;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Helper function to prepare pipeline payload for create/update operations
|
|
523
|
+
*/
|
|
524
|
+
function preparePipelinePayload(
|
|
525
|
+
api: CloudflareApi,
|
|
526
|
+
pipelineName: string,
|
|
527
|
+
props: PipelineProps,
|
|
528
|
+
currentPipeline?: CloudflarePipelineResponse
|
|
529
|
+
): any {
|
|
530
|
+
// Prepare the payload with name and source
|
|
531
|
+
const payload: any = {
|
|
532
|
+
name: pipelineName,
|
|
533
|
+
source: props.source ||
|
|
534
|
+
currentPipeline?.result.source || [
|
|
535
|
+
{
|
|
536
|
+
type: "http",
|
|
537
|
+
format: "json",
|
|
538
|
+
authentication: true,
|
|
539
|
+
cors: { origins: ["*"] },
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// Handle destination
|
|
545
|
+
if (props.destination) {
|
|
546
|
+
payload.destination = { ...props.destination };
|
|
547
|
+
|
|
548
|
+
// Handle special formatting for R2 destination
|
|
549
|
+
const r2Dest = props.destination as R2DestinationConfig;
|
|
550
|
+
|
|
551
|
+
// Format credentials for API
|
|
552
|
+
if (payload.destination.credentials) {
|
|
553
|
+
payload.destination.credentials = {
|
|
554
|
+
access_key_id: r2Dest.credentials.accessKeyId.unencrypted,
|
|
555
|
+
secret_access_key: r2Dest.credentials.secretAccessKey.unencrypted,
|
|
556
|
+
endpoint:
|
|
557
|
+
r2Dest.credentials.endpoint ??
|
|
558
|
+
`https://${api.accountId}.r2.cloudflarestorage.com`,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Format batch settings
|
|
563
|
+
payload.destination.batch = convertBatchSettings(payload.destination.batch);
|
|
564
|
+
} else if (currentPipeline?.result.destination) {
|
|
565
|
+
payload.destination = currentPipeline.result.destination;
|
|
566
|
+
} else if (!props.destination && !currentPipeline) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
"An R2 destination is required for creating/updating a pipeline"
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Add compression if not specified
|
|
573
|
+
if (!payload.destination.compression) {
|
|
574
|
+
payload.destination.compression = { type: "gzip" };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return payload;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* List all pipelines in an account
|
|
582
|
+
*/
|
|
583
|
+
export async function listPipelines(
|
|
584
|
+
api: CloudflareApi
|
|
585
|
+
): Promise<{ name: string; id: string }[]> {
|
|
586
|
+
const response = await api.get(`/accounts/${api.accountId}/pipelines`);
|
|
587
|
+
|
|
588
|
+
if (!response.ok) {
|
|
589
|
+
throw new CloudflareApiError(
|
|
590
|
+
`Failed to list pipelines: ${response.statusText}`,
|
|
591
|
+
response
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const data = (await response.json()) as {
|
|
596
|
+
success: boolean;
|
|
597
|
+
errors?: Array<{ code: number; message: string }>;
|
|
598
|
+
results?: Array<{
|
|
599
|
+
name: string;
|
|
600
|
+
id: string;
|
|
601
|
+
}>;
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
if (!data.success) {
|
|
605
|
+
const errorMessage = data.errors?.[0]?.message || "Unknown error";
|
|
606
|
+
throw new Error(`Failed to list pipelines: ${errorMessage}`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Transform API response
|
|
610
|
+
return (data.results || []).map((pipeline) => ({
|
|
611
|
+
name: pipeline.name,
|
|
612
|
+
id: pipeline.id,
|
|
613
|
+
}));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Helper function to convert batch settings to the format expected by the API
|
|
618
|
+
*/
|
|
619
|
+
interface CloudflareBatchSettings {
|
|
620
|
+
max_bytes?: number;
|
|
621
|
+
max_rows?: number;
|
|
622
|
+
max_duration_s?: number;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function convertBatchSettings(batch?: BatchSettings): CloudflareBatchSettings {
|
|
626
|
+
const result: CloudflareBatchSettings = {};
|
|
627
|
+
|
|
628
|
+
if (batch?.maxMb !== undefined) {
|
|
629
|
+
// Convert MB to bytes
|
|
630
|
+
result.max_bytes = batch.maxMb * 1024 * 1024;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (batch?.maxRows !== undefined) {
|
|
634
|
+
result.max_rows = batch.maxRows;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (batch?.maxSeconds !== undefined) {
|
|
638
|
+
result.max_duration_s = batch.maxSeconds;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return result;
|
|
642
|
+
}
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -99,6 +99,17 @@ export interface WorkerProps<B extends Bindings = Bindings>
|
|
|
99
99
|
* Whether to adopt the Worker if it already exists when creating
|
|
100
100
|
*/
|
|
101
101
|
adopt?: boolean;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The compatibility date for the worker
|
|
105
|
+
* @default "2024-09-09"
|
|
106
|
+
*/
|
|
107
|
+
compatibilityDate?: string;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The compatibility flags for the worker
|
|
111
|
+
*/
|
|
112
|
+
compatibilityFlags?: string[];
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
/**
|
|
@@ -456,6 +467,8 @@ class NotFoundError extends Error {
|
|
|
456
467
|
}
|
|
457
468
|
|
|
458
469
|
interface WorkerMetadata {
|
|
470
|
+
compatibility_date: string;
|
|
471
|
+
compatibility_flags?: string[];
|
|
459
472
|
bindings: WorkerBindingSpec[];
|
|
460
473
|
observability: {
|
|
461
474
|
enabled: boolean;
|
|
@@ -490,6 +503,8 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
490
503
|
): Promise<WorkerMetadata> {
|
|
491
504
|
// Prepare metadata with bindings
|
|
492
505
|
const meta: WorkerMetadata = {
|
|
506
|
+
compatibility_date: props.compatibilityDate ?? "2024-09-09",
|
|
507
|
+
compatibility_flags: props.compatibilityFlags,
|
|
493
508
|
bindings: [],
|
|
494
509
|
observability: {
|
|
495
510
|
enabled: props.observability?.enabled !== false,
|
|
@@ -590,6 +605,12 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
590
605
|
name: bindingName,
|
|
591
606
|
queue_name: binding.name,
|
|
592
607
|
});
|
|
608
|
+
} else if (binding.type === "pipeline") {
|
|
609
|
+
meta.bindings.push({
|
|
610
|
+
type: "pipelines",
|
|
611
|
+
name: bindingName,
|
|
612
|
+
pipeline: binding.name,
|
|
613
|
+
});
|
|
593
614
|
} else {
|
|
594
615
|
// @ts-expect-error - we should never reach here
|
|
595
616
|
throw new Error(`Unsupported binding type: ${binding.type}`);
|