diff --git a/eslint.config.mjs b/eslint.config.mjs index ea4be48..dd8bf28 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -45,6 +45,7 @@ export default [ }, languageOptions: { globals: { + __TITLE__: `readonly`, __ID__: `readonly`, __VERSION__: `readonly`, }, diff --git a/module/hooks/ready.mjs b/module/hooks/ready.mjs index 1d5e7c8..b9d5871 100644 --- a/module/hooks/ready.mjs +++ b/module/hooks/ready.mjs @@ -11,6 +11,38 @@ Hooks.on(`ready`, () => { CONFIG.stats.db = NilDatabase; }; + /* + Perform any required data migration if any is required for the version + jump that the user may have caused. This only migrates the data iff the + currently authenticated user is able to perform the full migration of + data. + */ + const db = CONFIG.stats.db; + const lastVersion = game.settings.get(__ID__, `lastVersion`); + const canDoMigration = db.canPerformMigration(); + const requiresMigration = db.requiresMigrationFrom(lastVersion); + if (requiresMigration) { + if (canDoMigration) { + const notif = ui.notifications.info( + `${__TITLE__} | Performing data migration, please do not close the window`, + { progress: true, permanent: true }, + ); + + // Fire and forget + CONFIG.stats.db.migrateData(notif) + .then(() => { + game.settings.set(__ID__, __VERSION__); + setTimeout(() => ui.notifications.remove(notif), 500); + }); + } else { + ui.notifications.error( + `The stat-tracker database is out of date, temporarily disabling the stat-tracker module's functionality until the migration can be performed by a GM user logging into the world.`, + { console: false, permanent: true }, + ); + CONFIG.stats.db = NilDatabase; + }; + }; + /* Prevent any run-time modifications to the CONFIG API so that users can't wreck themselves nor their data by fooling around with the values. diff --git a/module/settings/meta.mjs b/module/settings/meta.mjs index c236f7e..63e1b48 100644 --- a/module/settings/meta.mjs +++ b/module/settings/meta.mjs @@ -23,4 +23,11 @@ export function registerMetaSettings() { config: false, requiresReload: false, }); + + game.settings.register(__ID__, `lastVersion`, { + scope: `world`, + type: String, + config: false, + requiresReload: false, + }); }; diff --git a/module/utils/databases/Database.mjs b/module/utils/databases/Database.mjs index 8185b8f..65aa285 100644 --- a/module/utils/databases/Database.mjs +++ b/module/utils/databases/Database.mjs @@ -220,6 +220,43 @@ export class Database { static async triggerListeners() {}; static async unregisterListeners() {}; + + // MARK: Migrations + /** + * Determines if the currently authenticated user is capable of running + * the full migration on their own. + * + * @returns {boolean} + */ + static async canPerformMigration() { + // TODO: this *must* account for isActiveGM, because otherwise the + // world setting cannot be updated after the migration finishes. + return game.user.isActiveGM; + }; + + /** + * Determines if the previous version of the plugin that was active + * needs to be migrated in order to work with the new version. + * + * @param {string} lastVersion The version that was last active + * @returns {boolean} + */ + static async requiresMigrationFrom(lastVersion) { + return foundry.utils.isNewerVersion(__VERSION__, lastVersion); + }; + + /** + * This method migrates ALL of the database data from one version of + * the module to the currently installed module. This is not guaranteed + * to run only on one client, so it should be made to be either + * idempotent, or have an operation locking mechanism that can prevent + * other clients from executing it if there's a migration in-progress. + * + * @param {string} lastVersion The last version that the user had active + * @param {Notification} notif The progress bar notification used for + * user feedback while performing migrations. + */ + static async migrateData(lastVersion, notif) {}; }; /* eslint-enable no-unused-vars */ diff --git a/module/utils/databases/NilDatabase.mjs b/module/utils/databases/NilDatabase.mjs index 0a76981..c526a2d 100644 --- a/module/utils/databases/NilDatabase.mjs +++ b/module/utils/databases/NilDatabase.mjs @@ -31,4 +31,9 @@ export class NilDatabase extends Database { static async registerListeners() {}; static async triggerListeners() {}; static async unregisterListeners() {}; + + // MARK: Migrations + static async canPerformMigration() { return true }; + static async requiresMigrationFrom() { return false }; + static async migrateData() {}; }; diff --git a/vite.config.js b/vite.config.js index ed76b76..d849e07 100644 --- a/vite.config.js +++ b/vite.config.js @@ -80,6 +80,7 @@ export default defineConfig(({ mode }) => { return { plugins, define: { + __TITLE__: JSON.stringify(manifest.title), __ID__: JSON.stringify(manifest.id), __VERSION__: JSON.stringify(manifest.version), },