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