mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 06:56:35 +00:00
fix(db): refuse a database or backup a newer TREK migrated, and keep a copy before a restore
Rolling an image back, or restoring a backup a newer release made, ran older code against a schema it does not know and failed later, far from the cause. Boot now refuses a database that records migrations this build does not ship, and the restore check refuses such a backup while the current database is still in place. A restore also keeps the database it replaces as data/pre-restore-<time>.db.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const MIGRATION_FILE = /^(Migration\d+_\w+)\.(?:js|ts)$/;
|
||||
|
||||
/**
|
||||
* The migrations this build ships, by the name the migrator records in
|
||||
* `mikro_orm_migrations`. Read from the directory the migrator itself reads:
|
||||
* `src/db/migrations` under tsx and vitest, `dist/db/migrations` in a build.
|
||||
*/
|
||||
export function knownMigrationNames(dir = path.join(__dirname, 'migrations')): Set<string> {
|
||||
const names = new Set<string>();
|
||||
for (const entry of fs.readdirSync(dir)) {
|
||||
const match = MIGRATION_FILE.exec(entry);
|
||||
if (match) names.add(match[1]);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recorded migrations this build does not ship: the database was migrated by
|
||||
* a newer TREK. Running older code against it fails later and far from the
|
||||
* cause, so the callers refuse instead.
|
||||
*/
|
||||
export function unknownMigrations(executed: Iterable<string>, known: Set<string> = knownMigrationNames()): string[] {
|
||||
return [...executed].filter((name) => !known.has(name));
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { Connection, MigrationInfo } from '@mikro-orm/core';
|
||||
import type { Migrator } from '@mikro-orm/migrations';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { knownMigrationNames, unknownMigrations } from './known-migrations';
|
||||
|
||||
/**
|
||||
* Upgrading an install the retired hand-written runner migrated.
|
||||
@@ -162,8 +163,10 @@ export async function migrateToHead(
|
||||
connection: Connection,
|
||||
migrator: Migrator,
|
||||
readSource: ReadSource = readFromDisk,
|
||||
known: Set<string> = knownMigrationNames(),
|
||||
): Promise<void> {
|
||||
const baselined = await planLegacyBaseline(connection, migrator, readSource);
|
||||
refuseNewerDatabase((await migrator.getExecuted()).map((row) => row.name), known);
|
||||
const already = new Set(baselined);
|
||||
const pending = (await migrator.getPending()).filter((migration) => !already.has(migration.name));
|
||||
if (pending.length === 0 && baselined.length === 0) return;
|
||||
@@ -180,3 +183,18 @@ export async function migrateToHead(
|
||||
await migrator.up({ transaction: trx });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A database a newer TREK migrated carries migrations this build does not
|
||||
* ship. An image rolled back, or a newer backup restored, would otherwise run
|
||||
* older code against a schema it does not know and fail later, far from the
|
||||
* cause. Refused before anything is written.
|
||||
*/
|
||||
export function refuseNewerDatabase(executed: string[], known: Set<string>): void {
|
||||
const unknown = unknownMigrations(executed, known);
|
||||
if (unknown.length === 0) return;
|
||||
throw new Error(
|
||||
`${REFUSAL} the database was migrated by a newer TREK (${unknown.length} unknown migration(s), latest ${unknown[unknown.length - 1]}). ` +
|
||||
'Run that version again, or restore a backup taken with this one.',
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user