From 124824da8399ca27e063c8e56b203473648ec7b8 Mon Sep 17 00:00:00 2001
From: Johannes Loher <johannes.loher@fg4f.de>
Date: Sun, 9 Jul 2023 20:53:18 +0200
Subject: [PATCH] refactor: update each document individually during migrations

---
 src/migration/001.js              |  30 ++--
 src/migration/002.js              |  37 +++--
 src/migration/003.js              |  41 +++--
 src/migration/004.js              |  55 ++++---
 src/migration/005.js              |  49 +++---
 src/migration/006.js              |  63 ++++----
 src/migration/007.js              |  42 +++--
 src/migration/008.js              |  41 +++--
 src/migration/migration.js        |  22 +--
 src/migration/migrationHelpers.js | 250 ++++++++++--------------------
 10 files changed, 255 insertions(+), 375 deletions(-)

diff --git a/src/migration/001.js b/src/migration/001.js
index 1ea3b956..b0b67cf8 100644
--- a/src/migration/001.js
+++ b/src/migration/001.js
@@ -2,24 +2,18 @@
 //
 // SPDX-License-Identifier: MIT
 
-import {
-    getCompendiumMigrator,
-    getSceneUpdateDataGetter,
-    migrateActors,
-    migrateCompendiums,
-    migrateScenes,
-} from "./migrationHelpers";
+import { getSceneMigrator, migrateCollection, migrateCompendiums, getCompendiumMigrator } from "./migrationHelpers.js";
 
-/** @type {import("./migration").Migration["migrate"]} */
+/** @type {import("./migration.js").Migration["migrate"]} */
 async function migrate() {
-    await migrateActors(getActorUpdateData);
-    await migrateScenes(getSceneUpdateData);
+    await migrateCollection(game.actors, migrateActor);
+    await migrateCollection(game.scenes, migrateScene);
     await migrateCompendiums(migrateCompendium);
 }
 
-/** @type {import("./migrationHelpers").ActorUpdateDataGetter} */
-function getActorUpdateData() {
-    const updateData = {
+/** @type {import('./migrationHelpers.js').Migrator<Actor>} */
+async function migrateActor(actor) {
+    await actor.update({
         system: {
             combatValues: [
                 "hitPoints",
@@ -35,14 +29,14 @@ function getActorUpdateData() {
                 return acc;
             }, {}),
         },
-    };
-    return updateData;
+    });
 }
 
-const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
-const migrateCompendium = getCompendiumMigrator({ getActorUpdateData, getSceneUpdateData });
+const migrateScene = getSceneMigrator(migrateActor);
 
-/** @type {import("./migration").Migration} */
+const migrateCompendium = getCompendiumMigrator({ migrateActor, migrateScene });
+
+/** @type {import("./migration.js").Migration} */
 export const migration = {
     migrate,
     migrateCompendium,
diff --git a/src/migration/002.js b/src/migration/002.js
index 17d9fe90..f2848060 100644
--- a/src/migration/002.js
+++ b/src/migration/002.js
@@ -3,37 +3,36 @@
 // SPDX-License-Identifier: MIT
 
 import {
-    getActorUpdateDataGetter,
-    getCompendiumMigrator,
-    getSceneUpdateDataGetter,
-    migrateActors,
+    getSceneMigrator,
+    migrateCollection,
     migrateCompendiums,
-    migrateItems,
-    migrateScenes,
-} from "./migrationHelpers";
+    getCompendiumMigrator,
+    getActorMigrator,
+} from "./migrationHelpers.js";
 
-/** @type {import("./migration").Migration["migrate"]} */
+/** @type {import("./migration.js").Migration["migrate"]} */
 async function migrate() {
-    await migrateItems(getItemUpdateData);
-    await migrateActors(getActorUpdateData);
-    await migrateScenes(getSceneUpdateData);
+    await migrateCollection(game.items, migrateItem);
+    await migrateCollection(game.actors, migrateActor);
+    await migrateCollection(game.scenes, migrateScene);
     await migrateCompendiums(migrateCompendium);
 }
 
-/** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
-function getItemUpdateData(itemData) {
-    if (!["equipment", "trinket"].includes(itemData.type ?? "")) return undefined;
-    return { type: itemData.type === "equipment" ? "loot" : "equipment" };
+/** @type {import('./migrationHelpers.js').Migrator<Item>} */
+async function migrateItem(item) {
+    if (item.type === "equipment" || item.type === "trinket") {
+        await item.update({ type: item.type === "equipment" ? "loot" : "equipment" });
+    }
 }
 
-const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
-const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
+const migrateActor = getActorMigrator(migrateItem);
+const migrateScene = getSceneMigrator(migrateActor);
 const migrateCompendium = getCompendiumMigrator(
-    { getItemUpdateData, getActorUpdateData, getSceneUpdateData },
+    { migrateItem, migrateActor, migrateScene },
     { migrateToTemplateEarly: false },
 );
 
-/** @type {import("./migration").Migration} */
+/** @type {import("./migration.js").Migration} */
 export const migration = {
     migrate,
     migrateCompendium,
diff --git a/src/migration/003.js b/src/migration/003.js
index 28df66dd..4d62321e 100644
--- a/src/migration/003.js
+++ b/src/migration/003.js
@@ -3,41 +3,36 @@
 // SPDX-License-Identifier: MIT
 
 import {
-    getActorUpdateDataGetter,
-    getCompendiumMigrator,
-    getSceneUpdateDataGetter,
-    migrateActors,
+    getSceneMigrator,
+    migrateCollection,
     migrateCompendiums,
-    migrateItems,
-    migrateScenes,
-} from "./migrationHelpers";
+    getCompendiumMigrator,
+    getActorMigrator,
+} from "./migrationHelpers.js";
 
-/** @type {import("./migration").Migration["migrate"]} */
+/** @type {import("./migration.js").Migration["migrate"]} */
 async function migrate() {
-    await migrateItems(getItemUpdateData);
-    await migrateActors(getActorUpdateData);
-    await migrateScenes(getSceneUpdateData);
+    await migrateCollection(game.items, migrateItem);
+    await migrateCollection(game.actors, migrateActor);
+    await migrateCollection(game.scenes, migrateScene);
     await migrateCompendiums(migrateCompendium);
 }
 
-/** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
-function getItemUpdateData(itemData) {
-    if (!["loot"].includes(itemData.type ?? "")) return undefined;
-    return {
-        system: {
-            "-=equipped": null,
-        },
-    };
+/** @type {import("./migrationHelpers.js").Migrator<Item>} */
+async function migrateItem(item) {
+    if (item.type === "loot") {
+        await item.update({ system: { "-=equipped": null } });
+    }
 }
 
-const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
-const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
+const migrateActor = getActorMigrator(migrateItem);
+const migrateScene = getSceneMigrator(migrateActor);
 const migrateCompendium = getCompendiumMigrator(
-    { getItemUpdateData, getActorUpdateData },
+    { migrateItem, migrateActor, migrateScene },
     { migrateToTemplateEarly: false },
 );
 
-/** @type {import("./migration").Migration} */
+/** @type {import("./migration.js").Migration} */
 export const migration = {
     migrate,
     migrateCompendium,
diff --git a/src/migration/004.js b/src/migration/004.js
index 3f1b9168..d211ae6a 100644
--- a/src/migration/004.js
+++ b/src/migration/004.js
@@ -3,45 +3,44 @@
 // SPDX-License-Identifier: MIT
 
 import {
-    getActorUpdateDataGetter,
-    getCompendiumMigrator,
-    getSceneUpdateDataGetter,
-    migrateActors,
+    getSceneMigrator,
+    migrateCollection,
     migrateCompendiums,
-    migrateItems,
-    migrateScenes,
-} from "./migrationHelpers";
+    getCompendiumMigrator,
+    getActorMigrator,
+} from "./migrationHelpers.js";
 
-/** @type {import("./migration").Migration["migrate"]} */
+/** @type {import("./migration.js").Migration["migrate"]} */
 async function migrate() {
-    await migrateItems(getItemUpdateData);
-    await migrateActors(getActorUpdateData);
-    await migrateScenes(getSceneUpdateData);
+    await migrateCollection(game.items, migrateItem);
+    await migrateCollection(game.actors, migrateActor);
+    await migrateCollection(game.scenes, migrateScene);
     await migrateCompendiums(migrateCompendium);
 }
 
-/** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
-function getItemUpdateData(itemData) {
-    if (itemData.type !== "spell") return;
-    const cooldownDurationUnit = itemData.system?.cooldownDuration.unit;
-
-    const updateData = {
-        system: {
-            "-=scrollPrice": null,
-            minimumLevels: { healer: null, wizard: null, sorcerer: null },
-            cooldownDuration: {
-                unit: cooldownDurationUnit === "custom" ? "rounds" : cooldownDurationUnit,
+/** @type {import('./migrationHelpers.js').Migrator<Item>} */
+async function migrateItem(item) {
+    if (item.type === "spell") {
+        const cooldownDurationUnit = item.system?.cooldownDuration.unit;
+        await item.update({
+            system: {
+                "-=scrollPrice": null,
+                minimumLevels: { healer: null, wizard: null, sorcerer: null },
+                cooldownDuration: {
+                    unit: cooldownDurationUnit === "custom" ? "rounds" : cooldownDurationUnit,
+                },
             },
-        },
-    };
+        });
+    }
+
     return updateData;
 }
 
-const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
-const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
-const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
+const migrateActor = getActorMigrator(migrateItem);
+const migrateScene = getSceneMigrator(migrateActor);
+const migrateCompendium = getCompendiumMigrator({ migrateItem, migrateActor, migrateScene });
 
-/** @type {import("./migration").Migration} */
+/** @type {import("./migration.js").Migration} */
 export const migration = {
     migrate,
     migrateCompendium,
diff --git a/src/migration/005.js b/src/migration/005.js
index e91903ee..faeebd94 100644
--- a/src/migration/005.js
+++ b/src/migration/005.js
@@ -3,14 +3,12 @@
 // SPDX-License-Identifier: MIT
 
 import {
-    getActorUpdateDataGetter,
-    getCompendiumMigrator,
-    getSceneUpdateDataGetter,
-    migrateActors,
+    getSceneMigrator,
+    migrateCollection,
     migrateCompendiums,
-    migrateItems,
-    migrateScenes,
-} from "./migrationHelpers";
+    getCompendiumMigrator,
+    getActorMigrator,
+} from "./migrationHelpers.js";
 
 const secondsPerRound = 5;
 const secondsPerMinute = 60;
@@ -21,27 +19,22 @@ const hoursPerDay = 24;
 const roundsPerDay = hoursPerDay / roundsPerHour;
 const secondsPerDay = secondsPerMinute * minutesPerHour * hoursPerDay;
 
-/** @type {import("./migration").Migration["migrate"]} */
+/** @type {import("./migration.js").Migration["migrate"]} */
 async function migrate() {
-    await migrateItems(getItemUpdateData);
-    await migrateActors(getActorUpdateData);
-    await migrateScenes(getSceneUpdateData);
+    await migrateCollection(game.items, migrateItem);
+    await migrateCollection(game.actors, migrateActor);
+    await migrateCollection(game.scenes, migrateScene);
     await migrateCompendiums(migrateCompendium);
 }
 
-/** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
-function getItemUpdateData(itemData) {
-    if (itemData.type !== "spell") return;
-    const cooldownDurationUnit = itemData.system?.cooldownDuration.unit;
-    const cooldownDurationValue = itemData.system?.cooldownDuration.value;
-    const cooldownDuration = migrateCooldownDuration(cooldownDurationValue, cooldownDurationUnit);
-
-    const updateData = {
-        system: {
-            cooldownDuration,
-        },
-    };
-    return updateData;
+/** @type {import('./migrationHelpers.js').Migrator<Item>} */
+async function migrateItem(item) {
+    if (item.type === "spell") {
+        const cooldownDurationUnit = item.system?.cooldownDuration.unit;
+        const cooldownDurationValue = item.system?.cooldownDuration.value;
+        const cooldownDuration = migrateCooldownDuration(cooldownDurationValue, cooldownDurationUnit);
+        await item.update({ system: { cooldownDuration } });
+    }
 }
 
 function migrateCooldownDuration(cooldownDurationValue = "", cooldownDurationUnit = "") {
@@ -114,11 +107,11 @@ function getRounds(unit, value) {
     }
 }
 
-const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
-const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
-const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
+const migrateActor = getActorMigrator(migrateItem);
+const migrateScene = getSceneMigrator(migrateActor);
+const migrateCompendium = getCompendiumMigrator({ migrateItem, migrateActor, migrateScene });
 
-/** @type {import("./migration").Migration} */
+/** @type {import("./migration.js").Migration} */
 export const migration = {
     migrate,
     migrateCompendium,
diff --git a/src/migration/006.js b/src/migration/006.js
index 0f49cfc1..3afceaa9 100644
--- a/src/migration/006.js
+++ b/src/migration/006.js
@@ -3,42 +3,37 @@
 // SPDX-License-Identifier: MIT
 
 import {
-    getActorUpdateDataGetter,
-    getCompendiumMigrator,
-    getSceneUpdateDataGetter,
-    migrateActors,
+    getSceneMigrator,
+    migrateCollection,
     migrateCompendiums,
-    migrateItems,
-    migrateScenes,
-} from "./migrationHelpers";
+    getCompendiumMigrator,
+    getActorMigrator,
+} from "./migrationHelpers.js";
 
-/** @type {import("./migration").Migration["migrate"]} */
+/** @type {import("./migration.js").Migration["migrate"]} */
 async function migrate() {
-    await migrateItems(getItemUpdateData);
-    await migrateActors(getActorUpdateData);
-    await migrateScenes(getSceneUpdateData);
+    await migrateCollection(game.items, migrateItem);
+    await migrateCollection(game.actors, migrateActor);
+    await migrateCollection(game.scenes, migrateScene);
     await migrateCompendiums(migrateCompendium);
 }
 
-/** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
-function getItemUpdateData(itemData) {
-    if (itemData.type !== "spell") return;
-    const spellCategory = itemData.system?.spellCategory;
-    const spellGroups = migrateSpellCategory(spellCategory);
-
-    // @ts-expect-error bonus is removed with this migration
-    const bonus = itemData.system?.bonus;
-    const spellModifier = migrateBonus(bonus);
-
-    const updateData = {
-        system: {
-            spellGroups,
-            "-=spellCategory": null,
-            spellModifier,
-            "-=bonus": null,
-        },
-    };
-    return updateData;
+/** @type {import('./migrationHelpers.js').Migrator<Item>} */
+async function migrateItem(item) {
+    if (item.type === "spell") {
+        const spellCategory = item.system?.spellCategory;
+        const spellGroups = migrateSpellCategory(spellCategory);
+        const bonus = itemData.system?.bonus;
+        const spellModifier = migrateBonus(bonus);
+        await item.update({
+            system: {
+                spellGroups,
+                "-=spellCategory": null,
+                spellModifier,
+                "-=bonus": null,
+            },
+        });
+    }
 }
 
 /**
@@ -116,11 +111,11 @@ function migrateBonus(bonus) {
     return spellModifier;
 }
 
-const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
-const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
-const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
+const migrateActor = getActorMigrator(migrateItem);
+const migrateScene = getSceneMigrator(migrateActor);
+const migrateCompendium = getCompendiumMigrator({ migrateItem, migrateActor, migrateScene });
 
-/** @type {import("./migration").Migration} */
+/** @type {import("./migration.js").Migration} */
 export const migration = {
     migrate,
     migrateCompendium,
diff --git a/src/migration/007.js b/src/migration/007.js
index 540901ea..70a64437 100644
--- a/src/migration/007.js
+++ b/src/migration/007.js
@@ -3,39 +3,33 @@
 // SPDX-License-Identifier: MIT
 
 import {
-    getActorUpdateDataGetter,
-    getCompendiumMigrator,
-    getSceneUpdateDataGetter,
-    migrateActors,
+    getSceneMigrator,
+    migrateCollection,
     migrateCompendiums,
-    migrateItems,
-    migrateScenes,
-} from "./migrationHelpers";
+    getCompendiumMigrator,
+    getActorMigrator,
+} from "./migrationHelpers.js";
 
-/** @type {import("./migration").Migration["migrate"]} */
+/** @type {import("./migration.js").Migration["migrate"]} */
 async function migrate() {
-    await migrateItems(getItemUpdateData);
-    await migrateActors(getActorUpdateData);
-    await migrateScenes(getSceneUpdateData);
+    await migrateCollection(game.items, migrateItem);
+    await migrateCollection(game.actors, migrateActor);
+    await migrateCollection(game.scenes, migrateScene);
     await migrateCompendiums(migrateCompendium);
 }
 
-/** @type {import("./migrationHelpers").ItemUpdateDataGetter} */
-function getItemUpdateData(itemData) {
-    if (itemData.type !== "spell") return;
-
-    return {
-        system: {
-            allowsDefense: false,
-        },
-    };
+/** @type {import('./migrationHelpers.js').Migrator<Item>} */
+async function migrateItem(item) {
+    if (item.type === "spell") {
+        await item.update({ system: { allowsDefense: false } });
+    }
 }
 
-const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
-const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
-const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
+const migrateActor = getActorMigrator(migrateItem);
+const migrateScene = getSceneMigrator(migrateActor);
+const migrateCompendium = getCompendiumMigrator({ migrateItem, migrateActor, migrateScene });
 
-/** @type {import("./migration").Migration} */
+/** @type {import("./migration.js").Migration} */
 export const migration = {
     migrate,
     migrateCompendium,
diff --git a/src/migration/008.js b/src/migration/008.js
index bb04b1da..e517c429 100644
--- a/src/migration/008.js
+++ b/src/migration/008.js
@@ -3,28 +3,27 @@
 // SPDX-License-Identifier: MIT
 
 import {
-    getActorUpdateDataGetter,
-    getCompendiumMigrator,
-    getItemUpdateDataGetter,
-    getSceneUpdateDataGetter,
-    migrateActors,
+    getSceneMigrator,
+    migrateCollection,
     migrateCompendiums,
-    migrateItems,
-    migrateScenes,
-} from "./migrationHelpers";
+    getCompendiumMigrator,
+    getActorMigrator,
+    getItemMigrator,
+} from "./migrationHelpers.js";
 
-/** @type {import("./migration").Migration["migrate"]} */
+/** @type {import("./migration.js").Migration["migrate"]} */
 async function migrate() {
-    await migrateItems(getItemUpdateData);
-    await migrateActors(getActorUpdateData);
-    await migrateScenes(getSceneUpdateData);
+    await migrateCollection(game.items, migrateItem);
+    await migrateCollection(game.actors, migrateActor);
+    await migrateCollection(game.scenes, migrateScene);
     await migrateCompendiums(migrateCompendium);
 }
 
-/** @type {import("./migrationHelpers").EffectUpdateDataGetter} */
-function getEffectUpdateData(effectData) {
-    const data = foundry.utils.deepClone(effectData);
+/** @type {import('./migrationHelpers.js').Migrator<ActiveEffect>} */
+async function migrateActiveEffect(activeEffect) {
+    const data = activeEffect.toObject();
     let hasUpdates = false;
+
     if ("changes" in data) {
         for (const change of data.changes) {
             const newValue = change.value.replaceAll(/@data\./g, "@system.");
@@ -45,16 +44,16 @@ function getEffectUpdateData(effectData) {
         }
     }
     if (hasUpdates) {
-        return data;
+        await activeEffect.update(data);
     }
 }
 
-const getItemUpdateData = getItemUpdateDataGetter(getEffectUpdateData);
-const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData, getEffectUpdateData);
-const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
-const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
+const migrateItem = getItemMigrator(migrateActiveEffect);
+const migrateActor = getActorMigrator(migrateItem, migrateActiveEffect);
+const migrateScene = getSceneMigrator(migrateActor);
+const migrateCompendium = getCompendiumMigrator({ migrateItem, migrateActor, migrateScene });
 
-/** @type {import("./migration").Migration} */
+/** @type {import("./migration.js").Migration} */
 export const migration = {
     migrate,
     migrateCompendium,
diff --git a/src/migration/migration.js b/src/migration/migration.js
index 093559a7..4664c9fd 100644
--- a/src/migration/migration.js
+++ b/src/migration/migration.js
@@ -2,17 +2,17 @@
 //
 // SPDX-License-Identifier: MIT
 
-import { notifications } from "../ui/notifications";
-import { logger } from "../utils/logger";
-import { getGame } from "../utils/utils";
-import { migration as migration001 } from "./001";
-import { migration as migration002 } from "./002";
-import { migration as migration003 } from "./003";
-import { migration as migration004 } from "./004";
-import { migration as migration005 } from "./005";
-import { migration as migration006 } from "./006";
-import { migration as migration007 } from "./007";
-import { migration as migration008 } from "./008";
+import { notifications } from "../ui/notifications.js";
+import { logger } from "../utils/logger.js";
+import { getGame } from "../utils/utils.js";
+import { migration as migration001 } from "./001.js";
+import { migration as migration002 } from "./002.js";
+import { migration as migration003 } from "./003.js";
+import { migration as migration004 } from "./004.js";
+import { migration as migration005 } from "./005.js";
+import { migration as migration006 } from "./006.js";
+import { migration as migration007 } from "./007.js";
+import { migration as migration008 } from "./008.js";
 
 /**
  * Perform migrations.
diff --git a/src/migration/migrationHelpers.js b/src/migration/migrationHelpers.js
index 46b3304c..9b6b036a 100644
--- a/src/migration/migrationHelpers.js
+++ b/src/migration/migrationHelpers.js
@@ -2,82 +2,95 @@
 //
 // SPDX-License-Identifier: MIT
 
-import { DS4Actor } from "../documents/actor/actor";
-import { DS4Item } from "../documents/item/item";
-import { logger } from "../utils/logger";
-import { getGame } from "../utils/utils";
-
-/** @typedef {(effectData: object) => Record<string, unknown> | undefined} EffectUpdateDataGetter */
-
-/** @typedef {(itemData: object) => Record<string, unknown> | undefined} ItemUpdateDataGetter */
+import { DS4Actor } from "../documents/actor/actor.js";
+import { DS4Item } from "../documents/item/item.js";
+import { logger } from "../utils/logger.js";
+import { getGame } from "../utils/utils.js";
 
 /**
- * Migrate world items.
- * @param {ItemUpdateDataGetter} getItemUpdateData  A function for getting the update data for a given item data object
- * @returns {Promise<void>} A promise that resolves once the migration is complete
+ * @template T
+ * @typedef {(document: T) => Promise<void>} Migrator
  */
-export async function migrateItems(getItemUpdateData) {
-    for (const item of getGame().items ?? []) {
-        try {
-            const updateData = getItemUpdateData(item.toObject());
-            if (updateData) {
-                logger.info(`Migrating Item document ${item.name} (${item.id})`);
-                await item.update(updateData), { enforceTypes: false };
-            }
-        } catch (err) {
-            logger.error(`Error during migration of Item document ${item.name} (${item.id}), continuing anyways.`, err);
-        }
-    }
-}
-
-/** @typedef {(actorData: object>) => Record<string, unknown> | undefined} ActorUpdateDataGetter */
 
 /**
- * Migrate world actors.
- * @param {ActorUpdateDataGetter} getActorUpdateData    A function for getting the update data for a given actor data object
+ * Migrate a collection.
+ * @template T
+ * @param {WorldCollection} collection
+ * @param {Migrator<T>}     migrateDocument
  * @returns {Promise<void>} A promise that resolves once the migration is complete
  */
-export async function migrateActors(getActorUpdateData) {
-    for (const actor of getGame().actors ?? []) {
+export async function migrateCollection(collection, migrateDocument) {
+    const { documentName } = collection.constructor;
+    for (const document of collection) {
+        logger.info(`Migrating ${documentName} document ${document.name} (${document.id})`);
         try {
-            const updateData = getActorUpdateData(actor.toObject());
-            if (updateData) {
-                logger.info(`Migrating Actor document ${actor.name} (${actor.id})`);
-                await actor.update(updateData);
-            }
+            await migrateDocument(document);
         } catch (err) {
             logger.error(
-                `Error during migration of Actor document ${actor.name} (${actor.id}), continuing anyways.`,
+                `Error during migration of ${documentName} document ${document.name} (${document.id}), continuing anyways.`,
                 err,
             );
         }
     }
 }
 
-/** @typedef {(scene: Scene) => Record<string, unknown> | undefined} SceneUpdateDataGetter */
+/**
+ * @param {Migrator<ActiveEffect>}  [migrateActiveEffect]
+ * @returns {Migrator<Scene>}
+ */
+export function getItemMigrator(migrateActiveEffect) {
+    /**
+     * @param {Item} item
+     */
+    return async (item) => {
+        if (migrateActiveEffect) {
+            for (const effect of item.effects) {
+                await migrateActiveEffect(effect);
+            }
+        }
+    };
+}
 
 /**
- * Migrate world scenes.
- * @param {SceneUpdateDataGetter} getSceneUpdateData    A function for getting the update data for a given scene data object
- * @returns {Promise<void>} A promise that resolves once the migration is complete
+ * @param {Migrator<Item>}          [migrateItem]
+ * @param {Migrator<ActiveEffect>}  [migrateActiveEffect]
+ * @returns {Migrator<Scene>}
  */
-export async function migrateScenes(getSceneUpdateData) {
-    for (const scene of getGame().scenes ?? []) {
-        try {
-            const updateData = getSceneUpdateData(scene);
-            if (updateData) {
-                logger.info(`Migrating Scene document ${scene.name} (${scene.id})`);
-                await scene.update(updateData);
-                // We need to clear the old syntehtic actors from the cache
-                scene.tokens.forEach((t) => (t._actor = null));
+export function getActorMigrator(migrateItem, migrateActiveEffect) {
+    /**
+     * @param {Actor} actor
+     */
+    return async (actor) => {
+        if (migrateItem) {
+            for (const item of actor.items) {
+                await migrateItem(item);
             }
-        } catch (err) {
-            logger.error(
-                `Error during migration of Scene document ${scene.name} (${scene.id}), continuing anyways.`,
-                err,
-            );
         }
-    }
+        if (migrateActiveEffect) {
+            for (const effect of actor.effects) {
+                await migrateActiveEffect(effect);
+            }
+        }
+    };
+}
+
+/**
+ * @param {Migrator<Actor>} [migrateActor]
+ * @returns {Migrator<Scene>}
+ */
+export function getSceneMigrator(migrateActor) {
+    /**
+     * @param {Scene} scene
+     */
+    return async (scene) => {
+        if (migrateActor) {
+            for (const token of scene.tokens) {
+                if (!token.actorLink && token.actor) {
+                    await migrateActor(token.actor);
+                }
+            }
+        }
+    };
 }
 
 /** @typedef {(pack: CompendiumCollection) => Promise<void>} CompendiumMigrator */
@@ -96,118 +109,20 @@ export async function migrateCompendiums(migrateCompendium) {
 }
 
 /**
- * Get a function to create item update data based on the given function to update embedded documents.
- * @param {EffectUpdateDataGetter} [getEffectUpdateData]    A function to generate effect update data
- * @returns {ItemUpdateDataGetter} A function to get item update data
- */
-export function getItemUpdateDataGetter(getEffectUpdateData) {
-    return (itemData) => {
-        let hasEffectUpdates = false;
-        const effects = itemData.effects?.map((effectData) => {
-            const update = getEffectUpdateData(effectData);
-            if (update) {
-                hasEffectUpdates = true;
-                return foundry.utils.mergeObject(effectData, update, { inplace: false, performDeletions: true });
-            } else {
-                return effectData;
-            }
-        });
-        return hasEffectUpdates ? { effects } : undefined;
-    };
-}
-
-/**
- * Get a function to create actor update data based on the given function to update embedded documents.
- * @param {ItemUpdateDataGetter} [getItemUpdateData]        A function to generate item update data
- * @param {EffectUpdateDataGetter} [getEffectUpdateData]    A function to generate effect update data
- * @returns {ActorUpdateDataGetter} A function to get actor update data
- */
-export function getActorUpdateDataGetter(getItemUpdateData, getEffectUpdateData) {
-    return (actorData) => {
-        let hasItemUpdates = false;
-        const items = actorData.items?.map((itemData) => {
-            const update = getItemUpdateData?.(itemData);
-            if (update) {
-                hasItemUpdates = true;
-                return foundry.utils.mergeObject(itemData, update, { inplace: false, performDeletions: true });
-            } else {
-                return itemData;
-            }
-        });
-        let hasEffectUpdates = false;
-        const effects = actorData.effects?.map((effectData) => {
-            const update = getEffectUpdateData?.(effectData);
-            if (update) {
-                hasEffectUpdates = true;
-                return foundry.utils.mergeObject(effectData, update, { inplace: false, performDeletions: true });
-            } else {
-                return effectData;
-            }
-        });
-        const result = {
-            items: hasItemUpdates ? items : undefined,
-            effects: hasEffectUpdates ? effects : undefined,
-        };
-        return hasItemUpdates | hasEffectUpdates ? result : undefined;
-    };
-}
-
-/**
- * Get a function to create scene update data that adjusts the actors of the tokens of the scene according to the given function.
- * @param {ActorUpdateDataGetter} [getItemUpdateData]   The function to generate actor update data
- * @returns {SceneUpdateDataGetter} A function to get scene update data
- */
-export function getSceneUpdateDataGetter(getActorUpdateData) {
-    return (scene) => {
-        const tokens = scene.tokens.map((token) => {
-            const t = token.toObject();
-            if (!t.actorId || t.actorLink) {
-                t.actorData = {};
-            } else if (!getGame().actors?.has(t.actorId)) {
-                t.actorId = null;
-                t.actorData = {};
-            } else if (!t.actorLink) {
-                const actorData = foundry.utils.deepClone(t.actorData);
-                actorData.type = token.actor?.type;
-                const update = getActorUpdateData?.(actorData);
-                if (update !== undefined) {
-                    ["items", "effects"].forEach((embeddedName) => {
-                        const embeddedUpdates = update[embeddedName];
-                        if (!embeddedUpdates?.length) return;
-                        const updates = new Map(embeddedUpdates.flatMap((u) => (u && u._id ? [[u._id, u]] : [])));
-                        const originals = t.actorData[embeddedName];
-                        if (!originals) return;
-                        originals.forEach((original) => {
-                            if (!original._id) return;
-                            const update = updates.get(original._id);
-                            if (update) foundry.utils.mergeObject(original, update, { performDeletions: true });
-                        });
-                        delete update[embeddedName];
-                    });
-                    foundry.utils.mergeObject(t.actorData, update);
-                }
-            }
-            return t;
-        });
-        return { tokens };
-    };
-}
-
-/**
- * @typedef {object} UpdateDataGetters
- * @property {ItemUpdateDataGetter}     [getItemUpdateData]
- * @property {ActorUpdateDataGetter}    [getActorUpdateData]
- * @property {SceneUpdateDataGetter}    [getSceneUpdateData]
+ * @typedef {object} Migrators
+ * @property {Migrator<Item>}   [migrateItem]
+ * @property {Migrator<Actor>}  [migrateActor]
+ * @property {Migrator<Scene>}  [migrateScene]
  */
 
 /**
- * Get a compendium migrator for the given update data getters.
- * @param {UpdateDataGetters} [updateDataGetters={}]        The functions to use for getting update data
+ * Get a compendium migrator for the given migrators.
+ * @param {Migrators} [migrators={}]                        The functions to use for getting update data
  * @param {{migrateToTemplateEarly?: boolean}} [options={}] Additional options for the compendium migrator
  * @returns {CompendiumMigrator} The resulting compendium migrator
  */
 export function getCompendiumMigrator(
-    { getItemUpdateData, getActorUpdateData, getSceneUpdateData } = {},
+    { migrateItem, migrateActor, migrateScene } = {},
     { migrateToTemplateEarly = true } = {},
 ) {
     return async (pack) => {
@@ -224,15 +139,12 @@ export function getCompendiumMigrator(
         for (const doc of documents) {
             try {
                 logger.info(`Migrating document ${doc.name} (${doc.id}) in compendium ${pack.collection}`);
-                if (doc instanceof DS4Item && getItemUpdateData) {
-                    const updateData = getItemUpdateData(doc.toObject());
-                    updateData && (await doc.update(updateData));
-                } else if (doc instanceof DS4Actor && getActorUpdateData) {
-                    const updateData = getActorUpdateData(doc.toObject());
-                    updateData && (await doc.update(updateData));
-                } else if (doc instanceof Scene && getSceneUpdateData) {
-                    const updateData = getSceneUpdateData(doc);
-                    updateData && (await doc.update(updateData));
+                if (doc instanceof DS4Item && migrateItem) {
+                    await migrateItem(doc);
+                } else if (doc instanceof DS4Actor && migrateActor) {
+                    await migrateActor(doc);
+                } else if (doc instanceof Scene && migrateScene) {
+                    await migrateScene(doc);
                 }
             } catch (err) {
                 logger.error(