2022-10-21 15:53:39 +02:00
import { OfflineDbMeta , OfflineStorage , VersionMetadataBaseKey } from "./OfflineStorage.js"
2022-05-17 17:40:44 +02:00
import { ModelInfos } from "../../common/EntityFunctions.js"
import { typedKeys } from "@tutao/tutanota-utils"
import { ProgrammingError } from "../../common/error/ProgrammingError.js"
2022-06-07 09:57:44 +02:00
import { sys75 } from "./migrations/sys-v75.js"
import { sys76 } from "./migrations/sys-v76.js"
2022-06-16 17:23:48 +02:00
import { tutanota54 } from "./migrations/tutanota-v54.js"
2022-09-05 17:28:05 +02:00
import { sys79 } from "./migrations/sys-v79.js"
2022-10-04 12:34:40 +02:00
import { sys80 } from "./migrations/sys-v80.js"
2022-10-21 15:53:39 +02:00
import { SqlCipherFacade } from "../../../native/common/generatedipc/SqlCipherFacade.js"
import { offline1 } from "./migrations/offline-v1.js"
2022-11-03 17:54:32 +01:00
import { tutanota56 } from "./migrations/tutanota-v56.js"
2022-12-13 17:42:50 +01:00
import { tutanota57 } from "./migrations/tutanota-v57.js"
2022-05-17 17:40:44 +02:00
export interface OfflineMigration {
2022-10-21 15:53:39 +02:00
readonly app : VersionMetadataBaseKey
2022-05-17 17:40:44 +02:00
readonly version : number
2022-06-16 17:23:48 +02:00
2022-10-21 15:53:39 +02:00
migrate ( storage : OfflineStorage , sqlCipherFacade : SqlCipherFacade ) : Promise < void >
2022-05-17 17:40:44 +02:00
}
2022-11-01 09:47:19 +01:00
/ * *
* List of migrations that will be run when needed . Please add your migrations to the list .
*
* Normally you should only add them to the end of the list but with offline ones it can be a bit tricky since they change the db structure itself so sometimes
* they should rather be in the beginning .
* /
2022-05-17 17:40:44 +02:00
export const OFFLINE_STORAGE_MIGRATIONS : ReadonlyArray < OfflineMigration > = [
2022-10-21 15:53:39 +02:00
offline1 ,
2022-11-01 09:47:19 +01:00
sys75 , // DB dropped in offline1
sys76 , // DB dropped in offline1
sys79 , // DB dropped in offline1
sys80 , // DB dropped in offline1
tutanota54 , // DB dropped in offline1
2022-11-03 17:54:32 +01:00
tutanota56 ,
2022-12-13 17:42:50 +01:00
tutanota57 ,
2022-05-17 17:40:44 +02:00
]
/ * *
* Migrator for the offline storage between different versions of model . It is tightly couples to the versions of API entities : every time we make an
* "incompatible" change to the API model we need to update offline database somehow .
*
* Migrations are done manually but there are a few checks done :
* - compile time check that migration exists and is used in this file
* - runtime check that runtime model is compatible to the stored one after all the migrations are done .
*
* To add a new migration create a migration with the filename matching . / migrations / { app } - v { version } . ts and use it in the ` migrations ` field on this
* migrator .
*
* Migrations might read and write to the database and they should use StandardMigrations when needed .
* /
export class OfflineStorageMigrator {
constructor (
private readonly migrations : ReadonlyArray < OfflineMigration > ,
private readonly modelInfos : ModelInfos
) {
}
2022-10-21 15:53:39 +02:00
async migrate ( storage : OfflineStorage , sqlCipherFacade : SqlCipherFacade ) {
2022-05-17 17:40:44 +02:00
let meta = await storage . dumpMetadata ( )
2022-10-21 15:53:39 +02:00
const isNewDb = Object . keys ( meta ) . length === 0
2022-05-17 17:40:44 +02:00
// Populate model versions if they haven't been written already
for ( const app of typedKeys ( this . modelInfos ) ) {
2022-10-18 16:27:03 +02:00
await this . prepopulateVersionIfNecessary ( app , this . modelInfos [ app ] . version , meta , storage )
2022-05-17 17:40:44 +02:00
}
2022-10-21 15:53:39 +02:00
if ( isNewDb ) {
2022-11-01 09:47:19 +01:00
console . log ( ` new db, setting "offline" version to 1 ` )
2022-10-21 15:53:39 +02:00
// this migration is not necessary for new databases and we want our canonical table definitions to represent the current state
await this . prepopulateVersionIfNecessary ( "offline" , 1 , meta , storage )
2022-11-01 09:47:19 +01:00
} else {
// we need to put 0 in because we expect all versions to be popylated
await this . prepopulateVersionIfNecessary ( "offline" , 0 , meta , storage )
2022-10-21 15:53:39 +02:00
}
2022-10-18 16:27:03 +02:00
2022-05-17 17:40:44 +02:00
// Run the migrations
for ( const { app , version , migrate } of this . migrations ) {
const storedVersion = meta [ ` ${ app } -version ` ] !
if ( storedVersion < version ) {
console . log ( ` running offline db migration for ${ app } from ${ storedVersion } to ${ version } ` )
2022-10-21 15:53:39 +02:00
await migrate ( storage , sqlCipherFacade )
2022-05-17 17:40:44 +02:00
console . log ( "migration finished" )
await storage . setStoredModelVersion ( app , version )
}
}
// Check that all the necessary migrations have been run, at least to the point where we are compatible.
meta = await storage . dumpMetadata ( )
for ( const app of typedKeys ( this . modelInfos ) ) {
const compatibleSince = this . modelInfos [ app ] . compatibleSince
let metaVersion = meta [ ` ${ app } -version ` ] !
if ( metaVersion < compatibleSince ) {
throw new ProgrammingError ( ` You forgot to migrate your databases! ${ app } .version should be >= ${ this . modelInfos [ app ] . compatibleSince } but in db it is ${ metaVersion } ` )
}
}
}
2022-10-18 16:27:03 +02:00
/ * *
* update the metadata table to initialize the row of the app with the given model version
* /
2022-10-21 15:53:39 +02:00
private async prepopulateVersionIfNecessary ( app : VersionMetadataBaseKey , version : number , meta : Partial < OfflineDbMeta > , storage : OfflineStorage ) {
2022-10-18 16:27:03 +02:00
const key = ` ${ app } -version ` as const
const storedVersion = meta [ key ]
if ( storedVersion == null ) {
meta [ key ] = version
await storage . setStoredModelVersion ( app , version )
}
}
2022-05-17 17:40:44 +02:00
}