alchemy 0.7.0 → 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.
@@ -0,0 +1,335 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ import type { Secret } from "../secret";
4
+ import { CloudflareApi, type CloudflareApiOptions } from "./api";
5
+ /**
6
+ * Settings for compression of pipeline output
7
+ */
8
+ export interface CompressionSettings {
9
+ /**
10
+ * Type of compression to use for pipeline output
11
+ * @default "gzip"
12
+ */
13
+ type: "gzip" | "none";
14
+ }
15
+ /**
16
+ * Settings for batching behavior of pipeline output
17
+ */
18
+ export interface BatchSettings {
19
+ /**
20
+ * Maximum size of batch in megabytes before delivery (1-100 MB)
21
+ * @default 100
22
+ */
23
+ maxMb?: number;
24
+ /**
25
+ * Maximum number of rows in a batch before delivery (1-10,000,000 rows)
26
+ * @default 10000000
27
+ */
28
+ maxRows?: number;
29
+ /**
30
+ * Maximum duration of a batch in seconds before delivery (1-300 seconds)
31
+ * @default 300
32
+ */
33
+ maxSeconds?: number;
34
+ }
35
+ /**
36
+ * Configuration for a pipeline HTTP source
37
+ */
38
+ export interface HttpSource {
39
+ /**
40
+ * Format of the source data
41
+ * @default "json"
42
+ */
43
+ format: "json";
44
+ /**
45
+ * Type of source
46
+ */
47
+ type: "http";
48
+ /**
49
+ * Whether authentication is required
50
+ * @default true
51
+ */
52
+ authentication?: boolean;
53
+ /**
54
+ * CORS configuration for HTTP endpoint source
55
+ */
56
+ cors?: {
57
+ /**
58
+ * Allowed origins for CORS requests
59
+ * @default ["*"]
60
+ */
61
+ origins: string[];
62
+ };
63
+ }
64
+ /**
65
+ * Configuration for a pipeline binding source
66
+ */
67
+ export interface BindingSource {
68
+ /**
69
+ * Format of the source data
70
+ * @default "json"
71
+ */
72
+ format: "json";
73
+ /**
74
+ * Type of source
75
+ */
76
+ type: "binding";
77
+ }
78
+ /**
79
+ * Configuration for a pipeline source
80
+ */
81
+ export type PipelineSource = HttpSource | BindingSource;
82
+ /**
83
+ * Configuration for an R2 destination
84
+ */
85
+ export interface R2DestinationConfig {
86
+ /**
87
+ * Type of destination (R2)
88
+ */
89
+ type: "r2";
90
+ /**
91
+ * Format of the output data
92
+ * @default "json"
93
+ */
94
+ format: "json" | "ndjson";
95
+ /**
96
+ * Path configuration for the R2 destination
97
+ */
98
+ path: {
99
+ /**
100
+ * R2 bucket name
101
+ */
102
+ bucket: string;
103
+ /**
104
+ * Optional prefix for files in the bucket
105
+ */
106
+ prefix?: string;
107
+ /**
108
+ * Optional filename pattern
109
+ * @default "${slug}${extension}"
110
+ */
111
+ filename?: string;
112
+ /**
113
+ * Optional filepath pattern
114
+ * @default "${date}/${hour}"
115
+ */
116
+ filepath?: string;
117
+ };
118
+ /**
119
+ * Compression settings
120
+ */
121
+ compression?: CompressionSettings;
122
+ /**
123
+ * Batch settings
124
+ */
125
+ batch?: BatchSettings;
126
+ /**
127
+ * Credentials for the R2 bucket
128
+ * Required for R2 destinations
129
+ */
130
+ credentials: {
131
+ /**
132
+ * Access key ID for the R2 bucket
133
+ */
134
+ accessKeyId: Secret;
135
+ /**
136
+ * Secret access key for the R2 bucket
137
+ */
138
+ secretAccessKey: Secret;
139
+ /**
140
+ * Endpoint for the R2 bucket
141
+ */
142
+ endpoint?: string;
143
+ };
144
+ }
145
+ /**
146
+ * Allowed destination types
147
+ */
148
+ export type PipelineDestination = R2DestinationConfig;
149
+ /**
150
+ * Properties for creating or updating a Pipeline
151
+ */
152
+ export interface PipelineProps extends CloudflareApiOptions {
153
+ /**
154
+ * Name of the pipeline
155
+ */
156
+ name: string;
157
+ /**
158
+ * Source configuration for the pipeline
159
+ */
160
+ source: PipelineSource[];
161
+ /**
162
+ * Destination configuration for the pipeline
163
+ */
164
+ destination: PipelineDestination;
165
+ /**
166
+ * Compression settings for the pipeline
167
+ * @default { type: "gzip" }
168
+ */
169
+ compression?: CompressionSettings;
170
+ /**
171
+ * Whether to delete the pipeline.
172
+ * If set to false, the pipeline will remain but the resource will be removed from state
173
+ *
174
+ * @default true
175
+ */
176
+ delete?: boolean;
177
+ }
178
+ /**
179
+ * Base type for pipeline records
180
+ */
181
+ export interface PipelineRecord {
182
+ [key: string]: any;
183
+ }
184
+ /**
185
+ * Output returned after Pipeline creation/update
186
+ */
187
+ export interface Pipeline<T extends PipelineRecord = PipelineRecord> extends Resource<"cloudflare::Pipeline">, PipelineProps {
188
+ /**
189
+ * Type identifier for the Pipeline resource
190
+ */
191
+ type: "pipeline";
192
+ /**
193
+ * The unique ID of the pipeline
194
+ */
195
+ id: string;
196
+ /**
197
+ * HTTP endpoint URL for the pipeline
198
+ */
199
+ endpoint: string;
200
+ /**
201
+ * Version of the pipeline
202
+ */
203
+ version: number;
204
+ }
205
+ /**
206
+ * Creates and manages Cloudflare Pipelines.
207
+ *
208
+ * Pipelines provide a managed data pipeline service that lets you collect, transform,
209
+ * and route data to various destinations like R2 buckets.
210
+ *
211
+ * @example
212
+ * // Create a basic pipeline with an R2 bucket destination
213
+ * const bucket = await R2Bucket("logs-bucket", {
214
+ * name: "logs-bucket"
215
+ * });
216
+ *
217
+ * const accessKey = alchemy.secret(process.env.R2_ACCESS_KEY_ID!);
218
+ * const secretKey = alchemy.secret(process.env.R2_SECRET_ACCESS_KEY!);
219
+ *
220
+ * const pipeline = await Pipeline("logs-pipeline", {
221
+ * name: "logs-pipeline",
222
+ * destination: {
223
+ * type: "r2",
224
+ * format: "json",
225
+ * path: {
226
+ * bucket: bucket.name,
227
+ * prefix: "app-logs",
228
+ * },
229
+ * credentials: {
230
+ * accessKeyId: accessKey,
231
+ * secretAccessKey: secretKey
232
+ * }
233
+ * },
234
+ * batch: {
235
+ * maxMb: 50,
236
+ * maxSeconds: 60
237
+ * }
238
+ * });
239
+ *
240
+ * @example
241
+ * // Create a pipeline with custom source configuration
242
+ * const customPipeline = await Pipeline("custom-pipeline", {
243
+ * name: "custom-pipeline",
244
+ * source: [{
245
+ * type: "http",
246
+ * format: "json",
247
+ * authentication: true,
248
+ * cors: {
249
+ * origins: ["https://example.com"]
250
+ * }
251
+ * }],
252
+ * destination: {
253
+ * type: "r2",
254
+ * format: "json",
255
+ * path: {
256
+ * bucket: "my-bucket",
257
+ * prefix: "data"
258
+ * },
259
+ * credentials: {
260
+ * accessKeyId: alchemy.secret(process.env.R2_ACCESS_KEY_ID!),
261
+ * secretAccessKey: alchemy.secret(process.env.R2_SECRET_ACCESS_KEY!)
262
+ * },
263
+ * compression: {
264
+ * type: "gzip"
265
+ * }
266
+ * }
267
+ * });
268
+ *
269
+ * @see https://developers.cloudflare.com/pipelines/
270
+ */
271
+ export declare const Pipeline: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | (<T extends PipelineRecord = PipelineRecord>(this: Context<Pipeline<T>>, id: string, props: PipelineProps) => Promise<Pipeline<T>>);
272
+ interface CloudflarePipelineResponse {
273
+ result: {
274
+ id: string;
275
+ name: string;
276
+ endpoint: string;
277
+ version: number;
278
+ source: Array<{
279
+ type: "http" | "binding";
280
+ format: string;
281
+ authentication?: boolean;
282
+ cors?: {
283
+ origins: string[];
284
+ };
285
+ }>;
286
+ destination: {
287
+ type: string;
288
+ format: string;
289
+ path?: {
290
+ bucket: string;
291
+ prefix?: string;
292
+ filename?: string;
293
+ filepath?: string;
294
+ };
295
+ compression?: {
296
+ type: string;
297
+ };
298
+ batch: {
299
+ max_bytes?: number;
300
+ max_rows?: number;
301
+ max_duration_s?: number;
302
+ };
303
+ };
304
+ };
305
+ success: boolean;
306
+ errors: Array<{
307
+ code: number;
308
+ message: string;
309
+ }>;
310
+ messages: string[];
311
+ }
312
+ /**
313
+ * Get a pipeline
314
+ */
315
+ export declare function getPipeline(api: CloudflareApi, pipelineName: string): Promise<CloudflarePipelineResponse>;
316
+ /**
317
+ * Delete a pipeline
318
+ */
319
+ export declare function deletePipeline(api: CloudflareApi, pipelineName: string): Promise<void>;
320
+ /**
321
+ * Create a new pipeline
322
+ */
323
+ export declare function createPipeline(api: CloudflareApi, pipelineName: string, props: PipelineProps): Promise<CloudflarePipelineResponse>;
324
+ /**
325
+ * Update a pipeline
326
+ */
327
+ export declare function updatePipeline(api: CloudflareApi, pipelineName: string, props: PipelineProps): Promise<CloudflarePipelineResponse>;
328
+ /**
329
+ * List all pipelines in an account
330
+ */
331
+ export declare function listPipelines(api: CloudflareApi): Promise<{
332
+ name: string;
333
+ id: string;
334
+ }[]>;
335
+ export {};
@@ -0,0 +1,250 @@
1
+ import { Resource } from "../resource";
2
+ import { CloudflareApi, createCloudflareApi, } from "./api";
3
+ import { CloudflareApiError, handleApiError } from "./api-error";
4
+ /**
5
+ * Creates and manages Cloudflare Pipelines.
6
+ *
7
+ * Pipelines provide a managed data pipeline service that lets you collect, transform,
8
+ * and route data to various destinations like R2 buckets.
9
+ *
10
+ * @example
11
+ * // Create a basic pipeline with an R2 bucket destination
12
+ * const bucket = await R2Bucket("logs-bucket", {
13
+ * name: "logs-bucket"
14
+ * });
15
+ *
16
+ * const accessKey = alchemy.secret(process.env.R2_ACCESS_KEY_ID!);
17
+ * const secretKey = alchemy.secret(process.env.R2_SECRET_ACCESS_KEY!);
18
+ *
19
+ * const pipeline = await Pipeline("logs-pipeline", {
20
+ * name: "logs-pipeline",
21
+ * destination: {
22
+ * type: "r2",
23
+ * format: "json",
24
+ * path: {
25
+ * bucket: bucket.name,
26
+ * prefix: "app-logs",
27
+ * },
28
+ * credentials: {
29
+ * accessKeyId: accessKey,
30
+ * secretAccessKey: secretKey
31
+ * }
32
+ * },
33
+ * batch: {
34
+ * maxMb: 50,
35
+ * maxSeconds: 60
36
+ * }
37
+ * });
38
+ *
39
+ * @example
40
+ * // Create a pipeline with custom source configuration
41
+ * const customPipeline = await Pipeline("custom-pipeline", {
42
+ * name: "custom-pipeline",
43
+ * source: [{
44
+ * type: "http",
45
+ * format: "json",
46
+ * authentication: true,
47
+ * cors: {
48
+ * origins: ["https://example.com"]
49
+ * }
50
+ * }],
51
+ * destination: {
52
+ * type: "r2",
53
+ * format: "json",
54
+ * path: {
55
+ * bucket: "my-bucket",
56
+ * prefix: "data"
57
+ * },
58
+ * credentials: {
59
+ * accessKeyId: alchemy.secret(process.env.R2_ACCESS_KEY_ID!),
60
+ * secretAccessKey: alchemy.secret(process.env.R2_SECRET_ACCESS_KEY!)
61
+ * },
62
+ * compression: {
63
+ * type: "gzip"
64
+ * }
65
+ * }
66
+ * });
67
+ *
68
+ * @see https://developers.cloudflare.com/pipelines/
69
+ */
70
+ export const Pipeline = Resource("cloudflare::Pipeline", async function (id, props) {
71
+ const api = await createCloudflareApi(props);
72
+ const pipelineName = props.name || id;
73
+ if (this.phase === "delete") {
74
+ console.log("Deleting Cloudflare Pipeline:", pipelineName);
75
+ if (props.delete !== false) {
76
+ // Delete Pipeline
77
+ await deletePipeline(api, pipelineName);
78
+ }
79
+ // Return void (a deleted pipeline has no content)
80
+ return this.destroy();
81
+ }
82
+ else {
83
+ let pipelineData;
84
+ if (this.phase === "create") {
85
+ console.log("Creating Cloudflare Pipeline:", pipelineName);
86
+ pipelineData = await createPipeline(api, pipelineName, props);
87
+ }
88
+ else {
89
+ // Update operation
90
+ if (this.output?.id) {
91
+ console.log("Updating Cloudflare Pipeline:", pipelineName);
92
+ // Check if name is being changed, which is not allowed
93
+ if (props.name !== this.output.name) {
94
+ throw new Error("Cannot update Pipeline name after creation. Pipeline name is immutable.");
95
+ }
96
+ // Update the pipeline with new settings
97
+ pipelineData = await updatePipeline(api, pipelineName, props);
98
+ }
99
+ else {
100
+ // If no ID exists, fall back to creating a new pipeline
101
+ console.log("No existing Pipeline ID found, creating new Cloudflare Pipeline:", pipelineName);
102
+ pipelineData = await createPipeline(api, pipelineName, props);
103
+ }
104
+ }
105
+ return this({
106
+ type: "pipeline",
107
+ id: pipelineData.result.id,
108
+ name: pipelineName,
109
+ endpoint: pipelineData.result.endpoint,
110
+ version: pipelineData.result.version,
111
+ source: pipelineData.result.source.map((s) => ({
112
+ type: s.type,
113
+ format: s.format,
114
+ authentication: s.authentication,
115
+ cors: s.cors,
116
+ })),
117
+ destination: props.destination, // Use the input destination, not the API response
118
+ compression: props.compression,
119
+ accountId: api.accountId,
120
+ });
121
+ }
122
+ });
123
+ /**
124
+ * Get a pipeline
125
+ */
126
+ export async function getPipeline(api, pipelineName) {
127
+ const response = await api.get(`/accounts/${api.accountId}/pipelines/${pipelineName}`);
128
+ if (!response.ok) {
129
+ return await handleApiError(response, "getting", "Pipeline", pipelineName);
130
+ }
131
+ return (await response.json());
132
+ }
133
+ /**
134
+ * Delete a pipeline
135
+ */
136
+ export async function deletePipeline(api, pipelineName) {
137
+ // Delete Pipeline
138
+ const deleteResponse = await api.delete(`/accounts/${api.accountId}/pipelines/${pipelineName}`);
139
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
140
+ const errorData = await deleteResponse.json().catch(() => ({
141
+ errors: [{ message: deleteResponse.statusText }],
142
+ }));
143
+ throw new CloudflareApiError(`Error deleting Cloudflare Pipeline '${pipelineName}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`, deleteResponse);
144
+ }
145
+ }
146
+ /**
147
+ * Create a new pipeline
148
+ */
149
+ export async function createPipeline(api, pipelineName, props) {
150
+ // Prepare the create payload
151
+ const createPayload = preparePipelinePayload(api, pipelineName, props);
152
+ const createResponse = await api.post(`/accounts/${api.accountId}/pipelines`, createPayload);
153
+ if (!createResponse.ok) {
154
+ return await handleApiError(createResponse, "creating", "Pipeline", pipelineName);
155
+ }
156
+ return (await createResponse.json());
157
+ }
158
+ /**
159
+ * Update a pipeline
160
+ */
161
+ export async function updatePipeline(api, pipelineName, props) {
162
+ // Get current pipeline to build update payload
163
+ const currentPipeline = await getPipeline(api, pipelineName);
164
+ // Prepare the update payload
165
+ const updatePayload = preparePipelinePayload(api, pipelineName, props, currentPipeline);
166
+ const updateResponse = await api.put(`/accounts/${api.accountId}/pipelines/${pipelineName}`, updatePayload);
167
+ if (!updateResponse.ok) {
168
+ return await handleApiError(updateResponse, "updating", "Pipeline", pipelineName);
169
+ }
170
+ return (await updateResponse.json());
171
+ }
172
+ /**
173
+ * Helper function to prepare pipeline payload for create/update operations
174
+ */
175
+ function preparePipelinePayload(api, pipelineName, props, currentPipeline) {
176
+ // Prepare the payload with name and source
177
+ const payload = {
178
+ name: pipelineName,
179
+ source: props.source ||
180
+ currentPipeline?.result.source || [
181
+ {
182
+ type: "http",
183
+ format: "json",
184
+ authentication: true,
185
+ cors: { origins: ["*"] },
186
+ },
187
+ ],
188
+ };
189
+ // Handle destination
190
+ if (props.destination) {
191
+ payload.destination = { ...props.destination };
192
+ // Handle special formatting for R2 destination
193
+ const r2Dest = props.destination;
194
+ // Format credentials for API
195
+ if (payload.destination.credentials) {
196
+ payload.destination.credentials = {
197
+ access_key_id: r2Dest.credentials.accessKeyId.unencrypted,
198
+ secret_access_key: r2Dest.credentials.secretAccessKey.unencrypted,
199
+ endpoint: r2Dest.credentials.endpoint ??
200
+ `https://${api.accountId}.r2.cloudflarestorage.com`,
201
+ };
202
+ }
203
+ // Format batch settings
204
+ payload.destination.batch = convertBatchSettings(payload.destination.batch);
205
+ }
206
+ else if (currentPipeline?.result.destination) {
207
+ payload.destination = currentPipeline.result.destination;
208
+ }
209
+ else if (!props.destination && !currentPipeline) {
210
+ throw new Error("An R2 destination is required for creating/updating a pipeline");
211
+ }
212
+ // Add compression if not specified
213
+ if (!payload.destination.compression) {
214
+ payload.destination.compression = { type: "gzip" };
215
+ }
216
+ return payload;
217
+ }
218
+ /**
219
+ * List all pipelines in an account
220
+ */
221
+ export async function listPipelines(api) {
222
+ const response = await api.get(`/accounts/${api.accountId}/pipelines`);
223
+ if (!response.ok) {
224
+ throw new CloudflareApiError(`Failed to list pipelines: ${response.statusText}`, response);
225
+ }
226
+ const data = (await response.json());
227
+ if (!data.success) {
228
+ const errorMessage = data.errors?.[0]?.message || "Unknown error";
229
+ throw new Error(`Failed to list pipelines: ${errorMessage}`);
230
+ }
231
+ // Transform API response
232
+ return (data.results || []).map((pipeline) => ({
233
+ name: pipeline.name,
234
+ id: pipeline.id,
235
+ }));
236
+ }
237
+ function convertBatchSettings(batch) {
238
+ const result = {};
239
+ if (batch?.maxMb !== undefined) {
240
+ // Convert MB to bytes
241
+ result.max_bytes = batch.maxMb * 1024 * 1024;
242
+ }
243
+ if (batch?.maxRows !== undefined) {
244
+ result.max_rows = batch.maxRows;
245
+ }
246
+ if (batch?.maxSeconds !== undefined) {
247
+ result.max_duration_s = batch.maxSeconds;
248
+ }
249
+ return result;
250
+ }