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.
- package/bin/alchemy.js +1 -1
- package/bin/alchemy.js.map +1 -1
- package/lib/Cloudflare/D1/D1Clone.d.ts +13 -0
- package/lib/Cloudflare/D1/D1Clone.d.ts.map +1 -0
- package/lib/Cloudflare/D1/D1Clone.js +31 -0
- package/lib/Cloudflare/D1/D1Clone.js.map +1 -0
- package/lib/Cloudflare/D1/D1Database.d.ts +150 -5
- package/lib/Cloudflare/D1/D1Database.d.ts.map +1 -1
- package/lib/Cloudflare/D1/D1Database.js +253 -0
- package/lib/Cloudflare/D1/D1Database.js.map +1 -1
- package/lib/Cloudflare/D1/D1Export.d.ts +24 -0
- package/lib/Cloudflare/D1/D1Export.d.ts.map +1 -0
- package/lib/Cloudflare/D1/D1Export.js +34 -0
- package/lib/Cloudflare/D1/D1Export.js.map +1 -0
- package/lib/Cloudflare/D1/D1Import.d.ts +21 -0
- package/lib/Cloudflare/D1/D1Import.d.ts.map +1 -0
- package/lib/Cloudflare/D1/D1Import.js +87 -0
- package/lib/Cloudflare/D1/D1Import.js.map +1 -0
- package/lib/Cloudflare/D1/D1Migrations.d.ts +16 -0
- package/lib/Cloudflare/D1/D1Migrations.d.ts.map +1 -0
- package/lib/Cloudflare/D1/D1Migrations.js +110 -0
- package/lib/Cloudflare/D1/D1Migrations.js.map +1 -0
- package/lib/Cloudflare/D1/D1SqlFile.d.ts +20 -0
- package/lib/Cloudflare/D1/D1SqlFile.d.ts.map +1 -0
- package/lib/Cloudflare/D1/D1SqlFile.js +46 -0
- package/lib/Cloudflare/D1/D1SqlFile.js.map +1 -0
- package/lib/Cloudflare/Providers.d.ts +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Cloudflare/D1/D1Clone.ts +42 -0
- package/src/Cloudflare/D1/D1Database.ts +386 -3
- package/src/Cloudflare/D1/D1Export.ts +70 -0
- package/src/Cloudflare/D1/D1Import.ts +156 -0
- package/src/Cloudflare/D1/D1Migrations.ts +229 -0
- 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
|
-
*
|
|
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.
|
|
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
|
|
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
|
+
});
|