alchemy 0.82.0 → 0.82.1
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/lib/cloudflare/d1-database.d.ts +21 -35
- package/lib/cloudflare/d1-database.d.ts.map +1 -1
- package/lib/cloudflare/d1-database.js +57 -100
- package/lib/cloudflare/d1-database.js.map +1 -1
- package/lib/cloudflare/d1-local-migrations.d.ts +3 -4
- package/lib/cloudflare/d1-local-migrations.d.ts.map +1 -1
- package/lib/cloudflare/d1-local-migrations.js +40 -12
- package/lib/cloudflare/d1-local-migrations.js.map +1 -1
- package/lib/cloudflare/d1-migrations.d.ts +0 -8
- package/lib/cloudflare/d1-migrations.d.ts.map +1 -1
- package/lib/cloudflare/d1-migrations.js +0 -35
- package/lib/cloudflare/d1-migrations.js.map +1 -1
- package/lib/cloudflare/d1-sql-file.d.ts +12 -0
- package/lib/cloudflare/d1-sql-file.d.ts.map +1 -0
- package/lib/cloudflare/d1-sql-file.js +38 -0
- package/lib/cloudflare/d1-sql-file.js.map +1 -0
- package/lib/state/d1-state-store.js +8 -8
- package/lib/state/d1-state-store.js.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/d1-database.ts +125 -187
- package/src/cloudflare/d1-local-migrations.ts +56 -18
- package/src/cloudflare/d1-migrations.ts +0 -43
- package/src/cloudflare/d1-sql-file.ts +52 -0
- package/src/state/d1-state-store.ts +8 -10
- package/workers/tunnel-proxy.js +1 -1
|
@@ -2,16 +2,18 @@ import type { Context } from "../context.ts";
|
|
|
2
2
|
import { Resource, ResourceKind } from "../resource.ts";
|
|
3
3
|
import { Scope } from "../scope.ts";
|
|
4
4
|
import { logger } from "../util/logger.ts";
|
|
5
|
-
import { CloudflareApiError
|
|
5
|
+
import { CloudflareApiError } from "./api-error.ts";
|
|
6
|
+
import { extractCloudflareResult } from "./api-response.ts";
|
|
6
7
|
import {
|
|
7
8
|
createCloudflareApi,
|
|
8
9
|
type CloudflareApi,
|
|
9
10
|
type CloudflareApiOptions,
|
|
10
11
|
} from "./api.ts";
|
|
11
|
-
import { withJurisdiction } from "./bucket.ts";
|
|
12
12
|
import { cloneD1Database } from "./d1-clone.ts";
|
|
13
|
+
import { importD1Database } from "./d1-import.ts";
|
|
13
14
|
import { applyLocalD1Migrations } from "./d1-local-migrations.ts";
|
|
14
|
-
import { applyMigrations
|
|
15
|
+
import { applyMigrations } from "./d1-migrations.ts";
|
|
16
|
+
import { listSqlFiles, readSqlFile, type D1SqlFile } from "./d1-sql-file.ts";
|
|
15
17
|
import { deleteMiniflareBinding } from "./miniflare/delete.ts";
|
|
16
18
|
|
|
17
19
|
const DEFAULT_MIGRATIONS_TABLE = "d1_migrations";
|
|
@@ -24,7 +26,7 @@ type PrimaryLocationHint =
|
|
|
24
26
|
| "weur"
|
|
25
27
|
| "eeur"
|
|
26
28
|
| "apac"
|
|
27
|
-
| "
|
|
29
|
+
| "oc"
|
|
28
30
|
| (string & {});
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -85,11 +87,10 @@ export interface D1DatabaseProps extends CloudflareApiOptions {
|
|
|
85
87
|
clone?: D1Database | { id: string } | { name: string };
|
|
86
88
|
|
|
87
89
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* @private
|
|
90
|
+
* The names of SQL files to import.
|
|
91
|
+
* After migrations are applied, these files will be run using [Cloudflare's D1 import API](https://developers.cloudflare.com/d1/best-practices/import-export-data/).
|
|
91
92
|
*/
|
|
92
|
-
|
|
93
|
+
importFiles?: string[];
|
|
93
94
|
|
|
94
95
|
/**
|
|
95
96
|
* Name of the table used to track migrations. Only used if migrationsDir is specified. Defaults to 'd1_migrations'
|
|
@@ -102,6 +103,7 @@ export interface D1DatabaseProps extends CloudflareApiOptions {
|
|
|
102
103
|
* This is analogous to wrangler's `migrations_dir`.
|
|
103
104
|
*/
|
|
104
105
|
migrationsDir?: string;
|
|
106
|
+
|
|
105
107
|
/**
|
|
106
108
|
* Whether to emulate the database locally when Alchemy is running in watch mode.
|
|
107
109
|
*/
|
|
@@ -139,6 +141,7 @@ export type D1Database = Pick<
|
|
|
139
141
|
| "migrationsTable"
|
|
140
142
|
| "primaryLocationHint"
|
|
141
143
|
| "readReplication"
|
|
144
|
+
| "importFiles"
|
|
142
145
|
> & {
|
|
143
146
|
type: "d1";
|
|
144
147
|
/**
|
|
@@ -253,13 +256,21 @@ export async function D1Database(
|
|
|
253
256
|
id: string,
|
|
254
257
|
props: Omit<D1DatabaseProps, "migrationsFiles"> = {},
|
|
255
258
|
): Promise<D1Database> {
|
|
256
|
-
const migrationsFiles =
|
|
257
|
-
? await
|
|
258
|
-
|
|
259
|
+
const [migrationsFiles, importFiles] = await Promise.all([
|
|
260
|
+
props.migrationsDir ? await listSqlFiles(props.migrationsDir) : [],
|
|
261
|
+
props.importFiles
|
|
262
|
+
? await Promise.all(
|
|
263
|
+
props.importFiles.map((file) =>
|
|
264
|
+
readSqlFile(Scope.current.rootDir, file),
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
: [],
|
|
268
|
+
]);
|
|
259
269
|
|
|
260
270
|
return _D1Database(id, {
|
|
261
271
|
...props,
|
|
262
272
|
migrationsFiles,
|
|
273
|
+
importFiles,
|
|
263
274
|
dev: {
|
|
264
275
|
...(props.dev ?? {}),
|
|
265
276
|
// force local migrations to run even if the database was already deployed live
|
|
@@ -274,7 +285,10 @@ const _D1Database = Resource(
|
|
|
274
285
|
async function (
|
|
275
286
|
this: Context<D1Database>,
|
|
276
287
|
id: string,
|
|
277
|
-
props: D1DatabaseProps,
|
|
288
|
+
props: Omit<D1DatabaseProps, "importFiles"> & {
|
|
289
|
+
migrationsFiles: D1SqlFile[] | undefined;
|
|
290
|
+
importFiles: D1SqlFile[] | undefined;
|
|
291
|
+
},
|
|
278
292
|
): Promise<D1Database> {
|
|
279
293
|
const databaseName =
|
|
280
294
|
props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
|
|
@@ -286,17 +300,18 @@ const _D1Database = Resource(
|
|
|
286
300
|
|
|
287
301
|
const local = this.scope.local && !props.dev?.remote;
|
|
288
302
|
const dev = {
|
|
289
|
-
id: this.output?.dev?.id ?? this.output?.id ??
|
|
303
|
+
id: this.output?.dev?.id ?? this.output?.id ?? databaseName,
|
|
290
304
|
remote: props.dev?.remote ?? false,
|
|
291
305
|
};
|
|
292
306
|
const adopt = props.adopt ?? this.scope.adopt;
|
|
293
307
|
|
|
294
308
|
if (local) {
|
|
295
|
-
if (props.migrationsFiles
|
|
309
|
+
if (props.migrationsFiles?.length || props.importFiles?.length) {
|
|
296
310
|
await applyLocalD1Migrations({
|
|
297
311
|
databaseId: dev.id,
|
|
298
312
|
migrationsTable: props.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE,
|
|
299
|
-
migrations: props.migrationsFiles,
|
|
313
|
+
migrations: props.migrationsFiles ?? [],
|
|
314
|
+
imports: props.importFiles ?? [],
|
|
300
315
|
rootDir: this.scope.rootDir,
|
|
301
316
|
});
|
|
302
317
|
}
|
|
@@ -320,12 +335,12 @@ const _D1Database = Resource(
|
|
|
320
335
|
await deleteMiniflareBinding(this.scope, "d1", this.output.dev.id);
|
|
321
336
|
}
|
|
322
337
|
if (props.delete !== false && this.output?.id) {
|
|
323
|
-
await deleteDatabase(api, this.output.id
|
|
338
|
+
await deleteDatabase(api, this.output.id);
|
|
324
339
|
}
|
|
325
340
|
// Return void (a deleted database has no content)
|
|
326
341
|
return this.destroy();
|
|
327
342
|
}
|
|
328
|
-
let dbData:
|
|
343
|
+
let dbData: D1ResponseObject;
|
|
329
344
|
|
|
330
345
|
if (
|
|
331
346
|
this.phase === "create" ||
|
|
@@ -337,14 +352,9 @@ const _D1Database = Resource(
|
|
|
337
352
|
try {
|
|
338
353
|
dbData = await createDatabase(api, databaseName, props);
|
|
339
354
|
|
|
340
|
-
// Read replication cannot be set during creation, so update it after creation
|
|
341
|
-
if (props.readReplication && dbData.result.uuid) {
|
|
342
|
-
dbData = await updateDatabase(api, dbData.result.uuid, props);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
355
|
// If clone property is provided, perform cloning after database creation
|
|
346
|
-
if (props.clone && dbData.
|
|
347
|
-
await cloneDb(api, props.clone, dbData.
|
|
356
|
+
if (props.clone && dbData.uuid) {
|
|
357
|
+
await cloneDb(api, props.clone, dbData.uuid);
|
|
348
358
|
}
|
|
349
359
|
} catch (error) {
|
|
350
360
|
// Check if this is a "database already exists" error and adopt is enabled
|
|
@@ -355,7 +365,7 @@ const _D1Database = Resource(
|
|
|
355
365
|
) {
|
|
356
366
|
logger.log(`Database ${databaseName} already exists, adopting it`);
|
|
357
367
|
// Find the existing database by name
|
|
358
|
-
const databases = await listDatabases(api, databaseName
|
|
368
|
+
const databases = await listDatabases(api, databaseName);
|
|
359
369
|
const existingDb = databases.find((db) => db.name === databaseName);
|
|
360
370
|
|
|
361
371
|
if (!existingDb) {
|
|
@@ -365,11 +375,15 @@ const _D1Database = Resource(
|
|
|
365
375
|
}
|
|
366
376
|
|
|
367
377
|
// Get the database details using its ID
|
|
368
|
-
dbData = await getDatabase(api, existingDb.
|
|
378
|
+
dbData = await getDatabase(api, existingDb.uuid);
|
|
369
379
|
|
|
370
380
|
// Update the database with the provided properties
|
|
371
381
|
if (props.readReplication) {
|
|
372
|
-
dbData = await
|
|
382
|
+
dbData = await updateReadReplicationMode(
|
|
383
|
+
api,
|
|
384
|
+
existingDb.uuid,
|
|
385
|
+
props.readReplication?.mode,
|
|
386
|
+
);
|
|
373
387
|
}
|
|
374
388
|
} else {
|
|
375
389
|
// Re-throw the error if adopt is false or it's not a "database already exists" error
|
|
@@ -395,7 +409,11 @@ const _D1Database = Resource(
|
|
|
395
409
|
);
|
|
396
410
|
}
|
|
397
411
|
// Update the database with new properties
|
|
398
|
-
dbData = await
|
|
412
|
+
dbData = await updateReadReplicationMode(
|
|
413
|
+
api,
|
|
414
|
+
this.output.id,
|
|
415
|
+
props.readReplication?.mode ?? "disabled", // disabled seems to be the default
|
|
416
|
+
);
|
|
399
417
|
} else {
|
|
400
418
|
// If no ID exists, fall back to creating a new database
|
|
401
419
|
dbData = await createDatabase(api, databaseName, props);
|
|
@@ -406,17 +424,11 @@ const _D1Database = Resource(
|
|
|
406
424
|
try {
|
|
407
425
|
const migrationsTable =
|
|
408
426
|
props.migrationsTable || DEFAULT_MIGRATIONS_TABLE;
|
|
409
|
-
const databaseId = dbData.result.uuid || this.output?.id;
|
|
410
|
-
|
|
411
|
-
if (!databaseId) {
|
|
412
|
-
throw new Error("Database ID not found for migrations");
|
|
413
|
-
}
|
|
414
|
-
|
|
415
427
|
await applyMigrations({
|
|
416
428
|
migrationsFiles: props.migrationsFiles,
|
|
417
429
|
migrationsTable,
|
|
418
430
|
accountId: api.accountId,
|
|
419
|
-
databaseId,
|
|
431
|
+
databaseId: dbData.uuid,
|
|
420
432
|
api,
|
|
421
433
|
});
|
|
422
434
|
} catch (migrationErr) {
|
|
@@ -424,13 +436,20 @@ const _D1Database = Resource(
|
|
|
424
436
|
throw migrationErr;
|
|
425
437
|
}
|
|
426
438
|
}
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
|
|
439
|
+
if (props.importFiles?.length) {
|
|
440
|
+
await Promise.all(
|
|
441
|
+
props.importFiles.map(async (file) => {
|
|
442
|
+
await importD1Database(api, {
|
|
443
|
+
databaseId: dbData.uuid,
|
|
444
|
+
sqlData: file.sql,
|
|
445
|
+
filename: file.id,
|
|
446
|
+
});
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
430
449
|
}
|
|
431
450
|
return {
|
|
432
451
|
type: "d1",
|
|
433
|
-
id: dbData.
|
|
452
|
+
id: dbData.uuid,
|
|
434
453
|
name: databaseName,
|
|
435
454
|
readReplication: props.readReplication,
|
|
436
455
|
primaryLocationHint: props.primaryLocationHint,
|
|
@@ -442,22 +461,16 @@ const _D1Database = Resource(
|
|
|
442
461
|
},
|
|
443
462
|
);
|
|
444
463
|
|
|
445
|
-
interface
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
};
|
|
456
|
-
jurisdiction?: string;
|
|
457
|
-
};
|
|
458
|
-
success: boolean;
|
|
459
|
-
errors: Array<{ code: number; message: string }>;
|
|
460
|
-
messages: string[];
|
|
464
|
+
interface D1ResponseObject {
|
|
465
|
+
uuid: string;
|
|
466
|
+
name: string;
|
|
467
|
+
created_at: string;
|
|
468
|
+
version: string;
|
|
469
|
+
num_tables: number;
|
|
470
|
+
file_size: number;
|
|
471
|
+
running_in_region: "EEUR" | "APAC" | "WNAM" | "ENAM" | "WEUR" | "EEUR" | "OC";
|
|
472
|
+
read_replication: { mode: "auto" | "disabled" };
|
|
473
|
+
jurisdiction: "eu" | "fedramp" | null;
|
|
461
474
|
}
|
|
462
475
|
|
|
463
476
|
/**
|
|
@@ -466,37 +479,45 @@ interface CloudflareD1Response {
|
|
|
466
479
|
export async function createDatabase(
|
|
467
480
|
api: CloudflareApi,
|
|
468
481
|
databaseName: string,
|
|
469
|
-
props:
|
|
470
|
-
|
|
482
|
+
props: Pick<
|
|
483
|
+
D1DatabaseProps,
|
|
484
|
+
"jurisdiction" | "primaryLocationHint" | "readReplication"
|
|
485
|
+
>,
|
|
486
|
+
): Promise<D1ResponseObject> {
|
|
471
487
|
// Create new D1 database
|
|
472
|
-
const createPayload:
|
|
488
|
+
const createPayload: {
|
|
489
|
+
name: string;
|
|
490
|
+
jurisdiction?: "eu" | "fedramp";
|
|
491
|
+
primary_location_hint?:
|
|
492
|
+
| "wnam"
|
|
493
|
+
| "enam"
|
|
494
|
+
| "weur"
|
|
495
|
+
| "eeur"
|
|
496
|
+
| "apac"
|
|
497
|
+
| "oc"
|
|
498
|
+
| (string & {});
|
|
499
|
+
} = {
|
|
473
500
|
name: databaseName,
|
|
474
501
|
jurisdiction:
|
|
475
502
|
props.jurisdiction !== "default" ? props.jurisdiction : undefined,
|
|
503
|
+
primary_location_hint: props.primaryLocationHint,
|
|
476
504
|
};
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const createResponse = await api.post(
|
|
483
|
-
`/accounts/${api.accountId}/d1/database`,
|
|
484
|
-
createPayload,
|
|
485
|
-
{
|
|
486
|
-
headers: withJurisdiction(props),
|
|
487
|
-
},
|
|
505
|
+
const database = await extractCloudflareResult<D1ResponseObject>(
|
|
506
|
+
`create D1 database "${databaseName}"`,
|
|
507
|
+
api.post(`/accounts/${api.accountId}/d1/database`, createPayload),
|
|
488
508
|
);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
509
|
+
if (!database.uuid) {
|
|
510
|
+
// this is included in the spec as optional... why is it optional? we may never know...
|
|
511
|
+
throw new Error("Missing UUID for created database");
|
|
512
|
+
}
|
|
513
|
+
if (props.readReplication?.mode) {
|
|
514
|
+
return await updateReadReplicationMode(
|
|
515
|
+
api,
|
|
516
|
+
database.uuid,
|
|
517
|
+
props.readReplication?.mode,
|
|
496
518
|
);
|
|
497
519
|
}
|
|
498
|
-
|
|
499
|
-
return (await createResponse.json()) as CloudflareD1Response;
|
|
520
|
+
return database;
|
|
500
521
|
}
|
|
501
522
|
|
|
502
523
|
/**
|
|
@@ -504,25 +525,12 @@ export async function createDatabase(
|
|
|
504
525
|
*/
|
|
505
526
|
export async function getDatabase(
|
|
506
527
|
api: CloudflareApi,
|
|
507
|
-
databaseId
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const response = await api.get(
|
|
515
|
-
`/accounts/${api.accountId}/d1/database/${databaseId}`,
|
|
516
|
-
{
|
|
517
|
-
headers: withJurisdiction(props),
|
|
518
|
-
},
|
|
528
|
+
databaseId: string,
|
|
529
|
+
): Promise<D1ResponseObject> {
|
|
530
|
+
return await extractCloudflareResult<D1ResponseObject>(
|
|
531
|
+
`get D1 database "${databaseId}"`,
|
|
532
|
+
api.get(`/accounts/${api.accountId}/d1/database/${databaseId}`),
|
|
519
533
|
);
|
|
520
|
-
|
|
521
|
-
if (!response.ok) {
|
|
522
|
-
return await handleApiError(response, "getting", "D1 database", databaseId);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
return (await response.json()) as CloudflareD1Response;
|
|
526
534
|
}
|
|
527
535
|
|
|
528
536
|
/**
|
|
@@ -530,31 +538,12 @@ export async function getDatabase(
|
|
|
530
538
|
*/
|
|
531
539
|
export async function deleteDatabase(
|
|
532
540
|
api: CloudflareApi,
|
|
533
|
-
databaseId
|
|
534
|
-
props: D1DatabaseProps = {},
|
|
541
|
+
databaseId: string,
|
|
535
542
|
): Promise<void> {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Delete D1 database
|
|
542
|
-
const deleteResponse = await api.delete(
|
|
543
|
-
`/accounts/${api.accountId}/d1/database/${databaseId}`,
|
|
544
|
-
{
|
|
545
|
-
headers: withJurisdiction(props),
|
|
546
|
-
},
|
|
543
|
+
await extractCloudflareResult(
|
|
544
|
+
`delete D1 database "${databaseId}"`,
|
|
545
|
+
api.delete(`/accounts/${api.accountId}/d1/database/${databaseId}`),
|
|
547
546
|
);
|
|
548
|
-
|
|
549
|
-
if (!deleteResponse.ok && deleteResponse.status !== 404) {
|
|
550
|
-
const errorData: any = await deleteResponse.json().catch(() => ({
|
|
551
|
-
errors: [{ message: deleteResponse.statusText }],
|
|
552
|
-
}));
|
|
553
|
-
throw new CloudflareApiError(
|
|
554
|
-
`Error deleting D1 database '${databaseId}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
|
|
555
|
-
deleteResponse,
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
547
|
}
|
|
559
548
|
|
|
560
549
|
/**
|
|
@@ -563,44 +552,15 @@ export async function deleteDatabase(
|
|
|
563
552
|
export async function listDatabases(
|
|
564
553
|
api: CloudflareApi,
|
|
565
554
|
name?: string,
|
|
566
|
-
|
|
567
|
-
): Promise<{ name: string; id: string }[]> {
|
|
555
|
+
): Promise<D1ResponseObject[]> {
|
|
568
556
|
// Construct query string if name is provided
|
|
569
557
|
const queryParams = name ? `?name=${encodeURIComponent(name)}` : "";
|
|
570
558
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
{
|
|
574
|
-
|
|
575
|
-
},
|
|
559
|
+
// TODO(john): handle pagination (wasn't handled originally)
|
|
560
|
+
return await extractCloudflareResult<D1ResponseObject[]>(
|
|
561
|
+
`list D1 databases${name ? ` with name "${name}"` : ""}`,
|
|
562
|
+
api.get(`/accounts/${api.accountId}/d1/database${queryParams}`),
|
|
576
563
|
);
|
|
577
|
-
|
|
578
|
-
if (!response.ok) {
|
|
579
|
-
throw new CloudflareApiError(
|
|
580
|
-
`Failed to list databases: ${response.statusText}`,
|
|
581
|
-
response,
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const data = (await response.json()) as {
|
|
586
|
-
success: boolean;
|
|
587
|
-
errors?: Array<{ code: number; message: string }>;
|
|
588
|
-
result?: Array<{
|
|
589
|
-
name: string;
|
|
590
|
-
uuid: string;
|
|
591
|
-
}>;
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
if (!data.success) {
|
|
595
|
-
const errorMessage = data.errors?.[0]?.message || "Unknown error";
|
|
596
|
-
throw new Error(`Failed to list databases: ${errorMessage}`);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Transform API response
|
|
600
|
-
return (data.result || []).map((db) => ({
|
|
601
|
-
name: db.name,
|
|
602
|
-
id: db.uuid,
|
|
603
|
-
}));
|
|
604
564
|
}
|
|
605
565
|
|
|
606
566
|
/**
|
|
@@ -608,38 +568,19 @@ export async function listDatabases(
|
|
|
608
568
|
*
|
|
609
569
|
* Note: According to Cloudflare API, only read_replication.mode can be modified during updates.
|
|
610
570
|
*/
|
|
611
|
-
export async function
|
|
571
|
+
export async function updateReadReplicationMode(
|
|
612
572
|
api: CloudflareApi,
|
|
613
573
|
databaseId: string,
|
|
614
|
-
|
|
615
|
-
): Promise<
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const updateResponse = await api.patch(
|
|
626
|
-
`/accounts/${api.accountId}/d1/database/${databaseId}`,
|
|
627
|
-
updatePayload,
|
|
628
|
-
{
|
|
629
|
-
headers: withJurisdiction(props),
|
|
630
|
-
},
|
|
574
|
+
readReplicationMode: "auto" | "disabled",
|
|
575
|
+
): Promise<D1ResponseObject> {
|
|
576
|
+
return await extractCloudflareResult<D1ResponseObject>(
|
|
577
|
+
`update read replication mode for D1 database "${databaseId}"`,
|
|
578
|
+
api.patch(`/accounts/${api.accountId}/d1/database/${databaseId}`, {
|
|
579
|
+
read_replication: {
|
|
580
|
+
mode: readReplicationMode,
|
|
581
|
+
},
|
|
582
|
+
}),
|
|
631
583
|
);
|
|
632
|
-
|
|
633
|
-
if (!updateResponse.ok) {
|
|
634
|
-
return await handleApiError(
|
|
635
|
-
updateResponse,
|
|
636
|
-
"updating",
|
|
637
|
-
"D1 database",
|
|
638
|
-
databaseId,
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return (await updateResponse.json()) as CloudflareD1Response;
|
|
643
584
|
}
|
|
644
585
|
|
|
645
586
|
/**
|
|
@@ -654,7 +595,6 @@ async function cloneDb(
|
|
|
654
595
|
api: CloudflareApi,
|
|
655
596
|
sourceDb: D1Database | { id: string } | { name: string },
|
|
656
597
|
targetDbId: string,
|
|
657
|
-
jurisdiction: D1DatabaseJurisdiction,
|
|
658
598
|
): Promise<void> {
|
|
659
599
|
let sourceId: string;
|
|
660
600
|
|
|
@@ -664,9 +604,7 @@ async function cloneDb(
|
|
|
664
604
|
sourceId = sourceDb.id;
|
|
665
605
|
} else if ("name" in sourceDb && sourceDb.name) {
|
|
666
606
|
// Look up ID by name
|
|
667
|
-
const databases = await listDatabases(api, sourceDb.name
|
|
668
|
-
jurisdiction,
|
|
669
|
-
});
|
|
607
|
+
const databases = await listDatabases(api, sourceDb.name);
|
|
670
608
|
const foundDb = databases.find((db) => db.name === sourceDb.name);
|
|
671
609
|
|
|
672
610
|
if (!foundDb) {
|
|
@@ -675,7 +613,7 @@ async function cloneDb(
|
|
|
675
613
|
);
|
|
676
614
|
}
|
|
677
615
|
|
|
678
|
-
sourceId = foundDb.
|
|
616
|
+
sourceId = foundDb.uuid;
|
|
679
617
|
} else if ("type" in sourceDb && sourceDb.type === "d1" && "id" in sourceDb) {
|
|
680
618
|
// It's a D1Database object
|
|
681
619
|
sourceId = sourceDb.id;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import * as mf from "miniflare";
|
|
2
|
+
import type { D1SqlFile } from "./d1-sql-file.ts";
|
|
2
3
|
import { getDefaultPersistPath } from "./miniflare/paths.ts";
|
|
3
4
|
|
|
4
5
|
export interface D1LocalMigrationOptions {
|
|
5
6
|
rootDir: string;
|
|
6
7
|
databaseId: string;
|
|
7
8
|
migrationsTable: string;
|
|
8
|
-
migrations:
|
|
9
|
+
migrations: Array<D1SqlFile>;
|
|
10
|
+
imports: Array<D1SqlFile>;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export const applyLocalD1Migrations = async (
|
|
@@ -21,34 +23,70 @@ export const applyLocalD1Migrations = async (
|
|
|
21
23
|
});
|
|
22
24
|
try {
|
|
23
25
|
await miniflare.ready;
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
.
|
|
29
|
-
|
|
26
|
+
const db = await miniflare.getD1Database("DB");
|
|
27
|
+
const session = db.withSession("first-primary");
|
|
28
|
+
const tableInfo = await session
|
|
29
|
+
.prepare(`PRAGMA table_info(${options.migrationsTable});`)
|
|
30
|
+
.all<{
|
|
31
|
+
cid: number;
|
|
32
|
+
name: string;
|
|
33
|
+
type: string;
|
|
34
|
+
notnull: number;
|
|
35
|
+
dflt_value: string | null;
|
|
36
|
+
pk: number;
|
|
37
|
+
}>();
|
|
38
|
+
if (tableInfo.results.length === 0) {
|
|
39
|
+
await session
|
|
40
|
+
.prepare(
|
|
41
|
+
`CREATE TABLE ${options.migrationsTable} (
|
|
30
42
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
31
43
|
name TEXT NOT NULL,
|
|
32
|
-
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
|
44
|
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
45
|
+
type TEXT NOT NULL
|
|
33
46
|
)`,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
)
|
|
48
|
+
.run();
|
|
49
|
+
} else if (!tableInfo.results.some((col) => col.name === "type")) {
|
|
50
|
+
await session
|
|
51
|
+
.prepare(
|
|
52
|
+
`ALTER TABLE ${options.migrationsTable} ADD COLUMN type TEXT NOT NULL DEFAULT 'migration';`,
|
|
53
|
+
)
|
|
54
|
+
.run();
|
|
55
|
+
}
|
|
56
|
+
const applied: {
|
|
57
|
+
results: { name: string; type: "migration" | "import" }[];
|
|
38
58
|
} = await session
|
|
39
59
|
.prepare(
|
|
40
|
-
`SELECT name FROM ${options.migrationsTable} ORDER BY applied_at ASC`,
|
|
60
|
+
`SELECT name, type FROM ${options.migrationsTable} ORDER BY applied_at ASC`,
|
|
41
61
|
)
|
|
42
62
|
.all();
|
|
43
63
|
const insertRecord = session.prepare(
|
|
44
|
-
`INSERT INTO ${options.migrationsTable} (name) VALUES (?)`,
|
|
64
|
+
`INSERT INTO ${options.migrationsTable} (name, type) VALUES (?, ?)`,
|
|
45
65
|
);
|
|
46
|
-
for (const
|
|
47
|
-
if (
|
|
66
|
+
for (const { id, sql } of options.migrations) {
|
|
67
|
+
if (applied.results.some((m) => m.name === id)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const statements = sql
|
|
71
|
+
.split("--> statement-breakpoint")
|
|
72
|
+
.filter((s) => s.trim())
|
|
73
|
+
.map((s) => session.prepare(s));
|
|
74
|
+
statements.push(insertRecord.bind(id, "migration"));
|
|
75
|
+
await session.batch(statements);
|
|
76
|
+
}
|
|
77
|
+
for (const { id, sql, hash } of options.imports) {
|
|
78
|
+
const name = `${id}-${hash}`;
|
|
79
|
+
if (applied.results.some((m) => m.name === name)) {
|
|
48
80
|
continue;
|
|
49
81
|
}
|
|
50
|
-
|
|
51
|
-
|
|
82
|
+
// Split into statements to prevent D1_ERROR: statement too long: SQLITE_TOOBIG.
|
|
83
|
+
// This is split naively by semicolons followed by newlines - not perfect but should work 99% of the time.
|
|
84
|
+
const statements = sql
|
|
85
|
+
.split(/;\r?\n/)
|
|
86
|
+
.filter((s) => s.trim())
|
|
87
|
+
.map((s) => session.prepare(s));
|
|
88
|
+
statements.push(insertRecord.bind(name, "import"));
|
|
89
|
+
await session.batch(statements);
|
|
52
90
|
}
|
|
53
91
|
} finally {
|
|
54
92
|
await miniflare.dispose();
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { glob } from "glob";
|
|
2
|
-
import * as fs from "node:fs/promises";
|
|
3
|
-
import path from "pathe";
|
|
4
1
|
import { logger } from "../util/logger.ts";
|
|
5
2
|
import { handleApiError } from "./api-error.ts";
|
|
6
3
|
import type { CloudflareApi } from "./api.ts";
|
|
@@ -14,16 +11,6 @@ export interface D1MigrationOptions {
|
|
|
14
11
|
quiet?: boolean;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
const getPrefix = (name: string) => {
|
|
18
|
-
const prefix = name.split("_")[0];
|
|
19
|
-
const num = Number.parseInt(prefix, 10);
|
|
20
|
-
return Number.isNaN(num) ? null : num;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
async function readMigrationFile(filePath: string): Promise<string> {
|
|
24
|
-
return fs.readFile(filePath, "utf-8");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
14
|
/**
|
|
28
15
|
* Detects the current schema of the migration table.
|
|
29
16
|
* Returns info about the table structure to determine if migration is needed.
|
|
@@ -153,36 +140,6 @@ async function migrateLegacySchema(
|
|
|
153
140
|
}
|
|
154
141
|
}
|
|
155
142
|
|
|
156
|
-
/**
|
|
157
|
-
* Reads migration SQL files from the migrationsDir, sorted by filename.
|
|
158
|
-
* @param migrationsDir Directory containing .sql migration files
|
|
159
|
-
*/
|
|
160
|
-
export async function listMigrationsFiles(
|
|
161
|
-
migrationsDir: string,
|
|
162
|
-
): Promise<Array<{ id: string; sql: string }>> {
|
|
163
|
-
const entries = await glob("**/*.sql", {
|
|
164
|
-
cwd: migrationsDir,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const sqlFiles = entries.sort((a: string, b: string) => {
|
|
168
|
-
const aNum = getPrefix(a);
|
|
169
|
-
const bNum = getPrefix(b);
|
|
170
|
-
|
|
171
|
-
if (aNum !== null && bNum !== null) return aNum - bNum;
|
|
172
|
-
if (aNum !== null) return -1;
|
|
173
|
-
if (bNum !== null) return 1;
|
|
174
|
-
|
|
175
|
-
return a.localeCompare(b);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
return await Promise.all(
|
|
179
|
-
sqlFiles.map(async (file) => ({
|
|
180
|
-
id: file,
|
|
181
|
-
sql: await readMigrationFile(path.join(migrationsDir, file)),
|
|
182
|
-
})),
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
143
|
/**
|
|
187
144
|
* Ensures the migrations table exists in the D1 database with wrangler-compatible schema.
|
|
188
145
|
* Handles migration from legacy 2-column schema to 3-column schema if needed.
|