From 650084ac84a03c20c5f07f8cadef85502e069e49 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sun, 14 Feb 2021 13:36:00 +1100 Subject: [PATCH] Added ability for import to automatically upgrade older db versions --- src/database.js | 204 ++++++++++++++++++-------------- src/modals/ImportExportModal.js | 7 +- src/workers/DatabaseWorker.js | 16 ++- 3 files changed, 136 insertions(+), 91 deletions(-) diff --git a/src/database.js b/src/database.js index 51983f0..9a8b84d 100644 --- a/src/database.js +++ b/src/database.js @@ -1,22 +1,48 @@ -import Dexie from "dexie"; +// eslint-disable-next-line no-unused-vars +import Dexie, { Version, DexieOptions } from "dexie"; import blobToBuffer from "./helpers/blobToBuffer"; import { getGridDefaultInset } from "./helpers/grid"; import { convertOldActionsToShapes } from "./actions"; import { createThumbnail } from "./helpers/image"; -function loadVersions(db) { +// Helper to create a thumbnail for a file in a db +async function createDataThumbnail(data) { + const url = URL.createObjectURL(new Blob([data.file])); + return await Dexie.waitFor( + new Promise((resolve) => { + let image = new Image(); + image.onload = async () => { + const thumbnail = await createThumbnail(image); + resolve(thumbnail); + }; + image.src = url; + }) + ); +} + +/** + * @callback VersionCallback + * @param {Version} version + */ + +/** + * Mapping of version number to their upgrade function + * @type {Object.} + */ +const versions = { // v1.2.0 - db.version(1).stores({ - maps: "id, owner", - states: "mapId", - tokens: "id, owner", - user: "key", - }); + 1(v) { + v.stores({ + maps: "id, owner", + states: "mapId", + tokens: "id, owner", + user: "key", + }); + }, // v1.2.1 - Move from blob files to array buffers - db.version(2) - .stores({}) - .upgrade(async (tx) => { + 2(v) { + v.stores({}).upgrade(async (tx) => { const maps = await Dexie.waitFor(tx.table("maps").toArray()); let mapBuffers = {}; for (let map of maps) { @@ -29,10 +55,10 @@ function loadVersions(db) { map.file = mapBuffers[map.id]; }); }); + }, // v1.3.0 - Added new default tokens - db.version(3) - .stores({}) - .upgrade((tx) => { + 3(v) { + v.stores({}).upgrade((tx) => { return tx .table("states") .toCollection() @@ -92,10 +118,10 @@ function loadVersions(db) { } }); }); + }, // v1.3.1 - Added show grid option - db.version(4) - .stores({}) - .upgrade((tx) => { + 4(v) { + v.stores({}).upgrade((tx) => { return tx .table("maps") .toCollection() @@ -103,10 +129,10 @@ function loadVersions(db) { map.showGrid = false; }); }); + }, // v1.4.0 - Added fog subtraction - db.version(5) - .stores({}) - .upgrade((tx) => { + 5(v) { + v.stores({}).upgrade((tx) => { return tx .table("states") .toCollection() @@ -120,10 +146,10 @@ function loadVersions(db) { } }); }); + }, // v1.4.2 - Added map resolutions - db.version(6) - .stores({}) - .upgrade((tx) => { + 6(v) { + v.stores({}).upgrade((tx) => { return tx .table("maps") .toCollection() @@ -132,10 +158,10 @@ function loadVersions(db) { map.quality = "original"; }); }); + }, // v1.5.0 - Fixed default token rogue spelling - db.version(7) - .stores({}) - .upgrade((tx) => { + 7(v) { + v.stores({}).upgrade((tx) => { return tx .table("states") .toCollection() @@ -147,10 +173,10 @@ function loadVersions(db) { } }); }); + }, // v1.5.0 - Added map snap to grid option - db.version(8) - .stores({}) - .upgrade((tx) => { + 8(v) { + v.stores({}).upgrade((tx) => { return tx .table("maps") .toCollection() @@ -158,10 +184,10 @@ function loadVersions(db) { map.snapToGrid = true; }); }); + }, // v1.5.1 - Added lock, visibility and modified to tokens - db.version(9) - .stores({}) - .upgrade((tx) => { + 9(v) { + v.stores({}).upgrade((tx) => { return tx .table("states") .toCollection() @@ -175,10 +201,10 @@ function loadVersions(db) { } }); }); + }, // v1.5.1 - Added token prop category and remove isVehicle bool - db.version(10) - .stores({}) - .upgrade((tx) => { + 10(v) { + v.stores({}).upgrade((tx) => { return tx .table("tokens") .toCollection() @@ -187,10 +213,10 @@ function loadVersions(db) { delete token.isVehicle; }); }); + }, // v1.5.2 - Added automatic cache invalidation to maps - db.version(11) - .stores({}) - .upgrade((tx) => { + 11(v) { + v.stores({}).upgrade((tx) => { return tx .table("maps") .toCollection() @@ -198,10 +224,10 @@ function loadVersions(db) { map.lastUsed = map.lastModified; }); }); + }, // v1.5.2 - Added automatic cache invalidation to tokens - db.version(12) - .stores({}) - .upgrade((tx) => { + 12(v) { + v.stores({}).upgrade((tx) => { return tx .table("tokens") .toCollection() @@ -209,10 +235,10 @@ function loadVersions(db) { token.lastUsed = token.lastModified; }); }); + }, // v1.6.0 - Added map grouping and grid scale and offset - db.version(13) - .stores({}) - .upgrade((tx) => { + 13(v) { + v.stores({}).upgrade((tx) => { return tx .table("maps") .toCollection() @@ -232,10 +258,10 @@ function loadVersions(db) { delete map.gridType; }); }); + }, // v1.6.0 - Added token grouping - db.version(14) - .stores({}) - .upgrade((tx) => { + 14(v) { + v.stores({}).upgrade((tx) => { return tx .table("tokens") .toCollection() @@ -243,10 +269,10 @@ function loadVersions(db) { token.group = ""; }); }); + }, // v1.6.1 - Added width and height to tokens - db.version(15) - .stores({}) - .upgrade(async (tx) => { + 15(v) { + v.stores({}).upgrade(async (tx) => { const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); let tokenSizes = {}; for (let token of tokens) { @@ -269,10 +295,10 @@ function loadVersions(db) { token.height = tokenSizes[token.id].height; }); }); + }, // v1.7.0 - Added note tool - db.version(16) - .stores({}) - .upgrade((tx) => { + 16(v) { + v.stores({}).upgrade((tx) => { return tx .table("states") .toCollection() @@ -281,11 +307,10 @@ function loadVersions(db) { state.editFlags = [...state.editFlags, "notes"]; }); }); - + }, // 1.7.0 (hotfix) - Optimized fog shape edits to only include needed data - db.version(17) - .stores({}) - .upgrade((tx) => { + 17(v) { + v.stores({}).upgrade((tx) => { return tx .table("states") .toCollection() @@ -305,11 +330,10 @@ function loadVersions(db) { } }); }); - + }, // 1.8.0 - Added note text only mode, converted draw and fog representations - db.version(18) - .stores({}) - .upgrade((tx) => { + 18(v) { + v.stores({}).upgrade((tx) => { return tx .table("states") .toCollection() @@ -333,25 +357,10 @@ function loadVersions(db) { delete state.fogDrawActionIndex; }); }); - - async function createDataThumbnail(data) { - const url = URL.createObjectURL(new Blob([data.file])); - return await Dexie.waitFor( - new Promise((resolve) => { - let image = new Image(); - image.onload = async () => { - const thumbnail = await createThumbnail(image); - resolve(thumbnail); - }; - image.src = url; - }) - ); - } - + }, // 1.8.0 - Add thumbnail to maps and add measurement to grid - db.version(19) - .stores({}) - .upgrade(async (tx) => { + 19(v) { + v.stores({}).upgrade(async (tx) => { const maps = await Dexie.waitFor(tx.table("maps").toArray()); const thumbnails = {}; for (let map of maps) { @@ -365,11 +374,10 @@ function loadVersions(db) { map.grid.measurement = { type: "chebyshev", scale: "5ft" }; }); }); - + }, // 1.8.0 - Add thumbnail to tokens - db.version(20) - .stores({}) - .upgrade(async (tx) => { + 20(v) { + v.stores({}).upgrade(async (tx) => { const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); const thumbnails = {}; for (let token of tokens) { @@ -382,11 +390,35 @@ function loadVersions(db) { token.thumbnail = thumbnails[token.id]; }); }); + }, +}; + +const latestVersion = 20; + +/** + * Load versions onto a database up to a specific version number + * @param {Dexie} db + * @param {number=} upTo version number to load up to, latest version if undefined + */ +export function loadVersions(db, upTo = latestVersion) { + for (let versionNumber = 1; versionNumber <= upTo; versionNumber++) { + versions[versionNumber](db.version(versionNumber)); + } } -// Get the dexie database used in DatabaseContext -export function getDatabase(options, name="OwlbearRodeoDB") { +/** + * Get a Dexie database with a name and versions applied + * @param {DexieOptions} options + * @param {string=} name + * @param {number=} versionNumber + * @returns {Dexie} + */ +export function getDatabase( + options, + name = "OwlbearRodeoDB", + versionNumber = latestVersion +) { let db = new Dexie(name, options); - loadVersions(db); + loadVersions(db, versionNumber); return db; } diff --git a/src/modals/ImportExportModal.js b/src/modals/ImportExportModal.js index b30fb72..aa24fb4 100644 --- a/src/modals/ImportExportModal.js +++ b/src/modals/ImportExportModal.js @@ -48,14 +48,12 @@ function ImportExportModal({ isOpen, onRequestClose }) { setIsLoading(true); backgroundTaskRunningRef.current = true; try { - // Ensure import DB is cleared before importing new data - let importDB = getDatabase({}, importDBName); - importDB.delete(); await worker.importData( file, importDBName, Comlink.proxy(handleDBProgress) ); + setIsLoading(false); setShowImportSelector(true); backgroundTaskRunningRef.current = false; @@ -106,6 +104,7 @@ function ImportExportModal({ isOpen, onRequestClose }) { async function handleImportSelectorClose() { const importDB = getDatabase({}, importDBName); await importDB.delete(); + importDB.close(); setShowImportSelector(false); } @@ -157,6 +156,8 @@ function ImportExportModal({ isOpen, onRequestClose }) { } await importDB.delete(); + importDB.close(); + db.close(); setIsLoading(false); backgroundTaskRunningRef.current = false; window.location.reload(); diff --git a/src/workers/DatabaseWorker.js b/src/workers/DatabaseWorker.js index 2f5b92a..08e5dff 100644 --- a/src/workers/DatabaseWorker.js +++ b/src/workers/DatabaseWorker.js @@ -91,8 +91,19 @@ let service = { if (importMeta.data.databaseName !== db.name) { throw new Error("Unable to import database, name mismatch"); } + if (importMeta.data.databaseVersion > db.verno) { + throw new Error( + `Database version differs. Current database is in version ${db.verno} but export is ${importMeta.data.databaseVersion}` + ); + } - let importDB = getDatabase({}, databaseName); + // Ensure import DB is cleared before importing new data + let importDB = getDatabase({}, databaseName, 0); + await importDB.delete(); + importDB.close(); + + // Load import database up to it's desired version + importDB = getDatabase({}, databaseName, importMeta.data.databaseVersion); await importInto(importDB, data, { progressCallback, acceptNameDiff: true, @@ -107,9 +118,10 @@ let service = { } return true; }, + acceptVersionDiff: true, }); - db.close(); importDB.close(); + db.close(); }, };