/
OfflineStorageMigrator.ts
94 lines (83 loc) · 3.53 KB
/
OfflineStorageMigrator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import {Apps, OfflineDbMeta, OfflineStorage} from "./OfflineStorage.js"
import {ModelInfos} from "../../common/EntityFunctions.js"
import {typedKeys} from "@tutao/tutanota-utils"
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 {repair1} from "./migrations/repair-v1"
export interface OfflineMigration {
readonly app: Apps
readonly version: number
migrate(storage: OfflineStorage): Promise<void>
}
/** List of migrations that will be run when needed. Please add your migrations to the list. */
export const OFFLINE_STORAGE_MIGRATIONS: ReadonlyArray<OfflineMigration> = [
sys75,
sys76,
sys79,
sys80,
tutanota54,
repair1,
]
/**
* 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
) {
}
async migrate(storage: OfflineStorage) {
let meta = await storage.dumpMetadata()
// 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, meta, storage)
}
// always run all repair migrations if none were run on the db
await this.prepopulateVersionIfNecessary("repair", 0, meta, storage)
// 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}`)
await migrate(storage)
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}`)
}
}
}
/**
* update the metadata table to initialize the row of the app with the given model version
*/
private async prepopulateVersionIfNecessary(app: Apps, version: number, meta: Partial<OfflineDbMeta>, storage: OfflineStorage) {
const key = `${app}-version` as const
const storedVersion = meta[key]
if (storedVersion == null) {
meta[key] = version
await storage.setStoredModelVersion(app, version)
}
}
}