diff --git a/app/.meteor/packages b/app/.meteor/packages
index d0f672c2..f627a7cd 100644
--- a/app/.meteor/packages
+++ b/app/.meteor/packages
@@ -51,3 +51,4 @@ peerlibrary:subscription-data
seba:minifiers-autoprefixer
akryum:vue-component
akryum:vue-sass
+percolate:migrations
diff --git a/app/.meteor/versions b/app/.meteor/versions
index 586384a6..73bc1e23 100644
--- a/app/.meteor/versions
+++ b/app/.meteor/versions
@@ -93,6 +93,7 @@ peerlibrary:reactive-mongo@0.4.0
peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0
+percolate:migrations@1.0.3
percolate:synced-cron@1.3.2
promise@0.11.2
raix:eventemitter@1.0.0
diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js
index d690b620..4b7d170f 100644
--- a/app/imports/api/creature/creatureProperties/CreatureProperties.js
+++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js
@@ -10,6 +10,10 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureProperties = new Mongo.Collection('creatureProperties');
let CreaturePropertySchema = new SimpleSchema({
+ _migrationError: {
+ type: String,
+ optional: true,
+ },
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js
index 11782ffb..c1e058ce 100644
--- a/app/imports/api/properties/Actions.js
+++ b/app/imports/api/properties/Actions.js
@@ -1,7 +1,19 @@
import SimpleSchema from 'simpl-schema';
-import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
-import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
-import { storedIconsSchema } from '/imports/api/icons/Icons.js';
+import {
+ InlineCalculationFieldToComputeSchema,
+ ComputedOnlyInlineCalculationFieldSchema,
+ InlineCalculationFieldSchema,
+} from '/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js';
+import {
+ FieldToComputeSchema,
+ ComputedOnlyFieldSchema,
+ ComputedFieldSchema,
+} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js';
+import {
+ ResourcesSchema,
+ ResourcesComputedOnlySchema,
+ ResourcesComputedSchema,
+} from '/imports/api/properties/subSchemas/ResourcesSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/*
@@ -11,186 +23,75 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
* to this action
*/
let ActionSchema = new SimpleSchema({
- name: {
- type: String,
- optional: true,
+ name: {
+ type: String,
+ optional: true,
max: STORAGE_LIMITS.name,
- },
- summary: {
- type: String,
- optional: true,
- max: STORAGE_LIMITS.summary,
- },
- description: {
- type: String,
- optional: true,
- max: STORAGE_LIMITS.description,
- },
- // What time-resource is used to take the action in combat
- // long actions take longer than 1 round to cast
- actionType: {
- type: String,
- allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'],
- defaultValue: 'action',
- },
- // Who is the action directed at
- target: {
- type: String,
- defaultValue: 'singleTarget',
- allowedValues: [
+ },
+ summary: {
+ type: InlineCalculationFieldToComputeSchema,
+ optional: true,
+ },
+ description: {
+ type: InlineCalculationFieldToComputeSchema,
+ optional: true,
+ },
+ // What time-resource is used to take the action in combat
+ // long actions take longer than 1 round to cast
+ actionType: {
+ type: String,
+ allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'],
+ defaultValue: 'action',
+ },
+ // Who is the action directed at
+ target: {
+ type: String,
+ defaultValue: 'singleTarget',
+ allowedValues: [
'self',
'singleTarget',
- 'multipleTargets',
+ 'multipleTargets',
],
- },
- // Duplicate the ResourceSchema here so we can extend it elegantly.
+ },
+ // Resources schema changes for between standard, computed, and computedOnly
resources: {
- type: Object,
+ type: ResourcesSchema,
defaultValue: {},
},
- 'resources.itemsConsumed': {
- type: Array,
- defaultValue: [],
- maxCount: STORAGE_LIMITS.resourcesCount,
- },
- 'resources.itemsConsumed.$': {
- type: Object,
- },
- 'resources.itemsConsumed.$._id': {
- type: String,
- regEx: SimpleSchema.RegEx.Id,
- autoValue(){
- if (!this.isSet) return Random.id();
- }
- },
- 'resources.itemsConsumed.$.tag': {
- type: String,
+ // Calculation of how many times this action can be used
+ uses: {
+ type: FieldToComputeSchema,
optional: true,
- max: STORAGE_LIMITS.tagLength,
},
- 'resources.itemsConsumed.$.quantity': {
- type: Number,
- defaultValue: 1,
- },
- 'resources.itemsConsumed.$.itemId': {
- type: String,
- optional: true,
- max: STORAGE_LIMITS.name,
- },
- 'resources.attributesConsumed': {
- type: Array,
- defaultValue: [],
- maxCount: STORAGE_LIMITS.resourcesCount,
- },
- 'resources.attributesConsumed.$': {
- type: Object,
- },
- 'resources.attributesConsumed.$._id': {
- type: String,
- regEx: SimpleSchema.RegEx.Id,
- autoValue(){
- if (!this.isSet) return Random.id();
- }
- },
- 'resources.attributesConsumed.$.variableName': {
- type: String,
- optional: true,
- max: STORAGE_LIMITS.variableName,
- },
- 'resources.attributesConsumed.$.quantity': {
- type: Number,
- defaultValue: 1,
- },
- // Calculation of how many times this action can be used
- uses: {
- type: String,
- optional: true,
- max: STORAGE_LIMITS.calculation,
- },
- // Integer of how many times it has already been used
- usesUsed: {
- type: SimpleSchema.Integer,
- optional: true,
- },
- // How this action's uses are reset automatically
- reset: {
- type: String,
- allowedValues: ['longRest', 'shortRest'],
- optional: true,
- },
-});
-
-const ComputedOnlyActionSchema = new SimpleSchema({
- summaryCalculations: {
- type: Array,
- defaultValue: [],
- maxCount: STORAGE_LIMITS.inlineCalculationCount,
- },
- 'summaryCalculations.$': InlineComputationSchema,
-
- descriptionCalculations: {
- type: Array,
- defaultValue: [],
- maxCount: STORAGE_LIMITS.inlineCalculationCount,
- },
- 'descriptionCalculations.$': InlineComputationSchema,
-
- usesResult: {
+ // Integer of how many times it has already been used
+ usesUsed: {
type: SimpleSchema.Integer,
optional: true,
},
- usesErrors: {
- type: Array,
- optional: true,
- maxCount: STORAGE_LIMITS.errorCount,
- },
- 'usesErrors.$':{
- type: ErrorSchema,
- },
- resources: Object,
- 'resources.itemsConsumed': Array,
- 'resources.itemsConsumed.$': Object,
- 'resources.itemsConsumed.$.available': {
- type: Number,
- optional: true,
- },
- // This appears both in the computed and uncomputed schema because it can be
- // set by both a computation or a form
- 'resources.itemsConsumed.$.itemId': {
+ // How this action's uses are reset automatically
+ reset: {
type: String,
- regEx: SimpleSchema.RegEx.Id,
+ allowedValues: ['longRest', 'shortRest'],
optional: true,
},
- 'resources.itemsConsumed.$.itemName': {
- type: String,
- max: STORAGE_LIMITS.name,
+});
+
+const ComputedOnlyActionSchema = new SimpleSchema({
+ summary: {
+ type: ComputedOnlyInlineCalculationFieldSchema,
optional: true,
},
- 'resources.itemsConsumed.$.itemIcon': {
- type: storedIconsSchema,
- optional: true,
- max: STORAGE_LIMITS.icon,
- },
- 'resources.itemsConsumed.$.itemColor': {
- type: String,
- optional: true,
- max: STORAGE_LIMITS.color,
- },
- 'resources.attributesConsumed': Array,
- 'resources.attributesConsumed.$': Object,
- 'resources.attributesConsumed.$.available': {
- type: Number,
+ description: {
+ type: ComputedOnlyInlineCalculationFieldSchema,
optional: true,
},
- 'resources.attributesConsumed.$.statId': {
- type: String,
- regEx: SimpleSchema.RegEx.Id,
+ uses: {
+ type: ComputedOnlyFieldSchema,
optional: true,
},
- 'resources.attributesConsumed.$.statName': {
- type: String,
- optional: true,
- max: STORAGE_LIMITS.name,
+ resources: {
+ type: ResourcesComputedOnlySchema,
+ defaultValue: {},
},
// True if the uses left is zero, or any item or attribute consumed is
// insufficient
@@ -202,6 +103,24 @@ const ComputedOnlyActionSchema = new SimpleSchema({
const ComputedActionSchema = new SimpleSchema()
.extend(ActionSchema)
- .extend(ComputedOnlyActionSchema);
+ .extend(ComputedOnlyActionSchema)
+ .extend({
+ uses: {
+ type: ComputedFieldSchema,
+ optional: true,
+ },
+ summary: {
+ type: InlineCalculationFieldSchema,
+ optional: true,
+ },
+ description: {
+ type: InlineCalculationFieldSchema,
+ optional: true,
+ },
+ resources: {
+ type: ResourcesComputedSchema,
+ defaultValue: {},
+ },
+ });
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema};
diff --git a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js
index 8c85d13f..622e87a3 100644
--- a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js
+++ b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js
@@ -1,5 +1,11 @@
import SimpleSchema from 'simpl-schema';
import { Random } from 'meteor/random';
+import {
+ FieldToComputeSchema,
+ ComputedOnlyFieldSchema,
+ ComputedFieldSchema,
+} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js';
+import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const AttributeConsumedSchema = new SimpleSchema({
_id: {
@@ -12,11 +18,47 @@ const AttributeConsumedSchema = new SimpleSchema({
variableName: {
type: String,
optional: true,
+ max: STORAGE_LIMITS.variableName,
},
quantity: {
- type: Number,
- defaultValue: 1,
+ type: FieldToComputeSchema,
+ optional: true,
},
});
-export default AttributeConsumedSchema;
+const ComputedOnlyAttributeConsumedSchema = new SimpleSchema({
+ available: {
+ type: Number,
+ optional: true,
+ },
+ statId: {
+ type: String,
+ regEx: SimpleSchema.RegEx.Id,
+ optional: true,
+ },
+ statName: {
+ type: String,
+ optional: true,
+ max: STORAGE_LIMITS.name,
+ },
+ quantity: {
+ type: ComputedOnlyFieldSchema,
+ optional: true,
+ },
+});
+
+const ComputedAttributeConsumedSchema = new SimpleSchema()
+ .extend(AttributeConsumedSchema)
+ .extend(ComputedOnlyAttributeConsumedSchema)
+ .extend({
+ quantity: {
+ type: ComputedFieldSchema,
+ optional: true,
+ },
+ });
+
+export {
+ AttributeConsumedSchema,
+ ComputedOnlyAttributeConsumedSchema,
+ ComputedAttributeConsumedSchema
+};
diff --git a/app/imports/api/properties/subSchemas/ComputedFieldSchema.js b/app/imports/api/properties/subSchemas/ComputedFieldSchema.js
new file mode 100644
index 00000000..70307db4
--- /dev/null
+++ b/app/imports/api/properties/subSchemas/ComputedFieldSchema.js
@@ -0,0 +1,35 @@
+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,
+ },
+});
+
+const ComputedOnlyFieldSchema = new SimpleSchema({
+ value: {
+ type: SimpleSchema.oneOf(String, Number),
+ optional: true,
+ },
+ errors: {
+ type: Array,
+ optional: true,
+ maxCount: STORAGE_LIMITS.errorCount,
+ },
+ 'errors.$':{
+ type: ErrorSchema,
+ },
+});
+
+const ComputedFieldSchema = new SimpleSchema()
+ .extend(FieldToComputeSchema)
+ .extend(ComputedOnlyFieldSchema)
+
+export {
+ FieldToComputeSchema,
+ ComputedOnlyFieldSchema,
+ ComputedFieldSchema
+};
diff --git a/app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js b/app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js
new file mode 100644
index 00000000..085347b1
--- /dev/null
+++ b/app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js
@@ -0,0 +1,37 @@
+import SimpleSchema from 'simpl-schema';
+import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
+import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
+
+const InlineCalculationFieldToComputeSchema = new SimpleSchema({
+ text: {
+ type: String,
+ optional: true,
+ max: STORAGE_LIMITS.inlineCalculationField,
+ },
+});
+
+const ComputedOnlyInlineCalculationFieldSchema = new SimpleSchema({
+ 'inlineCalculations': {
+ type: Array,
+ defaultValue: [],
+ maxCount: STORAGE_LIMITS.inlineCalculationCount,
+ },
+ 'inlineCalculations.$': {
+ type: InlineComputationSchema,
+ },
+ value: {
+ type: String,
+ optional: true,
+ max: STORAGE_LIMITS.inlineCalculationField,
+ },
+});
+
+const InlineCalculationFieldSchema = new SimpleSchema()
+ .extend(InlineCalculationFieldToComputeSchema)
+ .extend(ComputedOnlyInlineCalculationFieldSchema)
+
+export {
+ InlineCalculationFieldToComputeSchema,
+ ComputedOnlyInlineCalculationFieldSchema,
+ InlineCalculationFieldSchema,
+};
diff --git a/app/imports/api/properties/subSchemas/InlineComputationSchema.js b/app/imports/api/properties/subSchemas/InlineComputationSchema.js
index ea17e4e5..e220a3aa 100644
--- a/app/imports/api/properties/subSchemas/InlineComputationSchema.js
+++ b/app/imports/api/properties/subSchemas/InlineComputationSchema.js
@@ -8,8 +8,8 @@ const InlineComputationSchema = new SimpleSchema({
type: String,
max: STORAGE_LIMITS.calculation,
},
- result: {
- type: String,
+ value: {
+ type: SimpleSchema.oneOf(String, Number),
optional: true,
max: STORAGE_LIMITS.calculation,
},
diff --git a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js
index 8545b6d1..b17d8b0f 100644
--- a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js
+++ b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js
@@ -1,5 +1,12 @@
import SimpleSchema from 'simpl-schema';
import { Random } from 'meteor/random';
+import {
+ FieldToComputeSchema,
+ ComputedOnlyFieldSchema,
+ ComputedFieldSchema,
+} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js';
+import { storedIconsSchema } from '/imports/api/icons/Icons.js';
+import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const ItemConsumedSchema = new SimpleSchema({
_id: {
@@ -14,13 +21,61 @@ const ItemConsumedSchema = new SimpleSchema({
optional: true,
},
quantity: {
- type: Number,
- defaultValue: 1,
+ type: FieldToComputeSchema,
+ optional: true,
},
itemId: {
type: String,
+ regEx: SimpleSchema.RegEx.Id,
optional: true,
},
});
-export default ItemConsumedSchema;
+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: {
+ type: String,
+ regEx: SimpleSchema.RegEx.Id,
+ optional: true,
+ },
+ itemName: {
+ type: String,
+ max: STORAGE_LIMITS.name,
+ optional: true,
+ },
+ itemIcon: {
+ type: storedIconsSchema,
+ optional: true,
+ max: STORAGE_LIMITS.icon,
+ },
+ itemColor: {
+ type: String,
+ optional: true,
+ max: STORAGE_LIMITS.color,
+ },
+})
+
+const ComputedItemConsumedSchema = new SimpleSchema()
+ .extend(ItemConsumedSchema)
+ .extend(ComputedOnlyItemConsumedSchema)
+ .extend({
+ quantity: {
+ type: ComputedFieldSchema,
+ optional: true,
+ },
+ });
+
+export {
+ ItemConsumedSchema,
+ ComputedOnlyItemConsumedSchema,
+ ComputedItemConsumedSchema
+};
diff --git a/app/imports/api/properties/subSchemas/ResourcesSchema.js b/app/imports/api/properties/subSchemas/ResourcesSchema.js
index bc7ae9da..4c36bbb2 100644
--- a/app/imports/api/properties/subSchemas/ResourcesSchema.js
+++ b/app/imports/api/properties/subSchemas/ResourcesSchema.js
@@ -1,6 +1,14 @@
import SimpleSchema from 'simpl-schema';
-import ItemConsumedSchema from '/imports/api/properties/subSchemas/ItemConsumedSchema.js';
-import AttributeConsumedSchema from '/imports/api/properties/subSchemas/AttributeConsumedSchema.js';
+import {
+ ItemConsumedSchema,
+ ComputedOnlyItemConsumedSchema,
+ ComputedItemConsumedSchema
+} from '/imports/api/properties/subSchemas/ItemConsumedSchema.js';
+import {
+ AttributeConsumedSchema,
+ ComputedOnlyAttributeConsumedSchema,
+ ComputedAttributeConsumedSchema
+} from '/imports/api/properties/subSchemas/AttributeConsumedSchema.js';
const ResourcesSchema = new SimpleSchema({
itemsConsumed: {
@@ -19,4 +27,42 @@ const ResourcesSchema = new SimpleSchema({
},
});
-export default ResourcesSchema;
+const ResourcesComputedOnlySchema = new SimpleSchema({
+ itemsConsumed: {
+ type: Array,
+ defaultValue: [],
+ },
+ 'itemsConsumed.$': {
+ type: ComputedOnlyItemConsumedSchema,
+ },
+ attributesConsumed: {
+ type: Array,
+ defaultValue: [],
+ },
+ 'attributesConsumed.$': {
+ type: ComputedOnlyAttributeConsumedSchema,
+ },
+});
+
+const ResourcesComputedSchema = new SimpleSchema({
+ itemsConsumed: {
+ type: Array,
+ defaultValue: [],
+ },
+ 'itemsConsumed.$': {
+ type: ComputedItemConsumedSchema,
+ },
+ attributesConsumed: {
+ type: Array,
+ defaultValue: [],
+ },
+ 'attributesConsumed.$': {
+ type: ComputedAttributeConsumedSchema,
+ },
+});
+
+export {
+ ResourcesSchema,
+ ResourcesComputedOnlySchema,
+ ResourcesComputedSchema,
+};
diff --git a/app/imports/constants/STORAGE_LIMITS.js b/app/imports/constants/STORAGE_LIMITS.js
index 23f07fbe..065089ca 100644
--- a/app/imports/constants/STORAGE_LIMITS.js
+++ b/app/imports/constants/STORAGE_LIMITS.js
@@ -4,6 +4,7 @@ const STORAGE_LIMITS = Object.freeze({
collectionName: 64,
color: 10000,
description: 49473, //the length of the Bee Movie script
+ inlineCalculationField: 49473,
errorMessage: 256,
icon: 10000,
name: 128,
diff --git a/app/imports/migrations/2.0-beta.33-dbv1.js b/app/imports/migrations/2.0-beta.33-dbv1.js
new file mode 100644
index 00000000..f8f107f8
--- /dev/null
+++ b/app/imports/migrations/2.0-beta.33-dbv1.js
@@ -0,0 +1,186 @@
+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,
+ },
+ };
+}
diff --git a/app/imports/migrations/index.js b/app/imports/migrations/index.js
new file mode 100644
index 00000000..97fcde14
--- /dev/null
+++ b/app/imports/migrations/index.js
@@ -0,0 +1,2 @@
+import './2.0-beta.33-dbv1.js';
+import './methods/index.js';
diff --git a/app/imports/migrations/methods/getVersion.js b/app/imports/migrations/methods/getVersion.js
new file mode 100644
index 00000000..8b67d5fe
--- /dev/null
+++ b/app/imports/migrations/methods/getVersion.js
@@ -0,0 +1,30 @@
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
+import { Migrations } from 'meteor/percolate:migrations';
+
+const dbVersionToGitVersion = {
+ 0: '2.0-beta.32 and lower',
+ 1: '2.0-beta.33',
+}
+
+const getVersion = new ValidatedMethod({
+ name: 'admin.getVersion',
+ validate: null,
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run() {
+ if (Meteor.isClient) return;
+ assertAdmin(this.userId);
+ const dbVersion = Migrations.getVersion();
+ return {
+ dbVersion,
+ gitVersion: dbVersionToGitVersion[dbVersion],
+ }
+ },
+});
+
+export default getVersion;
diff --git a/app/imports/migrations/methods/index.js b/app/imports/migrations/methods/index.js
new file mode 100644
index 00000000..6d5dc8c5
--- /dev/null
+++ b/app/imports/migrations/methods/index.js
@@ -0,0 +1,2 @@
+import './migrateTo.js';
+import './getVersion.js';
diff --git a/app/imports/migrations/methods/migrateTo.js b/app/imports/migrations/methods/migrateTo.js
new file mode 100644
index 00000000..c5248c9d
--- /dev/null
+++ b/app/imports/migrations/methods/migrateTo.js
@@ -0,0 +1,29 @@
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+import SimpleSchema from 'simpl-schema';
+import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
+import { Migrations } from 'meteor/percolate:migrations';
+
+const migrateTo = new ValidatedMethod({
+ name: 'admin.migrateTo',
+ validate: new SimpleSchema({
+ version: {
+ type: SimpleSchema.oneOf(
+ SimpleSchema.Integer,
+ String
+ ),
+ },
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({version}) {
+ if (Meteor.isClient) return;
+ assertAdmin(this.userId);
+ Migrations.migrateTo(version);
+ },
+});
+
+export default migrateTo;
diff --git a/app/imports/ui/pages/Admin.vue b/app/imports/ui/pages/Admin.vue
new file mode 100644
index 00000000..e905052a
--- /dev/null
+++ b/app/imports/ui/pages/Admin.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+ Database version: {{ versions && versions.dbVersion }}
+ Git version: {{ versions && versions.gitVersion }}
+
+ {{ versionError }}
+
+
+ mdi-refresh
+
+
+
+ Migrate
+
+
+ {{ migrateError }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/router.js b/app/imports/ui/router.js
index 0bda3df6..d4ed9fb6 100644
--- a/app/imports/ui/router.js
+++ b/app/imports/ui/router.js
@@ -24,6 +24,7 @@ import PatreonLevelTooLow from '/imports/ui/pages/PatreonLevelTooLow.vue';
import Tabletops from '/imports/ui/pages/Tabletops.vue';
import Tabletop from '/imports/ui/pages/Tabletop.vue';
import TabletopToolbar from '/imports/ui/tabletop/TabletopToolbar.vue';
+import Admin from '/imports/ui/pages/Admin.vue';
let userSubscription = Meteor.subscribe('user');
@@ -242,6 +243,11 @@ RouterFactory.configure(factory => {
name: 'iconAdmin',
component: IconAdmin,
beforeEnter: ensureAdmin,
+ },{
+ path: '/admin',
+ name: 'admin',
+ component: Admin,
+ beforeEnter: ensureAdmin,
},
]);
});
diff --git a/app/server/main.js b/app/server/main.js
index b8988bef..f7ac36c6 100644
--- a/app/server/main.js
+++ b/app/server/main.js
@@ -6,3 +6,4 @@ 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';