alchemy 0.15.10 → 0.15.12
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/account-api-token.d.ts +36 -19
- package/lib/cloudflare/account-api-token.d.ts.map +1 -1
- package/lib/cloudflare/account-api-token.js +34 -26
- package/lib/cloudflare/account-api-token.js.map +1 -1
- package/lib/cloudflare/d1-clone.d.ts +55 -0
- package/lib/cloudflare/d1-clone.d.ts.map +1 -0
- package/lib/cloudflare/d1-clone.js +45 -0
- package/lib/cloudflare/d1-clone.js.map +1 -0
- package/lib/cloudflare/d1-database.d.ts +36 -1
- package/lib/cloudflare/d1-database.d.ts.map +1 -1
- package/lib/cloudflare/d1-database.js +73 -9
- package/lib/cloudflare/d1-database.js.map +1 -1
- package/lib/cloudflare/d1-export.d.ts +63 -0
- package/lib/cloudflare/d1-export.d.ts.map +1 -0
- package/lib/cloudflare/d1-export.js +50 -0
- package/lib/cloudflare/d1-export.js.map +1 -0
- package/lib/cloudflare/d1-import.d.ts +112 -0
- package/lib/cloudflare/d1-import.d.ts.map +1 -0
- package/lib/cloudflare/d1-import.js +143 -0
- package/lib/cloudflare/d1-import.js.map +1 -0
- package/lib/cloudflare/index.d.ts +3 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +3 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/permission-groups.d.ts +2 -0
- package/lib/cloudflare/permission-groups.d.ts.map +1 -1
- package/lib/cloudflare/permission-groups.js.map +1 -1
- package/lib/util/sleep.d.ts +7 -0
- package/lib/util/sleep.d.ts.map +1 -0
- package/lib/util/sleep.js +9 -0
- package/lib/util/sleep.js.map +1 -0
- package/package.json +1 -1
- package/src/cloudflare/account-api-token.ts +72 -36
- package/src/cloudflare/d1-clone.ts +113 -0
- package/src/cloudflare/d1-database.ts +99 -15
- package/src/cloudflare/d1-export.ts +173 -0
- package/src/cloudflare/d1-import.ts +406 -0
- package/src/cloudflare/index.ts +3 -0
- package/src/cloudflare/permission-groups.ts +8 -0
- package/src/util/sleep.ts +8 -0
|
@@ -3,12 +3,16 @@ import type { Context } from "../context.js";
|
|
|
3
3
|
import { Resource } from "../resource.js";
|
|
4
4
|
import type { Secret } from "../secret.js";
|
|
5
5
|
import { sha256 } from "../util/sha256.js";
|
|
6
|
-
import { type CloudflareApiOptions
|
|
6
|
+
import { createCloudflareApi, type CloudflareApiOptions } from "./api.js";
|
|
7
|
+
import {
|
|
8
|
+
PermissionGroups,
|
|
9
|
+
type PermissionGroupName,
|
|
10
|
+
} from "./permission-groups.js";
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Permission group for a token policy
|
|
10
14
|
*/
|
|
11
|
-
export
|
|
15
|
+
export type TokenPolicyPermissionGroup = {
|
|
12
16
|
/**
|
|
13
17
|
* ID of the permission group
|
|
14
18
|
*/
|
|
@@ -18,7 +22,7 @@ export interface TokenPolicyPermissionGroup {
|
|
|
18
22
|
* Optional metadata for the permission group
|
|
19
23
|
*/
|
|
20
24
|
meta?: Record<string, any>;
|
|
21
|
-
}
|
|
25
|
+
};
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
28
|
* Policy that defines what the token can access
|
|
@@ -32,14 +36,27 @@ export interface TokenPolicy {
|
|
|
32
36
|
/**
|
|
33
37
|
* Permission groups to include in the policy
|
|
34
38
|
*/
|
|
35
|
-
permissionGroups: TokenPolicyPermissionGroup[];
|
|
39
|
+
permissionGroups: (PermissionGroupName | TokenPolicyPermissionGroup)[];
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
42
|
* Resources the policy applies to
|
|
39
43
|
*/
|
|
40
|
-
resources:
|
|
44
|
+
resources: {
|
|
45
|
+
[key in TokenPolicyResourceKey]?: string;
|
|
46
|
+
};
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @see https://developers.cloudflare.com/fundamentals/api/reference/permissions/
|
|
51
|
+
*/
|
|
52
|
+
type TokenPolicyResourceKey =
|
|
53
|
+
| `com.cloudflare.api.account`
|
|
54
|
+
| `com.cloudflare.api.account.${string}`
|
|
55
|
+
// | `com.cloudflare.api.account.zone`
|
|
56
|
+
| `com.cloudflare.api.account.zone.${string}`
|
|
57
|
+
// | `com.cloudflare.api.user`
|
|
58
|
+
| `com.cloudflare.api.user.${string}`;
|
|
59
|
+
|
|
43
60
|
/**
|
|
44
61
|
* Condition for token usage (e.g., IP restrictions)
|
|
45
62
|
*/
|
|
@@ -120,8 +137,7 @@ interface CloudflareApiToken {
|
|
|
120
137
|
* Output returned after Account API Token creation/update
|
|
121
138
|
*/
|
|
122
139
|
export interface AccountApiToken
|
|
123
|
-
extends Resource<"cloudflare::AccountApiToken"
|
|
124
|
-
AccountApiTokenProps {
|
|
140
|
+
extends Resource<"cloudflare::AccountApiToken"> {
|
|
125
141
|
/**
|
|
126
142
|
* The ID of the token
|
|
127
143
|
*
|
|
@@ -129,6 +145,11 @@ export interface AccountApiToken
|
|
|
129
145
|
*/
|
|
130
146
|
id: string;
|
|
131
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Name of the token
|
|
150
|
+
*/
|
|
151
|
+
name: string;
|
|
152
|
+
|
|
132
153
|
/**
|
|
133
154
|
* Status of the token
|
|
134
155
|
*/
|
|
@@ -147,7 +168,7 @@ export interface AccountApiToken
|
|
|
147
168
|
*
|
|
148
169
|
* An alias of {@link id}
|
|
149
170
|
*/
|
|
150
|
-
accessKeyId:
|
|
171
|
+
accessKeyId: Secret;
|
|
151
172
|
|
|
152
173
|
/**
|
|
153
174
|
* Secret access key for the token
|
|
@@ -156,7 +177,7 @@ export interface AccountApiToken
|
|
|
156
177
|
*
|
|
157
178
|
* @see https://developers.cloudflare.com/r2/api/tokens/#get-s3-api-credentials-from-an-api-token
|
|
158
179
|
*/
|
|
159
|
-
secretAccessKey:
|
|
180
|
+
secretAccessKey: Secret;
|
|
160
181
|
}
|
|
161
182
|
|
|
162
183
|
/**
|
|
@@ -169,21 +190,13 @@ export interface AccountApiToken
|
|
|
169
190
|
* @see https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/create/
|
|
170
191
|
*
|
|
171
192
|
* @example
|
|
172
|
-
* // First, fetch all permission groups
|
|
173
|
-
* const permissions = await PermissionGroups("cloudflare-permissions", {
|
|
174
|
-
* accountId: cfAccountId,
|
|
175
|
-
* });
|
|
176
|
-
*
|
|
177
193
|
* // Create a token with read-only permissions for specific zones
|
|
178
194
|
* const readOnlyToken = await AccountApiToken("readonly-token", {
|
|
179
195
|
* name: "Readonly Zone Token",
|
|
180
196
|
* policies: [
|
|
181
197
|
* {
|
|
182
198
|
* effect: "allow",
|
|
183
|
-
* permissionGroups: [
|
|
184
|
-
* { id: permissions["Zone Read"].id },
|
|
185
|
-
* { id: permissions["Analytics Read"].id }
|
|
186
|
-
* ],
|
|
199
|
+
* permissionGroups: ["Zone Read", "Analytics Read"],
|
|
187
200
|
* resources: {
|
|
188
201
|
* "com.cloudflare.api.account.zone.22b1de5f1c0e4b3ea97bb1e963b06a43": "*",
|
|
189
202
|
* "com.cloudflare.api.account.zone.eb78d65290b24279ba6f44721b3ea3c4": "*"
|
|
@@ -200,9 +213,7 @@ export interface AccountApiToken
|
|
|
200
213
|
* policies: [
|
|
201
214
|
* {
|
|
202
215
|
* effect: "allow",
|
|
203
|
-
* permissionGroups: [
|
|
204
|
-
* { id: permissions["Worker Routes Edit"].id }
|
|
205
|
-
* ],
|
|
216
|
+
* permissionGroups: ["Worker Routes Edit"],
|
|
206
217
|
* resources: {
|
|
207
218
|
* "com.cloudflare.api.account.worker.route.*": "*"
|
|
208
219
|
* }
|
|
@@ -217,6 +228,21 @@ export interface AccountApiToken
|
|
|
217
228
|
* }
|
|
218
229
|
* }
|
|
219
230
|
* });
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* // Create a token with bucket access permissions
|
|
234
|
+
* const storageToken = await AccountApiToken("account-access-token", {
|
|
235
|
+
* name: "alchemy-account-access-token",
|
|
236
|
+
* policies: [
|
|
237
|
+
* {
|
|
238
|
+
* effect: "allow",
|
|
239
|
+
* permissionGroups: ["Workers R2 Storage Write"],
|
|
240
|
+
* resources: {
|
|
241
|
+
* "com.cloudflare.api.account": "*",
|
|
242
|
+
* },
|
|
243
|
+
* },
|
|
244
|
+
* ],
|
|
245
|
+
* });
|
|
220
246
|
*/
|
|
221
247
|
export const AccountApiToken = Resource(
|
|
222
248
|
"cloudflare::AccountApiToken",
|
|
@@ -251,16 +277,34 @@ export const AccountApiToken = Resource(
|
|
|
251
277
|
return this.destroy();
|
|
252
278
|
}
|
|
253
279
|
|
|
280
|
+
const permissionGroups = await PermissionGroups(
|
|
281
|
+
"cloudflare-permission-groups",
|
|
282
|
+
props,
|
|
283
|
+
);
|
|
284
|
+
|
|
254
285
|
// Transform our properties to API format
|
|
255
286
|
const apiPayload = {
|
|
256
287
|
name: props.name,
|
|
257
288
|
policies: props.policies.map((policy) => ({
|
|
258
289
|
effect: policy.effect,
|
|
259
|
-
permission_groups: policy.permissionGroups.map((pg) =>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
290
|
+
permission_groups: policy.permissionGroups.map((pg) =>
|
|
291
|
+
typeof pg === "string"
|
|
292
|
+
? {
|
|
293
|
+
id: permissionGroups[pg].id,
|
|
294
|
+
}
|
|
295
|
+
: {
|
|
296
|
+
id: pg.id,
|
|
297
|
+
meta: pg.meta || {},
|
|
298
|
+
},
|
|
299
|
+
),
|
|
300
|
+
resources: Object.fromEntries(
|
|
301
|
+
Object.entries(policy.resources).map(([key, value]) => {
|
|
302
|
+
if (key === "com.cloudflare.api.account") {
|
|
303
|
+
return [`com.cloudflare.api.account.${api.accountId}`, value];
|
|
304
|
+
}
|
|
305
|
+
return [key, value];
|
|
306
|
+
}),
|
|
307
|
+
),
|
|
264
308
|
})),
|
|
265
309
|
// Format dates for Cloudflare API (removing milliseconds)
|
|
266
310
|
...(props.expiresOn
|
|
@@ -339,14 +383,6 @@ export const AccountApiToken = Resource(
|
|
|
339
383
|
id: tokenData.id,
|
|
340
384
|
name: tokenData.name,
|
|
341
385
|
status: tokenData.status,
|
|
342
|
-
policies: tokenData.policies.map((policy) => ({
|
|
343
|
-
effect: policy.effect,
|
|
344
|
-
permissionGroups: policy.permission_groups.map((pg) => ({
|
|
345
|
-
id: pg.id,
|
|
346
|
-
meta: pg.meta,
|
|
347
|
-
})),
|
|
348
|
-
resources: policy.resources,
|
|
349
|
-
})),
|
|
350
386
|
...(tokenData.expires_on ? { expiresOn: tokenData.expires_on } : {}),
|
|
351
387
|
...(tokenData.not_before ? { notBefore: tokenData.not_before } : {}),
|
|
352
388
|
...(tokenData.condition
|
|
@@ -362,8 +398,8 @@ export const AccountApiToken = Resource(
|
|
|
362
398
|
}
|
|
363
399
|
: {}),
|
|
364
400
|
value: tokenValue,
|
|
365
|
-
accessKeyId: tokenData.id,
|
|
366
|
-
secretAccessKey: sha256(tokenValue.unencrypted),
|
|
401
|
+
accessKeyId: alchemy.secret(tokenData.id),
|
|
402
|
+
secretAccessKey: alchemy.secret(sha256(tokenValue.unencrypted)),
|
|
367
403
|
});
|
|
368
404
|
},
|
|
369
405
|
);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { CloudflareApi } from "./api.js";
|
|
2
|
+
import {
|
|
3
|
+
exportD1Database,
|
|
4
|
+
type ExportD1DatabaseOptions,
|
|
5
|
+
type ExportD1DatabaseResult,
|
|
6
|
+
} from "./d1-export.js";
|
|
7
|
+
import {
|
|
8
|
+
importD1Database,
|
|
9
|
+
type ImportD1DatabaseOptions,
|
|
10
|
+
type ImportD1DatabaseResult,
|
|
11
|
+
} from "./d1-import.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for cloning a D1 database
|
|
15
|
+
*/
|
|
16
|
+
export interface CloneD1DatabaseOptions {
|
|
17
|
+
/**
|
|
18
|
+
* The ID of the source D1 database to clone from
|
|
19
|
+
*/
|
|
20
|
+
sourceDatabaseId: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The ID of the target D1 database to clone to
|
|
24
|
+
*/
|
|
25
|
+
targetDatabaseId: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional export options for the source database
|
|
29
|
+
*/
|
|
30
|
+
exportOptions?: Omit<ExportD1DatabaseOptions, "databaseId">;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optional import options for the target database
|
|
34
|
+
*/
|
|
35
|
+
importOptions?: Omit<ImportD1DatabaseOptions, "databaseId" | "sqlData">;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Response from the clone D1 database operation
|
|
40
|
+
*/
|
|
41
|
+
export interface CloneD1DatabaseResult {
|
|
42
|
+
/**
|
|
43
|
+
* Result of the export operation
|
|
44
|
+
*/
|
|
45
|
+
exportResult: ExportD1DatabaseResult;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Result of the import operation
|
|
49
|
+
*/
|
|
50
|
+
importResult: ImportD1DatabaseResult;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether the clone operation was successful
|
|
54
|
+
*/
|
|
55
|
+
success: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Clones a D1 database by exporting from a source database and importing to a target database.
|
|
60
|
+
* Handles the full workflow:
|
|
61
|
+
* 1. Export source database to get a signed URL
|
|
62
|
+
* 2. Fetch SQL content from the signed URL
|
|
63
|
+
* 3. Import SQL content into the target database
|
|
64
|
+
*
|
|
65
|
+
* @param api The CloudflareApi instance to use for requests
|
|
66
|
+
* @param options Options including source and target database IDs
|
|
67
|
+
* @returns An object containing results from both export and import operations
|
|
68
|
+
* @throws Will throw an error if any part of the clone process fails
|
|
69
|
+
*/
|
|
70
|
+
export async function cloneD1Database(
|
|
71
|
+
api: CloudflareApi,
|
|
72
|
+
options: CloneD1DatabaseOptions,
|
|
73
|
+
): Promise<CloneD1DatabaseResult> {
|
|
74
|
+
const exportResult = await exportD1Database(api, {
|
|
75
|
+
databaseId: options.sourceDatabaseId,
|
|
76
|
+
...options.exportOptions,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!exportResult.success) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Failed to export source database: ${options.sourceDatabaseId}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Step 2: Fetch SQL content from the signed URL
|
|
86
|
+
const sqlResponse = await fetch(exportResult.signed_url);
|
|
87
|
+
|
|
88
|
+
if (!sqlResponse.ok) {
|
|
89
|
+
throw new Error(`Failed to fetch SQL content: ${sqlResponse.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sqlData = await sqlResponse.text();
|
|
93
|
+
|
|
94
|
+
// Step 3: Import SQL content into target database
|
|
95
|
+
const importResult = await importD1Database(api, {
|
|
96
|
+
databaseId: options.targetDatabaseId,
|
|
97
|
+
sqlData,
|
|
98
|
+
filename: exportResult.filename,
|
|
99
|
+
...options.importOptions,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!importResult.success) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Failed to import to target database: ${options.targetDatabaseId}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
exportResult,
|
|
110
|
+
importResult,
|
|
111
|
+
success: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -2,10 +2,11 @@ import type { Context } from "../context.js";
|
|
|
2
2
|
import { Resource } from "../resource.js";
|
|
3
3
|
import { CloudflareApiError, handleApiError } from "./api-error.js";
|
|
4
4
|
import {
|
|
5
|
-
type CloudflareApi,
|
|
6
5
|
createCloudflareApi,
|
|
6
|
+
type CloudflareApi,
|
|
7
7
|
type CloudflareApiOptions,
|
|
8
8
|
} from "./api.js";
|
|
9
|
+
import { cloneD1Database } from "./d1-clone.js";
|
|
9
10
|
import { applyMigrations, listMigrationsFiles } from "./d1-migrations.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -61,6 +62,17 @@ export interface D1DatabaseProps extends CloudflareApiOptions {
|
|
|
61
62
|
*/
|
|
62
63
|
adopt?: boolean;
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Clone data from an existing database to this new database.
|
|
67
|
+
* Only applicable during creation phase.
|
|
68
|
+
*
|
|
69
|
+
* Can be specified as:
|
|
70
|
+
* - A D1Database object
|
|
71
|
+
* - An object with an id property
|
|
72
|
+
* - An object with a name property (will look up the ID by name)
|
|
73
|
+
*/
|
|
74
|
+
clone?: D1Database | { id: string } | { name: string };
|
|
75
|
+
|
|
64
76
|
/**
|
|
65
77
|
* These files will be generated internally with the D1Database wrapper function when migrationsDir is specified
|
|
66
78
|
*
|
|
@@ -123,20 +135,6 @@ export type D1Database = Resource<"cloudflare::D1Database"> &
|
|
|
123
135
|
};
|
|
124
136
|
};
|
|
125
137
|
|
|
126
|
-
export async function D1Database(
|
|
127
|
-
id: string,
|
|
128
|
-
props: Omit<D1DatabaseProps, "migrationsFiles">,
|
|
129
|
-
) {
|
|
130
|
-
const migrationsFiles = props.migrationsDir
|
|
131
|
-
? await listMigrationsFiles(props.migrationsDir)
|
|
132
|
-
: [];
|
|
133
|
-
|
|
134
|
-
return D1DatabaseResource(id, {
|
|
135
|
-
...props,
|
|
136
|
-
migrationsFiles,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
138
|
/**
|
|
141
139
|
* Creates and manages Cloudflare D1 Databases.
|
|
142
140
|
*
|
|
@@ -173,8 +171,43 @@ export async function D1Database(
|
|
|
173
171
|
* migrationsDir: "./migrations",
|
|
174
172
|
* });
|
|
175
173
|
*
|
|
174
|
+
* @example
|
|
175
|
+
* // Clone an existing database by ID
|
|
176
|
+
* const clonedDb = await D1Database("cloned-db", {
|
|
177
|
+
* name: "cloned-db",
|
|
178
|
+
* clone: otherDb
|
|
179
|
+
* });
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* // Clone an existing database by ID
|
|
183
|
+
* const clonedDb = await D1Database("cloned-db", {
|
|
184
|
+
* name: "cloned-db",
|
|
185
|
+
* clone: { id: "existing-db-uuid" }
|
|
186
|
+
* });
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* // Clone an existing database by name
|
|
190
|
+
* const clonedDb = await D1Database("cloned-db", {
|
|
191
|
+
* name: "cloned-db",
|
|
192
|
+
* clone: { name: "existing-db-name" }
|
|
193
|
+
* });
|
|
194
|
+
*
|
|
176
195
|
* @see https://developers.cloudflare.com/d1/
|
|
177
196
|
*/
|
|
197
|
+
export async function D1Database(
|
|
198
|
+
id: string,
|
|
199
|
+
props: Omit<D1DatabaseProps, "migrationsFiles">,
|
|
200
|
+
) {
|
|
201
|
+
const migrationsFiles = props.migrationsDir
|
|
202
|
+
? await listMigrationsFiles(props.migrationsDir)
|
|
203
|
+
: [];
|
|
204
|
+
|
|
205
|
+
return D1DatabaseResource(id, {
|
|
206
|
+
...props,
|
|
207
|
+
migrationsFiles,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
178
211
|
export const D1DatabaseResource = Resource(
|
|
179
212
|
"cloudflare::D1Database",
|
|
180
213
|
async function (
|
|
@@ -202,6 +235,11 @@ export const D1DatabaseResource = Resource(
|
|
|
202
235
|
console.log("Creating D1 database:", databaseName);
|
|
203
236
|
try {
|
|
204
237
|
dbData = await createDatabase(api, databaseName, props);
|
|
238
|
+
|
|
239
|
+
// If clone property is provided, perform cloning after database creation
|
|
240
|
+
if (props.clone && dbData.result.uuid) {
|
|
241
|
+
await cloneDb(api, props.clone, dbData.result.uuid);
|
|
242
|
+
}
|
|
205
243
|
} catch (error) {
|
|
206
244
|
// Check if this is a "database already exists" error and adopt is enabled
|
|
207
245
|
if (
|
|
@@ -480,3 +518,49 @@ export async function updateDatabase(
|
|
|
480
518
|
|
|
481
519
|
return (await updateResponse.json()) as CloudflareD1Response;
|
|
482
520
|
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Helper function to clone data from a source database to a target database
|
|
524
|
+
* Resolves the source database ID from different input formats and performs the cloning operation
|
|
525
|
+
*
|
|
526
|
+
* @param api CloudflareApi instance
|
|
527
|
+
* @param sourceDb Source database specification (can be an ID, a name, or a D1Database object)
|
|
528
|
+
* @param targetDbId Target database ID
|
|
529
|
+
*/
|
|
530
|
+
async function cloneDb(
|
|
531
|
+
api: CloudflareApi,
|
|
532
|
+
sourceDb: D1Database | { id: string } | { name: string },
|
|
533
|
+
targetDbId: string,
|
|
534
|
+
): Promise<void> {
|
|
535
|
+
let sourceId: string;
|
|
536
|
+
|
|
537
|
+
// Determine source database ID
|
|
538
|
+
if ("id" in sourceDb && sourceDb.id) {
|
|
539
|
+
// Use provided ID directly
|
|
540
|
+
sourceId = sourceDb.id;
|
|
541
|
+
} else if ("name" in sourceDb && sourceDb.name) {
|
|
542
|
+
// Look up ID by name
|
|
543
|
+
const databases = await listDatabases(api, sourceDb.name);
|
|
544
|
+
const foundDb = databases.find((db) => db.name === sourceDb.name);
|
|
545
|
+
|
|
546
|
+
if (!foundDb) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`Source database with name '${sourceDb.name}' not found for cloning`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
sourceId = foundDb.id;
|
|
553
|
+
} else if ("type" in sourceDb && sourceDb.type === "d1" && "id" in sourceDb) {
|
|
554
|
+
// It's a D1Database object
|
|
555
|
+
sourceId = sourceDb.id;
|
|
556
|
+
} else {
|
|
557
|
+
throw new Error("Invalid clone property: must provide either id or name");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Perform the cloning
|
|
561
|
+
console.log(`Cloning data from database ${sourceId} to ${targetDbId}`);
|
|
562
|
+
await cloneD1Database(api, {
|
|
563
|
+
sourceDatabaseId: sourceId,
|
|
564
|
+
targetDatabaseId: targetDbId,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { CloudflareApi } from "./api.js"; // Ensure CloudflareApi is exported if not already
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for exporting a D1 database
|
|
5
|
+
*/
|
|
6
|
+
export interface ExportD1DatabaseOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The ID of the D1 database to export
|
|
9
|
+
*/
|
|
10
|
+
databaseId: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Optional dump options to control what is exported
|
|
14
|
+
*/
|
|
15
|
+
dumpOptions?: {
|
|
16
|
+
/**
|
|
17
|
+
* Optional list of tables to export
|
|
18
|
+
*/
|
|
19
|
+
tables?: string[];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether to exclude schema in the export
|
|
23
|
+
*/
|
|
24
|
+
no_schema?: boolean;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether to exclude data in the export
|
|
28
|
+
*/
|
|
29
|
+
no_data?: boolean;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Response from the export D1 database operation
|
|
35
|
+
*/
|
|
36
|
+
export interface ExportD1DatabaseResult {
|
|
37
|
+
/**
|
|
38
|
+
* The filename of the exported database
|
|
39
|
+
*/
|
|
40
|
+
filename: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The signed URL for downloading the exported database
|
|
44
|
+
*/
|
|
45
|
+
signed_url: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The status of the export operation
|
|
49
|
+
*/
|
|
50
|
+
status: "complete";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether the export operation was successful
|
|
54
|
+
*/
|
|
55
|
+
success: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Error response from Cloudflare API
|
|
60
|
+
*/
|
|
61
|
+
interface CloudflareErrorResponse {
|
|
62
|
+
error?: string;
|
|
63
|
+
errors?: Array<{
|
|
64
|
+
message: string;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Response during polling for D1 database export
|
|
70
|
+
*/
|
|
71
|
+
interface ExportPollingResponse {
|
|
72
|
+
/**
|
|
73
|
+
* Bookmark for continuing the polling process
|
|
74
|
+
*/
|
|
75
|
+
at_bookmark?: string;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Error message if any
|
|
79
|
+
*/
|
|
80
|
+
error?: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Progress messages from the export process
|
|
84
|
+
*/
|
|
85
|
+
messages: string[];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Result data if export is complete
|
|
89
|
+
*/
|
|
90
|
+
result?: {
|
|
91
|
+
filename: string;
|
|
92
|
+
signed_url: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Status of the export process
|
|
97
|
+
*/
|
|
98
|
+
status: "complete" | "error" | "processing";
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether the current polling request was successful
|
|
102
|
+
*/
|
|
103
|
+
success: boolean;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Type of operation
|
|
107
|
+
*/
|
|
108
|
+
type: "export";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Initiates an export of a Cloudflare D1 database and returns a download URL.
|
|
113
|
+
* Implements recursive polling with bookmark handling, similar to wrangler.
|
|
114
|
+
* Waits indefinitely until the export process completes or fails.
|
|
115
|
+
*
|
|
116
|
+
* Based on Cloudflare API:
|
|
117
|
+
* https://developers.cloudflare.com/api/resources/d1/subresources/database/methods/export/
|
|
118
|
+
* and wrangler implementation.
|
|
119
|
+
*
|
|
120
|
+
* @param options Options including the database ID and optional dump parameters.
|
|
121
|
+
* @returns An object containing the download URL, filename, and status upon completion.
|
|
122
|
+
* @throws Will throw an error if the API call fails or the export process reports an error.
|
|
123
|
+
*/
|
|
124
|
+
export async function exportD1Database(
|
|
125
|
+
api: CloudflareApi,
|
|
126
|
+
options: ExportD1DatabaseOptions,
|
|
127
|
+
currentBookmark?: string,
|
|
128
|
+
): Promise<ExportD1DatabaseResult> {
|
|
129
|
+
const response = await api.post(
|
|
130
|
+
`/accounts/${api.accountId}/d1/database/${options.databaseId}/export`,
|
|
131
|
+
{
|
|
132
|
+
output_format: "polling",
|
|
133
|
+
dump_options: options.dumpOptions || {},
|
|
134
|
+
current_bookmark: currentBookmark,
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const errorData = (await response.json()) as CloudflareErrorResponse;
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Failed to export D1 database: ${errorData.error || errorData.errors?.[0]?.message || "Unknown error"}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { result: data } = (await response.json()) as {
|
|
146
|
+
result: ExportPollingResponse;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (!data.success) {
|
|
150
|
+
throw new Error(data.error || "Unknown error during D1 export");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Log messages for visibility
|
|
154
|
+
if (data.messages && data.messages.length > 0) {
|
|
155
|
+
for (const message of data.messages) {
|
|
156
|
+
console.log(`D1 Export: ${message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (data.status === "complete" && data.result) {
|
|
161
|
+
return {
|
|
162
|
+
filename: data.result.filename,
|
|
163
|
+
signed_url: data.result.signed_url,
|
|
164
|
+
status: data.status,
|
|
165
|
+
success: data.success,
|
|
166
|
+
};
|
|
167
|
+
} else if (data.status === "error") {
|
|
168
|
+
throw new Error(data.error || "Error during D1 export");
|
|
169
|
+
} else {
|
|
170
|
+
// Continue polling with bookmark
|
|
171
|
+
return exportD1Database(api, options, data.at_bookmark);
|
|
172
|
+
}
|
|
173
|
+
}
|