Added archive migrations to schema version 2

This commit is contained in:
Stefan Zermatten
2023-06-20 13:30:35 +02:00
parent 3af4528788
commit beb4d94676
8 changed files with 125 additions and 51 deletions

View File

@@ -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);

View File

@@ -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)
);
});

View File

@@ -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;
});
}

View File

@@ -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';
}

View File

@@ -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;
});
}

View File

@@ -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;
});
}

View File

@@ -1,3 +0,0 @@
export default function cleanAt2() {
return;
}

View File

@@ -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';
}