2022-12-27 15:37:40 +01:00
|
|
|
import { OfflineDbMeta, OfflineStorage, VersionMetadataBaseKey } from "./OfflineStorage.js"
|
2023-03-16 10:25:02 +01:00
|
|
|
import { ModelInfos } from "../../common/EntityFunctions.js"
|
2023-02-02 14:17:02 +01:00
|
|
|
import { assertNotNull, typedEntries, typedKeys } from "@tutao/tutanota-utils"
|
2022-12-27 15:37:40 +01:00
|
|
|
import { ProgrammingError } from "../../common/error/ProgrammingError.js"
|
|
|
|
|
import { sys75 } from "./migrations/sys-v75.js"
|
|
|
|
|
import { sys76 } from "./migrations/sys-v76.js"
|
|
|
|
|
import { tutanota54 } from "./migrations/tutanota-v54.js"
|
|
|
|
|
import { sys79 } from "./migrations/sys-v79.js"
|
|
|
|
|
import { sys80 } from "./migrations/sys-v80.js"
|
|
|
|
|
import { SqlCipherFacade } from "../../../native/common/generatedipc/SqlCipherFacade.js"
|
|
|
|
|
import { offline1 } from "./migrations/offline-v1.js"
|
|
|
|
|
import { tutanota56 } from "./migrations/tutanota-v56.js"
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
import { tutanota58 } from "./migrations/tutanota-v58.js"
|
|
|
|
|
import { storage6 } from "./migrations/storage-v6.js"
|
2022-12-27 15:37:40 +01:00
|
|
|
import { tutanota57 } from "./migrations/tutanota-v57.js"
|
2023-01-04 10:54:28 +01:00
|
|
|
import { OutOfSyncError } from "../../common/error/OutOfSyncError.js"
|
2023-01-26 16:34:37 +01:00
|
|
|
import { sys83 } from "./migrations/sys-v83.js"
|
2023-02-07 18:27:07 +01:00
|
|
|
import { tutanota60 } from "./migrations/tutanota-v60.js"
|
2023-02-02 17:21:34 +01:00
|
|
|
import { sys84 } from "./migrations/sys-v84.js"
|
2023-03-16 10:25:02 +01:00
|
|
|
import { tutanota61 } from "./migrations/tutanota-v61.js"
|
|
|
|
|
import { sys85 } from "./migrations/sys-v85.js"
|
2023-02-21 21:07:13 +01:00
|
|
|
import { accounting5 } from "./migrations/accounting-v5.js"
|
2023-05-25 11:07:39 +02:00
|
|
|
import { sys86 } from "./migrations/sys-v86.js"
|
2023-06-12 15:51:22 +02:00
|
|
|
import { sys87 } from "./migrations/sys-v87.js"
|
2023-06-22 08:54:13 +02:00
|
|
|
import { sys88 } from "./migrations/sys-v88.js"
|
2023-07-17 11:42:02 +02:00
|
|
|
import { sys89 } from "./migrations/sys-v89.js"
|
2023-06-19 17:44:21 +02:00
|
|
|
import { tutanota62 } from "./migrations/tutanota-v62.js"
|
2023-07-17 11:42:02 +02:00
|
|
|
import { tutanota63 } from "./migrations/tutanota-v63.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,
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
tutanota58,
|
2023-02-03 16:38:48 +01:00
|
|
|
tutanota60,
|
add MailDetails feature, #4719
server issues: 1276, 1271, 1279, 1272, 1270, 1258, 1254, 1253, 1242, 1241
2022-11-03 19:03:54 +01:00
|
|
|
storage6,
|
2023-01-26 16:34:37 +01:00
|
|
|
sys83,
|
2023-02-21 21:07:13 +01:00
|
|
|
accounting5,
|
2023-02-02 17:21:34 +01:00
|
|
|
sys84,
|
2023-03-16 10:25:02 +01:00
|
|
|
tutanota61,
|
|
|
|
|
sys85,
|
2023-05-25 11:07:39 +02:00
|
|
|
sys86,
|
2023-06-12 15:51:22 +02:00
|
|
|
sys87,
|
2023-06-22 08:54:13 +02:00
|
|
|
sys88,
|
2023-06-19 17:44:21 +02:00
|
|
|
tutanota62,
|
2023-07-17 11:42:02 +02:00
|
|
|
sys89,
|
|
|
|
|
tutanota63,
|
2022-05-17 17:40:44 +02:00
|
|
|
]
|
|
|
|
|
|
2023-02-02 14:17:02 +01:00
|
|
|
const CURRENT_OFFLINE_VERSION = 1
|
|
|
|
|
|
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 {
|
2022-12-27 15:37:40 +01:00
|
|
|
constructor(private readonly migrations: ReadonlyArray<OfflineMigration>, private readonly modelInfos: ModelInfos) {}
|
2022-05-17 17:40:44 +02:00
|
|
|
|
2022-10-21 15:53:39 +02:00
|
|
|
async migrate(storage: OfflineStorage, sqlCipherFacade: SqlCipherFacade) {
|
2023-02-02 15:07:12 +01:00
|
|
|
const meta = await storage.dumpMetadata()
|
2022-05-17 17:40:44 +02:00
|
|
|
|
2023-02-02 14:17:02 +01:00
|
|
|
// We did not write down the "offline" version from the beginning, so we need to figure out if we need to run the migration for the db structure or
|
|
|
|
|
// not. Previously we've been checking that there's something in the meta table which is a pretty decent check. Unfortunately we had multiple bugs
|
|
|
|
|
// which resulted in a state where we would re-create the offline db but not populate the meta table with the versions, the only thing that would be
|
|
|
|
|
// written is lastUpdateTime.
|
|
|
|
|
// {} -> new db, do not migrate offline
|
|
|
|
|
// {"base-version": 1, "lastUpdateTime": 123, "offline-version": 1} -> up-to-date db, do not migrate offline
|
|
|
|
|
// {"lastUpdateTime": 123} -> broken state after the buggy recreation of db, delete the db
|
|
|
|
|
// {"base-version": 1, "lastUpdateTime": 123} -> some very old state where we would actually have to migrate offline
|
|
|
|
|
if (Object.keys(meta).length === 1 && meta.lastUpdateTime != null) {
|
|
|
|
|
throw new OutOfSyncError("Invalid DB state, missing model versions")
|
2022-05-17 17:40:44 +02:00
|
|
|
}
|
|
|
|
|
|
2023-02-02 15:07:12 +01:00
|
|
|
const populatedMeta = await this.populateModelVersions(meta, storage)
|
2022-10-18 16:27:03 +02:00
|
|
|
|
2023-02-02 15:07:12 +01:00
|
|
|
if (this.isDbNewerThanCurrentClient(populatedMeta)) {
|
2023-01-04 10:54:28 +01:00
|
|
|
throw new OutOfSyncError(`offline database has newer schema than client`)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-02 15:07:12 +01:00
|
|
|
await this.runMigrations(meta, storage, sqlCipherFacade)
|
|
|
|
|
await this.checkStateAfterMigrations(storage)
|
|
|
|
|
}
|
2022-05-17 17:40:44 +02:00
|
|
|
|
2023-02-02 15:07:12 +01:00
|
|
|
private async checkStateAfterMigrations(storage: OfflineStorage) {
|
2022-05-17 17:40:44 +02:00
|
|
|
// Check that all the necessary migrations have been run, at least to the point where we are compatible.
|
2023-02-02 15:07:12 +01:00
|
|
|
const meta = await storage.dumpMetadata()
|
2022-05-17 17:40:44 +02:00
|
|
|
for (const app of typedKeys(this.modelInfos)) {
|
|
|
|
|
const compatibleSince = this.modelInfos[app].compatibleSince
|
|
|
|
|
let metaVersion = meta[`${app}-version`]!
|
|
|
|
|
if (metaVersion < compatibleSince) {
|
2022-12-27 15:37:40 +01:00
|
|
|
throw new ProgrammingError(
|
|
|
|
|
`You forgot to migrate your databases! ${app}.version should be >= ${this.modelInfos[app].compatibleSince} but in db it is ${metaVersion}`,
|
|
|
|
|
)
|
2022-05-17 17:40:44 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-18 16:27:03 +02:00
|
|
|
|
2023-02-02 15:07:12 +01:00
|
|
|
private async runMigrations(meta: Partial<OfflineDbMeta>, storage: OfflineStorage, sqlCipherFacade: SqlCipherFacade) {
|
|
|
|
|
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}`)
|
|
|
|
|
await migrate(storage, sqlCipherFacade)
|
|
|
|
|
console.log("migration finished")
|
|
|
|
|
await storage.setStoredModelVersion(app, version)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-02 14:17:02 +01:00
|
|
|
private async populateModelVersions(meta: Readonly<Partial<OfflineDbMeta>>, storage: OfflineStorage): Promise<Partial<OfflineDbMeta>> {
|
|
|
|
|
// We did not write down the "offline" version from the beginning, so we need to figure out if we need to run the migration for the db structure or
|
|
|
|
|
// not. New DB will have up-to-date table definition but no metadata keys.
|
|
|
|
|
const isNewDb = Object.keys(meta).length === 0
|
|
|
|
|
|
|
|
|
|
// copy metadata because it's going to be mutated
|
|
|
|
|
const newMeta = { ...meta }
|
|
|
|
|
// Populate model versions if they haven't been written already
|
|
|
|
|
for (const app of typedKeys(this.modelInfos)) {
|
|
|
|
|
await this.prepopulateVersionIfNecessary(app, this.modelInfos[app].version, newMeta, storage)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isNewDb) {
|
|
|
|
|
console.log(`new db, setting "offline" version to ${CURRENT_OFFLINE_VERSION}`)
|
|
|
|
|
// this migration is not necessary for new databases and we want our canonical table definitions to represent the current state
|
|
|
|
|
await this.prepopulateVersionIfNecessary("offline", CURRENT_OFFLINE_VERSION, newMeta, storage)
|
|
|
|
|
} else {
|
|
|
|
|
// we need to put 0 in because we expect all versions to be populated
|
|
|
|
|
await this.prepopulateVersionIfNecessary("offline", 0, newMeta, storage)
|
|
|
|
|
}
|
|
|
|
|
return newMeta
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-18 16:27:03 +02:00
|
|
|
/**
|
|
|
|
|
* update the metadata table to initialize the row of the app with the given model version
|
2023-02-02 14:17:02 +01:00
|
|
|
*
|
|
|
|
|
* NB: mutates meta
|
2022-10-18 16:27:03 +02:00
|
|
|
*/
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-04 10:54:28 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* it's possible that the user installed an older client over a newer one, and we don't have backwards migrations.
|
|
|
|
|
* in that case, it's likely that the client can't even understand the contents of the db.
|
|
|
|
|
* we're going to delete it and not migrate at all.
|
|
|
|
|
* @private
|
2023-01-04 16:54:58 +01:00
|
|
|
*
|
|
|
|
|
* @returns true if the database we're supposed to migrate has any higher model versions than our highest migration for that model, false otherwise
|
2023-01-04 10:54:28 +01:00
|
|
|
*/
|
2023-01-04 16:54:58 +01:00
|
|
|
private isDbNewerThanCurrentClient(meta: Partial<OfflineDbMeta>): boolean {
|
2023-02-02 14:17:02 +01:00
|
|
|
for (const [app, { version }] of typedEntries(this.modelInfos)) {
|
2023-01-04 10:54:28 +01:00
|
|
|
const storedVersion = meta[`${app}-version`]!
|
2023-02-02 14:17:02 +01:00
|
|
|
if (storedVersion > version) {
|
2023-01-04 16:54:58 +01:00
|
|
|
return true
|
2023-01-04 10:54:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-02 14:17:02 +01:00
|
|
|
return assertNotNull(meta[`offline-version`]) > CURRENT_OFFLINE_VERSION
|
2023-01-04 10:54:28 +01:00
|
|
|
}
|
2022-12-27 15:37:40 +01:00
|
|
|
}
|