alchemy 0.7.1 → 0.7.3
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/r2-rest-state-store.js +20 -32
- package/lib/cloudflare/worker.d.ts +9 -0
- package/lib/cloudflare/worker.js +13 -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/r2-rest-state-store.ts +23 -45
- package/src/cloudflare/worker.ts +25 -0
- package/src/esbuild/bundle.ts +3 -3
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { deserialize, serialize } from "../serde";
|
|
2
2
|
import { withExponentialBackoff } from "../util/retry";
|
|
3
3
|
import { createCloudflareApi, } from "./api";
|
|
4
|
+
import { CloudflareApiError, handleApiError } from "./api-error";
|
|
4
5
|
/**
|
|
5
6
|
* State store implementation using Cloudflare R2 API
|
|
6
7
|
* Uses R2 for immediate consistency compared to KV's eventual consistency
|
|
@@ -69,17 +70,12 @@ export class R2RestStateStore {
|
|
|
69
70
|
const response = await withExponentialBackoff(async () => {
|
|
70
71
|
const response = await this.api.get(listPath);
|
|
71
72
|
if (!response.ok) {
|
|
72
|
-
|
|
73
|
-
errors: [{ message: response.statusText }],
|
|
74
|
-
}));
|
|
75
|
-
throw new Error(`Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`);
|
|
73
|
+
await handleApiError(response, "list", "bucket", this.bucketName);
|
|
76
74
|
}
|
|
77
75
|
return response;
|
|
78
76
|
},
|
|
79
77
|
// Retry on transient errors
|
|
80
|
-
|
|
81
|
-
error.message?.includes("503") ||
|
|
82
|
-
error.message?.includes("timeout"), 5, // 5 retry attempts
|
|
78
|
+
isRetryableError, 5, // 5 retry attempts
|
|
83
79
|
1000 // Start with 1 second delay
|
|
84
80
|
);
|
|
85
81
|
const data = (await response.json());
|
|
@@ -117,21 +113,13 @@ export class R2RestStateStore {
|
|
|
117
113
|
try {
|
|
118
114
|
const response = await withExponentialBackoff(async () => {
|
|
119
115
|
const response = await this.api.get(`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`);
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
|
|
122
|
-
return response;
|
|
123
|
-
}
|
|
124
|
-
const errorData = await response.json().catch(() => ({
|
|
125
|
-
errors: [{ message: response.statusText }],
|
|
126
|
-
}));
|
|
127
|
-
throw new Error(`Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`);
|
|
116
|
+
if (!response.ok && response.status !== 404) {
|
|
117
|
+
await handleApiError(response, "get", "object", key);
|
|
128
118
|
}
|
|
129
119
|
return response;
|
|
130
120
|
},
|
|
131
121
|
// Retry on transient errors
|
|
132
|
-
|
|
133
|
-
error.message?.includes("503") ||
|
|
134
|
-
error.message?.includes("timeout"), 5, // 5 retry attempts
|
|
122
|
+
isRetryableError, 5, // 5 retry attempts
|
|
135
123
|
1000 // Start with 1 second delay
|
|
136
124
|
);
|
|
137
125
|
if (response.status === 404) {
|
|
@@ -202,15 +190,12 @@ export class R2RestStateStore {
|
|
|
202
190
|
},
|
|
203
191
|
});
|
|
204
192
|
if (!response.ok) {
|
|
205
|
-
|
|
206
|
-
errors: [{ message: response.statusText }],
|
|
207
|
-
}));
|
|
208
|
-
throw new Error(`Error writing to R2: ${errorData.errors?.[0]?.message || response.statusText}`);
|
|
193
|
+
await handleApiError(response, "put", "object", objectKey);
|
|
209
194
|
}
|
|
210
195
|
return response;
|
|
211
196
|
},
|
|
212
197
|
// Retry on transient errors
|
|
213
|
-
|
|
198
|
+
isRetryableError, 5, // 5 retry attempts
|
|
214
199
|
1000 // Start with 1 second delay
|
|
215
200
|
);
|
|
216
201
|
}
|
|
@@ -224,17 +209,10 @@ export class R2RestStateStore {
|
|
|
224
209
|
await withExponentialBackoff(async () => {
|
|
225
210
|
const response = await this.api.delete(`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`);
|
|
226
211
|
if (!response.ok && response.status !== 404) {
|
|
227
|
-
|
|
228
|
-
errors: [{ message: response.statusText }],
|
|
229
|
-
}));
|
|
230
|
-
throw new Error(`Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`);
|
|
212
|
+
await handleApiError(response, "delete", "object", key);
|
|
231
213
|
}
|
|
232
214
|
return response;
|
|
233
|
-
},
|
|
234
|
-
// Retry on transient errors
|
|
235
|
-
(error) => error.message?.includes("502") ||
|
|
236
|
-
error.message?.includes("503") ||
|
|
237
|
-
error.message?.includes("timeout"), 5, // 5 retry attempts
|
|
215
|
+
}, isRetryableError, 5, // 5 retry attempts
|
|
238
216
|
1000 // Start with 1 second delay
|
|
239
217
|
);
|
|
240
218
|
}
|
|
@@ -275,3 +253,13 @@ export class R2RestStateStore {
|
|
|
275
253
|
}
|
|
276
254
|
}
|
|
277
255
|
}
|
|
256
|
+
function isRetryableError(error) {
|
|
257
|
+
if (error instanceof CloudflareApiError) {
|
|
258
|
+
return (error.status === 500 ||
|
|
259
|
+
error.status === 502 ||
|
|
260
|
+
error.status === 503 ||
|
|
261
|
+
error.message.includes("timeout") ||
|
|
262
|
+
error.message.includes("internal error"));
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
@@ -74,6 +74,15 @@ export interface WorkerProps<B extends Bindings = Bindings> extends CloudflareAp
|
|
|
74
74
|
* Whether to adopt the Worker if it already exists when creating
|
|
75
75
|
*/
|
|
76
76
|
adopt?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The compatibility date for the worker
|
|
79
|
+
* @default "2024-09-09"
|
|
80
|
+
*/
|
|
81
|
+
compatibilityDate?: string;
|
|
82
|
+
/**
|
|
83
|
+
* The compatibility flags for the worker
|
|
84
|
+
*/
|
|
85
|
+
compatibilityFlags?: string[];
|
|
77
86
|
}
|
|
78
87
|
/**
|
|
79
88
|
* Output returned after Worker creation/update
|
package/lib/cloudflare/worker.js
CHANGED
|
@@ -239,6 +239,8 @@ class NotFoundError extends Error {
|
|
|
239
239
|
async function prepareWorkerMetadata(ctx, oldBindings, props, assetUploadResult) {
|
|
240
240
|
// Prepare metadata with bindings
|
|
241
241
|
const meta = {
|
|
242
|
+
compatibility_date: props.compatibilityDate ?? "2024-09-09",
|
|
243
|
+
compatibility_flags: props.compatibilityFlags,
|
|
242
244
|
bindings: [],
|
|
243
245
|
observability: {
|
|
244
246
|
enabled: props.observability?.enabled !== false,
|
|
@@ -344,6 +346,13 @@ async function prepareWorkerMetadata(ctx, oldBindings, props, assetUploadResult)
|
|
|
344
346
|
queue_name: binding.name,
|
|
345
347
|
});
|
|
346
348
|
}
|
|
349
|
+
else if (binding.type === "pipeline") {
|
|
350
|
+
meta.bindings.push({
|
|
351
|
+
type: "pipelines",
|
|
352
|
+
name: bindingName,
|
|
353
|
+
pipeline: binding.name,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
347
356
|
else {
|
|
348
357
|
// @ts-expect-error - we should never reach here
|
|
349
358
|
throw new Error(`Unsupported binding type: ${binding.type}`);
|
|
@@ -422,6 +431,10 @@ async function bundleWorkerScript(props) {
|
|
|
422
431
|
minify: true,
|
|
423
432
|
options: {
|
|
424
433
|
keepNames: true, // Important for Durable Object classes
|
|
434
|
+
loader: {
|
|
435
|
+
".sql": "text",
|
|
436
|
+
".json": "json",
|
|
437
|
+
},
|
|
425
438
|
},
|
|
426
439
|
};
|
|
427
440
|
// Merge with user-provided options
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import type { R2Bucket } from "./bucket";
|
|
|
9
9
|
import type { D1Database } from "./d1-database";
|
|
10
10
|
import type { DurableObjectNamespace } from "./durable-object-namespace";
|
|
11
11
|
import type { KVNamespace } from "./kv-namespace";
|
|
12
|
+
import type { Pipeline } from "./pipeline";
|
|
12
13
|
import type { Queue } from "./queue";
|
|
13
14
|
import type { Worker } from "./worker";
|
|
14
15
|
import type { Workflow } from "./workflow";
|
|
@@ -25,6 +26,7 @@ export type Binding =
|
|
|
25
26
|
| D1Database
|
|
26
27
|
| DurableObjectNamespace
|
|
27
28
|
| KVNamespace
|
|
29
|
+
| Pipeline
|
|
28
30
|
| Queue
|
|
29
31
|
| R2Bucket
|
|
30
32
|
| Secret
|
|
@@ -47,6 +49,7 @@ export type WorkerBindingSpec =
|
|
|
47
49
|
| WorkerBindingJson
|
|
48
50
|
| WorkerBindingKVNamespace
|
|
49
51
|
| WorkerBindingMTLSCertificate
|
|
52
|
+
| WorkerBindingPipeline
|
|
50
53
|
| WorkerBindingPlainText
|
|
51
54
|
| WorkerBindingQueue
|
|
52
55
|
| WorkerBindingR2Bucket
|
|
@@ -335,3 +338,15 @@ export interface WorkerBindingWorkflow {
|
|
|
335
338
|
*/
|
|
336
339
|
script_name?: string;
|
|
337
340
|
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Pipeline binding type
|
|
344
|
+
*/
|
|
345
|
+
export interface WorkerBindingPipeline {
|
|
346
|
+
/** The name of the binding */
|
|
347
|
+
name: string;
|
|
348
|
+
/** Type identifier for Pipeline binding */
|
|
349
|
+
type: "pipelines";
|
|
350
|
+
/** Pipeline name */
|
|
351
|
+
pipeline: string;
|
|
352
|
+
}
|
package/src/cloudflare/bound.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Pipeline } from "cloudflare:pipelines";
|
|
1
2
|
import type { Secret } from "../secret";
|
|
2
3
|
import type { Assets } from "./assets";
|
|
3
4
|
import type { Binding } from "./bindings";
|
|
@@ -5,6 +6,7 @@ import type { R2Bucket as _R2Bucket } from "./bucket";
|
|
|
5
6
|
import type { D1Database as _D1Database } from "./d1-database";
|
|
6
7
|
import type { DurableObjectNamespace as _DurableObjectNamespace } from "./durable-object-namespace";
|
|
7
8
|
import type { KVNamespace as _KVNamespace } from "./kv-namespace";
|
|
9
|
+
import type { Pipeline as _Pipeline } from "./pipeline";
|
|
8
10
|
import type { Queue as _Queue } from "./queue";
|
|
9
11
|
import type { Worker as _Worker } from "./worker";
|
|
10
12
|
import type { Workflow as _Workflow } from "./workflow";
|
|
@@ -27,4 +29,6 @@ export type Bound<T extends Binding> = T extends _DurableObjectNamespace
|
|
|
27
29
|
? D1Database
|
|
28
30
|
: T extends _Queue
|
|
29
31
|
? Queue
|
|
30
|
-
:
|
|
32
|
+
: T extends _Pipeline<infer R>
|
|
33
|
+
? Pipeline<R>
|
|
34
|
+
: Service;
|
package/src/cloudflare/bucket.ts
CHANGED
|
@@ -49,6 +49,14 @@ export interface D1DatabaseProps extends CloudflareApiOptions {
|
|
|
49
49
|
* @default true
|
|
50
50
|
*/
|
|
51
51
|
delete?: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether to adopt an existing database with the same name if it exists
|
|
55
|
+
* If true and a database with the same name exists, it will be adopted rather than creating a new one
|
|
56
|
+
*
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
adopt?: boolean;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
/**
|
|
@@ -108,6 +116,16 @@ export interface D1Database
|
|
|
108
116
|
* primaryLocationHint: "wnam"
|
|
109
117
|
* });
|
|
110
118
|
*
|
|
119
|
+
* @example
|
|
120
|
+
* // Adopt an existing database if it already exists instead of failing
|
|
121
|
+
* const existingDb = await D1Database("existing-db", {
|
|
122
|
+
* name: "existing-db",
|
|
123
|
+
* adopt: true,
|
|
124
|
+
* readReplication: {
|
|
125
|
+
* mode: "auto"
|
|
126
|
+
* }
|
|
127
|
+
* });
|
|
128
|
+
*
|
|
111
129
|
* @see https://developers.cloudflare.com/d1/
|
|
112
130
|
*/
|
|
113
131
|
export const D1Database = Resource(
|
|
@@ -135,7 +153,41 @@ export const D1Database = Resource(
|
|
|
135
153
|
|
|
136
154
|
if (this.phase === "create") {
|
|
137
155
|
console.log("Creating D1 database:", databaseName);
|
|
138
|
-
|
|
156
|
+
try {
|
|
157
|
+
dbData = await createDatabase(api, databaseName, props);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// Check if this is a "database already exists" error and adopt is enabled
|
|
160
|
+
if (
|
|
161
|
+
props.adopt &&
|
|
162
|
+
error instanceof CloudflareApiError &&
|
|
163
|
+
error.message.includes("already exists")
|
|
164
|
+
) {
|
|
165
|
+
console.log(`Database ${databaseName} already exists, adopting it`);
|
|
166
|
+
// Find the existing database by name
|
|
167
|
+
const databases = await listDatabases(api, databaseName);
|
|
168
|
+
const existingDb = databases.find((db) => db.name === databaseName);
|
|
169
|
+
|
|
170
|
+
if (!existingDb) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Failed to find existing database '${databaseName}' for adoption`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Get the database details using its ID
|
|
177
|
+
dbData = await getDatabase(api, existingDb.id);
|
|
178
|
+
|
|
179
|
+
// Update the database with the provided properties
|
|
180
|
+
if (props.readReplication) {
|
|
181
|
+
console.log(
|
|
182
|
+
`Updating adopted database ${databaseName} with new properties`
|
|
183
|
+
);
|
|
184
|
+
dbData = await updateDatabase(api, existingDb.id, props);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
// Re-throw the error if adopt is false or it's not a "database already exists" error
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
139
191
|
} else {
|
|
140
192
|
// Update operation
|
|
141
193
|
if (this.output?.id) {
|
|
@@ -272,9 +324,15 @@ export async function deleteDatabase(
|
|
|
272
324
|
* List all D1 databases in an account
|
|
273
325
|
*/
|
|
274
326
|
export async function listDatabases(
|
|
275
|
-
api: CloudflareApi
|
|
327
|
+
api: CloudflareApi,
|
|
328
|
+
name?: string
|
|
276
329
|
): Promise<{ name: string; id: string }[]> {
|
|
277
|
-
|
|
330
|
+
// Construct query string if name is provided
|
|
331
|
+
const queryParams = name ? `?name=${encodeURIComponent(name)}` : "";
|
|
332
|
+
|
|
333
|
+
const response = await api.get(
|
|
334
|
+
`/accounts/${api.accountId}/d1/database${queryParams}`
|
|
335
|
+
);
|
|
278
336
|
|
|
279
337
|
if (!response.ok) {
|
|
280
338
|
throw new CloudflareApiError(
|
package/src/cloudflare/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./dns-records";
|
|
|
10
10
|
export * from "./durable-object-namespace";
|
|
11
11
|
export * from "./kv-namespace";
|
|
12
12
|
export * from "./permission-groups";
|
|
13
|
+
export * from "./pipeline";
|
|
13
14
|
export * from "./queue";
|
|
14
15
|
export * from "./r2-rest-state-store";
|
|
15
16
|
export * from "./worker";
|