alchemy 2.0.0-beta.21 → 2.0.0-beta.22

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 (35) hide show
  1. package/bin/alchemy.js +1 -1
  2. package/bin/alchemy.js.map +1 -1
  3. package/lib/Cloudflare/D1/D1Clone.d.ts +13 -0
  4. package/lib/Cloudflare/D1/D1Clone.d.ts.map +1 -0
  5. package/lib/Cloudflare/D1/D1Clone.js +31 -0
  6. package/lib/Cloudflare/D1/D1Clone.js.map +1 -0
  7. package/lib/Cloudflare/D1/D1Database.d.ts +150 -5
  8. package/lib/Cloudflare/D1/D1Database.d.ts.map +1 -1
  9. package/lib/Cloudflare/D1/D1Database.js +253 -0
  10. package/lib/Cloudflare/D1/D1Database.js.map +1 -1
  11. package/lib/Cloudflare/D1/D1Export.d.ts +24 -0
  12. package/lib/Cloudflare/D1/D1Export.d.ts.map +1 -0
  13. package/lib/Cloudflare/D1/D1Export.js +34 -0
  14. package/lib/Cloudflare/D1/D1Export.js.map +1 -0
  15. package/lib/Cloudflare/D1/D1Import.d.ts +21 -0
  16. package/lib/Cloudflare/D1/D1Import.d.ts.map +1 -0
  17. package/lib/Cloudflare/D1/D1Import.js +87 -0
  18. package/lib/Cloudflare/D1/D1Import.js.map +1 -0
  19. package/lib/Cloudflare/D1/D1Migrations.d.ts +16 -0
  20. package/lib/Cloudflare/D1/D1Migrations.d.ts.map +1 -0
  21. package/lib/Cloudflare/D1/D1Migrations.js +110 -0
  22. package/lib/Cloudflare/D1/D1Migrations.js.map +1 -0
  23. package/lib/Cloudflare/D1/D1SqlFile.d.ts +20 -0
  24. package/lib/Cloudflare/D1/D1SqlFile.d.ts.map +1 -0
  25. package/lib/Cloudflare/D1/D1SqlFile.js +46 -0
  26. package/lib/Cloudflare/D1/D1SqlFile.js.map +1 -0
  27. package/lib/Cloudflare/Providers.d.ts +1 -1
  28. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/Cloudflare/D1/D1Clone.ts +42 -0
  31. package/src/Cloudflare/D1/D1Database.ts +386 -3
  32. package/src/Cloudflare/D1/D1Export.ts +70 -0
  33. package/src/Cloudflare/D1/D1Import.ts +156 -0
  34. package/src/Cloudflare/D1/D1Migrations.ts +229 -0
  35. package/src/Cloudflare/D1/D1SqlFile.ts +57 -0
@@ -7,6 +7,10 @@ import * as Provider from "../../Provider.ts";
7
7
  import { Resource } from "../../Resource.ts";
8
8
  import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
9
9
  import type { Providers } from "../Providers.ts";
10
+ import { cloneD1Database } from "./D1Clone.ts";
11
+ import { importD1Database } from "./D1Import.ts";
12
+ import { applyMigrations } from "./D1Migrations.ts";
13
+ import { listSqlFiles, readSqlFile } from "./D1SqlFile.ts";
10
14
 
11
15
  export type Jurisdiction = "default" | "eu" | "fedramp";
12
16
  export type PrimaryLocationHint =
@@ -17,6 +21,13 @@ export type PrimaryLocationHint =
17
21
  | "apac"
18
22
  | "oc";
19
23
 
24
+ const DEFAULT_MIGRATIONS_TABLE = "d1_migrations";
25
+
26
+ export type CloneSource =
27
+ | D1Database
28
+ | { databaseId: string }
29
+ | { name: string };
30
+
20
31
  export type DatabaseProps = {
21
32
  /**
22
33
  * Name of the database. If omitted, a unique name will be generated.
@@ -24,20 +35,72 @@ export type DatabaseProps = {
24
35
  */
25
36
  name?: string;
26
37
  /**
27
- * Optional primary location hint for the database.
38
+ * Region in which the primary copy of the data is stored. Cannot be
39
+ * changed after creation — updating this property triggers a replacement.
40
+ *
41
+ * - `wnam` — Western North America
42
+ * - `enam` — Eastern North America
43
+ * - `weur` — Western Europe
44
+ * - `eeur` — Eastern Europe
45
+ * - `apac` — Asia Pacific
46
+ * - `oc` — Oceania
28
47
  */
29
48
  primaryLocationHint?: PrimaryLocationHint;
30
49
  /**
31
- * Read replication configuration. Only mutable property during updates.
50
+ * Read replication configuration. The only mutable property after
51
+ * creation; toggling `mode` triggers an in-place update.
52
+ *
53
+ * @default { mode: "disabled" }
32
54
  */
33
55
  readReplication?: {
34
56
  mode: "auto" | "disabled";
35
57
  };
36
58
  /**
37
- * Jurisdiction where data is guaranteed to be stored.
59
+ * Jurisdiction in which the database data is guaranteed to be stored.
60
+ * Cannot be changed after creation.
61
+ *
38
62
  * @default "default"
39
63
  */
40
64
  jurisdiction?: Jurisdiction;
65
+ /**
66
+ * Directory containing `.sql` migration files. Files are sorted by their
67
+ * numeric prefix (e.g. `0001_init.sql`, `0002_add_users.sql`) and applied
68
+ * in order. Pending migrations are detected on each deploy and applied as
69
+ * part of `update`. Equivalent to wrangler's `migrations_dir`.
70
+ */
71
+ migrationsDir?: string;
72
+ /**
73
+ * Name of the table used to track applied migrations. Useful for
74
+ * compatibility with frameworks that expect a specific name (e.g.
75
+ * `drizzle_migrations`).
76
+ *
77
+ * The table schema is the wrangler-compatible
78
+ * `(id TEXT PRIMARY KEY, name TEXT, applied_at TEXT)`. A pre-existing
79
+ * legacy 2-column table is migrated in place.
80
+ *
81
+ * @default "d1_migrations"
82
+ */
83
+ migrationsTable?: string;
84
+ /**
85
+ * Paths to additional `.sql` files to import after migrations are
86
+ * applied. Each file is uploaded via Cloudflare's D1 import API and
87
+ * hashed; only files whose contents change are re-imported on subsequent
88
+ * deploys.
89
+ *
90
+ * @see https://developers.cloudflare.com/d1/best-practices/import-export-data/
91
+ */
92
+ importFiles?: string[];
93
+ /**
94
+ * Clone data from an existing database during creation by exporting the
95
+ * source and importing it into the new database. Only applied during the
96
+ * `create` phase.
97
+ *
98
+ * Accepts:
99
+ * - another `D1Database` resource (uses its `databaseId`)
100
+ * - `{ databaseId }` — clone by explicit UUID
101
+ * - `{ name }` — look up the source by name and clone it
102
+ */
103
+ clone?: CloneSource;
41
104
  };
42
105
 
43
106
  export type D1Database = Resource<
@@ -49,6 +112,10 @@ export type D1Database = Resource<
49
112
  jurisdiction: Jurisdiction;
50
113
  readReplication: { mode: "auto" | "disabled" } | undefined;
51
114
  accountId: string;
115
+ migrationsDir: string | undefined;
116
+ migrationsTable: string | undefined;
117
+ migrationsHashes: Record<string, string>;
118
+ importHashes: Record<string, string>;
52
119
  },
53
120
  never,
54
121
  Providers
@@ -67,12 +134,95 @@ export type D1Database = Resource<
67
134
  * ```
68
135
  *
69
136
  * @example Database with location hint
137
+ * The primary copy of the data is stored in the chosen region; reads can be
138
+ * served closer to users when read replication is enabled.
70
139
  * ```typescript
71
140
  * const db = yield* Cloudflare.D1Database("my-db", {
72
141
  * primaryLocationHint: "wnam",
73
142
  * });
74
143
  * ```
75
144
  *
145
+ * @example Database with read replication
146
+ * Read replication is the only mutable property after creation — toggling it
147
+ * triggers an update rather than a replacement.
148
+ * ```typescript
149
+ * const db = yield* Cloudflare.D1Database("my-db", {
150
+ * readReplication: { mode: "auto" },
151
+ * });
152
+ * ```
153
+ *
154
+ * @example Database in a specific jurisdiction
155
+ * ```typescript
156
+ * const db = yield* Cloudflare.D1Database("my-db", {
157
+ * jurisdiction: "eu",
158
+ * });
159
+ * ```
160
+ *
161
+ * @section Migrations
162
+ * Point `migrationsDir` at a folder of `.sql` files. Files are sorted by
163
+ * numeric prefix (e.g. `0001_`, `0002_`) and applied in order. Already-applied
164
+ * migrations are skipped on subsequent deploys; new files are detected
165
+ * automatically and applied as part of the next update.
166
+ *
167
+ * Migration tracking uses the wrangler-compatible
168
+ * `(id TEXT PRIMARY KEY, name TEXT, applied_at TEXT)` schema. The resource
169
+ * also detects and upgrades a legacy 2-column tracking table in place if one
170
+ * already exists.
171
+ *
172
+ * @example Apply migrations from a directory
173
+ * ```typescript
174
+ * const db = yield* Cloudflare.D1Database("my-db", {
175
+ * migrationsDir: "./migrations",
176
+ * });
177
+ * ```
178
+ *
179
+ * @example Custom migrations table (e.g. for Drizzle)
180
+ * ```typescript
181
+ * const db = yield* Cloudflare.D1Database("my-db", {
182
+ * migrationsDir: "./migrations",
183
+ * migrationsTable: "drizzle_migrations",
184
+ * });
185
+ * ```
186
+ *
187
+ * @section Importing SQL
188
+ * Use `importFiles` to seed the database with raw `.sql` files via Cloudflare's
189
+ * D1 import API. Each file is hashed; only files whose contents change are
190
+ * re-imported on subsequent deploys.
191
+ *
192
+ * @example Seed a database with SQL files
193
+ * ```typescript
194
+ * const db = yield* Cloudflare.D1Database("my-db", {
195
+ * importFiles: ["./seed/users.sql", "./seed/posts.sql"],
196
+ * });
197
+ * ```
198
+ *
199
+ * @section Cloning a Database
200
+ * `clone` performs a full export → import from a source database during
201
+ * creation. It accepts a `D1Database` resource, a `{ databaseId }`, or a
202
+ * `{ name }` to look up by name.
203
+ *
204
+ * @example Clone by passing the source resource directly
205
+ * ```typescript
206
+ * const source = yield* Cloudflare.D1Database("source-db");
207
+ * const cloned = yield* Cloudflare.D1Database("cloned-db", {
208
+ * clone: source,
209
+ * });
210
+ * ```
211
+ *
212
+ * @example Clone by databaseId
213
+ * ```typescript
214
+ * const cloned = yield* Cloudflare.D1Database("cloned-db", {
215
+ * clone: { databaseId: "abcdef12-3456-7890-abcd-ef1234567890" },
216
+ * });
217
+ * ```
218
+ *
219
+ * @example Clone by name
220
+ * ```typescript
221
+ * const cloned = yield* Cloudflare.D1Database("cloned-db", {
222
+ * clone: { name: "source-db" },
223
+ * });
224
+ * ```
225
+ *
76
226
  * @section Binding to a Worker
77
227
  * @example Using D1 inside a Worker
78
228
  * ```typescript
@@ -88,6 +238,8 @@ export type D1Database = Resource<
88
238
  * .bind(newId, name)
89
239
  * .run();
90
240
  * ```
241
+ *
242
+ * @see https://developers.cloudflare.com/d1/
91
243
  */
92
244
  export const D1Database = Resource<D1Database>("Cloudflare.D1Database");
93
245
 
@@ -101,6 +253,8 @@ export const DatabaseProvider = () =>
101
253
  const patchDb = yield* d1.patchDatabase;
102
254
  const deleteDb = yield* d1.deleteDatabase;
103
255
  const listDbs = yield* d1.listDatabases;
256
+ // rootDir for resolving relative `importFiles` paths
257
+ const rootDir = process.cwd();
104
258
 
105
259
  const createDatabaseName = (id: string, name: string | undefined) =>
106
260
  Effect.gen(function* () {
@@ -136,6 +290,34 @@ export const DatabaseProvider = () =>
136
290
  if (oldReplicationMode !== newReplicationMode) {
137
291
  return { action: "update" } as const;
138
292
  }
293
+ // Detect migration/import file drift.
294
+ if (news.migrationsDir) {
295
+ const newHashes = yield* hashMigrations(news.migrationsDir);
296
+ const oldHashes = output?.migrationsHashes ?? {};
297
+ if (!recordsEqual(newHashes, oldHashes)) {
298
+ return { action: "update" } as const;
299
+ }
300
+ if (
301
+ (news.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE) !==
302
+ (output?.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE)
303
+ ) {
304
+ return { action: "update" } as const;
305
+ }
306
+ } else if (
307
+ output?.migrationsHashes &&
308
+ Object.keys(output.migrationsHashes).length > 0
309
+ ) {
310
+ // migrationsDir was removed but state still tracks migrations: nothing
311
+ // to do remotely (we never un-apply), but no diff needed either.
312
+ }
313
+ if (news.importFiles?.length) {
314
+ const newHashes = yield* hashImports(news.importFiles, rootDir);
315
+ const oldHashes = output?.importHashes ?? {};
316
+ if (!recordsEqual(newHashes, oldHashes)) {
317
+ return { action: "update" } as const;
318
+ }
319
+ }
320
+ return undefined;
139
321
  }),
140
322
  read: Effect.fn(function* ({ id, output, olds }) {
141
323
  if (output?.databaseId) {
@@ -149,6 +331,10 @@ export const DatabaseProvider = () =>
149
331
  jurisdiction: output.jurisdiction,
150
332
  readReplication: db.readReplication ?? undefined,
151
333
  accountId: output.accountId,
334
+ migrationsDir: output.migrationsDir,
335
+ migrationsTable: output.migrationsTable,
336
+ migrationsHashes: output.migrationsHashes,
337
+ importHashes: output.importHashes,
152
338
  })),
153
339
  Effect.catchTag("DatabaseNotFound", () =>
154
340
  Effect.succeed(undefined),
@@ -165,6 +351,10 @@ export const DatabaseProvider = () =>
165
351
  jurisdiction: (olds?.jurisdiction ?? "default") as Jurisdiction,
166
352
  readReplication: olds?.readReplication,
167
353
  accountId,
354
+ migrationsDir: olds?.migrationsDir,
355
+ migrationsTable: olds?.migrationsTable,
356
+ migrationsHashes: {},
357
+ importHashes: {},
168
358
  };
169
359
  }
170
360
  return undefined;
@@ -202,12 +392,49 @@ export const DatabaseProvider = () =>
202
392
  });
203
393
  }
204
394
 
395
+ if (news.clone) {
396
+ const sourceId = yield* resolveCloneSource(
397
+ news.clone,
398
+ accountId,
399
+ listDbs,
400
+ );
401
+ yield* cloneD1Database({
402
+ accountId,
403
+ sourceDatabaseId: sourceId,
404
+ targetDatabaseId: databaseId,
405
+ });
406
+ }
407
+
408
+ const migrationsTable =
409
+ news.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE;
410
+ const migrationsHashes = news.migrationsDir
411
+ ? yield* runMigrations(
412
+ accountId,
413
+ databaseId,
414
+ news.migrationsDir,
415
+ migrationsTable,
416
+ )
417
+ : {};
418
+ const importHashes = news.importFiles?.length
419
+ ? yield* runImports(
420
+ accountId,
421
+ databaseId,
422
+ news.importFiles,
423
+ rootDir,
424
+ {},
425
+ )
426
+ : {};
427
+
205
428
  return {
206
429
  databaseId,
207
430
  databaseName: db.name ?? name,
208
431
  jurisdiction,
209
432
  readReplication: news.readReplication,
210
433
  accountId,
434
+ migrationsDir: news.migrationsDir,
435
+ migrationsTable: news.migrationsDir ? migrationsTable : undefined,
436
+ migrationsHashes,
437
+ importHashes,
211
438
  };
212
439
  }),
213
440
  update: Effect.fn(function* ({ news = {}, output }) {
@@ -217,12 +444,39 @@ export const DatabaseProvider = () =>
217
444
  databaseId: output.databaseId,
218
445
  readReplication: { mode: replicationMode },
219
446
  });
447
+
448
+ const migrationsTable =
449
+ news.migrationsTable ??
450
+ output.migrationsTable ??
451
+ DEFAULT_MIGRATIONS_TABLE;
452
+ const migrationsHashes = news.migrationsDir
453
+ ? yield* runMigrations(
454
+ output.accountId,
455
+ output.databaseId,
456
+ news.migrationsDir,
457
+ migrationsTable,
458
+ )
459
+ : output.migrationsHashes;
460
+ const importHashes = news.importFiles?.length
461
+ ? yield* runImports(
462
+ output.accountId,
463
+ output.databaseId,
464
+ news.importFiles,
465
+ rootDir,
466
+ output.importHashes ?? {},
467
+ )
468
+ : {};
469
+
220
470
  return {
221
471
  databaseId: updated.uuid ?? output.databaseId,
222
472
  databaseName: updated.name ?? output.databaseName,
223
473
  jurisdiction: output.jurisdiction,
224
474
  readReplication: news.readReplication,
225
475
  accountId: output.accountId,
476
+ migrationsDir: news.migrationsDir,
477
+ migrationsTable: news.migrationsDir ? migrationsTable : undefined,
478
+ migrationsHashes,
479
+ importHashes,
226
480
  };
227
481
  }),
228
482
  delete: Effect.fn(function* ({ output }) {
@@ -234,3 +488,132 @@ export const DatabaseProvider = () =>
234
488
  };
235
489
  }),
236
490
  );
491
+
492
+ /**
493
+ * Resolve a clone source spec into a concrete database UUID. Looks up by
494
+ * name through `listDatabases` when only a name is provided.
495
+ */
496
+ const resolveCloneSource = (
497
+ source: CloneSource,
498
+ accountId: string,
499
+ listDbs: (input: {
500
+ accountId: string;
501
+ name?: string;
502
+ }) => Effect.Effect<d1.ListDatabasesResponse, d1.ListDatabasesError, never>,
503
+ ) =>
504
+ Effect.gen(function* () {
505
+ if ("databaseId" in source && source.databaseId) {
506
+ // At lifecycle time, Output<string> attributes have resolved to strings.
507
+ return source.databaseId as unknown as string;
508
+ }
509
+ if ("name" in source && source.name) {
510
+ const name = source.name as unknown as string;
511
+ const dbs = yield* listDbs({ accountId, name });
512
+ const match = dbs.result.find((db) => db.name === name);
513
+ if (!match?.uuid) {
514
+ return yield* Effect.die(
515
+ `Source database "${name}" not found for cloning`,
516
+ );
517
+ }
518
+ return match.uuid;
519
+ }
520
+ return yield* Effect.die(
521
+ "Invalid clone source: must provide databaseId or name",
522
+ );
523
+ });
524
+
525
+ /**
526
+ * Read all migration files from `migrationsDir`, run pending migrations,
527
+ * and return the per-file content hashes for state tracking.
528
+ */
529
+ const runMigrations = (
530
+ accountId: string,
531
+ databaseId: string,
532
+ migrationsDir: string,
533
+ migrationsTable: string,
534
+ ) =>
535
+ Effect.gen(function* () {
536
+ const files = yield* listSqlFiles(migrationsDir);
537
+ if (files.length > 0) {
538
+ yield* applyMigrations({
539
+ accountId,
540
+ databaseId,
541
+ migrationsTable,
542
+ migrationsFiles: files,
543
+ });
544
+ }
545
+ const hashes: Record<string, string> = {};
546
+ for (const file of files) hashes[file.id] = file.hash;
547
+ return hashes;
548
+ });
549
+
550
+ /**
551
+ * Read each `importFiles` entry and run it through the D1 import flow,
552
+ * skipping files whose hash matches the previously-imported hash.
553
+ */
554
+ const runImports = (
555
+ accountId: string,
556
+ databaseId: string,
557
+ importFiles: ReadonlyArray<string>,
558
+ rootDir: string,
559
+ previous: Record<string, string>,
560
+ ) =>
561
+ Effect.gen(function* () {
562
+ const hashes: Record<string, string> = { ...previous };
563
+ for (const filePath of importFiles) {
564
+ const file = yield* readSqlFile(rootDir, filePath);
565
+ if (previous[filePath] === file.hash) {
566
+ hashes[filePath] = file.hash;
567
+ continue;
568
+ }
569
+ yield* importD1Database({
570
+ accountId,
571
+ databaseId,
572
+ sqlData: file.sql,
573
+ filename: file.id,
574
+ });
575
+ hashes[filePath] = file.hash;
576
+ }
577
+ // Drop entries for files no longer listed.
578
+ const tracked = new Set(importFiles);
579
+ for (const key of Object.keys(hashes)) {
580
+ if (!tracked.has(key)) delete hashes[key];
581
+ }
582
+ return hashes;
583
+ });
584
+
585
+ /**
586
+ * Hash all `.sql` files in `migrationsDir` without applying them; used by
587
+ * `diff` to detect drift relative to previously-applied state.
588
+ */
589
+ const hashMigrations = (migrationsDir: string) =>
590
+ listSqlFiles(migrationsDir).pipe(
591
+ Effect.map((files) => {
592
+ const hashes: Record<string, string> = {};
593
+ for (const file of files) hashes[file.id] = file.hash;
594
+ return hashes;
595
+ }),
596
+ );
597
+
598
+ const hashImports = (importFiles: ReadonlyArray<string>, rootDir: string) =>
599
+ Effect.gen(function* () {
600
+ const hashes: Record<string, string> = {};
601
+ for (const filePath of importFiles) {
602
+ const file = yield* readSqlFile(rootDir, filePath);
603
+ hashes[filePath] = file.hash;
604
+ }
605
+ return hashes;
606
+ });
607
+
608
+ const recordsEqual = (
609
+ a: Record<string, string>,
610
+ b: Record<string, string>,
611
+ ): boolean => {
612
+ const aKeys = Object.keys(a);
613
+ const bKeys = Object.keys(b);
614
+ if (aKeys.length !== bKeys.length) return false;
615
+ for (const k of aKeys) {
616
+ if (a[k] !== b[k]) return false;
617
+ }
618
+ return true;
619
+ };
@@ -0,0 +1,70 @@
1
+ import * as d1 from "@distilled.cloud/cloudflare/d1";
2
+ import * as Effect from "effect/Effect";
3
+ import type * as HttpClient from "effect/unstable/http/HttpClient";
4
+ import type { Credentials } from "@distilled.cloud/cloudflare/Credentials";
5
+
6
+ export interface ExportD1DatabaseOptions {
7
+ accountId: string;
8
+ databaseId: string;
9
+ dumpOptions?: {
10
+ tables?: string[];
11
+ noSchema?: boolean;
12
+ noData?: boolean;
13
+ };
14
+ }
15
+
16
+ export interface ExportD1DatabaseResult {
17
+ filename: string;
18
+ signedUrl: string;
19
+ }
20
+
21
+ /**
22
+ * Initiates an export of a Cloudflare D1 database and returns a signed
23
+ * download URL. Recursively polls with the bookmark until the export
24
+ * completes or fails.
25
+ */
26
+ export const exportD1Database = (
27
+ options: ExportD1DatabaseOptions,
28
+ ): Effect.Effect<
29
+ ExportD1DatabaseResult,
30
+ d1.ExportDatabaseError,
31
+ Credentials | HttpClient.HttpClient
32
+ > =>
33
+ Effect.gen(function* () {
34
+ const exportDb = yield* d1.exportDatabase;
35
+
36
+ const poll = (
37
+ currentBookmark?: string,
38
+ ): Effect.Effect<
39
+ ExportD1DatabaseResult,
40
+ d1.ExportDatabaseError,
41
+ never
42
+ > =>
43
+ Effect.gen(function* () {
44
+ const data = yield* exportDb({
45
+ accountId: options.accountId,
46
+ databaseId: options.databaseId,
47
+ outputFormat: "polling",
48
+ currentBookmark,
49
+ dumpOptions: options.dumpOptions,
50
+ });
51
+
52
+ if (data.status === "complete" && data.result) {
53
+ if (!data.result.filename || !data.result.signedUrl) {
54
+ return yield* Effect.die(
55
+ "D1 export completed but missing filename/signedUrl",
56
+ );
57
+ }
58
+ return {
59
+ filename: data.result.filename,
60
+ signedUrl: data.result.signedUrl,
61
+ };
62
+ }
63
+ if (data.status === "error") {
64
+ return yield* Effect.die(data.error ?? "Error during D1 export");
65
+ }
66
+ return yield* poll(data.atBookmark ?? undefined);
67
+ });
68
+
69
+ return yield* poll();
70
+ });