@supacloud/db 0.13.2 → 0.14.0
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/dist/index.d.ts +1 -0
- package/dist/index.js +126 -0
- package/dist/migration-rebase.d.ts +28 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -11,3 +11,4 @@ export { createRpcClient, defineRpcContract, RpcContractError, type RpcArgs, typ
|
|
|
11
11
|
export { COMMAND_PERSISTENCE_SQL, COMMAND_PERSISTENCE_UPGRADE_SQL } from "./command-schema";
|
|
12
12
|
export { defineReadQuery, decodePostgrestQuery, QueryBoundaryError, type QueryEvent, type QueryObserver, type ReadQueryDefinition } from "./query";
|
|
13
13
|
export { createPostgresCommandStore, type CommandDatabase, type CommandTransaction, type CommandSubmissionBinding, } from "./command-adapter";
|
|
14
|
+
export { planMigrationRebase, rebaseMigrations, type MigrationRebaseOptions, type MigrationRebasePlan, type RebaseMigrationInput, } from "./migration-rebase";
|
package/dist/index.js
CHANGED
|
@@ -1301,6 +1301,130 @@ function createPostgresCommandStore(database, options = {}) {
|
|
|
1301
1301
|
}
|
|
1302
1302
|
};
|
|
1303
1303
|
}
|
|
1304
|
+
// src/migration-rebase.ts
|
|
1305
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1306
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
1307
|
+
import { basename, join, resolve } from "node:path";
|
|
1308
|
+
var MAX_MIGRATION_VERSION = 9223372036854775807n;
|
|
1309
|
+
function sha256(value) {
|
|
1310
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
1311
|
+
}
|
|
1312
|
+
function normalizedSql(value) {
|
|
1313
|
+
return value.replace(/\r\n?/g, `
|
|
1314
|
+
`).trimEnd() + `
|
|
1315
|
+
`;
|
|
1316
|
+
}
|
|
1317
|
+
function validVersion(value) {
|
|
1318
|
+
if (!/^\d{1,19}$/.test(value))
|
|
1319
|
+
return false;
|
|
1320
|
+
const version = BigInt(value);
|
|
1321
|
+
return version > 0n && version <= MAX_MIGRATION_VERSION;
|
|
1322
|
+
}
|
|
1323
|
+
function migrationVersion(file) {
|
|
1324
|
+
const match = basename(file).match(/^(\d{8,19})[_-]/);
|
|
1325
|
+
if (!match || !validVersion(match[1]))
|
|
1326
|
+
throw new Error(`Invalid migration version in ${file}`);
|
|
1327
|
+
return BigInt(match[1]).toString();
|
|
1328
|
+
}
|
|
1329
|
+
function migrationName(file) {
|
|
1330
|
+
return basename(file, ".sql").replace(/^\d{8,19}[_-]/, "");
|
|
1331
|
+
}
|
|
1332
|
+
function sortMigrations(migrations) {
|
|
1333
|
+
const sorted = [...migrations].sort((left, right) => {
|
|
1334
|
+
const leftVersion = BigInt(left.version);
|
|
1335
|
+
const rightVersion = BigInt(right.version);
|
|
1336
|
+
if (leftVersion !== rightVersion)
|
|
1337
|
+
return leftVersion < rightVersion ? -1 : 1;
|
|
1338
|
+
return left.file.localeCompare(right.file);
|
|
1339
|
+
});
|
|
1340
|
+
for (let index = 1;index < sorted.length; index += 1) {
|
|
1341
|
+
if (sorted[index - 1].version === sorted[index].version) {
|
|
1342
|
+
throw new Error(`Duplicate migration version ${sorted[index].version}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return sorted;
|
|
1346
|
+
}
|
|
1347
|
+
function planMigrationRebase(migrations, options, baselineSql) {
|
|
1348
|
+
const sorted = sortMigrations([...migrations]);
|
|
1349
|
+
if (!sorted.length)
|
|
1350
|
+
throw new Error("No migration files found");
|
|
1351
|
+
if (!validVersion(options.baselineVersion))
|
|
1352
|
+
throw new Error("Invalid baseline version");
|
|
1353
|
+
if (options.retainAfterVersion !== undefined && !validVersion(options.retainAfterVersion)) {
|
|
1354
|
+
throw new Error("Invalid retain-after version");
|
|
1355
|
+
}
|
|
1356
|
+
const baselineName = options.baselineName?.trim() || "reconstructed_schema";
|
|
1357
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(baselineName))
|
|
1358
|
+
throw new Error("Invalid baseline name");
|
|
1359
|
+
const baselineMigration = `${options.baselineVersion}_${baselineName}.sql`;
|
|
1360
|
+
if (sorted.some((migration) => migration.version === options.baselineVersion)) {
|
|
1361
|
+
throw new Error(`Baseline version ${options.baselineVersion} collides with existing history`);
|
|
1362
|
+
}
|
|
1363
|
+
const retainAfter = options.retainAfterVersion === undefined ? null : BigInt(options.retainAfterVersion);
|
|
1364
|
+
const retained = sorted.filter((migration) => retainAfter !== null && BigInt(migration.version) > retainAfter);
|
|
1365
|
+
const archived = sorted.filter((migration) => !retained.includes(migration));
|
|
1366
|
+
const normalizedBaseline = normalizedSql(baselineSql);
|
|
1367
|
+
if (!normalizedBaseline.trim())
|
|
1368
|
+
throw new Error("Baseline SQL is empty");
|
|
1369
|
+
if (!/\bCREATE\b/i.test(normalizedBaseline))
|
|
1370
|
+
throw new Error("Baseline SQL does not look like a schema snapshot");
|
|
1371
|
+
const sourceHistory = sorted.map((migration) => ({
|
|
1372
|
+
file: migration.file,
|
|
1373
|
+
version: migration.version,
|
|
1374
|
+
name: migration.name,
|
|
1375
|
+
sha256: sha256(normalizedSql(migration.sql))
|
|
1376
|
+
}));
|
|
1377
|
+
return {
|
|
1378
|
+
version: 1,
|
|
1379
|
+
sourceDirectory: options.sourceDirectory,
|
|
1380
|
+
outputDirectory: options.outputDirectory,
|
|
1381
|
+
baselineFile: options.baselineFile,
|
|
1382
|
+
baselineMigration,
|
|
1383
|
+
retainedMigrations: retained.map((migration) => migration.file),
|
|
1384
|
+
archivedMigrations: archived.map((migration) => migration.file),
|
|
1385
|
+
sourceHistorySha256: sha256(JSON.stringify(sourceHistory)),
|
|
1386
|
+
baselineSha256: sha256(normalizedBaseline),
|
|
1387
|
+
warnings: [
|
|
1388
|
+
"The baseline must come from a schema-only snapshot of the verified target database.",
|
|
1389
|
+
"Data backfills, reference data and non-replayable side effects need a separate review.",
|
|
1390
|
+
"Archive the old migration directory until clean replay and remote read-back have passed."
|
|
1391
|
+
]
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
async function rebaseMigrations(options) {
|
|
1395
|
+
const sourceDirectory = resolve(options.sourceDirectory);
|
|
1396
|
+
const outputDirectory = resolve(options.outputDirectory);
|
|
1397
|
+
const baselineFile = resolve(options.baselineFile);
|
|
1398
|
+
if (!(await stat(sourceDirectory)).isDirectory())
|
|
1399
|
+
throw new Error("Source migration directory is not a directory");
|
|
1400
|
+
if (!(await stat(baselineFile)).isFile())
|
|
1401
|
+
throw new Error("Baseline file is not a file");
|
|
1402
|
+
if (sourceDirectory === outputDirectory)
|
|
1403
|
+
throw new Error("Output directory must be separate from source directory");
|
|
1404
|
+
const files = (await readdir(sourceDirectory)).filter((file) => file.endsWith(".sql")).sort();
|
|
1405
|
+
const migrations = await Promise.all(files.map(async (file) => {
|
|
1406
|
+
const sql = await readFile(join(sourceDirectory, file), "utf8");
|
|
1407
|
+
return { file, version: migrationVersion(file), name: migrationName(file), sql };
|
|
1408
|
+
}));
|
|
1409
|
+
const plan = planMigrationRebase(migrations, { ...options, sourceDirectory, outputDirectory, baselineFile }, await readFile(baselineFile, "utf8"));
|
|
1410
|
+
await mkdir(outputDirectory, { recursive: true });
|
|
1411
|
+
await writeFile(join(outputDirectory, plan.baselineMigration), normalizedSql(await readFile(baselineFile, "utf8")));
|
|
1412
|
+
for (const migration of migrations.filter((item) => plan.retainedMigrations.includes(item.file))) {
|
|
1413
|
+
await writeFile(join(outputDirectory, migration.file), normalizedSql(migration.sql));
|
|
1414
|
+
}
|
|
1415
|
+
await writeFile(join(outputDirectory, "migration-rebase.manifest.json"), JSON.stringify({
|
|
1416
|
+
...plan,
|
|
1417
|
+
generatedAt: new Date().toISOString(),
|
|
1418
|
+
sourceHistory: migrations.map((migration) => ({
|
|
1419
|
+
file: migration.file,
|
|
1420
|
+
version: migration.version,
|
|
1421
|
+
name: migration.name,
|
|
1422
|
+
sha256: sha256(normalizedSql(migration.sql))
|
|
1423
|
+
}))
|
|
1424
|
+
}, null, 2) + `
|
|
1425
|
+
`);
|
|
1426
|
+
return plan;
|
|
1427
|
+
}
|
|
1304
1428
|
export {
|
|
1305
1429
|
COMMAND_PERSISTENCE_SQL,
|
|
1306
1430
|
COMMAND_PERSISTENCE_UPGRADE_SQL,
|
|
@@ -1322,8 +1446,10 @@ export {
|
|
|
1322
1446
|
lintSql,
|
|
1323
1447
|
migrationBindingSha256,
|
|
1324
1448
|
parseMigrationBindingManifest,
|
|
1449
|
+
planMigrationRebase,
|
|
1325
1450
|
planModule,
|
|
1326
1451
|
readCatalog,
|
|
1452
|
+
rebaseMigrations,
|
|
1327
1453
|
reconcileModule,
|
|
1328
1454
|
renderMigrationBindings,
|
|
1329
1455
|
splitQualifiedName
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface RebaseMigrationInput {
|
|
2
|
+
file: string;
|
|
3
|
+
version: string;
|
|
4
|
+
name: string;
|
|
5
|
+
sql: string;
|
|
6
|
+
}
|
|
7
|
+
export interface MigrationRebasePlan {
|
|
8
|
+
version: 1;
|
|
9
|
+
sourceDirectory: string;
|
|
10
|
+
outputDirectory: string;
|
|
11
|
+
baselineFile: string;
|
|
12
|
+
baselineMigration: string;
|
|
13
|
+
retainedMigrations: string[];
|
|
14
|
+
archivedMigrations: string[];
|
|
15
|
+
sourceHistorySha256: string;
|
|
16
|
+
baselineSha256: string;
|
|
17
|
+
warnings: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface MigrationRebaseOptions {
|
|
20
|
+
sourceDirectory: string;
|
|
21
|
+
outputDirectory: string;
|
|
22
|
+
baselineFile: string;
|
|
23
|
+
baselineVersion: string;
|
|
24
|
+
baselineName?: string;
|
|
25
|
+
retainAfterVersion?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function planMigrationRebase(migrations: readonly RebaseMigrationInput[], options: Pick<MigrationRebaseOptions, "sourceDirectory" | "outputDirectory" | "baselineFile" | "baselineVersion" | "baselineName" | "retainAfterVersion">, baselineSql: string): MigrationRebasePlan;
|
|
28
|
+
export declare function rebaseMigrations(options: MigrationRebaseOptions): Promise<MigrationRebasePlan>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Database governance layer for SupaCloud: RLS policies, RPC functions and grants as first-class resources, with manifest/catalog reconciliation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|