import { Migrations } from 'meteor/percolate:migrations'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js'; import { restoreCreature } from '/imports/api/creature/archive/methods/restoreCreatures.js'; import { archiveCreature } from '/imports/api/creature/archive/methods/archiveCreatureToFile.js'; import transformFields from '/imports/migrations/server/transformFields.js'; import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; // Git version 2.0-beta.33 // Database version 1 Migrations.add({ version: 1, name: 'Unifies calculated field schema', up(){ migrate(); }, down(){ migrate({reversed: true}); }, }); function migrate({reversed} = {}){ console.log('restoring all characters from database archive'); const restoredIds = restoreAllCreatures(); console.log('migrating creature properties'); migrateCollection({collection: CreatureProperties, reversed}); console.log('migrating library nodes') migrateCollection({collection: LibraryNodes, reversed}); console.log('archiving characters to file system archive'); rearchiveAllCreatures(restoredIds); } function restoreAllCreatures(){ const ids = []; ArchivedCreatures.find({}, { fields: {_id: 1} }).forEach(archive => { const id = restoreCreature(archive._id); ids.push(id); }); return ids; } function rearchiveAllCreatures(ids){ ids.forEach(id => { archiveCreature(id); }); } function migrateCollection({collection, reversed}){ const bulk = collection.rawCollection().initializeUnorderedBulkOp(); collection.find({}).forEach(prop => { const newProp = migrateProperty({collection, reversed, prop}); bulk.find({ _id: prop._id }).replaceOne(newProp); }); bulk.execute(); } export function migrateProperty({collection, reversed, prop}){ const transforms = [ ...(transformsByPropType[prop.type] || []), {from: 'dependencies'} ]; let migratedProp = transformFields(prop, transforms, reversed); const schema = collection.simpleSchema({type: migratedProp.type}); // Only clean if the schema version matches our destination version if(!reversed && SCHEMA_VERSION === 1){ try { migratedProp = schema.clean(migratedProp); schema.validate(migratedProp); } catch(e){ if (e.details[0]?.type === 'maxString'){ console.log({ prop: prop, details: e.details, }); } else { console.warn({prop, error: e}); } } } return migratedProp; } const actionTransforms = [ ...getComputedPropertyTransforms('uses'), ...getComputedPropertyTransforms('resources.attributesConsumed.$.quantity'), ...getComputedPropertyTransforms('resources.itemsConsumed.$.quantity'), ...getInlineComputationTransforms('summary'), ...getInlineComputationTransforms('description'), ]; const transformsByPropType = { 'action': actionTransforms, 'adjustment': [ ...getComputedPropertyTransforms('amount'), {from: 'target', to: 'target', up: simplifyTarget}, ], 'attack': [ ...actionTransforms, ...getComputedPropertyTransforms('rollBonus', 'attackRoll'), //change type to action {from: 'type', to: 'type', up: () => 'action'}, ], 'attribute': [ // from: baseValue must be first or else it will delete the field we need {from: 'baseValue', to: 'baseValue.value', up: nanToNull}, {from: 'baseValueCalculation', to: 'baseValue.calculation', up: calculationUp, down: calculationDown}, {from: 'baseValueErrors', to: 'baseValue.errors', up: trimErrors}, ...getComputedPropertyTransforms('spellSlotLevel'), ...getInlineComputationTransforms('description'), {from: 'value', to: 'total', up: nanToNull}, {from: 'currentValue', to: 'value', up: nanToNull}, {from: 'proficiency', to: 'proficiency', up: stripZero}, ], 'buff': [ ...getComputedPropertyTransforms('duration'), ...getInlineComputationTransforms('description'), {from: 'value', to: 'total', up: nanToNull}, {from: 'target', to: 'target', up: simplifyTarget}, {from: 'applied'}, ], 'classLevel': [ ...getInlineComputationTransforms('description'), ], 'container': [ ...getInlineComputationTransforms('description'), ], 'damage': [ ...getComputedPropertyTransforms('amount'), {from: 'target', to: 'target', up: simplifyTarget}, ], 'effect': [ {from: 'calculation', to: 'amount.calculation'}, {from: 'result', to: 'amount.value', up: nanToNull}, {from: 'errors', to: 'amount.errors', up: trimErrors}, {from: 'name', to: 'name', up(val, src, doc){ if (src.operation === 'conditional'){ doc.text = val; return; } else { return val; } }}, ], 'feature': [ ...getInlineComputationTransforms('summary'), ...getInlineComputationTransforms('description'), ], 'item': [ ...getInlineComputationTransforms('description'), ], 'note': [ ...getInlineComputationTransforms('summary'), ...getInlineComputationTransforms('description'), ], 'roll': [ ...getComputedPropertyTransforms('roll'), ], 'savingThrow': [ ...getComputedPropertyTransforms('dc'), {from: 'target', to: 'target', up: simplifyTarget}, ], 'skill': [ ...getComputedPropertyTransforms('baseValue'), ...getInlineComputationTransforms('description'), {from: 'value', to: 'value', up: nanToNull}, {from: 'passiveBonus', to: 'passiveBonus', up: nanToNull}, {from: 'proficiency', to: 'proficiency', up: stripZero}, ], 'spell': [ ...actionTransforms, ], 'proficiency': [ {from: 'value', to: 'value', up: stripZero}, ], 'propertySlot': [ ...getComputedPropertyTransforms('quantityExpected'), ...getComputedPropertyTransforms('slotCondition'), ...getInlineComputationTransforms('description'), ], 'spellList': [ ...getComputedPropertyTransforms('maxPrepared'), ...getComputedPropertyTransforms('dc'), ...getComputedPropertyTransforms('attackRollBonus'), ...getInlineComputationTransforms('description'), ], 'toggle': [ {from: 'condition', to: 'condition.calculation'}, {from: 'toggleResult', to: 'condition.value', up: nanToNull}, {from: 'errors', to: 'condition.errors', up: trimErrors}, ], }; function getComputedPropertyTransforms(key, toKey){ if (!toKey) toKey = key; return [ {from: key, to: `${toKey}.calculation`, up: calculationUp, down: calculationDown}, {from: `${key}Result`, to: `${toKey}.value`, up: nanToNull}, {from: `${key}Errors`, to: `${toKey}.errors`, up: trimErrors}, ]; } function getInlineComputationTransforms(key){ return [ {from: key, to: `${key}.text`, up: calculationUp, down: calculationDown}, {from: `${key}Calculations`, to: `${key}.inlineCalculations`, up: calculationUp, down: calculationDown}, {from: `${key}Calculations.$.result`, to: `${key}.inlineCalculations.$.value`}, ]; } export 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'); } function calculationDown(val){ if (typeof val !== 'string') return val; return val.replace(/\.value/g, '.currentValue').replace(/\.total/g, '.value'); } function nanToNull(val){ if (Number.isNaN(val)){ return null; } else { return val; } } function stripZero(val){ if (val === 0){ return undefined; } else { return val; } } function simplifyTarget(val){ if (val === 'self'){ return val; } else { return 'target'; } } function trimErrors(arr){ if(!arr) return arr; arr.forEach(e => { if (e.message.length > STORAGE_LIMITS.errorMessage){ e.message = e.message.slice(0, STORAGE_LIMITS.errorMessage); } }); return arr; }