Skip to content

Commit

Permalink
Add support for migrating journal entries (foundryvtt#5235)
Browse files Browse the repository at this point in the history
  • Loading branch information
stwlam committed Dec 19, 2022
1 parent 65f7703 commit 2ebbd29
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 44 deletions.
26 changes: 23 additions & 3 deletions packs/scripts/run-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,19 @@ const itemTypes = [
const isActorData = (docSource: CompendiumSource): docSource is ActorSourcePF2e => {
return "type" in docSource && actorTypes.includes(docSource.type);
};

const isItemData = (docSource: CompendiumSource): docSource is ItemSourcePF2e => {
return "type" in docSource && itemTypes.includes(docSource.type);
};

const isJournalEntryData = (docSource: CompendiumSource): docSource is foundry.data.JournalEntrySource => {
return "pages" in docSource && Array.isArray(docSource.pages);
};

const isMacroData = (docSource: CompendiumSource): docSource is foundry.data.MacroSource => {
return "type" in docSource && ["chat", "script"].includes(docSource.type);
};

const isTableData = (docSource: CompendiumSource): docSource is foundry.data.RollTableSource => {
return "results" in docSource && Array.isArray(docSource.results);
};
Expand Down Expand Up @@ -172,7 +179,12 @@ async function migrate() {
for (const filePath of allEntries) {
const content = await fs.readFile(filePath, { encoding: "utf-8" });

let source: ActorSourcePF2e | ItemSourcePF2e | foundry.data.MacroSource | foundry.data.RollTableSource;
let source:
| ActorSourcePF2e
| ItemSourcePF2e
| foundry.data.JournalEntrySource
| foundry.data.MacroSource
| foundry.data.RollTableSource;
try {
// Parse file content
source = JSON.parse(content);
Expand All @@ -183,9 +195,12 @@ async function migrate() {
return;
}

// skip journal entries, rollable tables, and macros
const updated = await (async (): Promise<
ActorSourcePF2e | ItemSourcePF2e | foundry.data.MacroSource | foundry.data.RollTableSource
| ActorSourcePF2e
| ItemSourcePF2e
| foundry.data.JournalEntrySource
| foundry.data.MacroSource
| foundry.data.RollTableSource
> => {
source.flags ??= {};
try {
Expand Down Expand Up @@ -224,6 +239,11 @@ async function migrate() {
pruneFlags(updatedItem);

return updatedItem;
} else if (isJournalEntryData(source)) {
const updated = await migrationRunner.getUpdatedJournalEntry(source, migrationRunner.migrations);
pruneFlags(source);
pruneFlags(updated);
return updated;
} else if (isMacroData(source)) {
const updated = await migrationRunner.getUpdatedMacro(source, migrationRunner.migrations);
pruneFlags(source);
Expand Down
30 changes: 18 additions & 12 deletions src/module/migration/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,42 @@ abstract class MigrationBase {
interface MigrationBase {
/**
* Update the actor to the latest schema version.
* @param actor This should be effectively a `ActorSourcePF2e` from the previous version.
* @param source This should be effectively a `ActorSourcePF2e` from the previous version.
*/
updateActor?(actor: ActorSourcePF2e): Promise<void>;
updateActor?(source: ActorSourcePF2e): Promise<void>;

/**
* Update the item to the latest schema version, handling changes that must happen before any other migration in a
* given list.
* @param item Item to update. This should be an `ItemData` from the previous version.
* @param actor If the item is part of an actor, this is set to the actor. For instance
* @param source Item to update. This should be an `ItemData` from the previous version
* @param actorSource If the item is part of an actor, this is set to the actor source
*/
preUpdateItem?(item: ItemSourcePF2e, actor?: ActorSourcePF2e): Promise<void>;
preUpdateItem?(source: ItemSourcePF2e, actorSource?: ActorSourcePF2e): Promise<void>;

/**
* Update the item to the latest schema version.
* @param item Item to update. This should be an `ItemData` from the previous version.
* @param actor If the item is part of an actor, this is set to the actor. For instance
* @param source Item to update. This should be an `ItemData` from the previous version.
* @param actorSource If the item is part of an actor, this is set to the actor. For instance
*/
updateItem?(item: ItemSourcePF2e, actor?: ActorSourcePF2e): Promise<void>;
updateItem?(source: ItemSourcePF2e, actorSource?: ActorSourcePF2e): Promise<void>;

/**
* Update the macro to the latest schema version.
* @param macroData Macro data to update. This should be a `MacroData` from the previous version.
* @param source Macro data to update. This should be a `MacroData` from the previous version.
*/
updateMacro?(macroData: foundry.data.MacroSource): Promise<void>;
updateJournalEntry?(source: foundry.data.JournalEntrySource): Promise<void>;

/**
* Update the macro to the latest schema version.
* @param source Macro data to update. This should be a `MacroData` from the previous version.
*/
updateMacro?(source: foundry.data.MacroSource): Promise<void>;

/**
* Update the rollable table to the latest schema version.
* @param tableData Rolltable data to update. This should be a `RollTableData` from the previous version.
* @param source Rolltable data to update. This should be a `RollTableData` from the previous version.
*/
updateTable?(tableData: foundry.data.RollTableSource): Promise<void>;
updateTable?(source: foundry.data.RollTableSource): Promise<void>;

/**
* Update the token to the latest schema version.
Expand Down
53 changes: 35 additions & 18 deletions src/module/migration/runner/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,21 @@ export class MigrationRunnerBase {
return current;
}

private updateSchemaRecord(schema: DocumentSchemaRecord, latestMigration: MigrationBase): void {
if (!("game" in globalThis && latestMigration)) return;
async getUpdatedTable(
tableSource: foundry.data.RollTableSource,
migrations: MigrationBase[]
): Promise<foundry.data.RollTableSource> {
const current = deepClone(tableSource);

const fromVersion = typeof schema.version === "number" ? schema.version : null;
schema.version = latestMigration.version;
schema.lastMigration = {
datetime: DateTime.now().toISO(),
version: {
schema: fromVersion,
foundry: "game" in globalThis ? game.version : undefined,
system: "game" in globalThis ? game.system.version : undefined,
},
};
for (const migration of migrations) {
try {
await migration.updateTable?.(current);
} catch (err) {
console.error(err);
}
}

return current;
}

async getUpdatedMacro(
Expand All @@ -164,21 +166,21 @@ export class MigrationRunnerBase {
return current;
}

async getUpdatedTable(
tableSource: foundry.data.RollTableSource,
async getUpdatedJournalEntry(
source: foundry.data.JournalEntrySource,
migrations: MigrationBase[]
): Promise<foundry.data.RollTableSource> {
const current = deepClone(tableSource);
): Promise<foundry.data.JournalEntrySource> {
const clone = deepClone(source);

for (const migration of migrations) {
try {
await migration.updateTable?.(current);
await migration.updateJournalEntry?.(clone);
} catch (err) {
console.error(err);
}
}

return current;
return clone;
}

async getUpdatedToken(token: TokenDocumentPF2e, migrations: MigrationBase[]): Promise<foundry.data.TokenSource> {
Expand All @@ -205,4 +207,19 @@ export class MigrationRunnerBase {

return current;
}

private updateSchemaRecord(schema: DocumentSchemaRecord, latestMigration: MigrationBase): void {
if (!("game" in globalThis && latestMigration)) return;

const fromVersion = typeof schema.version === "number" ? schema.version : null;
schema.version = latestMigration.version;
schema.lastMigration = {
datetime: DateTime.now().toISO(),
version: {
schema: fromVersion,
foundry: "game" in globalThis ? game.version : undefined,
system: "game" in globalThis ? game.system.version : undefined,
},
};
}
}
37 changes: 28 additions & 9 deletions src/module/migration/runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,21 @@ export class MigrationRunner extends MigrationRunnerBase {
return updatedActor;
}

private async migrateWorldMacro(migrations: MigrationBase[], macro: MacroPF2e): Promise<void> {
private async migrateWorldJournalEntry(journalEntry: JournalEntry, migrations: MigrationBase[]): Promise<void> {
if (!migrations.some((migration) => !!migration.updateJournalEntry)) return;

try {
const updated = await this.getUpdatedJournalEntry(journalEntry.toObject(), migrations);
const changes = diffObject(journalEntry.toObject(), updated);
if (Object.keys(changes).length > 0) {
await journalEntry.update(changes, { noHook: true });
}
} catch (error) {
console.warn(error);
}
}

private async migrateWorldMacro(macro: MacroPF2e, migrations: MigrationBase[]): Promise<void> {
if (!migrations.some((migration) => !!migration.updateMacro)) return;

try {
Expand All @@ -194,7 +208,7 @@ export class MigrationRunner extends MigrationRunnerBase {
}
}

private async migrateWorldTable(migrations: MigrationBase[], table: RollTable): Promise<void> {
private async migrateWorldTable(table: RollTable, migrations: MigrationBase[]): Promise<void> {
if (!migrations.some((migration) => !!migration.updateTable)) return;

try {
Expand All @@ -209,8 +223,8 @@ export class MigrationRunner extends MigrationRunnerBase {
}

private async migrateSceneToken(
migrations: MigrationBase[],
token: TokenDocumentPF2e
token: TokenDocumentPF2e,
migrations: MigrationBase[]
): Promise<foundry.data.TokenSource | null> {
if (!migrations.some((migration) => !!migration.updateToken)) return token.toObject();

Expand All @@ -232,7 +246,7 @@ export class MigrationRunner extends MigrationRunnerBase {
}
}

private async migrateUser(migrations: MigrationBase[], user: UserPF2e): Promise<void> {
private async migrateUser(user: UserPF2e, migrations: MigrationBase[]): Promise<void> {
if (!migrations.some((migration) => !!migration.updateUser)) return;

try {
Expand All @@ -256,19 +270,24 @@ export class MigrationRunner extends MigrationRunnerBase {
// Migrate World Items
await this.migrateWorldDocuments(game.items, migrations);

// Migrate world journal entries
for (const entry of game.journal) {
await this.migrateWorldJournalEntry(entry, migrations);
}

const promises: Promise<unknown>[] = [];
// Migrate World Macros
for (const macro of game.macros) {
promises.push(this.migrateWorldMacro(migrations, macro));
promises.push(this.migrateWorldMacro(macro, migrations));
}

// Migrate World RollTables
for (const table of game.tables) {
promises.push(this.migrateWorldTable(migrations, table));
promises.push(this.migrateWorldTable(table, migrations));
}

for (const user of game.users) {
promises.push(this.migrateUser(migrations, user));
promises.push(this.migrateUser(user, migrations));
}

// call the free-form migration function. can really do anything
Expand All @@ -287,7 +306,7 @@ export class MigrationRunner extends MigrationRunnerBase {
const { actor } = token;
if (!actor) continue;

const wasSuccessful = !!(await this.migrateSceneToken(migrations, token));
const wasSuccessful = !!(await this.migrateSceneToken(token, migrations));
if (!wasSuccessful) continue;

// Only migrate if the synthetic actor has replaced migratable data
Expand Down
11 changes: 11 additions & 0 deletions tests/fakes/journal-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @ts-nocheck
export class FakeJournalEntry {
_source: foundry.data.JournalEntrySource;

readonly pages: object[] = [];

constructor(source: foundry.data.JournalEntrySource) {
this._source = duplicate(source);
this.pages = source.pages;
}
}
2 changes: 2 additions & 0 deletions tests/module/migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ArmorSource } from "@item/data";
import { FoundryUtils } from "tests/utils";
import { FakeActors, FakeCollection, FakeItems, FakeWorldCollection } from "tests/fakes/fake-collection";
import { LocalizePF2e } from "@module/system/localize";
import { FakeJournalEntry } from "tests/fakes/journal-entry";

const characterData = FoundryUtils.duplicate(characterJSON) as unknown as CharacterSource;
characterData.effects = [];
Expand Down Expand Up @@ -61,6 +62,7 @@ describe("test migration runner", () => {
actors: new FakeActors(),
i18n: { format: (stringId: string, data: object): string => {} },
items: new FakeItems(),
journal: new FakeWorldCollection<FakeJournalEntry>(),
macros: new FakeWorldCollection<FakeMacro>(),
messages: new FakeWorldCollection<FakeChatMessage>(),
tables: new FakeWorldCollection<FakeRollTable>(),
Expand Down
5 changes: 3 additions & 2 deletions types/foundry/common/data/data/journal-entry-data.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ declare module foundry {
*
* @property _id The _id which uniquely identifies this JournalEntry document
* @property name The name of this JournalEntry
* @property content The HTML content of the JournalEntry
* @property pages The pages contained within this JournalEntry document
* @property [img] An image file path which provides the artwork for this JournalEntry
* @property folder The _id of a Folder which contains this JournalEntry
* @property [sort] The numeric sort value which orders this JournalEntry relative to its siblings
Expand All @@ -19,12 +19,13 @@ declare module foundry {
interface JournalEntrySource extends abstract.DocumentSource {
_id: string;
name: string;
pages: JournalEntryPageSource[];
content: string;
img: ImagePath;
folder: string | null;
sort: number;
ownership: Record<string, PermissionLevel>;
flags: Record<string, unknown>;
flags: Record<string, Record<string, unknown>>;
}

class JournalEntryData<
Expand Down

0 comments on commit 2ebbd29

Please sign in to comment.