Improved migration code substantially, wrote migrations for more properties
This commit is contained in:
@@ -52,3 +52,4 @@ seba:minifiers-autoprefixer
|
||||
akryum:vue-component
|
||||
akryum:vue-sass
|
||||
percolate:migrations
|
||||
meteortesting:mocha
|
||||
|
||||
@@ -62,6 +62,9 @@ mdg:validated-method@1.2.0
|
||||
meteor@1.9.3
|
||||
meteor-base@1.4.0
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
meteortesting:browser-tests@1.3.4
|
||||
meteortesting:mocha@2.0.2
|
||||
meteortesting:mocha-core@8.0.1
|
||||
mikowals:batch-insert@1.2.0
|
||||
minifier-css@1.5.4
|
||||
minifier-js@2.6.1
|
||||
|
||||
@@ -10,6 +10,10 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let CreatureProperties = new Mongo.Collection('creatureProperties');
|
||||
|
||||
let CreaturePropertySchema = new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
_migrationError: {
|
||||
type: String,
|
||||
optional: true,
|
||||
|
||||
@@ -5,9 +5,8 @@ import {
|
||||
InlineCalculationFieldSchema,
|
||||
} from '/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js';
|
||||
import {
|
||||
FieldToComputeSchema,
|
||||
ComputedOnlyFieldSchema,
|
||||
ComputedFieldSchema,
|
||||
fieldToCompute,
|
||||
computedOnlyField,
|
||||
} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js';
|
||||
import {
|
||||
ResourcesSchema,
|
||||
@@ -60,7 +59,7 @@ let ActionSchema = new SimpleSchema({
|
||||
},
|
||||
// Calculation of how many times this action can be used
|
||||
uses: {
|
||||
type: FieldToComputeSchema,
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
// Integer of how many times it has already been used
|
||||
@@ -74,7 +73,7 @@ let ActionSchema = new SimpleSchema({
|
||||
allowedValues: ['longRest', 'shortRest'],
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
}).extend(fieldToCompute('uses'));
|
||||
|
||||
const ComputedOnlyActionSchema = new SimpleSchema({
|
||||
summary: {
|
||||
@@ -85,10 +84,6 @@ const ComputedOnlyActionSchema = new SimpleSchema({
|
||||
type: ComputedOnlyInlineCalculationFieldSchema,
|
||||
optional: true,
|
||||
},
|
||||
uses: {
|
||||
type: ComputedOnlyFieldSchema,
|
||||
optional: true,
|
||||
},
|
||||
resources: {
|
||||
type: ResourcesComputedOnlySchema,
|
||||
defaultValue: {},
|
||||
@@ -99,16 +94,12 @@ const ComputedOnlyActionSchema = new SimpleSchema({
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
}).extend(computedOnlyField('uses'));
|
||||
|
||||
const ComputedActionSchema = new SimpleSchema()
|
||||
.extend(ActionSchema)
|
||||
.extend(ComputedOnlyActionSchema)
|
||||
.extend({
|
||||
uses: {
|
||||
type: ComputedFieldSchema,
|
||||
optional: true,
|
||||
},
|
||||
summary: {
|
||||
type: InlineCalculationFieldSchema,
|
||||
optional: true,
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import {
|
||||
fieldToCompute,
|
||||
computedOnlyField,
|
||||
} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js';
|
||||
|
||||
const AdjustmentSchema = new SimpleSchema({
|
||||
// The roll that determines how much to change the attribute
|
||||
// This can be simplified, but should only compute when activated
|
||||
amount: {
|
||||
type: String,
|
||||
type: Object,
|
||||
optional: true,
|
||||
defaultValue: '1',
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
},
|
||||
'amount.calculation': {
|
||||
type: String,
|
||||
defaultValue: 1,
|
||||
},
|
||||
// Who this adjustment applies to
|
||||
target: {
|
||||
@@ -32,22 +37,9 @@ const AdjustmentSchema = new SimpleSchema({
|
||||
allowedValues: ['set', 'increment'],
|
||||
defaultValue: 'increment',
|
||||
},
|
||||
});
|
||||
}).extend(fieldToCompute('amount'));
|
||||
|
||||
const ComputedOnlyAdjustmentSchema = new SimpleSchema({
|
||||
amountResult: {
|
||||
type: SimpleSchema.oneOf(String, Number),
|
||||
optional: true,
|
||||
},
|
||||
amountErrors: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: STORAGE_LIMITS.errorCount,
|
||||
},
|
||||
'amountErrors.$':{
|
||||
type: ErrorSchema,
|
||||
},
|
||||
});
|
||||
const ComputedOnlyAdjustmentSchema = computedOnlyField('amount');
|
||||
|
||||
const ComputedAdjustmentSchema = new SimpleSchema()
|
||||
.extend(AdjustmentSchema)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js';
|
||||
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
|
||||
import {
|
||||
fieldToCompute,
|
||||
computedOnlyField,
|
||||
} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
// Attacks are special instances of actions
|
||||
@@ -9,10 +12,12 @@ let AttackSchema = new SimpleSchema()
|
||||
.extend({
|
||||
// What gets added to the d20 roll
|
||||
rollBonus: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
'rollBonus.calculation': {
|
||||
type: String,
|
||||
defaultValue: 'strength.modifier + proficiencyBonus',
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
},
|
||||
// Set better defaults for the action
|
||||
actionType: {
|
||||
@@ -29,24 +34,11 @@ let AttackSchema = new SimpleSchema()
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.tagLength,
|
||||
},
|
||||
});
|
||||
}).extend(fieldToCompute('rollBonus'));
|
||||
|
||||
const ComputedOnlyAttackSchema = new SimpleSchema()
|
||||
.extend(ComputedOnlyActionSchema)
|
||||
.extend({
|
||||
rollBonusResult: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
rollBonusErrors: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: STORAGE_LIMITS.errorCount,
|
||||
},
|
||||
'rollBonusErrors.$':{
|
||||
type: ErrorSchema,
|
||||
},
|
||||
});
|
||||
.extend(computedOnlyField('rollBonus'));
|
||||
|
||||
const ComputedAttackSchema = new SimpleSchema()
|
||||
.extend(AttackSchema)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { Random } from 'meteor/random';
|
||||
import {
|
||||
FieldToComputeSchema,
|
||||
ComputedOnlyFieldSchema,
|
||||
ComputedFieldSchema,
|
||||
fieldToCompute,
|
||||
computedOnlyField,
|
||||
} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
@@ -21,10 +20,10 @@ const AttributeConsumedSchema = new SimpleSchema({
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
quantity: {
|
||||
type: FieldToComputeSchema,
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
}).extend(fieldToCompute('quantity'));
|
||||
|
||||
const ComputedOnlyAttributeConsumedSchema = new SimpleSchema({
|
||||
available: {
|
||||
@@ -41,21 +40,11 @@ const ComputedOnlyAttributeConsumedSchema = new SimpleSchema({
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
quantity: {
|
||||
type: ComputedOnlyFieldSchema,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
}).extend(computedOnlyField('quantity'));
|
||||
|
||||
const ComputedAttributeConsumedSchema = new SimpleSchema()
|
||||
.extend(AttributeConsumedSchema)
|
||||
.extend(ComputedOnlyAttributeConsumedSchema)
|
||||
.extend({
|
||||
quantity: {
|
||||
type: ComputedFieldSchema,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
.extend(ComputedOnlyAttributeConsumedSchema);
|
||||
|
||||
export {
|
||||
AttributeConsumedSchema,
|
||||
|
||||
@@ -2,34 +2,53 @@ import SimpleSchema from 'simpl-schema';
|
||||
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
const FieldToComputeSchema = new SimpleSchema({
|
||||
// This is required, if we don't have a calculation delete the whole object
|
||||
calculation: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
// Get schemas that apply fields directly so they can be gracefully extended
|
||||
// because {type: Schema} fields can't be extended
|
||||
function fieldToCompute(field){
|
||||
return new SimpleSchema({
|
||||
// The object should already be set, but set again just in case
|
||||
[field]: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
// This is required, if we don't have a calculation delete the whole object
|
||||
[`${field}.calculation`]: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const ComputedOnlyFieldSchema = new SimpleSchema({
|
||||
value: {
|
||||
type: SimpleSchema.oneOf(String, Number),
|
||||
optional: true,
|
||||
},
|
||||
errors: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: STORAGE_LIMITS.errorCount,
|
||||
},
|
||||
'errors.$':{
|
||||
type: ErrorSchema,
|
||||
},
|
||||
});
|
||||
function computedOnlyField(field){
|
||||
return new SimpleSchema({
|
||||
// The object should already be set, but set again just in case
|
||||
[field]: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
[`${field}.value`]: {
|
||||
type: SimpleSchema.oneOf(String, Number),
|
||||
optional: true,
|
||||
},
|
||||
[`${field}.errors`]: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: STORAGE_LIMITS.errorCount,
|
||||
},
|
||||
[`${field}.errors.$`]:{
|
||||
type: ErrorSchema,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const ComputedFieldSchema = new SimpleSchema()
|
||||
.extend(FieldToComputeSchema)
|
||||
.extend(ComputedOnlyFieldSchema)
|
||||
// This should rarely be used, since the other two will merge correctly when
|
||||
// uncomputed and computedOnly schemas are merged
|
||||
function computedField(field){
|
||||
return computedField(field).extend(computedOnlyField(field));
|
||||
}
|
||||
|
||||
export {
|
||||
FieldToComputeSchema,
|
||||
ComputedOnlyFieldSchema,
|
||||
ComputedFieldSchema
|
||||
fieldToCompute,
|
||||
computedOnlyField,
|
||||
computedField,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { Random } from 'meteor/random';
|
||||
import {
|
||||
FieldToComputeSchema,
|
||||
ComputedOnlyFieldSchema,
|
||||
ComputedFieldSchema,
|
||||
fieldToCompute,
|
||||
computedOnlyField,
|
||||
} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js';
|
||||
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
@@ -21,7 +20,7 @@ const ItemConsumedSchema = new SimpleSchema({
|
||||
optional: true,
|
||||
},
|
||||
quantity: {
|
||||
type: FieldToComputeSchema,
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
itemId: {
|
||||
@@ -29,17 +28,13 @@ const ItemConsumedSchema = new SimpleSchema({
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
}).extend(fieldToCompute('quantity'));
|
||||
|
||||
const ComputedOnlyItemConsumedSchema = new SimpleSchema({
|
||||
available: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
quantity: {
|
||||
type: ComputedOnlyFieldSchema,
|
||||
optional: true,
|
||||
},
|
||||
// This appears both in the computed and uncomputed schema because it can be
|
||||
// set by both a computation or a form
|
||||
itemId: {
|
||||
@@ -62,17 +57,11 @@ const ComputedOnlyItemConsumedSchema = new SimpleSchema({
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.color,
|
||||
},
|
||||
})
|
||||
}).extend(computedOnlyField('quantity'));
|
||||
|
||||
const ComputedItemConsumedSchema = new SimpleSchema()
|
||||
.extend(ItemConsumedSchema)
|
||||
.extend(ComputedOnlyItemConsumedSchema)
|
||||
.extend({
|
||||
quantity: {
|
||||
type: ComputedFieldSchema,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
.extend(ComputedOnlyItemConsumedSchema);
|
||||
|
||||
export {
|
||||
ItemConsumedSchema,
|
||||
|
||||
3
app/imports/constants/SCHEMA_VERSION.js
Normal file
3
app/imports/constants/SCHEMA_VERSION.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const SCHEMA_VERSION = 1;
|
||||
|
||||
export default SCHEMA_VERSION;
|
||||
@@ -1,186 +0,0 @@
|
||||
import { Migrations } from 'meteor/percolate:migrations';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { get, merge } from 'lodash';
|
||||
|
||||
// Git version 2.0-beta.33
|
||||
// Database version 1
|
||||
Migrations.add({
|
||||
version: 1,
|
||||
name: 'Unifies calculated field schema',
|
||||
up(){
|
||||
CreatureProperties.find({}).forEach(prop => {
|
||||
const modifier = getUpPropModifier(prop);
|
||||
if (!modifier) return;
|
||||
updateOrStoreError(CreatureProperties, prop, modifier);
|
||||
});
|
||||
},
|
||||
down(){
|
||||
CreatureProperties.find({}).forEach(prop => {
|
||||
const modifier = getDownPropModifier(prop);
|
||||
if (!modifier) return;
|
||||
updateOrStoreError(CreatureProperties, prop, modifier);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function updateOrStoreError(collection, prop, modifier){
|
||||
try {
|
||||
collection.update(prop._id, modifier, {
|
||||
bypassCollection2: true,
|
||||
//selector: {type: prop.type},
|
||||
});
|
||||
} catch(e){
|
||||
let errorString = e.toString();
|
||||
if (errorString){
|
||||
console.warn(errorString, prop._id);
|
||||
collection.update(prop._id, {
|
||||
$set: {_migrationError: e.toString()}
|
||||
}, {
|
||||
bypassCollection2: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUpPropModifier(prop){
|
||||
const modifiers = typeUpModifiers[prop.type]?.(prop);
|
||||
if (!modifiers) return;
|
||||
return cleanModifier(merge(...modifiers));
|
||||
}
|
||||
|
||||
function getDownPropModifier(prop){
|
||||
const modifiers = typeDownModifiers[prop.type]?.(prop);
|
||||
if (!modifiers) return;
|
||||
return cleanModifier(merge(...modifiers));
|
||||
}
|
||||
|
||||
function cleanModifier(modifier){
|
||||
if (modifier.$set && !Object.keys(modifier.$set).length){
|
||||
delete modifier.$set;
|
||||
}
|
||||
if (modifier.$unset && !Object.keys(modifier.$unset).length){
|
||||
delete modifier.$unset;
|
||||
}
|
||||
if (!modifier.$set && !modifier.$unset) return;
|
||||
return modifier;
|
||||
}
|
||||
|
||||
const typeUpModifiers = {
|
||||
action(prop){
|
||||
return [
|
||||
convertComputedField(prop, 'uses'),
|
||||
// TODO: This doesn't work on itemsConsumed because it is an array field
|
||||
// Need to iterate over every item consumed
|
||||
convertComputedField(prop, 'resources.itemsConsumed.quantity'),
|
||||
convertComputedField(prop, 'resources.attributesConsumed.quantity'),
|
||||
convertInlineComputationField(prop, 'summary'),
|
||||
convertInlineComputationField(prop, 'description'),
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
const typeDownModifiers = {
|
||||
action(prop){
|
||||
const modifiers = [
|
||||
unConvertComputedField(prop, 'uses'),
|
||||
unConvertComputedField(prop, 'resources.itemsConsumed.quantity'),
|
||||
unConvertComputedField(prop, 'resources.attributesConsumed.quantity'),
|
||||
unConvertInlineComputationField(prop, 'summary'),
|
||||
unConvertInlineComputationField(prop, 'description'),
|
||||
];
|
||||
return modifiers;
|
||||
},
|
||||
};
|
||||
|
||||
function convertComputedField(object, field){
|
||||
const calculation = get(object, field);
|
||||
if (!calculation) return {
|
||||
$unset: {
|
||||
[field]: 1,
|
||||
[field + 'Errors']: 1,
|
||||
[field + 'Result']: 1,
|
||||
}
|
||||
};
|
||||
const errors = get(object, field + 'Errors');
|
||||
let value = get(object, field + 'Result');
|
||||
// If the calculation can be cast to number, use that for value
|
||||
if (value === undefined && Number.isFinite(+calculation)){
|
||||
value = +calculation;
|
||||
}
|
||||
const modifier = {
|
||||
$unset:{
|
||||
[field + 'Errors']: 1,
|
||||
[field + 'Result']: 1,
|
||||
},
|
||||
$set: {
|
||||
[field]: {
|
||||
value,
|
||||
calculation,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
};
|
||||
return modifier;
|
||||
}
|
||||
|
||||
function unConvertComputedField(object, field){
|
||||
const calculation = get(object, field)?.calculation;
|
||||
if (!calculation) return {
|
||||
$unset: {
|
||||
[field]: 1,
|
||||
}
|
||||
};
|
||||
const errors = get(object, field).errors;
|
||||
let value = get(object, field).value;
|
||||
// If the calculation can be cast to number, use that for value
|
||||
if (value === undefined && Number.isFinite(+calculation)){
|
||||
value = +calculation;
|
||||
}
|
||||
const modifier = {
|
||||
$set:{
|
||||
[field]: calculation,
|
||||
[field + 'Errors']: errors,
|
||||
[field + 'Result']: value,
|
||||
},
|
||||
};
|
||||
return modifier;
|
||||
}
|
||||
|
||||
function convertInlineComputationField(object, field){
|
||||
const text = get(object, field);
|
||||
const inlineCalculations = get(object, field + 'Calculations');
|
||||
if (inlineCalculations){
|
||||
inlineCalculations.forEach(calc => {
|
||||
calc.value = calc.result;
|
||||
delete calc.result;
|
||||
});
|
||||
}
|
||||
return {
|
||||
$unset: {
|
||||
[field + 'Calculations']: 1,
|
||||
},
|
||||
$set: {
|
||||
[field]: {
|
||||
text,
|
||||
inlineCalculations,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unConvertInlineComputationField(object, field){
|
||||
const text = get(object, field)?.text;
|
||||
const inlineCalculations = get(object, field)?.inlineCalculations;
|
||||
if (inlineCalculations) {
|
||||
inlineCalculations.forEach(calc => {
|
||||
calc.result = calc.value;
|
||||
delete calc.value;
|
||||
});
|
||||
}
|
||||
return {
|
||||
$set: {
|
||||
[field]: text,
|
||||
[field + 'Calculations']: inlineCalculations,
|
||||
},
|
||||
};
|
||||
}
|
||||
88
app/imports/migrations/server/2.0-beta.33-dbv1.js
Normal file
88
app/imports/migrations/server/2.0-beta.33-dbv1.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Migrations } from 'meteor/percolate:migrations';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import { get } from 'lodash';
|
||||
import embedInlineCalculations from '/imports/api/creature/computation/afterComputation/embedInlineCalculations.js';
|
||||
import transformFields from '/imports/migrations/server/transformFields.js';
|
||||
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.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} = {}){
|
||||
migrateCollection({collection: CreatureProperties, reversed});
|
||||
migrateCollection({collection: LibraryNodes, reversed});
|
||||
}
|
||||
|
||||
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 default function migrateProperty({collection, reversed, prop}){
|
||||
const transforms = transformsByPropType[prop.type];
|
||||
let migratedProp = transformFields(prop, transforms, reversed);
|
||||
const schema = collection.simpleSchema({type: prop.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){
|
||||
console.warn(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'),
|
||||
],
|
||||
'attack': [
|
||||
...actionTransforms,
|
||||
...getComputedPropertyTransforms('rollBonus'),
|
||||
],
|
||||
};
|
||||
|
||||
function getComputedPropertyTransforms(key){
|
||||
return [
|
||||
{from: key, to: `${key}.calculation`},
|
||||
{from: `${key}Result`, to: `${key}.value`},
|
||||
{from: `${key}Errors`, to: `${key}.errors`},
|
||||
];
|
||||
}
|
||||
|
||||
function getInlineComputationTransforms(key){
|
||||
return [
|
||||
{from: key, to: `${key}.text`},
|
||||
{from: `${key}Calculations`, to: `${key}.inlineCalculations`},
|
||||
{to: `${key}.value`, up: (val, doc) =>
|
||||
embedInlineCalculations(get(doc, key), get(doc, `${key}Calculations`))
|
||||
},
|
||||
{from: `${key}Calculations.$.result`, to: `${key}.inlineCalculations.$.value`},
|
||||
];
|
||||
}
|
||||
85
app/imports/migrations/server/2.0-beta.33-dbv1.test.js
Normal file
85
app/imports/migrations/server/2.0-beta.33-dbv1.test.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import migrateProperty from './2.0-beta.33-dbv1.js';
|
||||
import { assert } from 'chai';
|
||||
|
||||
const exampleAction = {
|
||||
'_id':'hY5MKZ4ivaoTRpNWy',
|
||||
'actionType':'bonus',
|
||||
'target':'singleTarget',
|
||||
'tags':[],
|
||||
'resources':{
|
||||
'itemsConsumed':[],
|
||||
'attributesConsumed':[{
|
||||
'_id':'FaK6jXEj3pSe7mNuu',
|
||||
'quantity':1,
|
||||
'variableName':'HunterTech',
|
||||
'statId':'qccf9j5tfNJjZ3GGn',
|
||||
'statName':'Hunter\'s Technique',
|
||||
'available':5
|
||||
}],
|
||||
},
|
||||
'type':'action',
|
||||
'name':'Hexblade\\\'s Curse',
|
||||
'parent':{
|
||||
'id':'JqtDmqa5Zd3xpts5G',
|
||||
'collection':'creatureProperties'
|
||||
},
|
||||
'ancestors':[
|
||||
{
|
||||
'collection':'creatures',
|
||||
'id':'X9rzFhsgFhodYfHmG'
|
||||
},
|
||||
],
|
||||
'order':315,
|
||||
'summary':'Curse a creature for 1 minute. The curse ends early if {warlock.level >14 ? "" : "the target dies, or"} you are incapacitated. \nGain the following benefits: \n- *Bonus to damage rolls against the cursed target of* **+{proficiencyBonus}**. \n- Any attack roll you make against the cursed target is a **critical hit on a roll of 19 or 20**. \n- If the cursed target dies, you **regain {warlock.level+charisma.modifier} hit points**. \n{warlock.level <9 ? "" : "- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."}',
|
||||
'uses':'1',
|
||||
'usesResult':1,
|
||||
'reset':'shortRest',
|
||||
'usesUsed':0,
|
||||
'description':'Starting at 1st level, you gain the ability to place a baleful curse on someone. As a bonus action, choose one creature you can see within 30 feet of you. The target is cursed for 1 minute. The curse ends early if the target dies, you die, or you are incapacitated. Until the curse ends, you gain the following benefits:\n\n- You gain a bonus to damage rolls against the cursed target. The bonus equals your proficiency bonus.\n- Any attack roll you make against the cursed target is a critical hit on a roll of 19 or 20 on the d20.\n- If the cursed target dies, you regain hit points equal to your warlock level + your Charisma modifier (minimum of 1 hit point). \n{warlock.level <10 ? "" :"- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."} \nYou can\\\'t use this feature again until you finish a short or long rest.',
|
||||
'color':'#8e24aa',
|
||||
'dependencies':[
|
||||
'4eM4YkgAaoCJfCfQ8',
|
||||
],
|
||||
'descriptionCalculations':[
|
||||
{
|
||||
'calculation':'warlock.level <10 ? "" :"- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."',
|
||||
'result':'- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses.'
|
||||
}
|
||||
],
|
||||
'summaryCalculations':[
|
||||
{
|
||||
'calculation':'warlock.level >14 ? "" : "the target dies, or"',
|
||||
'result':'the target dies, or'
|
||||
},
|
||||
{
|
||||
'calculation':'proficiencyBonus',
|
||||
'result':'4'
|
||||
},
|
||||
{
|
||||
'calculation':'warlock.level+charisma.modifier',
|
||||
'result':'15'
|
||||
},
|
||||
{
|
||||
'calculation':'warlock.level <9 ? "" : "- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."',
|
||||
'result':'- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses.'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
describe('migrateProperty', function () {
|
||||
it('Migrates actions reversibly', function () {
|
||||
const action = {...exampleAction};
|
||||
const newAction = migrateProperty({
|
||||
collection: CreatureProperties,
|
||||
prop: action
|
||||
});
|
||||
const reversedAction = migrateProperty({
|
||||
collection: CreatureProperties,
|
||||
prop: newAction,
|
||||
reversed: true,
|
||||
});
|
||||
assert.deepEqual(action, exampleAction, 'action should not be bashed');
|
||||
assert.deepEqual(exampleAction, reversedAction, 'operation should be reversible');
|
||||
});
|
||||
});
|
||||
@@ -1,2 +1 @@
|
||||
import './2.0-beta.33-dbv1.js';
|
||||
import './methods/index.js';
|
||||
112
app/imports/migrations/server/transformFields.js
Normal file
112
app/imports/migrations/server/transformFields.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { get, set, unset, forEachRight, cloneDeep } from 'lodash';
|
||||
|
||||
export default function transformFields(src, transformList, reversed = false){
|
||||
// don't bash the old document during the transforms
|
||||
let doc = cloneDeep(src);
|
||||
for(let originalTransform of transformList){
|
||||
let transform;
|
||||
// Swap to and from when reversing
|
||||
if (reversed){
|
||||
transform = {
|
||||
to: originalTransform.from,
|
||||
from: originalTransform.to,
|
||||
up: originalTransform.down,
|
||||
}
|
||||
} else {
|
||||
transform = {...originalTransform};
|
||||
}
|
||||
if (transform.from?.includes('$.')){
|
||||
transformArrayField(src, doc, transform, reversed);
|
||||
} else {
|
||||
transformSingleField(src, doc, transform);
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
function transformSingleField(src, doc, {from, to, up}){
|
||||
// Get the value in the `from` path and delete it
|
||||
let value = undefined;
|
||||
if (from){
|
||||
value = get(src, from);
|
||||
unset(doc, from);
|
||||
}
|
||||
|
||||
// apply the transform function
|
||||
if (up){
|
||||
value = up(value, src, doc);
|
||||
}
|
||||
|
||||
// Store the value in the `to` path or unset it if undefined
|
||||
if (to){
|
||||
if (value === undefined){
|
||||
unset(doc, to);
|
||||
} else {
|
||||
set(doc, to, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* from: 'from.$.here', to: 'to.$.here'
|
||||
* where from and to are an [array, of, objects] that each need to be modified
|
||||
* documents at 'from.x.here' will map to 'to.x.here'
|
||||
* Attempts to support 'from.$.here.$.nested'
|
||||
* by mapping 'from.x.here.y.nest.z.deep' to 'to.y.nest.z.lessDeep'
|
||||
* from depth must be >= to depth
|
||||
*/
|
||||
function transformArrayField(src, doc, {from, to, up}, reversed){
|
||||
const fromSplit = from.split('.$');
|
||||
const toSplit = to.split('.$');
|
||||
|
||||
if (toSplit.length > fromSplit.length){
|
||||
throw 'Can\'t transform array fields where "to" is deeper than "from"'
|
||||
}
|
||||
|
||||
// Stack based depth first traversal of arrays
|
||||
const stack = [{
|
||||
array: get(src, fromSplit[0]),
|
||||
paths: fromSplit.slice(1),
|
||||
currentPath: fromSplit[0],
|
||||
indices: [],
|
||||
}];
|
||||
while(stack.length){
|
||||
const state = stack.pop();
|
||||
// Iterate forwads or backwads defpending on our migration direction
|
||||
if (reversed){
|
||||
forEachRight(state.array, iterate(stack, state, src, doc, toSplit, up));
|
||||
} else {
|
||||
state.array.forEach(iterate(stack, state, src, doc, toSplit, up));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function iterate(stack, state, src, doc, toSplit, up){return function(key, index){
|
||||
const currentPath = `${state.currentPath}[${index}]${state.paths[0]}`
|
||||
if (state.paths.length == 1){
|
||||
transformSingleField(src, doc, {
|
||||
from: currentPath,
|
||||
to: buildToPath(toSplit, [...state.indices, index]),
|
||||
up
|
||||
});
|
||||
} else {
|
||||
stack.push({
|
||||
array: get(src, currentPath),
|
||||
paths: state.paths.slice(1),
|
||||
currentPath,
|
||||
indices: [...state.indices, index],
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
||||
function buildToPath(toSplit, indices){
|
||||
let toPath = '';
|
||||
let offset = indices.length - toSplit.length + 1;
|
||||
toSplit.forEach((path, i) => {
|
||||
toPath += `${path}`;
|
||||
if (i < toSplit.length - 1){
|
||||
toPath += `[${indices[i + offset]}]`
|
||||
}
|
||||
});
|
||||
return toPath;
|
||||
}
|
||||
85
app/imports/migrations/server/transformFields.test.js
Normal file
85
app/imports/migrations/server/transformFields.test.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import transformFields from './transformFields.js';
|
||||
import { assert } from 'chai';
|
||||
|
||||
const originalDoc = {
|
||||
name: 'doc name',
|
||||
description: 'a document to test transforms on',
|
||||
nest: {
|
||||
deeper: {
|
||||
field: 'some nested field'
|
||||
},
|
||||
},
|
||||
array: [{num: 1}, {num: 3}, {num: 5}],
|
||||
nestArray: [
|
||||
{array: [{item: 2},{item: 4},{item: 6}]},
|
||||
{array: [{item: 8},{item: 10},{item: 12}]},
|
||||
{array: [{item: 14},{item: 16},{item: 18}]},
|
||||
],
|
||||
};
|
||||
|
||||
describe('transformFields', function () {
|
||||
|
||||
it('Takes a doc and transforms it according to single field rules', function () {
|
||||
let doc = {...originalDoc};
|
||||
const transformList = [
|
||||
{from: 'name', to: 'title'},
|
||||
];
|
||||
|
||||
assert.equal(doc.name, 'doc name', '.name is set');
|
||||
assert.doesNotHaveAnyKeys(doc, ['title'], '.title doesn\'t exist');
|
||||
|
||||
doc = transformFields(doc, transformList);
|
||||
|
||||
assert.equal(doc.title, 'doc name', '.name -> .title');
|
||||
assert.doesNotHaveAnyKeys(doc, ['name'], '.name deleted');
|
||||
});
|
||||
|
||||
it('Takes a doc and transforms it with functions', function () {
|
||||
let doc = {...originalDoc};
|
||||
const transformList = [
|
||||
{from: 'name', to: 'name', up: name => name.toUpperCase()},
|
||||
];
|
||||
assert.equal(doc.name, 'doc name', 'name in lowercase');
|
||||
doc = transformFields(doc, transformList);
|
||||
assert.equal(doc.name, 'DOC NAME', 'name in uppercase');
|
||||
});
|
||||
|
||||
it('Handles empty to and from fields', function () {
|
||||
let doc = {...originalDoc};
|
||||
const transformList = [
|
||||
{to: 'created', up: () => 'from thin air'},
|
||||
{from: 'description'},
|
||||
];
|
||||
doc = transformFields(doc, transformList);
|
||||
assert.equal(doc.created, 'from thin air', 'created field success');
|
||||
assert.doesNotHaveAnyKeys(doc, ['description'], '.description deleted');
|
||||
});
|
||||
|
||||
it('Takes a nested field and transforms it into a different nested field', function () {
|
||||
let doc = {...originalDoc};
|
||||
const transformList = [
|
||||
{from: 'nest.deeper', to: 'different.deep'},
|
||||
];
|
||||
doc = transformFields(doc, transformList);
|
||||
assert.equal(doc.different.deep.field, 'some nested field', 'field moved correctly');
|
||||
assert.doesNotHaveAnyKeys(doc.nest, ['deeper'], 'doc.nest.deeper deleted');
|
||||
});
|
||||
|
||||
it('Transforms arrays', function () {
|
||||
let doc = {...originalDoc};
|
||||
const transformList = [
|
||||
{from: 'array.$.num', to: 'list.$.number'},
|
||||
];
|
||||
doc = transformFields(doc, transformList);
|
||||
assert.equal(doc.list[1].number, 3, 'array field moved correctly');
|
||||
});
|
||||
|
||||
it('Transforms deep arrays', function () {
|
||||
let doc = {...originalDoc};
|
||||
const transformList = [
|
||||
{from: 'nestArray.$.array.$.item', to: 'nestList.$.list.$.ting'},
|
||||
];
|
||||
doc = transformFields(doc, transformList);
|
||||
assert.equal(doc.nestList[2].list[1].ting, 16, 'nested array field moved correctly');
|
||||
});
|
||||
});
|
||||
59
app/package-lock.json
generated
59
app/package-lock.json
generated
@@ -289,6 +289,12 @@
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||
},
|
||||
"assertion-error": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
|
||||
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
|
||||
"dev": true
|
||||
},
|
||||
"astral-regex": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
|
||||
@@ -433,6 +439,20 @@
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
},
|
||||
"chai": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz",
|
||||
"integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"assertion-error": "^1.1.0",
|
||||
"check-error": "^1.0.2",
|
||||
"deep-eql": "^3.0.1",
|
||||
"get-func-name": "^2.0.0",
|
||||
"pathval": "^1.1.1",
|
||||
"type-detect": "^4.0.5"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||
@@ -484,6 +504,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"check-error": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
|
||||
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
|
||||
"dev": true
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
|
||||
@@ -651,6 +677,15 @@
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||
},
|
||||
"deep-eql": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
|
||||
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-detect": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"deep-is": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
|
||||
@@ -1106,6 +1141,12 @@
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
},
|
||||
"get-func-name": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
|
||||
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
|
||||
"dev": true
|
||||
},
|
||||
"getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
@@ -2491,6 +2532,12 @@
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true
|
||||
},
|
||||
"pathval": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
|
||||
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
@@ -2966,9 +3013,9 @@
|
||||
}
|
||||
},
|
||||
"tar": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.6.tgz",
|
||||
"integrity": "sha512-oaWyu5dQbHaYcyZCTfyPpC+VmI62/OM2RTUYavTk1MDr1cwW5Boi3baeYQKiZbY2uSQJGr+iMOzb/JFxLrft+g==",
|
||||
"version": "6.1.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
|
||||
"integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
|
||||
"requires": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
@@ -3024,6 +3071,12 @@
|
||||
"prelude-ls": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"type-detect": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
|
||||
"dev": true
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"vuex": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.4",
|
||||
"eslint": "^7.31.0",
|
||||
"eslint-plugin-vue": "^7.14.0",
|
||||
"eslint-plugin-vuetify": "^1.0.1",
|
||||
@@ -96,7 +97,8 @@
|
||||
"es2020": true,
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"meteor": true
|
||||
"meteor": true,
|
||||
"mocha": true
|
||||
},
|
||||
"rules": {
|
||||
"quotes": [
|
||||
|
||||
@@ -6,4 +6,5 @@ import '/imports/server/publications/index.js';
|
||||
import '/imports/server/cron/deleteSoftRemovedDocuments.js';
|
||||
import '/imports/api/parenting/organizeMethods.js';
|
||||
import '/imports/api/users/patreon/updatePatreonOnLogin.js';
|
||||
import '/imports/migrations/index.js';
|
||||
import '/imports/migrations/server/index.js';
|
||||
import '/imports/migrations/methods/index.js'
|
||||
|
||||
Reference in New Issue
Block a user