2024-07-18 21:35:17 +03:00
import backupService from "./backup.js" ;
import sql from "./sql.js" ;
2024-07-18 21:37:45 +03:00
import fs from "fs-extra" ;
2024-07-18 21:35:17 +03:00
import log from "./log.js" ;
2025-01-02 13:47:44 +01:00
import { crash } from "./utils.js" ;
2024-07-18 21:35:17 +03:00
import resourceDir from "./resource_dir.js" ;
import appInfo from "./app_info.js" ;
import cls from "./cls.js" ;
2025-02-20 18:06:19 +02:00
import { t } from "i18next" ;
2024-02-17 19:42:30 +02:00
interface MigrationInfo {
dbVersion : number ;
name : string ;
file : string ;
2025-03-02 20:43:01 +02:00
type : "sql" | "js" | "ts" | string ;
/ * *
* Contains the JavaScript / TypeScript migration as a callback method that must be called to trigger the migration .
* The method cannot be async since it runs in an SQL transaction .
* For SQL migrations , this value is falsy .
* /
module ? : ( ) = > void ;
2024-02-17 19:42:30 +02:00
}
async function migrate() {
const currentDbVersion = getDbVersion ( ) ;
if ( currentDbVersion < 214 ) {
2025-02-20 18:06:19 +02:00
await crash ( t ( "migration.old_version" ) ) ;
2024-02-17 19:42:30 +02:00
return ;
}
// backup before attempting migration
await backupService . backupNow (
// creating a special backup for version 0.60.4, the changes in 0.61 are major.
2025-01-09 18:07:02 +02:00
currentDbVersion === 214 ? ` before-migration-v060 ` : "before-migration"
2024-02-17 19:42:30 +02:00
) ;
2025-03-02 20:43:01 +02:00
const migrations = await prepareMigrations ( currentDbVersion ) ;
2024-02-17 19:42:30 +02:00
migrations . sort ( ( a , b ) = > a . dbVersion - b . dbVersion ) ;
// all migrations are executed in one transaction - upgrade either succeeds, or the user can stay at the old version
// otherwise if half of the migrations succeed, user can't use any version - DB is too "new" for the old app,
// and too old for the new app version.
cls . setMigrationRunning ( true ) ;
2025-03-02 20:43:01 +02:00
sql . transactional ( ( ) = > {
2024-02-17 19:42:30 +02:00
for ( const mig of migrations ) {
try {
log . info ( ` Attempting migration to version ${ mig . dbVersion } ` ) ;
2025-03-02 20:43:01 +02:00
executeMigration ( mig ) ;
2024-02-17 19:42:30 +02:00
2025-01-09 18:07:02 +02:00
sql . execute (
` UPDATE options
2024-12-22 15:42:15 +02:00
SET value = ?
2025-01-09 18:07:02 +02:00
WHERE name = ? ` ,
[ mig . dbVersion . toString ( ) , "dbVersion" ]
) ;
2024-02-17 19:42:30 +02:00
log . info ( ` Migration to version ${ mig . dbVersion } has been successful. ` ) ;
} catch ( e : any ) {
2025-03-02 19:59:50 +02:00
console . error ( e ) ;
2025-02-20 18:06:19 +02:00
crash ( t ( "migration.error_message" , { version : mig.dbVersion , stack : e.stack } ) ) ;
2024-07-18 23:26:21 +03:00
break ; // crash() is sometimes async
2024-02-17 19:42:30 +02:00
}
}
} ) ;
if ( currentDbVersion === 214 ) {
// special VACUUM after the big migration
log . info ( "VACUUMing database, this might take a while ..." ) ;
sql . execute ( "VACUUM" ) ;
}
}
2025-03-02 20:43:01 +02:00
async function prepareMigrations ( currentDbVersion : number ) : Promise < MigrationInfo [ ] > {
const migrationFiles = fs . readdirSync ( resourceDir . MIGRATIONS_DIR ) ? ? [ ] ;
const migrations : MigrationInfo [ ] = [ ] ;
for ( const file of migrationFiles ) {
const match = file . match ( /^([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js|ts)$/ ) ;
if ( ! match ) {
continue ;
}
const dbVersion = parseInt ( match [ 1 ] ) ;
if ( dbVersion > currentDbVersion ) {
const name = match [ 2 ] ;
const type = match [ 3 ] ;
const migration : MigrationInfo = {
dbVersion : dbVersion ,
name : name ,
file : file ,
type : type
} ;
if ( type === "js" || type === "ts" ) {
// Due to ESM imports, the migration file needs to be imported asynchronously and thus cannot be loaded at migration time (since migration is not asynchronous).
// As such we have to preload the ESM.
migration . module = ( await import ( ` file:// ${ resourceDir . MIGRATIONS_DIR } / ${ file } ` ) ) . default ;
}
migrations . push ( migration ) ;
}
}
return migrations ;
}
function executeMigration ( mig : MigrationInfo ) {
if ( mig . module ) {
console . log ( "Migration with JS module" ) ;
mig . module ( ) ;
} else if ( mig . type === "sql" ) {
2025-01-09 18:07:02 +02:00
const migrationSql = fs . readFileSync ( ` ${ resourceDir . MIGRATIONS_DIR } / ${ mig . file } ` ) . toString ( "utf8" ) ;
2024-02-17 19:42:30 +02:00
console . log ( ` Migration with SQL script: ${ migrationSql } ` ) ;
sql . executeScript ( migrationSql ) ;
} else {
throw new Error ( ` Unknown migration type ' ${ mig . type } ' ` ) ;
}
}
function getDbVersion() {
return parseInt ( sql . getValue ( "SELECT value FROM options WHERE name = 'dbVersion'" ) ) ;
}
function isDbUpToDate() {
const dbVersion = getDbVersion ( ) ;
const upToDate = dbVersion >= appInfo . dbVersion ;
if ( ! upToDate ) {
log . info ( ` App db version is ${ appInfo . dbVersion } , while db version is ${ dbVersion } . Migration needed. ` ) ;
}
return upToDate ;
}
async function migrateIfNecessary() {
const currentDbVersion = getDbVersion ( ) ;
2025-01-09 18:07:02 +02:00
if ( currentDbVersion > appInfo . dbVersion && process . env . TRILIUM_IGNORE_DB_VERSION !== "true" ) {
2025-02-20 18:06:19 +02:00
await crash ( t ( "migration.wrong_db_version" , { version : currentDbVersion , targetVersion : appInfo.dbVersion } ) ) ;
2024-02-17 19:42:30 +02:00
}
if ( ! isDbUpToDate ( ) ) {
await migrate ( ) ;
}
}
2024-07-18 21:47:30 +03:00
export default {
2024-02-17 19:42:30 +02:00
migrateIfNecessary ,
isDbUpToDate
} ;