alchemy 0.15.9 → 0.15.11

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.
Files changed (41) hide show
  1. package/lib/aws/function.d.ts +4 -0
  2. package/lib/aws/function.d.ts.map +1 -1
  3. package/lib/aws/function.js +2 -0
  4. package/lib/aws/function.js.map +1 -1
  5. package/lib/cloudflare/d1-clone.d.ts +55 -0
  6. package/lib/cloudflare/d1-clone.d.ts.map +1 -0
  7. package/lib/cloudflare/d1-clone.js +45 -0
  8. package/lib/cloudflare/d1-clone.js.map +1 -0
  9. package/lib/cloudflare/d1-database.d.ts +36 -1
  10. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  11. package/lib/cloudflare/d1-database.js +73 -9
  12. package/lib/cloudflare/d1-database.js.map +1 -1
  13. package/lib/cloudflare/d1-export.d.ts +63 -0
  14. package/lib/cloudflare/d1-export.d.ts.map +1 -0
  15. package/lib/cloudflare/d1-export.js +50 -0
  16. package/lib/cloudflare/d1-export.js.map +1 -0
  17. package/lib/cloudflare/d1-import.d.ts +112 -0
  18. package/lib/cloudflare/d1-import.d.ts.map +1 -0
  19. package/lib/cloudflare/d1-import.js +143 -0
  20. package/lib/cloudflare/d1-import.js.map +1 -0
  21. package/lib/cloudflare/index.d.ts +3 -0
  22. package/lib/cloudflare/index.d.ts.map +1 -1
  23. package/lib/cloudflare/index.js +3 -0
  24. package/lib/cloudflare/index.js.map +1 -1
  25. package/lib/stripe/price.d.ts +11 -0
  26. package/lib/stripe/price.d.ts.map +1 -1
  27. package/lib/stripe/price.js +4 -0
  28. package/lib/stripe/price.js.map +1 -1
  29. package/lib/util/sleep.d.ts +7 -0
  30. package/lib/util/sleep.d.ts.map +1 -0
  31. package/lib/util/sleep.js +9 -0
  32. package/lib/util/sleep.js.map +1 -0
  33. package/package.json +1 -1
  34. package/src/aws/function.ts +7 -0
  35. package/src/cloudflare/d1-clone.ts +113 -0
  36. package/src/cloudflare/d1-database.ts +99 -15
  37. package/src/cloudflare/d1-export.ts +173 -0
  38. package/src/cloudflare/d1-import.ts +406 -0
  39. package/src/cloudflare/index.ts +3 -0
  40. package/src/stripe/price.ts +17 -0
  41. package/src/util/sleep.ts +8 -0
@@ -0,0 +1,406 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { CloudflareApi } from "./api.js";
3
+
4
+ /**
5
+ * Options for importing SQL into a D1 database
6
+ */
7
+ export interface ImportD1DatabaseOptions {
8
+ /**
9
+ * The ID of the D1 database to import into
10
+ */
11
+ databaseId: string;
12
+
13
+ /**
14
+ * The SQL file data to upload
15
+ */
16
+ sqlData: Blob | BufferSource | string;
17
+
18
+ /**
19
+ * Optional custom filename for the imported SQL
20
+ */
21
+ filename?: string;
22
+ }
23
+
24
+ /**
25
+ * Response from the import D1 database operation
26
+ */
27
+ export interface ImportD1DatabaseResult {
28
+ /**
29
+ * The filename of the imported database
30
+ */
31
+ filename: string;
32
+
33
+ /**
34
+ * Additional result information from the import operation
35
+ */
36
+ result: {
37
+ /**
38
+ * Final bookmark used during import
39
+ */
40
+ final_bookmark?: string;
41
+
42
+ /**
43
+ * Metadata about the import operation
44
+ */
45
+ meta: {
46
+ /**
47
+ * Whether the database was changed
48
+ */
49
+ changed_db: boolean;
50
+
51
+ /**
52
+ * Number of changes made
53
+ */
54
+ changes: number;
55
+
56
+ /**
57
+ * Duration of the operation
58
+ */
59
+ duration: number;
60
+
61
+ /**
62
+ * ID of the last row
63
+ */
64
+ last_row_id: number;
65
+
66
+ /**
67
+ * Number of rows read
68
+ */
69
+ rows_read: number;
70
+
71
+ /**
72
+ * Number of rows written
73
+ */
74
+ rows_written: number;
75
+
76
+ /**
77
+ * Whether served by primary
78
+ */
79
+ served_by_primary: boolean;
80
+
81
+ /**
82
+ * Region that served the request
83
+ */
84
+ served_by_region: string;
85
+
86
+ /**
87
+ * Size after import
88
+ */
89
+ size_after: number;
90
+
91
+ /**
92
+ * Timing information
93
+ */
94
+ timings: {
95
+ /**
96
+ * SQL execution duration in milliseconds
97
+ */
98
+ sql_duration_ms: number;
99
+ };
100
+ };
101
+
102
+ /**
103
+ * Number of queries executed
104
+ */
105
+ num_queries: number;
106
+ };
107
+
108
+ /**
109
+ * The status of the import operation
110
+ */
111
+ status: "complete";
112
+
113
+ /**
114
+ * Whether the import operation was successful
115
+ */
116
+ success: boolean;
117
+ }
118
+
119
+ /**
120
+ * Error response from Cloudflare API
121
+ */
122
+ interface CloudflareErrorResponse {
123
+ error?: string;
124
+ errors?: Array<{
125
+ message: string;
126
+ }>;
127
+ }
128
+
129
+ /**
130
+ * Response during polling for D1 database import
131
+ */
132
+ interface ImportPollingResponse {
133
+ /**
134
+ * Bookmark for continuing the polling process
135
+ */
136
+ at_bookmark?: string;
137
+
138
+ /**
139
+ * Error message if any
140
+ */
141
+ error?: string;
142
+
143
+ /**
144
+ * The filename of the imported SQL
145
+ */
146
+ filename?: string;
147
+
148
+ /**
149
+ * Progress messages from the export process
150
+ */
151
+ messages: string[];
152
+
153
+ /**
154
+ * Result data if import is complete
155
+ */
156
+ result?: {
157
+ final_bookmark?: string;
158
+ meta: {
159
+ changed_db: boolean;
160
+ changes: number;
161
+ duration: number;
162
+ last_row_id: number;
163
+ rows_read: number;
164
+ rows_written: number;
165
+ served_by_primary: boolean;
166
+ served_by_region: string;
167
+ size_after: number;
168
+ timings: {
169
+ sql_duration_ms: number;
170
+ };
171
+ };
172
+ num_queries: number;
173
+ };
174
+
175
+ /**
176
+ * Status of the import process
177
+ */
178
+ status: "complete" | "error" | "processing";
179
+
180
+ /**
181
+ * Whether the current polling request was successful
182
+ */
183
+ success: boolean;
184
+
185
+ /**
186
+ * Type of operation
187
+ */
188
+ type: "import";
189
+
190
+ /**
191
+ * The URL to upload the SQL file to
192
+ */
193
+ upload_url?: string;
194
+ }
195
+
196
+ /**
197
+ * Cloudflare API response structure
198
+ */
199
+ interface CloudflareApiResponse<T> {
200
+ result: T;
201
+ success: boolean;
202
+ errors: Array<{
203
+ message: string;
204
+ }>;
205
+ messages: Array<{
206
+ message: string;
207
+ }>;
208
+ }
209
+
210
+ /**
211
+ * Calculates MD5 hash for a string
212
+ */
213
+ function getMd5Hash(data: string): string {
214
+ return createHash("md5").update(data).digest("hex");
215
+ }
216
+
217
+ /**
218
+ * Initiates an import of SQL into a Cloudflare D1 database.
219
+ * Implements a flow to get an upload URL, upload the SQL file, and poll for status updates.
220
+ *
221
+ * Based on Cloudflare API:
222
+ * https://developers.cloudflare.com/api/resources/d1/subresources/database/methods/import/
223
+ *
224
+ * @param api The CloudflareApi instance to use for requests
225
+ * @param options Options including the database ID and SQL data to import
226
+ * @returns An object containing the import results and status upon completion
227
+ * @throws Will throw an error if the API call fails or the import process reports an error
228
+ */
229
+ export async function importD1Database(
230
+ api: CloudflareApi,
231
+ options: ImportD1DatabaseOptions,
232
+ ): Promise<ImportD1DatabaseResult> {
233
+ // Convert data to string for hashing if it's not already
234
+ let sqlDataString: string;
235
+ if (typeof options.sqlData === "string") {
236
+ sqlDataString = options.sqlData;
237
+ } else if (options.sqlData instanceof Blob) {
238
+ sqlDataString = await options.sqlData.text();
239
+ } else {
240
+ // BufferSource (ArrayBuffer, etc.)
241
+ sqlDataString = new TextDecoder().decode(options.sqlData);
242
+ }
243
+
244
+ // Calculate MD5 hash of SQL data
245
+ const etag = getMd5Hash(sqlDataString);
246
+
247
+ // Step 1: Initialize the import process to get an upload URL
248
+ const initResponse = await api.post(
249
+ `/accounts/${api.accountId}/d1/database/${options.databaseId}/import`,
250
+ {
251
+ action: "init",
252
+ etag,
253
+ },
254
+ );
255
+
256
+ if (!initResponse.ok) {
257
+ const errorData = (await initResponse.json()) as CloudflareErrorResponse;
258
+ throw new Error(
259
+ `Failed to initialize D1 database import: ${errorData.error || errorData.errors?.[0]?.message || "Unknown error"}`,
260
+ );
261
+ }
262
+
263
+ const initData =
264
+ (await initResponse.json()) as CloudflareApiResponse<ImportPollingResponse>;
265
+
266
+ const uploadResult = initData.result;
267
+ if (!uploadResult.success || !uploadResult.upload_url) {
268
+ throw new Error(
269
+ uploadResult.error || "Failed to get upload URL for D1 import",
270
+ );
271
+ }
272
+
273
+ // Step 2: Upload the SQL file
274
+ await uploadSqlFile(
275
+ uploadResult.upload_url,
276
+ options.sqlData,
277
+ options.filename || uploadResult.filename,
278
+ );
279
+
280
+ // Step 3: Ingest the uploaded file
281
+ const ingestResponse = await api.post(
282
+ `/accounts/${api.accountId}/d1/database/${options.databaseId}/import`,
283
+ {
284
+ action: "ingest",
285
+ etag,
286
+ filename: uploadResult.filename,
287
+ },
288
+ );
289
+
290
+ if (!ingestResponse.ok) {
291
+ const errorData = (await ingestResponse.json()) as CloudflareErrorResponse;
292
+ throw new Error(
293
+ `Failed to ingest D1 database upload: ${errorData.error || errorData.errors?.[0]?.message || "Unknown error"}`,
294
+ );
295
+ }
296
+
297
+ const ingestData =
298
+ (await ingestResponse.json()) as CloudflareApiResponse<ImportPollingResponse>;
299
+
300
+ const ingestResult = ingestData.result;
301
+ if (!ingestResult.success) {
302
+ throw new Error(
303
+ ingestResult.error || "Failed to start ingestion for D1 import",
304
+ );
305
+ }
306
+
307
+ if (!ingestResult.at_bookmark) {
308
+ throw new Error("Ingest response did not contain a bookmark for polling");
309
+ }
310
+
311
+ // Step 4: Poll for status
312
+ return pollImportStatus(api, options.databaseId, ingestResult.at_bookmark);
313
+ }
314
+
315
+ /**
316
+ * Uploads an SQL file to the provided URL
317
+ */
318
+ async function uploadSqlFile(
319
+ uploadUrl: string,
320
+ sqlData: Blob | BufferSource | string,
321
+ filename?: string,
322
+ ): Promise<void> {
323
+ // Convert data to the right format if needed
324
+ let blob: Blob;
325
+ if (typeof sqlData === "string") {
326
+ blob = new Blob([sqlData], { type: "application/sql" });
327
+ } else if (sqlData instanceof Blob) {
328
+ blob = sqlData;
329
+ } else {
330
+ // BufferSource (ArrayBuffer, etc.)
331
+ blob = new Blob([sqlData], { type: "application/sql" });
332
+ }
333
+
334
+ // Create a FormData object with the file
335
+ const formData = new FormData();
336
+ formData.append("file", blob, filename || "import.sql");
337
+
338
+ const response = await fetch(uploadUrl, {
339
+ method: "PUT",
340
+ body: blob,
341
+ headers: {
342
+ "Content-Type": "application/sql",
343
+ },
344
+ });
345
+
346
+ if (!response.ok) {
347
+ throw new Error(`Failed to upload SQL file: ${response.statusText}`);
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Polls for the import status with the provided bookmark
353
+ */
354
+ async function pollImportStatus(
355
+ api: CloudflareApi,
356
+ databaseId: string,
357
+ bookmark?: string,
358
+ ): Promise<ImportD1DatabaseResult> {
359
+ if (!bookmark) {
360
+ throw new Error("Cannot poll an import without a bookmark");
361
+ }
362
+
363
+ const response = await api.post(
364
+ `/accounts/${api.accountId}/d1/database/${databaseId}/import`,
365
+ {
366
+ action: "poll",
367
+ current_bookmark: bookmark,
368
+ },
369
+ );
370
+
371
+ if (!response.ok) {
372
+ const errorData = (await response.json()) as CloudflareErrorResponse;
373
+ throw new Error(
374
+ `Failed to poll D1 import status: ${errorData.error || errorData.errors?.[0]?.message || "Unknown error"}`,
375
+ );
376
+ }
377
+
378
+ const data = (
379
+ (await response.json()) as CloudflareApiResponse<ImportPollingResponse>
380
+ ).result;
381
+
382
+ if (!data.success) {
383
+ throw new Error(data.error || "Unknown error during D1 import");
384
+ }
385
+
386
+ // Log messages for visibility
387
+ if (data.messages && data.messages.length > 0) {
388
+ for (const message of data.messages) {
389
+ console.log(`D1 Import: ${message}`);
390
+ }
391
+ }
392
+
393
+ if (data.status === "complete" && data.result) {
394
+ return {
395
+ filename: data.filename || "import.sql",
396
+ result: data.result,
397
+ status: data.status,
398
+ success: data.success,
399
+ };
400
+ } else if (data.status === "error") {
401
+ throw new Error(data.error || "Error during D1 import");
402
+ } else {
403
+ // Continue polling with bookmark
404
+ return pollImportStatus(api, databaseId, data.at_bookmark);
405
+ }
406
+ }
@@ -11,7 +11,10 @@ export * from "./bucket.js";
11
11
  export * from "./bundle/external.js";
12
12
  export * from "./bundle/local-dev-cloudflare-shim.js";
13
13
  export * from "./custom-domain.js";
14
+ export * from "./d1-clone.js";
14
15
  export * from "./d1-database.js";
16
+ export * from "./d1-export.js";
17
+ export * from "./d1-import.js";
15
18
  export * from "./dns-records.js";
16
19
  export * from "./durable-object-namespace.js";
17
20
  export * from "./hyperdrive.js";
@@ -93,6 +93,19 @@ export interface PriceProps {
93
93
  * One of inclusive, exclusive, or unspecified.
94
94
  */
95
95
  taxBehavior?: TaxBehavior;
96
+
97
+ /**
98
+ * A lookup key to uniquely identify this price.
99
+ * This key can be used to retrieve the price object without needing its ID.
100
+ * It must be unique across all prices in your Stripe account, otherwise creation will fail.
101
+ * If you want to transfer the lookup key from another price, set `transferLookupKey` to true
102
+ */
103
+ lookupKey?: string;
104
+
105
+ /**
106
+ * If set to true, will atomically transfer the lookup key from an existing price to this price.
107
+ */
108
+ transferLookupKey?: boolean;
96
109
  }
97
110
 
98
111
  /**
@@ -213,6 +226,8 @@ export const Price = Resource(
213
226
  active: props.active,
214
227
  metadata: props.metadata,
215
228
  nickname: props.nickname,
229
+ lookup_key: props.lookupKey,
230
+ transfer_lookup_key: props.transferLookupKey,
216
231
  });
217
232
  } else {
218
233
  // Create new price
@@ -224,6 +239,8 @@ export const Price = Resource(
224
239
  nickname: props.nickname,
225
240
  metadata: props.metadata,
226
241
  tax_behavior: props.taxBehavior,
242
+ lookup_key: props.lookupKey,
243
+ transfer_lookup_key: props.transferLookupKey,
227
244
  };
228
245
 
229
246
  // Add unit amount fields
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Pauses execution for a specified number of milliseconds.
3
+ * @param ms The number of milliseconds to sleep.
4
+ * @returns A promise that resolves after the specified time.
5
+ */
6
+ export function sleep(ms: number): Promise<void> {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }