diff --git a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js index 9479b0c7..112ea06d 100644 --- a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js +++ b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js @@ -13,12 +13,12 @@ import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileS import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety.js'; let migrateArchive; -if (Meteor.isServer){ - migrateArchive = require('/imports/migrations/server/migrateArchive.js').default; +if (Meteor.isServer) { + migrateArchive = require('/imports/migrations/archive/migrateArchive.js').default; } -function restoreCreature(archive, userId){ - if (SCHEMA_VERSION < archive.meta.schemaVersion){ +function restoreCreature(archive, userId) { + if (SCHEMA_VERSION < archive.meta.schemaVersion) { throw new Meteor.Error('Incompatible', 'The archive file is from a newer version. Update required to read.') } @@ -35,7 +35,7 @@ function restoreCreature(archive, userId){ }); if (existingCreature) throw new Meteor.Error('Already exists', 'The creature you are trying to restore already exists.') - + // Ensure the user owns the restored creature archive.creature.owner = userId; @@ -44,13 +44,13 @@ function restoreCreature(archive, userId){ Creatures.insert(archive.creature); try { // Add all the properties - if (archive.properties && archive.properties.length){ + if (archive.properties && archive.properties.length) { CreatureProperties.batchInsert(archive.properties); } - if (archive.experiences && archive.experiences.length){ + if (archive.experiences && archive.experiences.length) { Experiences.batchInsert(archive.experiences); } - if (archive.logs && archive.logs.length){ + if (archive.logs && archive.logs.length) { CreatureLogs.batchInsert(archive.logs); } } catch (e) { @@ -73,23 +73,23 @@ const restoreCreaturefromFile = new ValidatedMethod({ numRequests: 10, timeInterval: 5000, }, - async run({fileId}) { + async run({ fileId }) { // fetch the file - const file = ArchiveCreatureFiles.findOne({_id: fileId}).get(); - if (!file){ + const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get(); + if (!file) { throw new Meteor.Error('File not found', - 'The requested creature archive does not exist'); + 'The requested creature archive does not exist'); } // Assert ownership const userId = file?.userId; - if (!userId || userId !== this.userId){ + if (!userId || userId !== this.userId) { throw new Meteor.Error('Permission denied', - 'You can only restore creatures you own'); + 'You can only restore creatures you own'); } assertHasCharactersSlots(this.userId); - if (Meteor.isServer){ + if (Meteor.isServer) { // Read the file data const archive = await ArchiveCreatureFiles.readJSONFile(file); restoreCreature(archive, this.userId); diff --git a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js index 9c783df0..d87fe3b3 100644 --- a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js +++ b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js @@ -1,9 +1,9 @@ import applyFnToKey from '../utility/applyFnToKey.js'; import { unset } from 'lodash'; -export default function removeSchemaFields(schemas, prop){ +export default function removeSchemaFields(schemas, prop) { schemas.forEach(schema => { - schema.removeBeforeComputeFields().forEach( + schema?.removeBeforeComputeFields?.().forEach( key => applyFnToKey(prop, key, unset) ); }); diff --git a/app/imports/migrations/archive/cleanArchiveAt2.js b/app/imports/migrations/archive/cleanArchiveAt2.js new file mode 100644 index 00000000..224734ba --- /dev/null +++ b/app/imports/migrations/archive/cleanArchiveAt2.js @@ -0,0 +1,16 @@ +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; + +export default function cleanAt2(archive) { + archive.properties = archive.properties.map(prop => { + let cleanProp = prop; + try { + const schema = CreatureProperties.simpleSchema(prop); + // Clean according to schema + cleanProp = schema.clean(prop); + schema.validate(cleanProp); + } catch (e) { + console.warn('Failed to clean archive prop', { propId: prop._id, error: e.message || e.reason || e.toString() }); + } + return cleanProp; + }); +} diff --git a/app/imports/migrations/archive/migrateArchive.js b/app/imports/migrations/archive/migrateArchive.js new file mode 100644 index 00000000..029b146e --- /dev/null +++ b/app/imports/migrations/archive/migrateArchive.js @@ -0,0 +1,28 @@ +import migrateTo1 from './migrateArchiveTo1.js'; +import migrate1To2 from './migrateArchive1To2.js'; +import cleanAt2 from './cleanArchiveAt2.js'; + +/* eslint no-fallthrough: "off" -- Using switch fallthrough to run all +migration steps after the current version of the file. */ +export default function migrateArchive(archive) { + switch (archive.meta.schemaVersion) { + // V1 of DiceCloud + case 'version1': + migrateLegacyArchive(archive); + // V2 of DiceCloud, Schema version 1 + case 1: + migrateTo1(archive); + migrate1To2(archive); + // V2 of DiceCloud, Schema version 2 + case 2: + cleanAt2(archive); + break; + default: + throw 'Archive version not supported'; + } +} + +function migrateLegacyArchive() { + // TODO: + throw 'Not implemented'; +} diff --git a/app/imports/migrations/archive/migrateArchive1To2.js b/app/imports/migrations/archive/migrateArchive1To2.js new file mode 100644 index 00000000..20e934aa --- /dev/null +++ b/app/imports/migrations/archive/migrateArchive1To2.js @@ -0,0 +1,46 @@ +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; +import { get } from 'lodash'; + +const dollarSignRegex = /(\W)\$(\w+)/gi; + +export default function migrate1To2(archive) { + archive.properties = archive.properties.map(prop => { + try { + // Migrate slot fillers to folders + if (prop.type === 'slotFiller') { + prop.type = 'folder'; + } + // Get the schema + const schema = CreatureProperties.simpleSchema(prop); + // Replace dollar signs in calculations with tildes + schema.inlineCalculationFields().forEach(key => { + applyFnToKey(prop, key, (prop, key) => { + const inlineCalcObj = get(prop, key); + const string = inlineCalcObj?.text; + if (!string) return; + const newString = string.replace(dollarSignRegex, '$1~$2'); + if (string !== newString) { + inlineCalcObj.text = newString; + inlineCalcObj.hash = null; + } + }); + }); + schema.computedFields().forEach(key => { + applyFnToKey(prop, key, (prop, key) => { + const inlineCalcObj = get(prop, key); + const string = inlineCalcObj?.calculation; + if (!string) return; + const newString = string.replace(dollarSignRegex, '$1~$2'); + if (string !== newString) { + inlineCalcObj.calculation = newString; + inlineCalcObj.hash = null; + } + }); + }); + } catch (e) { + console.warn('Property migration 1 -> 2 failed: ', { propId: prop._id, error: e.message || e.reason || e.toString() }); + } + return prop; + }); +} diff --git a/app/imports/migrations/server/dbv1/cleanAt1.js b/app/imports/migrations/archive/migrateArchiveTo1.js similarity index 62% rename from app/imports/migrations/server/dbv1/cleanAt1.js rename to app/imports/migrations/archive/migrateArchiveTo1.js index d58e3c74..6e8fd558 100644 --- a/app/imports/migrations/server/dbv1/cleanAt1.js +++ b/app/imports/migrations/archive/migrateArchiveTo1.js @@ -1,39 +1,45 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { get, set } from 'lodash'; import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; -import { calculationUp } from '/imports/migrations/server/dbv1/dbv1.js'; -export default function cleanAt1(archive){ + +function calculationUp(val) { + if (typeof val !== 'string') return val; + if (!val.replace) console.log({ val, replace: val.replace }); + return val.replace(/#(\w+).(\w+)Result/g, '#$1.$2') + .replace(/\.value/g, '.total') + .replace(/\.currentValue/g, '.value'); +} + +export default function migrateTo1(archive) { archive.properties = archive.properties.map(prop => { - let cleanProp = prop; try { if (prop.type === 'attack') prop.type = 'action'; + if (prop.type === 'slotFiller') prop.type = 'folder'; // Get the schema const schema = CreatureProperties.simpleSchema(prop); // Clean all the text fields with inline calcs schema.inlineCalculationFields().forEach(key => { applyFnToKey(prop, key, (prop, key) => { let field = get(prop, key); - if (typeof field === 'string' || typeof field === 'number'){ + if (typeof field === 'string' || typeof field === 'number') { field = calculationUp(field); - set(prop, key, {text: `${field}`}); + set(prop, key, { text: `${field}` }); } }); }); schema.computedFields().forEach(key => { applyFnToKey(prop, key, (prop, key) => { let field = get(prop, key) || get(prop, key + 'Calculation'); - if (typeof field === 'string' || typeof field === 'number'){ + if (typeof field === 'string' || typeof field === 'number') { field = calculationUp(field); - set(prop, key, {calculation: `${field}`}); + set(prop, key, { calculation: `${field}` }); } }); }); - cleanProp = schema.clean(prop); - schema.validate(cleanProp); - } catch (e){ - console.warn({propId: prop._id, error: e.message || e.reason || e.toString()}); + } catch (e) { + console.warn('Property migration -> 1 failed: ', { propId: prop._id, error: e.message || e.reason || e.toString() }); } - return cleanProp; + return prop; }); } diff --git a/app/imports/migrations/server/dbv2/cleanAt2.js b/app/imports/migrations/server/dbv2/cleanAt2.js deleted file mode 100644 index f2dc9957..00000000 --- a/app/imports/migrations/server/dbv2/cleanAt2.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function cleanAt2() { - return; -} diff --git a/app/imports/migrations/server/migrateArchive.js b/app/imports/migrations/server/migrateArchive.js deleted file mode 100644 index 0fb557ed..00000000 --- a/app/imports/migrations/server/migrateArchive.js +++ /dev/null @@ -1,19 +0,0 @@ -import cleanAt1 from '/imports/migrations/server/dbv1/cleanAt1.js'; - -/* eslint no-fallthrough: "off" -- Using switch fallthrough to run all -migration steps after the current version of the file. */ -export default function migrateArchive(archive){ - switch (archive.meta.schemaVersion){ - // V1 of DiceCloud - case 'version1': - migrateLegacyArchive(archive); - // V2 of DiceCloud, Schema version 1 - case 1: - cleanAt1(archive); - } -} - -function migrateLegacyArchive(archive){ - // TODO: - throw 'Not implemented'; -}