diff --git a/app/.meteor/packages b/app/.meteor/packages index 55c83ae9..8f08cd9e 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -50,3 +50,4 @@ littledata:synced-cron typescript@4.9.4 seba:minifiers-autoprefixer mixmax:smart-disconnect +zodern:types diff --git a/app/.meteor/versions b/app/.meteor/versions index 977f2a7c..3a927074 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -125,3 +125,4 @@ url@1.3.2 webapp@1.13.5 webapp-hashing@1.1.1 zer0th:meteor-vuetify-loader@0.1.41 +zodern:types@1.0.9 diff --git a/app/client/main.js b/app/client/main.js index 6305c8ae..7bb201aa 100644 --- a/app/client/main.js +++ b/app/client/main.js @@ -1,7 +1,7 @@ -import '/imports/api/simpleSchemaConfig.js'; -import '/imports/client/ui/vueSetup.js'; -import '/imports/client/ui/styles/stylesIndex.js'; -import '/imports/client/config.js'; -import '/imports/client/serviceWorker.js'; +import '/imports/api/simpleSchemaConfig'; +import '/imports/client/ui/vueSetup'; +import '/imports/client/ui/styles/stylesIndex'; +import '/imports/client/config'; +import '/imports/client/serviceWorker'; import 'ngraph.graph'; diff --git a/app/imports/api/creature/archive/ArchiveCreatureFiles.js b/app/imports/api/creature/archive/ArchiveCreatureFiles.js index 044e6158..dda1499f 100644 --- a/app/imports/api/creature/archive/ArchiveCreatureFiles.js +++ b/app/imports/api/creature/archive/ArchiveCreatureFiles.js @@ -1,13 +1,13 @@ import SimpleSchema from 'simpl-schema'; -import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js'; -import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js'; +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed'; +import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { CreatureSchema } from '/imports/api/creature/creatures/Creatures'; let createS3FilesCollection; if (Meteor.isServer) { - createS3FilesCollection = require('/imports/api/files/server/s3FileStorage.js').createS3FilesCollection + createS3FilesCollection = require('/imports/api/files/server/s3FileStorage').createS3FilesCollection } else { - createS3FilesCollection = require('/imports/api/files/client/s3FileStorage.js').createS3FilesCollection + createS3FilesCollection = require('/imports/api/files/client/s3FileStorage').createS3FilesCollection } const ArchiveCreatureFiles = createS3FilesCollection({ diff --git a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js index 92b6f3bd..e10d29cd 100644 --- a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js +++ b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js @@ -1,21 +1,22 @@ -import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js'; +import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION'; import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; -import Experiences from '/imports/api/creature/experience/Experiences.js'; -import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js'; -import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; +import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs'; +import Experiences from '/imports/api/creature/experience/Experiences'; +import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature'; +import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; -export function getArchiveObj(creatureId){ +export function getArchiveObj(creatureId) { // Build the archive document const creature = Creatures.findOne(creatureId); - const properties = CreatureProperties.find({'ancestors.id': creatureId}).fetch(); - const experiences = Experiences.find({creatureId}).fetch(); - const logs = CreatureLogs.find({creatureId}).fetch(); + const properties = CreatureProperties.find({ ...getFilter.descendantsOfRoot(creatureId) }).fetch(); + const experiences = Experiences.find({ creatureId }).fetch(); + const logs = CreatureLogs.find({ creatureId }).fetch(); let archiveCreature = { meta: { type: 'DiceCloud V2 Creature Archive', @@ -31,7 +32,7 @@ export function getArchiveObj(creatureId){ return archiveCreature; } -export function archiveCreature(creatureId){ +export function archiveCreature(creatureId) { const archive = getArchiveObj(creatureId); const buffer = Buffer.from(JSON.stringify(archive, null, 2)); ArchiveCreatureFiles.write(buffer, { @@ -44,7 +45,7 @@ export function archiveCreature(creatureId){ creatureName: archive.creature.name, }, }, (error) => { - if (error){ + if (error) { throw error; } else { removeCreatureWork(creatureId); @@ -65,10 +66,10 @@ const archiveCreatureToFile = new ValidatedMethod({ numRequests: 10, timeInterval: 5000, }, - async run({creatureId}) { + async run({ creatureId }) { assertOwnership(creatureId, this.userId); - if (Meteor.isServer){ - archiveCreature(creatureId, this.userId); + if (Meteor.isServer) { + archiveCreature(creatureId); } else { removeCreatureWork(creatureId); } diff --git a/app/imports/api/creature/archive/methods/index.js b/app/imports/api/creature/archive/methods/index.js index 491ba038..abb0d9b6 100644 --- a/app/imports/api/creature/archive/methods/index.js +++ b/app/imports/api/creature/archive/methods/index.js @@ -1,3 +1,3 @@ -import '/imports/api/creature/archive/methods/archiveCreatureToFile.js'; -import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js'; -import '/imports/api/creature/archive/methods/removeArchiveCreature.js'; +import '/imports/api/creature/archive/methods/archiveCreatureToFile'; +import '/imports/api/creature/archive/methods/restoreCreatureFromFile'; +import '/imports/api/creature/archive/methods/removeArchiveCreature'; diff --git a/app/imports/api/creature/archive/methods/removeArchiveCreature.js b/app/imports/api/creature/archive/methods/removeArchiveCreature.js index f4ba608b..c7a20fb2 100644 --- a/app/imports/api/creature/archive/methods/removeArchiveCreature.js +++ b/app/imports/api/creature/archive/methods/removeArchiveCreature.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; -import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js'; +import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed'; const removeArchiveCreature = new ValidatedMethod({ name: 'ArchiveCreatureFiles.methods.removeArchiveCreature', diff --git a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js index 112ea06d..416782a0 100644 --- a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js +++ b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js @@ -1,20 +1,20 @@ -import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js'; +import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION'; import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; -import Experiences from '/imports/api/creature/experience/Experiences.js'; -import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js'; -import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; -import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js'; -import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js'; -import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs'; +import Experiences from '/imports/api/creature/experience/Experiences'; +import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature'; +import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; +import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots'; +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed'; +import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety'; let migrateArchive; if (Meteor.isServer) { - migrateArchive = require('/imports/migrations/archive/migrateArchive.js').default; + migrateArchive = require('/imports/migrations/archive/migrateArchive').default; } function restoreCreature(archive, userId) { diff --git a/app/imports/api/creature/archive/methods/verifyArchiveSafety.js b/app/imports/api/creature/archive/methods/verifyArchiveSafety.js index 734b2582..fffb5a73 100644 --- a/app/imports/api/creature/archive/methods/verifyArchiveSafety.js +++ b/app/imports/api/creature/archive/methods/verifyArchiveSafety.js @@ -1,7 +1,7 @@ import { slice } from 'lodash'; -import PER_CREATURE_LOG_LIMIT from '/imports/api/creature/log/CreatureLogs.js'; +import PER_CREATURE_LOG_LIMIT from '/imports/api/creature/log/CreatureLogs'; -export default function verifyArchiveSafety({ meta, creature, properties, experiences, logs }){ +export default function verifyArchiveSafety({ meta, creature, properties, experiences, logs }) { const creatureId = creature._id; // Check lengths of arrays @@ -21,8 +21,14 @@ export default function verifyArchiveSafety({ meta, creature, properties, experi } }); properties.forEach(prop => { - if (prop.ancestors[0].id !== creatureId) { - throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature'); + if (meta.schemaVersion.schemaVersion >= 3) { + if (prop.root?.id !== creatureId) { + throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature'); + } + } else { + if (prop.ancestors?.[0]?.id !== creatureId) { + throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature'); + } } }); } diff --git a/app/imports/api/creature/creatureFolders/CreatureFolders.js b/app/imports/api/creature/creatureFolders/CreatureFolders.js index e0e784cc..6339014a 100644 --- a/app/imports/api/creature/creatureFolders/CreatureFolders.js +++ b/app/imports/api/creature/creatureFolders/CreatureFolders.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let CreatureFolders = new Mongo.Collection('creatureFolders'); @@ -35,5 +35,5 @@ let creatureFolderSchema = new SimpleSchema({ CreatureFolders.attachSchema(creatureFolderSchema); -import '/imports/api/creature/creatureFolders/methods.js/index.js'; +import '/imports/api/creature/creatureFolders/methods.js/index'; export default CreatureFolders; diff --git a/app/imports/api/creature/creatureFolders/methods.js/index.js b/app/imports/api/creature/creatureFolders/methods.js/index.js index 26782576..94a5bbc7 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/index.js +++ b/app/imports/api/creature/creatureFolders/methods.js/index.js @@ -1,4 +1,4 @@ -import '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js'; -import '/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js'; -import '/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js'; -import '/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js'; +import '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder'; +import '/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName'; +import '/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder'; +import '/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder'; diff --git a/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js index 8df44d40..497943b4 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js +++ b/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js @@ -1,4 +1,4 @@ -import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -15,23 +15,23 @@ const insertCreatureFolder = new ValidatedMethod({ let userId = this.userId; if (!userId) { throw new Meteor.Error('creatureFolders.methods.insert.denied', - 'You need to be logged in to insert a folder'); + 'You need to be logged in to insert a folder'); } // Limit folders to 50 per user let existingFolders = CreatureFolders.find({ owner: userId }, { - fields: {order: 1}, - sort: {order :-1} + fields: { order: 1 }, + sort: { order: -1 } }); - if (existingFolders.count() >= 50){ + if (existingFolders.count() >= 50) { throw new Meteor.Error('creatureFolders.methods.insert.denied', - 'You can not have more than 50 folders'); + 'You can not have more than 50 folders'); } // Make the new folder the last in the order let order = 0; let lastFolder = existingFolders.fetch()[0]; - if (lastFolder){ + if (lastFolder) { order = (lastFolder.order || 0) + 1; } // Insert diff --git a/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js b/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js index 46f9b696..2fa90c49 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js +++ b/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js @@ -1,4 +1,4 @@ -import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -10,33 +10,33 @@ const moveCreatureToFolder = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({creatureId, folderId}) { + run({ creatureId, folderId }) { // Ensure logged in let userId = this.userId; if (!userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'You need to be logged in to remove a folder'); + 'You need to be logged in to remove a folder'); } // Check that this folder is owned by the user - if (folderId){ + if (folderId) { let existingFolder = CreatureFolders.findOne(folderId); - if (existingFolder.owner !== userId){ + if (existingFolder.owner !== userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'This folder does not belong to you'); + 'This folder does not belong to you'); } } // Remove from other folders CreatureFolders.update({ owner: userId }, { - $pull: {creatures: creatureId}, + $pull: { creatures: creatureId }, }, { multi: true, }); - if (folderId){ + if (folderId) { // Add to this folder CreatureFolders.update(folderId, { - $addToSet: {creatures: creatureId}, + $addToSet: { creatures: creatureId }, }); } }, diff --git a/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js index 71006cb7..4502fcb6 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js +++ b/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js @@ -1,4 +1,4 @@ -import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -10,18 +10,18 @@ const removeCreatureFolder = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({_id}) { + run({ _id }) { // Ensure logged in let userId = this.userId; if (!userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'You need to be logged in to remove a folder'); + 'You need to be logged in to remove a folder'); } // Check that this folder is owned by the user let existingFolder = CreatureFolders.findOne(_id); - if (existingFolder.owner !== userId){ + if (existingFolder.owner !== userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'This folder does not belong to you'); + 'This folder does not belong to you'); } // Remove return CreatureFolders.remove(_id); diff --git a/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js index 92ecb418..3cdc7c5a 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js +++ b/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js @@ -1,4 +1,4 @@ -import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -10,31 +10,31 @@ const reorderCreatureFolder = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({_id, order}) { + run({ _id, order }) { // Ensure logged in let userId = this.userId; if (!userId) { throw new Meteor.Error('creatureFolders.methods.reorder.denied', - 'You need to be logged in to reorder a folder'); + 'You need to be logged in to reorder a folder'); } // Check that this folder is owned by the user let existingFolder = CreatureFolders.findOne(_id); - if (existingFolder.owner !== userId){ + if (existingFolder.owner !== userId) { throw new Meteor.Error('creatureFolders.methods.reorder.denied', - 'This folder does not belong to you'); + 'This folder does not belong to you'); } // First give it the new order, it should end in 0.5 putting it between two other docs - CreatureFolders.update(_id, {$set: {order}}); + CreatureFolders.update(_id, { $set: { order } }); this.unblock(); // Reorder all the folders with integer numbers in this new order CreatureFolders.find({ owner: userId }, { - fields: {order: 1,}, - sort: {order: -1} + fields: { order: 1, }, + sort: { order: -1 } }).forEach((folder, index) => { - if (folder.order !== index){ - CreatureFolders.update(_id, {$set: {order: index}}) + if (folder.order !== index) { + CreatureFolders.update(_id, { $set: { order: index } }) } }); }, diff --git a/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js b/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js index d798e59a..4fbb2a93 100644 --- a/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js +++ b/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js @@ -1,4 +1,4 @@ -import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -10,21 +10,21 @@ const updateCreatureFolderName = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({_id, name}) { + run({ _id, name }) { // Ensure logged in let userId = this.userId; if (!userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'You need to be logged in to update a folder'); + 'You need to be logged in to update a folder'); } // Check that this folder is owned by the user let existingFolder = CreatureFolders.findOne(_id); - if (existingFolder.owner !== userId){ + if (existingFolder.owner !== userId) { throw new Meteor.Error('creatureFolders.methods.updateName.denied', - 'This folder does not belong to you'); + 'This folder does not belong to you'); } // Update - return CreatureFolders.update(_id, {$set: {name}}); + return CreatureFolders.update(_id, { $set: { name } }); }, }); diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.ts similarity index 77% rename from app/imports/api/creature/creatureProperties/CreatureProperties.js rename to app/imports/api/creature/creatureProperties/CreatureProperties.ts index 81fc8ffc..0aa23411 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.ts @@ -1,15 +1,35 @@ import { Mongo } from 'meteor/mongo'; import SimpleSchema from 'simpl-schema'; -import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js'; -import ChildSchema from '/imports/api/parenting/ChildSchema.js'; -import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js'; -import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js'; -import { storedIconsSchema } from '/imports/api/icons/Icons.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema'; +import ChildSchema, { TreeDoc } from '/imports/api/parenting/ChildSchema'; +import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema'; +import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex'; +import { storedIconsSchema } from '/imports/api/icons/Icons'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; -let CreatureProperties = new Mongo.Collection('creatureProperties'); +const CreatureProperties: Mongo.Collection = new Mongo.Collection('creatureProperties'); -let CreaturePropertySchema = new SimpleSchema({ +export interface CreatureProperty extends TreeDoc { + _id: string + _migrationError?: string + type: string + tags: string[] + disabled?: boolean + icon?: { + name: string + shape: string + }, + libraryNodeId?: string + slotQuantityFilled?: number + inactive?: boolean + deactivatedByAncestor?: boolean + deactivatedBySelf?: boolean + deactivatedByToggle?: boolean + deactivatingToggleId?: boolean + dirty?: boolean +} + +const CreaturePropertySchema = new SimpleSchema({ _id: { type: String, regEx: SimpleSchema.RegEx.Id, @@ -56,7 +76,7 @@ let CreaturePropertySchema = new SimpleSchema({ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ // Denormalised flag if this property is inactive on the sheet for any reason - // Including being disabled, or a decendent of a disabled property + // Including being disabled, or a descendant of a disabled property inactive: { type: Boolean, optional: true, @@ -135,13 +155,14 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema); -for (let key in propertySchemasIndex) { - let schema = new SimpleSchema({}); +for (const key in propertySchemasIndex) { + const schema = new SimpleSchema({}); schema.extend(propertySchemasIndex[key]); schema.extend(CreaturePropertySchema); schema.extend(ColorSchema); schema.extend(ChildSchema); schema.extend(SoftRemovableSchema); + // @ts-expect-error don't have types for .attachSchema CreatureProperties.attachSchema(schema, { selector: { type: key } }); diff --git a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js index ae8bd797..d1e62ab3 100644 --- a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js +++ b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js @@ -1,5 +1,5 @@ import { getCreature } from '/imports/api/engine/loadCreatures'; export default function getRootCreatureAncestor(property) { - return getCreature(property.ancestors[0].id); + return getCreature(property.root.id); } diff --git a/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js b/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js index d08c43bb..8ed733c3 100644 --- a/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js +++ b/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js @@ -1,9 +1,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; const adjustQuantity = new ValidatedMethod({ name: 'creatureProperties.adjustQuantity', diff --git a/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js b/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js index b46205b9..69f072ec 100644 --- a/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js +++ b/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js @@ -1,22 +1,21 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; import { assertEditPermission, assertDocEditPermission, assertCopyPermission -} from '/imports/api/sharing/sharingPermissions.js'; +} from '/imports/api/sharing/sharingPermissions'; import { - setLineageOfDocs, - getAncestry, + fetchDocByRef, + getFilter, renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; -import { setDocToLastOrder } from '/imports/api/parenting/order.js'; -import Libraries from '/imports/api/library/Libraries.js'; +} from '/imports/api/parenting/parentingFunctions'; +import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; +import Libraries from '/imports/api/library/Libraries'; const DUPLICATE_CHILDREN_LIMIT = 500; const copyPropertyToLibrary = new ValidatedMethod({ @@ -41,33 +40,30 @@ const copyPropertyToLibrary = new ValidatedMethod({ }, run({ propId, parentRef, order }) { // get the new ancestry for the properties - let { parentDoc, ancestors } = getAncestry({ parentRef }); + const parentDoc = fetchDocByRef(parentRef); // Check permission to edit the destination let rootLibrary; if (parentRef.collection === 'libraries') { rootLibrary = parentDoc; } else if (parentRef.collection === 'libraryNodes') { - rootLibrary = Libraries.findOne(parentDoc.ancestors[0].id) + rootLibrary = Libraries.findOne(parentDoc.root.id) } else { throw `${parentRef.collection} is not a valid parent collection` } assertEditPermission(rootLibrary, this.userId); - const insertedRootNode = insertNodeFromProperty(propId, ancestors, order, this); + const insertedRootNode = insertNodeFromProperty(propId, order, this); // Tree structure changed by inserts, reorder the tree - reorderDocs({ - collection: LibraryNodes, - ancestorId: rootLibrary._id, - }); + rebuildNestedSets(LibraryNodes, rootLibrary._id); // Return the docId of the inserted root property return insertedRootNode?._id; }, }); -function insertNodeFromProperty(propId, ancestors, order, method) { +function insertNodeFromProperty(propId, order, method) { // Fetch the property and its descendants, provided they have not been // removed let prop = CreatureProperties.findOne({ @@ -87,9 +83,9 @@ function insertNodeFromProperty(propId, ancestors, order, method) { // Make sure we can edit this property assertDocEditPermission(prop, method.userId); - let oldParent = prop.parent; + let oldParentId = prop.parentId; const propCursor = CreatureProperties.find({ - 'ancestors.id': propId, + ...getFilter.descendants(prop), removed: { $ne: true }, }); @@ -109,13 +105,6 @@ function insertNodeFromProperty(propId, ancestors, order, method) { // properties assertSourceLibraryCopyPermission(props, method); - // re-map all the ancestors - setLineageOfDocs({ - docArray: props, - newAncestry: ancestors, - oldParent, - }); - // Give the docs new IDs without breaking internal references renewDocIds({ docArray: props, @@ -123,14 +112,8 @@ function insertNodeFromProperty(propId, ancestors, order, method) { }); // Order the root node - if (order === undefined) { - setDocToLastOrder({ - collection: LibraryNodes, - doc: prop, - }); - } else { - prop.order = order; - } + prop.left = Number.MAX_SAFE_INTEGER - 1; + prop.right = Number.MAX_SAFE_INTEGER; // Clean the props props = cleanProps(props); @@ -142,8 +125,8 @@ function insertNodeFromProperty(propId, ancestors, order, method) { /** * - * @param {[Property]} props The properties to check - * @param {String} userId The userId trying to copy these properties to a library + * @param props The properties to check + * @param userId The userId trying to copy these properties to a library * Checks that every property can be copied out of the library that originated it by this user */ function assertSourceLibraryCopyPermission(props, method) { @@ -162,9 +145,9 @@ function assertSourceLibraryCopyPermission(props, method) { LibraryNodes.find({ _id: { $in: libraryNodeIds } }, { - fields: { ancestors: 1 } + fields: { root: 1 } }).forEach(node => { - sourceLibIds.add(node.ancestors?.[0]?.id); + sourceLibIds.add(node.root.id); }); // Assert copy permission on each of those libraries diff --git a/app/imports/api/creature/creatureProperties/methods/damageProperty.js b/app/imports/api/creature/creatureProperties/methods/damageProperty.js index a8cd0696..eaa3fa85 100644 --- a/app/imports/api/creature/creatureProperties/methods/damageProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/damageProperty.js @@ -1,10 +1,10 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import ActionContext from '/imports/api/engine/actions/ActionContext.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { applyTriggers } from '/imports/api/engine/actions/applyTriggers'; +import ActionContext from '/imports/api/engine/actions/ActionContext'; const damageProperty = new ValidatedMethod({ name: 'creatureProperties.damage', @@ -28,7 +28,7 @@ const damageProperty = new ValidatedMethod({ if (!prop) throw new Meteor.Error( 'Damage property failed', 'Property doesn\'t exist' ); - const creatureId = prop.ancestors[0].id; + const creatureId = prop.root.id; const actionContext = new ActionContext(creatureId, [creatureId], this); // Check permissions @@ -59,7 +59,7 @@ const damageProperty = new ValidatedMethod({ }, }); -export function damagePropertyWork({ prop, operation, value, actionContext, logFunction }) { +export function damagePropertyWork({ prop, operation, value, actionContext, logFunction = undefined }) { // Save the value to the scope before applying the before triggers if (operation === 'increment') { diff --git a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js index 88e2f900..ad5665c3 100644 --- a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js @@ -1,18 +1,18 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; import { - setLineageOfDocs, + getFilter, renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; +} from '/imports/api/parenting/parentingFunctions'; +import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; var snackbar; if (Meteor.isClient) { snackbar = require( - '/imports/client/ui/components/snackbars/SnackbarQueue.js' + '/imports/client/ui/components/snackbars/SnackbarQueue' ).snackbar } @@ -33,6 +33,8 @@ const duplicateProperty = new ValidatedMethod({ }, run({ _id }) { let property = CreatureProperties.findOne(_id); + if (!property) throw new Meteor.Error('not-found', 'The source property was not found'); + let creature = getRootCreatureAncestor(property); assertEditPermission(creature, this.userId); @@ -49,7 +51,7 @@ const duplicateProperty = new ValidatedMethod({ // Get all the descendants let nodes = CreatureProperties.find({ - 'ancestors.id': _id, + ...getFilter.descendants(property), removed: { $ne: true }, }, { limit: DUPLICATE_CHILDREN_LIMIT + 1, @@ -66,22 +68,13 @@ const duplicateProperty = new ValidatedMethod({ } } - // re-map all the ancestors - setLineageOfDocs({ - docArray: nodes, - newAncestry: [ - ...property.ancestors, - { id: propertyId, collection: 'creatureProperties' } - ], - oldParent: { id: _id, collection: 'creatureProperties' }, - }); - // Give the docs new IDs without breaking internal references const allNodes = [property, ...nodes]; renewDocIds({ docArray: allNodes }); // Order the root node - property.order += 0.5; + property.left = Number.MAX_SAFE_INTEGER - 1; + property.right = Number.MAX_SAFE_INTEGER; // Mark the sheet as needing recompute property.dirty = true; @@ -90,10 +83,7 @@ const duplicateProperty = new ValidatedMethod({ CreatureProperties.batchInsert(allNodes); // Tree structure changed by inserts, reorder the tree - reorderDocs({ - collection: CreatureProperties, - ancestorId: property.ancestors[0].id, - }); + rebuildNestedSets(CreatureProperties, property.root.id); return propertyId; }, diff --git a/app/imports/api/creature/creatureProperties/methods/equipItem.js b/app/imports/api/creature/creatureProperties/methods/equipItem.js index 671908b3..8261e882 100644 --- a/app/imports/api/creature/creatureProperties/methods/equipItem.js +++ b/app/imports/api/creature/creatureProperties/methods/equipItem.js @@ -1,11 +1,11 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { organizeDoc } from '/imports/api/parenting/organizeMethods.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js'; -import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { organizeDoc } from '/imports/api/parenting/organizeMethods'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; +import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS'; +import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag'; // Equipping or unequipping an item will also change its parent const equipItem = new ValidatedMethod({ diff --git a/app/imports/api/creature/creatureProperties/methods/flipToggle.js b/app/imports/api/creature/creatureProperties/methods/flipToggle.js index dbd814ff..c97b3ac9 100644 --- a/app/imports/api/creature/creatureProperties/methods/flipToggle.js +++ b/app/imports/api/creature/creatureProperties/methods/flipToggle.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; const flipToggle = new ValidatedMethod({ name: 'creatureProperties.flipToggle', @@ -17,7 +17,7 @@ const flipToggle = new ValidatedMethod({ run({ _id }) { // Permission let property = CreatureProperties.findOne(_id, { - fields: { type: 1, ancestors: 1, enabled: 1, disabled: 1 } + fields: { type: 1, root: 1, enabled: 1, disabled: 1 } }); if (property.type !== 'toggle') { throw new Meteor.Error('wrong property', diff --git a/app/imports/api/creature/creatureProperties/methods/getParentRefByTag.js b/app/imports/api/creature/creatureProperties/methods/getParentRefByTag.js index f5236c9f..1c664692 100644 --- a/app/imports/api/creature/creatureProperties/methods/getParentRefByTag.js +++ b/app/imports/api/creature/creatureProperties/methods/getParentRefByTag.js @@ -1,13 +1,14 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; -export default function getParentRefByTag(creatureId, tag){ +export default function getParentRefByTag(creatureId, tag) { let prop = CreatureProperties.findOne({ - 'ancestors.id': creatureId, - removed: {$ne: true}, - inactive: {$ne: true}, + ...getFilter.descendantsOfRoot(creatureId), + removed: { $ne: true }, + inactive: { $ne: true }, tags: tag, }, { - sort: {order: 1}, + sort: { order: 1 }, }); - return prop && {id: prop._id, collection: 'creatureProperties'}; + return prop && { id: prop._id, collection: 'creatureProperties' }; } diff --git a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js index f3d82f75..d080e4f8 100644 --- a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js +++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js @@ -1,3 +1,5 @@ +import { getFilter } from "/imports/api/parenting/parentingFunctions"; + export default function getSlotFillFilter({ slot, libraryIds }) { if (!slot) throw 'Slot is required for getSlotFillFilter'; @@ -6,9 +8,14 @@ export default function getSlotFillFilter({ slot, libraryIds }) { let filter = { fillSlots: true, removed: { $ne: true }, - $and: [] + $and: [], }; - filter['ancestors.id'] = { $in: libraryIds }; + if (libraryIds.length) { + Object.assign( + filter, + getFilter.descendantsOfAllRoots(libraryIds) + ); + } if (slot.slotType) { filter.$and.push({ $or: [{ diff --git a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js index d7b33cc0..cd8f1c5a 100644 --- a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js +++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js @@ -1,5 +1,5 @@ import { assert } from 'chai'; -import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'; +import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter'; describe('Slot fill filter', function () { @@ -33,7 +33,7 @@ describe('Slot fill filter', function () { $or: [{ libraryTags: { $all: ['tag1', 'tag2'] } }], - 'ancestors.id': { $in: ['libraryId1', 'libraryId2'] }, + 'root.id': { $in: ['libraryId1', 'libraryId2'] }, removed: { $ne: true }, fillSlots: true, }); @@ -76,7 +76,7 @@ describe('Slot fill filter', function () { $and: [ { libraryTags: { $nin: ['tag5', 'tag6', 'tag7', 'tag8'] } }, ], - 'ancestors.id': { $in: ['libraryId1', 'libraryId2'] }, + 'root.id': { $in: ['libraryId1', 'libraryId2'] }, removed: { $ne: true }, fillSlots: true, }); diff --git a/app/imports/api/creature/creatureProperties/methods/index.js b/app/imports/api/creature/creatureProperties/methods/index.js index 94bedae3..e66019cc 100644 --- a/app/imports/api/creature/creatureProperties/methods/index.js +++ b/app/imports/api/creature/creatureProperties/methods/index.js @@ -1,14 +1,14 @@ -import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; -import '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js'; -import '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js'; -import '/imports/api/creature/creatureProperties/methods/equipItem.js'; -import '/imports/api/creature/creatureProperties/methods/insertProperty.js'; -import '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; -import '/imports/api/creature/creatureProperties/methods/pullFromProperty.js'; -import '/imports/api/creature/creatureProperties/methods/pushToProperty.js'; -import '/imports/api/creature/creatureProperties/methods/restoreProperty.js'; -import '/imports/api/creature/creatureProperties/methods/selectAmmoItem.js'; -import '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js'; -import '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js'; -import '/imports/api/creature/creatureProperties/methods/flipToggle.js'; +import '/imports/api/creature/creatureProperties/methods/adjustQuantity'; +import '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary'; +import '/imports/api/creature/creatureProperties/methods/damageProperty'; +import '/imports/api/creature/creatureProperties/methods/duplicateProperty'; +import '/imports/api/creature/creatureProperties/methods/equipItem'; +import '/imports/api/creature/creatureProperties/methods/insertProperty'; +import '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode'; +import '/imports/api/creature/creatureProperties/methods/pullFromProperty'; +import '/imports/api/creature/creatureProperties/methods/pushToProperty'; +import '/imports/api/creature/creatureProperties/methods/restoreProperty'; +import '/imports/api/creature/creatureProperties/methods/selectAmmoItem'; +import '/imports/api/creature/creatureProperties/methods/softRemoveProperty'; +import '/imports/api/creature/creatureProperties/methods/updateCreatureProperty'; +import '/imports/api/creature/creatureProperties/methods/flipToggle'; diff --git a/app/imports/api/creature/creatureProperties/methods/insertProperty.js b/app/imports/api/creature/creatureProperties/methods/insertProperty.js index 2d872868..570a301d 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/insertProperty.js @@ -1,14 +1,12 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; import SimpleSchema from 'simpl-schema'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; -import { getAncestry } from '/imports/api/parenting/parenting.js'; -import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; -import { getHighestOrder } from '/imports/api/parenting/order.js'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { fetchDocByRef, rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; +import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; const insertProperty = new ValidatedMethod({ name: 'creatureProperties.insert', @@ -25,27 +23,23 @@ const insertProperty = new ValidatedMethod({ timeInterval: 5000, }, run({ creatureProperty, parentRef }) { - // get the new ancestry for the properties - let { parentDoc, ancestors } = getAncestry({ parentRef }); + let rootCreature; + const parentDoc = fetchDocByRef(parentRef); // Check permission to edit - let rootCreature; if (parentRef.collection === 'creatures') { rootCreature = parentDoc; } else if (parentRef.collection === 'creatureProperties') { rootCreature = getRootCreatureAncestor(parentDoc); + creatureProperty.parentId = parentDoc._id; } else { throw `${parentRef.collection} is not a valid parent collection` } assertEditPermission(rootCreature, this.userId); - creatureProperty.parent = parentRef; - creatureProperty.ancestors = ancestors; + creatureProperty.root = { collection: 'creatures', id: rootCreature._id }; - return insertPropertyWork({ - property: creatureProperty, - creature: rootCreature, - }); + return insertPropertyWork(creatureProperty); }, }); @@ -77,18 +71,17 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({ }, run({ creatureProperty, creatureId, tag, tagDefaultName }) { let parentRef = getParentRefByTag(creatureId, tag); + let insertFolderFirst = false; if (!parentRef) { // Use the creature as the parent and mark that we need to insert the folder first later - var insertFolderFirst = true; + insertFolderFirst = true; parentRef = { id: creatureId, collection: 'creatures' }; } - // get the new ancestry for the properties - let { parentDoc, ancestors } = getAncestry({ parentRef }); - // Check permission to edit let rootCreature; + const parentDoc = fetchDocByRef(parentRef); if (parentRef.collection === 'creatures') { rootCreature = parentDoc; } else if (parentRef.collection === 'creatureProperties') { @@ -98,46 +91,34 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({ } assertEditPermission(rootCreature, this.userId); + const root = { collection: 'creatures', id: rootCreature._id }; + // Add the folder first if we need to if (insertFolderFirst) { - let order = getHighestOrder({ - collection: CreatureProperties, - ancestorId: parentRef.id, - }) + 1; let id = CreatureProperties.insert({ type: 'folder', name: tagDefaultName || (tag.charAt(0).toUpperCase() + tag.slice(1)), tags: [tag], - parent: parentRef, - ancestors: [parentRef], - order, + // parentId: undefined, + root, }); // Make the folder our new parent - let newParentRef = { id, collection: 'creatureProperties' }; - ancestors = [parentRef, newParentRef]; - parentRef = newParentRef; - creatureProperty.order = order + 1; + parentRef = { id, collection: 'creatureProperties' }; } - creatureProperty.parent = parentRef; - creatureProperty.ancestors = ancestors; + creatureProperty.root = root; + creatureProperty.parentId = parentRef.id; - return insertPropertyWork({ - property: creatureProperty, - creature: rootCreature, - }); + return insertPropertyWork(creatureProperty); }, }); -export function insertPropertyWork({ property, creature }) { +export function insertPropertyWork(property) { delete property._id; property.dirty = true; let _id = CreatureProperties.insert(property); // Tree structure changed by insert, reorder the tree - reorderDocs({ - collection: CreatureProperties, - ancestorId: creature._id, - }); + rebuildNestedSets(CreatureProperties, property.root.id); return _id; } diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index 8f0eefa6..f582ad29 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -1,19 +1,17 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; import { - setLineageOfDocs, - getAncestry, - renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; -import { setDocToLastOrder } from '/imports/api/parenting/order.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; + renewDocIds, + fetchDocByRef, + rebuildNestedSets, + getFilter +} from '/imports/api/parenting/parentingFunctions'; import { union } from 'lodash'; const insertPropertyFromLibraryNode = new ValidatedMethod({ @@ -30,19 +28,15 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ parentRef: { type: RefSchema, }, - order: { - type: Number, - optional: true, - }, }).validator(), mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, timeInterval: 5000, }, - run({ nodeIds, parentRef, order }) { + run({ nodeIds, parentRef }) { // get the new ancestry for the properties - let { parentDoc, ancestors } = getAncestry({ parentRef }); + const parentDoc = fetchDocByRef(parentRef); // Check permission to edit let rootCreature; @@ -55,37 +49,32 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ } assertEditPermission(rootCreature, this.userId); - // {libraryId: hasViewPermission} - //let libraryPermissionMemoir = {}; + const root = { collection: 'creatures', id: rootCreature._id }; + const parentId = parentRef.id; + let node; nodeIds.forEach(nodeId => { - // TODO: Check library view permission for each node before starting - node = insertPropertyFromNode(nodeId, ancestors, order); + node = insertPropertyFromNode(nodeId, root, parentId); }); - // get one of the root inserted docs - let rootId = node._id; - // Tree structure changed by inserts, reorder the tree - reorderDocs({ - collection: CreatureProperties, - ancestorId: rootCreature._id, - }); - // Return the docId of the last property, the inserted root property - return rootId; + rebuildNestedSets(CreatureProperties, rootCreature._id); + + // get one of the root inserted docs + const lastInsertedId = node?._id; + return lastInsertedId; }, }); -function insertPropertyFromNode(nodeId, ancestors, order) { - // Fetch the library node and its decendents, provided they have not been +function insertPropertyFromNode(nodeId, root, parentId) { + // Fetch the library node and its descendants, provided they have not been // removed - // TODO: Check permission to read the library this node is in let node = LibraryNodes.findOne({ _id: nodeId, removed: { $ne: true }, }); if (!node) { - if (Meteor.isClient) return; + if (Meteor.isClient) return {}; else { throw new Meteor.Error( 'Insert property from library failed', @@ -93,13 +82,12 @@ function insertPropertyFromNode(nodeId, ancestors, order) { ); } } - let oldParent = node.parent; + let nodes = LibraryNodes.find({ - 'ancestors.id': nodeId, + ...getFilter.descendants(node), removed: { $ne: true }, }).fetch(); - // The root node is first in the array of nodes // It must get the first generated ID to prevent flickering nodes = [node, ...nodes]; @@ -112,31 +100,17 @@ function insertPropertyFromNode(nodeId, ancestors, order) { // set libraryNodeIds storeLibraryNodeReferences(nodes); - // re-map all the ancestors - setLineageOfDocs({ - docArray: nodes, - newAncestry: ancestors, - oldParent, - }); - // Give the docs new IDs without breaking internal references renewDocIds({ docArray: nodes, collectionMap: { 'libraryNodes': 'creatureProperties' } }); - // Order the root node - if (order === undefined) { - setDocToLastOrder({ - collection: CreatureProperties, - doc: node, - }); - } else { - node.order = order; - } + // Mark root node as dirty + node.dirty = true; - // Mark all nodes as dirty - dirtyNodes(nodes); + // Move the root node to the end of the order + node.left = Number.MAX_SAFE_INTEGER; // Insert the creature properties CreatureProperties.batchInsert(nodes); @@ -150,12 +124,6 @@ function storeLibraryNodeReferences(nodes) { }); } -function dirtyNodes(nodes) { - nodes.forEach(node => { - node.dirty = true; - }); -} - // Covert node references into actual nodes // TODO: check permissions for each library a reference node references function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) { @@ -178,7 +146,6 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) { let referencedNode try { referencedNode = fetchDocByRef(node.ref); - referencedNode.order = node.order; referencedNode.tags = union(node.tags, referencedNode.tags); // We are definitely replacing this node, so add it to the list visitedRefs.add(node._id); @@ -188,23 +155,15 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) { } // Get all the descendants of the referenced node - let descendents = LibraryNodes.find({ - 'ancestors.id': referencedNode._id, + let descendants = LibraryNodes.find({ + ...getFilter.descendants(referencedNode), removed: { $ne: true }, }, { sort: { order: 1 }, }).fetch(); // We are adding the referenced node and its descendants - let addedNodes = [referencedNode, ...descendents]; - - // re-map all the ancestors to parent the new sub-tree into our existing - // node tree - setLineageOfDocs({ - docArray: addedNodes, - newAncestry: node.ancestors, - oldParent: referencedNode.parent, - }); + let addedNodes = [referencedNode, ...descendants]; // Filter all the looped references addedNodes = addedNodes.filter(addedNode => { diff --git a/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js b/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js index 4dd8245e..86303ab6 100644 --- a/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; const pullFromProperty = new ValidatedMethod({ name: 'creatureProperties.pull', diff --git a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js index 95735d9b..e064273e 100644 --- a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; import { get } from 'lodash'; const pushToProperty = new ValidatedMethod({ diff --git a/app/imports/api/creature/creatureProperties/methods/restoreProperty.js b/app/imports/api/creature/creatureProperties/methods/restoreProperty.js index 9e66448e..37713d56 100644 --- a/app/imports/api/creature/creatureProperties/methods/restoreProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/restoreProperty.js @@ -1,10 +1,10 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { restore } from '/imports/api/parenting/softRemove.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { restore } from '/imports/api/parenting/softRemove'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; const restoreProperty = new ValidatedMethod({ name: 'creatureProperties.restore', diff --git a/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js b/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js index 57bbe6bd..b6578898 100644 --- a/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js +++ b/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js @@ -1,9 +1,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; const selectAmmoItem = new ValidatedMethod({ name: 'creatureProperties.selectAmmoItem', diff --git a/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js b/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js index a4240ac7..7c0753bc 100644 --- a/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js @@ -1,10 +1,10 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { softRemove } from '/imports/api/parenting/softRemove.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { softRemove } from '/imports/api/parenting/softRemove'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; const softRemoveProperty = new ValidatedMethod({ name: 'creatureProperties.softRemove', diff --git a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js index 5664d8bd..01ebb388 100644 --- a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; const updateCreatureProperty = new ValidatedMethod({ name: 'creatureProperties.update', @@ -14,6 +14,8 @@ const updateCreatureProperty = new ValidatedMethod({ case 'order': case 'parent': case 'ancestors': + case 'root': + case 'parentId': case 'damage': throw new Meteor.Error('Permission denied', 'This property can\'t be updated directly'); @@ -27,7 +29,7 @@ const updateCreatureProperty = new ValidatedMethod({ run({ _id, path, value }) { // Permission let property = CreatureProperties.findOne(_id, { - fields: { type: 1, ancestors: 1 } + fields: { type: 1, root: 1 } }); let rootCreature = getRootCreatureAncestor(property); assertEditPermission(rootCreature, this.userId); diff --git a/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js b/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js index a2190860..06fe42c7 100644 --- a/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js +++ b/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js @@ -1,12 +1,9 @@ -import computeCreature from '/imports/api/engine/computeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature'; /** * Recomputes all ancestor creatures of this property + * @deprecated */ -export default function recomputeCreaturesByProperty(property){ - for (let ref of property.ancestors){ - if (ref.collection === 'creatures') { - computeCreature.call(ref.id); - } - } +export default function recomputeCreaturesByProperty(property) { + computeCreature.call(property.root.id); } diff --git a/app/imports/api/creature/creatures/Creatures.js b/app/imports/api/creature/creatures/Creatures.js index bd05254e..219da0a5 100644 --- a/app/imports/api/creature/creatures/Creatures.js +++ b/app/imports/api/creature/creatures/Creatures.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; -import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js' -import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js'; -import SharingSchema from '/imports/api/sharing/SharingSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema' +import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema'; +import SharingSchema from '/imports/api/sharing/SharingSchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; //set up the collection for creatures let Creatures = new Mongo.Collection('creatures'); @@ -195,4 +195,4 @@ Creatures.attachSchema(CreatureSchema); export default Creatures; export { CreatureSchema }; -import '/imports/api/engine/actions/doAction.js'; +import '/imports/api/engine/actions/doAction'; diff --git a/app/imports/api/creature/creatures/creaturePermissions.js b/app/imports/api/creature/creatures/creaturePermissions.js index 380a5258..1e8c4d2c 100644 --- a/app/imports/api/creature/creatures/creaturePermissions.js +++ b/app/imports/api/creature/creatures/creaturePermissions.js @@ -1,29 +1,29 @@ -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; import { assertEditPermission as editPermission, assertViewPermission as viewPermission, assertOwnership as ownership -} from '/imports/api/sharing/sharingPermissions.js'; +} from '/imports/api/sharing/sharingPermissions'; -function getCreature(creature, fields){ - if (typeof creature === 'string'){ - return Creatures.findOne(creature, {fields}); +function getCreature(creature, fields) { + if (typeof creature === 'string') { + return Creatures.findOne(creature, { fields }); } else { return creature; } } -export function assertOwnership(creature, userId){ - creature = getCreature(creature, {owner: 1}); +export function assertOwnership(creature, userId) { + creature = getCreature(creature, { owner: 1 }); ownership(creature, userId); } export function assertEditPermission(creature, userId) { - creature = getCreature(creature, {owner: 1, writers: 1}); + creature = getCreature(creature, { owner: 1, writers: 1 }); editPermission(creature, userId); } export function assertViewPermission(creature, userId) { - creature = getCreature(creature, {owner: 1, readers:1, writers: 1, public: 1}); + creature = getCreature(creature, { owner: 1, readers: 1, writers: 1, public: 1 }); viewPermission(creature, userId); } diff --git a/app/imports/api/creature/creatures/defaultCharacterProperties.js b/app/imports/api/creature/creatures/defaultCharacterProperties.js index e7f05a11..dc94cd96 100644 --- a/app/imports/api/creature/creatures/defaultCharacterProperties.js +++ b/app/imports/api/creature/creatures/defaultCharacterProperties.js @@ -1,47 +1,50 @@ -import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js'; +import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS'; -export default function defaultCharacterProperties(creatureId){ +export default function defaultCharacterProperties(creatureId) { if (!creatureId) throw 'creatureId is required'; - const creatureRef = {collection: 'creatures', id: creatureId}; + const creatureRef = { collection: 'creatures', id: creatureId }; let randomSrc = DDP.randomStream('defaultProperties'); const inventoryId = randomSrc.id(); - const inventoryRef = {collection: 'creatureProperties', id: inventoryId}; return [ { type: 'propertySlot', name: 'Ruleset', - description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base ruleset, your sheet will be empty.'}, + description: { text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base ruleset, your sheet will be empty.' }, slotTags: ['base'], tags: [], - quantityExpected: {calculation: '1'}, + quantityExpected: { calculation: '1' }, hideWhenFull: true, spaceLeft: 1, totalFilled: 0, - order: 0, - parent: creatureRef, - ancestors: [creatureRef], + left: 1, + right: 2, + parentId: creatureId, + root: creatureRef, }, { _id: inventoryId, type: 'folder', name: 'Inventory', tags: [BUILT_IN_TAGS.inventory], - order: 1, - parent: creatureRef, - ancestors: [creatureRef], + left: 3, + right: 8, + parentId: creatureId, + root: creatureRef, }, { type: 'folder', name: 'Equipment', tags: [BUILT_IN_TAGS.equipment], - order: 2, - parent: inventoryRef, - ancestors: [creatureRef, inventoryRef], + left: 4, + right: 5, + parentId: inventoryId, + root: creatureRef, }, { type: 'folder', name: 'Carried', tags: [BUILT_IN_TAGS.carried], - order: 3, - parent: inventoryRef, - ancestors: [creatureRef, inventoryRef], + left: 6, + right: 7, + parent: inventoryId, + root: creatureRef, }, ]; } diff --git a/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js b/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js index 5b368e5f..2dc31832 100644 --- a/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js +++ b/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js @@ -1,5 +1,5 @@ -import { getUserTier } from '/imports/api/users/patreon/tiers.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import { getUserTier } from '/imports/api/users/patreon/tiers'; +import Creatures from '/imports/api/creature/creatures/Creatures'; export default function assertHasCharactersSlots(userId) { if (characterSlotsRemaining(userId) <= 0) { diff --git a/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js b/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js index 05a8e9d6..c95aa533 100644 --- a/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js +++ b/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js @@ -1,9 +1,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; import SimpleSchema from 'simpl-schema'; -import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin'; const changeAllowedLibraries = new ValidatedMethod({ name: 'creatures.changeAllowedLibraries', diff --git a/app/imports/api/creature/creatures/methods/index.js b/app/imports/api/creature/creatures/methods/index.js index 85aefc77..3e8073b6 100644 --- a/app/imports/api/creature/creatures/methods/index.js +++ b/app/imports/api/creature/creatures/methods/index.js @@ -1,5 +1,5 @@ -import '/imports/api/creature/creatures/methods/insertCreature.js'; -import '/imports/api/creature/creatures/methods/removeCreature.js'; -import '/imports/api/creature/creatures/methods/restCreature.js'; -import '/imports/api/creature/creatures/methods/updateCreature.js'; -import '/imports/api/creature/creatures/methods/changeAllowedLibraries.js'; +import '/imports/api/creature/creatures/methods/insertCreature'; +import '/imports/api/creature/creatures/methods/removeCreature'; +import '/imports/api/creature/creatures/methods/restCreature'; +import '/imports/api/creature/creatures/methods/updateCreature'; +import '/imports/api/creature/creatures/methods/changeAllowedLibraries'; diff --git a/app/imports/api/creature/creatures/methods/insertCreature.js b/app/imports/api/creature/creatures/methods/insertCreature.js index c978785b..bddb9136 100644 --- a/app/imports/api/creature/creatures/methods/insertCreature.js +++ b/app/imports/api/creature/creatures/methods/insertCreature.js @@ -1,15 +1,15 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; -import Creatures, { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js'; -import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; -import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js'; -import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'; -import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences.js'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin'; +import Creatures, { CreatureSchema } from '/imports/api/creature/creatures/Creatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties'; +import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode'; +import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots'; +import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter'; +import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences'; import SimpleSchema from 'simpl-schema'; const insertCreature = new ValidatedMethod({ diff --git a/app/imports/api/creature/creatures/methods/removeCreature.js b/app/imports/api/creature/creatures/methods/removeCreature.js index d157f5d7..646a6bc8 100644 --- a/app/imports/api/creature/creatures/methods/removeCreature.js +++ b/app/imports/api/creature/creatures/methods/removeCreature.js @@ -1,18 +1,19 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; -import Experiences from '/imports/api/creature/experience/Experiences.js'; +import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs'; +import Experiences from '/imports/api/creature/experience/Experiences'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; -function removeRelatedDocuments(creatureId){ - CreatureVariables.remove({_creatureId: creatureId}); - CreatureProperties.remove({'ancestors.id': creatureId}); - CreatureLogs.remove({creatureId}); - Experiences.remove({creatureId}); +function removeRelatedDocuments(creatureId) { + CreatureVariables.remove({ _creatureId: creatureId }); + CreatureProperties.remove(getFilter.descendantsOfRoot(creatureId)); + CreatureLogs.remove({ creatureId }); + Experiences.remove({ creatureId }); } const removeCreature = new ValidatedMethod({ @@ -28,14 +29,14 @@ const removeCreature = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({charId}) { + run({ charId }) { assertOwnership(charId, this.userId) this.unblock(); removeCreatureWork(charId) }, }); -export function removeCreatureWork(creatureId){ +export function removeCreatureWork(creatureId) { Creatures.remove(creatureId); removeRelatedDocuments(creatureId); } diff --git a/app/imports/api/creature/creatures/methods/restCreature.js b/app/imports/api/creature/creatures/methods/restCreature.js index bb351c03..f281bbf2 100644 --- a/app/imports/api/creature/creatures/methods/restCreature.js +++ b/app/imports/api/creature/creatures/methods/restCreature.js @@ -1,12 +1,13 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; import { union } from 'lodash'; -import ActionContext from '/imports/api/engine/actions/ActionContext.js'; -import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; +import ActionContext from '/imports/api/engine/actions/ActionContext'; +import { applyTriggers } from '/imports/api/engine/actions/applyTriggers'; +import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; const restCreature = new ValidatedMethod({ name: 'creature.methods.rest', @@ -74,7 +75,7 @@ function doRestWork(restType, actionContext) { export function resetProperties(creatureId, resetFilter, actionContext) { // Only apply to active properties const filter = { - 'ancestors.id': creatureId, + ...getFilter.descendantsOfRoot(creatureId), reset: resetFilter, removed: { $ne: true }, inactive: { $ne: true }, @@ -128,7 +129,7 @@ export function resetProperties(creatureId, resetFilter, actionContext) { function resetHitDice(creatureId, actionContext) { let hitDice = CreatureProperties.find({ - 'ancestors.id': creatureId, + ...getFilter.descendantsOfRoot(creatureId), type: 'attribute', attributeType: 'hitDice', removed: { $ne: true }, diff --git a/app/imports/api/creature/creatures/methods/updateCreature.js b/app/imports/api/creature/creatures/methods/updateCreature.js index 4da290bc..58efc7a6 100644 --- a/app/imports/api/creature/creatures/methods/updateCreature.js +++ b/app/imports/api/creature/creatures/methods/updateCreature.js @@ -1,7 +1,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; const updateCreature = new ValidatedMethod({ name: 'creatures.update', diff --git a/app/imports/api/creature/experience/Experiences.js b/app/imports/api/creature/experience/Experiences.js index d1c5c527..3bd29a57 100644 --- a/app/imports/api/creature/experience/Experiences.js +++ b/app/imports/api/creature/experience/Experiences.js @@ -1,9 +1,9 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let Experiences = new Mongo.Collection('experiences'); diff --git a/app/imports/api/creature/journal/JournalEntry.js b/app/imports/api/creature/journal/JournalEntry.js index 74b885ea..bb1a5059 100644 --- a/app/imports/api/creature/journal/JournalEntry.js +++ b/app/imports/api/creature/journal/JournalEntry.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let ExperienceSchema = new SimpleSchema({ title: { diff --git a/app/imports/api/creature/log/CreatureLogs.js b/app/imports/api/creature/log/CreatureLogs.js index 62fbf798..9d6c6a79 100644 --- a/app/imports/api/creature/log/CreatureLogs.js +++ b/app/imports/api/creature/log/CreatureLogs.js @@ -1,18 +1,18 @@ import SimpleSchema from 'simpl-schema'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; -import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import LogContentSchema from '/imports/api/creature/log/LogContentSchema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import { parse, prettifyParseError } from '/imports/parser/parser.js'; -import resolve, { toString } from '/imports/parser/resolve.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; +import { parse, prettifyParseError } from '/imports/parser/parser'; +import resolve, { toString } from '/imports/parser/resolve'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; const PER_CREATURE_LOG_LIMIT = 100; if (Meteor.isServer) { - var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature; + var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook').sendWebhookAsCreature; } let CreatureLogs = new Mongo.Collection('creatureLogs'); diff --git a/app/imports/api/creature/log/LogContentSchema.js b/app/imports/api/creature/log/LogContentSchema.ts similarity index 82% rename from app/imports/api/creature/log/LogContentSchema.js rename to app/imports/api/creature/log/LogContentSchema.ts index 8288157e..af98b288 100644 --- a/app/imports/api/creature/log/LogContentSchema.js +++ b/app/imports/api/creature/log/LogContentSchema.ts @@ -1,7 +1,18 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; +import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; + +export interface LogContent { + name?: string + value?: string + inline?: boolean + context?: { + errors: any[] + rolls: any[] + doubleRolls?: boolean + } +} let LogContentSchema = new SimpleSchema({ // The name of the field, included in discord webhook message diff --git a/app/imports/api/creature/mixins/creaturePermissionMixin.js b/app/imports/api/creature/mixins/creaturePermissionMixin.js index 18b96fbd..de419e82 100644 --- a/app/imports/api/creature/mixins/creaturePermissionMixin.js +++ b/app/imports/api/creature/mixins/creaturePermissionMixin.js @@ -2,7 +2,7 @@ import { assertEditPermission, assertViewPermission, assertOwnership, -} from '/imports/api/creature/creatures/creaturePermissions.js'; +} from '/imports/api/creature/creatures/creaturePermissions'; // Checks if the method has permission to run on the document. If the document // has a charId, that creature is checked, otherwise if it has an _id and the @@ -13,36 +13,36 @@ import { // Because this mixin injects the charId into argument objects that don't // already contain it, it should always come last in the mixin list, so that it // the outermost wrapper of the run function -export default function creaturePermissionMixin(methodOptions){ +export default function creaturePermissionMixin(methodOptions) { let assertPermission; - if (methodOptions.permission === 'owner'){ + if (methodOptions.permission === 'owner') { assertPermission = assertOwnership; - } else if (methodOptions.permission === 'edit'){ + } else if (methodOptions.permission === 'edit') { assertPermission = assertEditPermission; - } else if (methodOptions.permission === 'view'){ + } else if (methodOptions.permission === 'view') { assertPermission = assertViewPermission; } else { throw "`permission` missing in method options"; } let getCharId; - if (methodOptions.getCharId){ + if (methodOptions.getCharId) { getCharId = methodOptions.getCharId; } else if (methodOptions.collection) { - getCharId = function({_id}){ + getCharId = function ({ _id }) { return methodOptions.collection.findOne(_id, { - fields: {charId: 1} + fields: { charId: 1 } }).charId; }; } else { - getCharId = function(){ + getCharId = function () { throw "`getCharId` or `collection` missing in method options," + " or {charId} missing in call"; }; } let runFunc = methodOptions.run; - methodOptions.run = function(doc, ...rest){ + methodOptions.run = function (doc, ...rest) { // Store the charId on the doc for other mixins if it had to be fetched doc.charId = doc.charId || getCharId.apply(this, arguments); assertPermission(doc.charId, this.userId); diff --git a/app/imports/api/creature/mixins/propagateInheritanceUpdateMixin.js b/app/imports/api/creature/mixins/propagateInheritanceUpdateMixin.js deleted file mode 100644 index 0ed6ee9f..00000000 --- a/app/imports/api/creature/mixins/propagateInheritanceUpdateMixin.js +++ /dev/null @@ -1,59 +0,0 @@ -import { - updateChildren, - updateDescendants, -} from '/imports/api/parenting/parenting.js'; -import { inheritedFields } from '/imports/api/parenting/ChildSchema.js'; -import MONGO_OPERATORS from '/imports/constants/MONGO_OPERATORS.js'; - -// This mixin can be safely applied to all update methods which are validated -// with the updateSchemaMixin. It will propagate updates to fields which -// are inherited and normalised on the parent or ancestor docs -// It should have neglible performance impact for updates that aren't inherited -function propagateInheritanceUpdate({_id, update}){ - let childModifier = {}; - let descendantModifier = {}; - // For each operator - for (let operator of MONGO_OPERATORS){ - // If the operator is in the update, for each field - if (update[operator]) for (let field in update[operator]){ - // Get the top level field that was actually modified - const indexOfDot = field.indexOf('.'); - let modifiedField; - if (indexOfDot !== -1) { - modifiedField = field.substring(0, indexOfDot); - } else { - modifiedField = field; - } - // If that field is updated and inherited - if (inheritedFields.has(modifiedField)){ - // Perform the same update on the descendants - if (!childModifier[operator]) childModifier[operator] = {}; - if (!descendantModifier[operator]) descendantModifier[operator] = {}; - childModifier[operator][`parent.${field}`] = update[operator][field]; - descendantModifier[operator][`ancestors.$.${field}`] = update[operator][field]; - } - } - } - - // Update the parent object of its children - updateChildren({ - parentId: _id, - modifier: childModifier, - }); - - // Update the ancestors object of its descendants - updateDescendants({ - ancestorId: _id, - modifier: descendantModifier, - }); -} - -export default function propagateInheritanceUpdateMixin(methodOptions){ - let runFunc = methodOptions.run; - methodOptions.run = function({_id, update}){ - const result = runFunc.apply(this, arguments); - propagateInheritanceUpdate({_id, update}); - return result; - }; - return methodOptions; -} diff --git a/app/imports/api/creature/mixins/recomputeCreatureMixin.js b/app/imports/api/creature/mixins/recomputeCreatureMixin.js index a6b991a7..32caa2f6 100644 --- a/app/imports/api/creature/mixins/recomputeCreatureMixin.js +++ b/app/imports/api/creature/mixins/recomputeCreatureMixin.js @@ -1,8 +1,8 @@ -import computeCreature from '/imports/api/engine/computeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature'; -export default function recomputeCreatureMixin(methodOptions){ +export default function recomputeCreatureMixin(methodOptions) { let runFunc = methodOptions.run; - methodOptions.run = function({charId}){ + methodOptions.run = function ({ charId }) { const result = runFunc.apply(this, arguments); if ( methodOptions.skipRecompute && diff --git a/app/imports/api/creature/mixins/setDocToLastMixin.js b/app/imports/api/creature/mixins/setDocToLastMixin.js deleted file mode 100644 index 53a4a0f9..00000000 --- a/app/imports/api/creature/mixins/setDocToLastMixin.js +++ /dev/null @@ -1,27 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import { setDocToLastOrder } from '/imports/api/parenting/order.js'; - -export function setDocToLastMixin(methodOptions){ - // Make sure the doc has a charId - // This mixin should come before simpleSchemaMixin so that it can extend the - // schema before it is turned into a validate function - if (methodOptions.validate){ - throw new Meteor.Error(`setDocToLastMixin should come before simpleSchemaMixin`); - } - methodOptions.schema = new SimpleSchema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - }).extend(methodOptions.schema); - let collection = methodOptions.collection; - if (!collection){ - throw new Meteor.Error("`collection` required in method options for setDocToLastMixin"); - } - let runFunc = methodOptions.run; - methodOptions.run = function(doc){ - setDocToLastOrder({collection, doc}); - return runFunc.apply(this, arguments); - }; - return methodOptions; -} diff --git a/app/imports/api/docs/Docs.js b/app/imports/api/docs/Docs.js index 6f7e2207..ceba1b37 100644 --- a/app/imports/api/docs/Docs.js +++ b/app/imports/api/docs/Docs.js @@ -3,60 +3,17 @@ import { Mongo } from 'meteor/mongo'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import { softRemove } from '/imports/api/parenting/softRemove.js'; -import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js'; -import { storedIconsSchema } from '/imports/api/icons/Icons.js'; -import '/imports/api/library/methods/index.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import { restore } from '/imports/api/parenting/softRemove.js'; -import { getAncestry, updateParent } from '/imports/api/parenting/parenting.js'; -import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree'; -import { getDocsInDepthFirstOrder } from '/imports/api/parenting/getDescendantsInDepthFirstOrder'; +import { softRemove } from '/imports/api/parenting/softRemove'; +import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema'; +import { storedIconsSchema } from '/imports/api/icons/Icons'; +import '/imports/api/library/methods/index'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import { restore } from '/imports/api/parenting/softRemove'; +import { getFilter, rebuildNestedSets, changeParent } from '/imports/api/parenting/parentingFunctions'; +import ChildSchema from '/imports/api/parenting/ChildSchema'; const Docs = new Mongo.Collection('docs'); -const RefSchema = new SimpleSchema({ - id: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1 - }, - collection: { - type: String, - max: STORAGE_LIMITS.collectionName, - }, - urlName: { - type: String, - regEx: /[a-z]+(?:[a-z]|-)+/, - min: 2, - max: STORAGE_LIMITS.variableName, - optional: true, - }, - name: { - type: String, - max: STORAGE_LIMITS.description, - optional: true, - }, -}); - -let ChildSchema = new SimpleSchema({ - order: { - type: Number, - }, - parent: { - type: RefSchema, - optional: true, - }, - ancestors: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.ancestorCount, - }, - 'ancestors.$': { - type: RefSchema, - }, -}); - let DocSchema = new SimpleSchema({ _id: { type: String, @@ -106,38 +63,14 @@ function assertDocsEditPermission(userId) { function getDocLink(doc, urlName) { if (!urlName) urlName = doc.urlName; const address = ['/docs']; - doc.ancestors?.forEach(a => { + const ancestorDocs = Docs.find(getFilter.ancestors(doc)); + ancestorDocs?.forEach(a => { address.push(a.urlName); }); address.push(urlName); return address.join('/'); } -function rebuildDocAncestors(docId) { - const newDoc = Docs.findOne(docId); - Docs.find({ 'ancestors.id': docId }).forEach(doc => { - doc.ancestors.forEach((a, i) => { - if (a.id === docId) { - Docs.update(doc._id, { - $set: { - [`ancestors.${i}`]: { - id: newDoc._id, - collection: 'docs', - urlName: newDoc.urlName, - name: newDoc.name, - } - } - }); - } - }); - doc = Docs.findOne(doc._id); - const newLink = getDocLink(doc); - if (doc.href !== newLink) { - Docs.update(doc._id, { $set: { href: newLink } }) - } - }); -} - // Add a means of seeding new servers with documentation if (Meteor.isClient) { Docs.getJsonDocs = function () { @@ -162,18 +95,11 @@ const insertDoc = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({ doc, parentRef }) { + run({ doc, parentId }) { delete doc._id; assertDocsEditPermission(this.userId); - // get the new ancestry for the properties - if (parentRef) { - var { ancestors } = getAncestry({ - parentRef, - inheritedFields: { name: 1, urlName: 1 }, - }); - } - doc.parent = parentRef; - doc.ancestors = ancestors; + + doc.parentId = parentId; const lastOrder = Docs.find({}, { sort: { order: -1 } }).fetch()[0]?.order || 0; doc.order = lastOrder + 1; @@ -185,7 +111,7 @@ const insertDoc = new ValidatedMethod({ } const docId = Docs.insert(doc); - reorderDocs(); + rebuildNestedSets(Docs); return docId; }, }); @@ -223,13 +149,9 @@ const updateDoc = new ValidatedMethod({ } modifier.$set = modifier.$set || {}; modifier.$set.href = newLink; - rebuildDocAncestors(_id); } const updates = Docs.update(_id, modifier); - if (pathString === 'name' || pathString === 'urlName') { - rebuildDocAncestors(_id); - } - reorderDocs(); + rebuildNestedSets(Docs); return updates; }, }); @@ -279,7 +201,7 @@ const softRemoveDoc = new ValidatedMethod({ run({ _id }) { assertDocsEditPermission(this.userId); softRemove({ _id, collection: Docs }); - reorderDocs(); + rebuildNestedSets(Docs); } }); @@ -295,8 +217,8 @@ const restoreDoc = new ValidatedMethod({ }, run({ _id }) { assertDocsEditPermission(this.userId); - restore({ _id, collection: Docs }); - reorderDocs(); + restore('docs', _id); + rebuildNestedSets(Docs); } }); @@ -316,17 +238,18 @@ const organizeDoc = new ValidatedMethod({ timeInterval: 5000, }, run({ docId, parentId, order }) { - let doc = Docs.findOne(docId); + const doc = Docs.findOne(docId); + const parent = Docs.findOne(parentId); // The user must be able to edit both the doc and its parent to move it // successfully assertDocsEditPermission(this.userId); // Change the doc's parent - updateParent({ docRef: { id: docId, collection: 'docs' }, parentRef: { id: parentId, collection: 'docs' } }); + changeParent(doc, parent, Docs); // Change the doc's order to be a half step ahead of its target location Docs.update(doc._id, { $set: { order } }); - reorderDocs(); + rebuildNestedSets(Docs); }, }); @@ -349,52 +272,10 @@ const reorderDoc = new ValidatedMethod({ Docs.update(docId, { $set: { order } }); - reorderDocs(); + rebuildNestedSets(Docs); }, }); -function reorderDocs() { - const docs = Docs.find({ removed: { $ne: true } }, { sort: { order: 1 } }).fetch(); - const forest = nodeArrayToTree(docs); - const orderedDocs = getDocsInDepthFirstOrder(forest); - const bulkWrite = []; - orderedDocs.forEach((doc, index) => { - if (doc.order !== index) { - bulkWrite.push({ - updateOne: { - filter: { _id: doc._id }, - update: { $set: { order: index } }, - }, - }); - } - }); - if (Meteor.isServer && bulkWrite.length) { - Docs.rawCollection().bulkWrite( - bulkWrite, - { ordered: false }, - function (e) { - if (e) { - console.error('Bulk write failed: '); - console.error(e); - } - // Rebuild the ancestors of all the docs - // This is a pretty slow way to do anything, but docs hardly ever get rearranged - docs.forEach(doc => { - rebuildDocAncestors(doc._id); - }); - } - ); - } else { - bulkWrite.forEach(op => { - Docs.update( - op.updateOne.filter, - op.updateOne.update, - { selector: { type: 'any' } } - ); - }); - } -} - export { DocSchema, insertDoc, diff --git a/app/imports/api/engine/actions/ActionContext.js b/app/imports/api/engine/actions/ActionContext.ts similarity index 93% rename from app/imports/api/engine/actions/ActionContext.js rename to app/imports/api/engine/actions/ActionContext.ts index 7de72dc7..969f9523 100644 --- a/app/imports/api/engine/actions/ActionContext.js +++ b/app/imports/api/engine/actions/ActionContext.ts @@ -1,7 +1,7 @@ -import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js'; +import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs'; import { getCreature, getVariables, getPropertiesOfType, replaceLinkedVariablesWithProps -} from '/imports/api/engine/loadCreatures.js'; +} from '/imports/api/engine/loadCreatures'; import { groupBy, remove } from 'lodash'; export default class ActionContext { @@ -56,10 +56,10 @@ export default class ActionContext { // Group the triggers into triggers.. or // triggers.doActionProperty.. this.triggers = groupBy(this.triggers, 'event'); - for (let event in this.triggers) { + for (const event in this.triggers) { if (event === 'doActionProperty') { this.triggers[event] = groupBy(this.triggers[event], 'actionPropertyType'); - for (let propertyType in this.triggers[event]) { + for (const propertyType in this.triggers[event]) { this.triggers[event][propertyType] = groupBy(this.triggers[event][propertyType], 'timing'); } } else { diff --git a/app/imports/api/engine/actions/applyProperty.js b/app/imports/api/engine/actions/applyProperty.js deleted file mode 100644 index 46b1775a..00000000 --- a/app/imports/api/engine/actions/applyProperty.js +++ /dev/null @@ -1,37 +0,0 @@ -import action from './applyPropertyByType/applyAction.js'; -import ammo from './applyPropertyByType/applyItemAsAmmo.js' -import adjustment from './applyPropertyByType/applyAdjustment.js'; -import branch from './applyPropertyByType/applyBranch.js'; -import buff from './applyPropertyByType/applyBuff.js'; -import buffRemover from './applyPropertyByType/applyBuffRemover.js'; -import damage from './applyPropertyByType/applyDamage.js'; -import folder from './applyPropertyByType/applyFolder.js'; -import note from './applyPropertyByType/applyNote.js'; -import roll from './applyPropertyByType/applyRoll.js'; -import savingThrow from './applyPropertyByType/applySavingThrow.js'; -import toggle from './applyPropertyByType/applyToggle.js'; - -const applyPropertyByType = { - action, - ammo, - adjustment, - branch, - buff, - buffRemover, - damage, - folder, - note, - propertySlot: folder, - roll, - savingThrow, - spell: action, - toggle, -}; - -export default async function applyProperty(node, actionContext, ...rest) { - if (node.node.deactivatedByToggle) return; - actionContext.scope[`#${node.node.type}`] = node.node; - console.log('start apply props by type', node.node.type) - await applyPropertyByType[node.node.type]?.(node, actionContext, ...rest); - console.log('end apply prop by type', node.node.type) -} diff --git a/app/imports/api/engine/actions/applyProperty.ts b/app/imports/api/engine/actions/applyProperty.ts new file mode 100644 index 00000000..eeb010e6 --- /dev/null +++ b/app/imports/api/engine/actions/applyProperty.ts @@ -0,0 +1,38 @@ +import action from './applyPropertyByType/applyAction'; +import ammo from './applyPropertyByType/applyItemAsAmmo' +import adjustment from './applyPropertyByType/applyAdjustment'; +import branch from './applyPropertyByType/applyBranch'; +import buff from './applyPropertyByType/applyBuff'; +import buffRemover from './applyPropertyByType/applyBuffRemover'; +import damage from './applyPropertyByType/applyDamage'; +import folder from './applyPropertyByType/applyFolder'; +import note from './applyPropertyByType/applyNote'; +import roll from './applyPropertyByType/applyRoll'; +import savingThrow from './applyPropertyByType/applySavingThrow'; +import toggle from './applyPropertyByType/applyToggle'; +import ActionContext from '/imports/api/engine/actions/ActionContext'; +import { TreeNode } from '/imports/api/parenting/parentingFunctions'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; + +const applyPropertyByType = { + action, + ammo, + adjustment, + branch, + buff, + buffRemover, + damage, + folder, + note, + propertySlot: folder, + roll, + savingThrow, + spell: action, + toggle, +}; + +export default function applyProperty(node: TreeNode, actionContext: ActionContext, ...rest) { + if (node.doc.deactivatedByToggle) return; + actionContext.scope[`#${node.doc.type}`] = node.doc; + applyPropertyByType[node.doc.type]?.(node, actionContext, ...rest); +} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.ts similarity index 79% rename from app/imports/api/engine/actions/applyPropertyByType/applyAction.js rename to app/imports/api/engine/actions/applyPropertyByType/applyAction.ts index 51577b44..c8b36fc9 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.ts @@ -1,22 +1,31 @@ -import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js'; -import recalculateCalculation from './shared/recalculateCalculation.js'; -import rollDice from '/imports/parser/rollDice.js'; -import applyProperty from '../applyProperty.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; -import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import numberToSignedString from '/imports/api/utility/numberToSignedString.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js'; +import recalculateInlineCalculations from './shared/recalculateInlineCalculations'; +import recalculateCalculation from './shared/recalculateCalculation'; +import rollDice from '/imports/parser/rollDice'; +import applyProperty from '../applyProperty'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; +import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty'; +import numberToSignedString from '/imports/api/utility/numberToSignedString'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; +import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature'; +import { TreeNode, hasAncestorRelationship } from '/imports/api/parenting/parentingFunctions'; +import { Action } from '/imports/api/properties/Actions'; +import { LogContent } from '/imports/api/creature/log/LogContentSchema'; +import { Item } from '/imports/api/properties/Items'; -export default async function applyAction(node, actionContext) { - await applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; +interface Ammo extends Item { + type: 'ammo' + adjustment: number +} + +export default function applyAction(node: TreeNode, actionContext) { + applyNodeTriggers(node, 'before', actionContext); + const prop = node.doc; if (prop.target === 'self') actionContext.targets = [actionContext.creature]; const targets = actionContext.targets; // Log the name and summary - let content = { name: prop.name }; + const content: LogContent = { name: prop.name, }; if (prop.summary?.text) { recalculateInlineCalculations(prop.summary, actionContext); content.value = prop.summary.value; @@ -27,7 +36,7 @@ export default async function applyAction(node, actionContext) { const failed = await spendResources(prop, actionContext); if (failed) return; - const attack = prop.attackRoll || prop.attackRollBonus; + const attack = prop.attackRoll; // Attack if there is an attack roll if (attack && attack.calculation) { @@ -59,7 +68,7 @@ function applyAttackWithoutTarget({ attack, actionContext }) { recalculateCalculation(attack, actionContext); const scope = actionContext.scope; - let { + const { resultPrefix, result, criticalHit, @@ -96,7 +105,7 @@ function applyAttackToTarget({ attack, target, actionContext }) { recalculateCalculation(attack, actionContext); - let { + const { resultPrefix, result, criticalHit, @@ -176,7 +185,7 @@ function applyCrits(value, scope) { scopeCrit = scopeCrit.value; } const criticalHitTarget = scopeCrit || 20; - let criticalHit = value >= criticalHitTarget; + const criticalHit = value >= criticalHitTarget; let criticalMiss; if (criticalHit) { scope['~criticalHit'] = { value: true }; @@ -189,9 +198,9 @@ function applyCrits(value, scope) { return { criticalHit, criticalMiss }; } -async function spendResources(prop, actionContext) { +function spendResources(prop: Action, actionContext) { // Check Uses - if (prop.usesLeft <= 0) { + if (!prop.usesLeft || prop.usesLeft <= 0) { if (!prop.silent) actionContext.addLog({ name: 'Error', value: `${prop.name || 'action'} does not have enough uses left`, @@ -207,42 +216,45 @@ async function spendResources(prop, actionContext) { return true; } // Items - let spendLog = []; - let gainLog = []; - const ammoToApply = []; + const spendLog: string[] = []; + const gainLog: string[] = []; + const ammoToApply: TreeNode[] = []; try { prop.resources.itemsConsumed.forEach(itemConsumed => { recalculateCalculation(itemConsumed.quantity, actionContext); if (!itemConsumed.itemId) { throw 'No ammo was selected for this prop'; } - let item = CreatureProperties.findOne(itemConsumed.itemId); - if (!item || item.ancestors[0].id !== prop.ancestors[0].id) { + const item = CreatureProperties.findOne(itemConsumed.itemId) as Item; + if (!item || item.root.id !== prop.root.id) { throw 'The prop\'s ammo was not found on the creature'; } + if ( !itemConsumed?.quantity?.value || - !isFinite(itemConsumed.quantity.value) + !isFinite(+itemConsumed.quantity.value) ) return; + const quantityConsumed = +itemConsumed.quantity.value; + let logName = item.name; - if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) { + if (quantityConsumed > 1 || quantityConsumed < -1) { logName = item.plural || logName; } - if (itemConsumed.quantity.value > 0) { - spendLog.push(logName + ': ' + itemConsumed.quantity.value); - } else if (itemConsumed.quantity.value < 0) { - gainLog.push(logName + ': ' + -itemConsumed.quantity.value); + if (quantityConsumed > 0) { + spendLog.push(logName + ': ' + quantityConsumed); + } else if (quantityConsumed < 0) { + gainLog.push(logName + ': ' + -quantityConsumed); } // So long as the item isn't an ancestor of the current prop apply it // If it was an ancestor this would be an infinite loop if (!hasAncestorRelationship(item, prop)) { ammoToApply.push({ - node: { + doc: { ...item, // Use ammo pseudo-type type: 'ammo', // Store the adjustment to be applied - adjustment: itemConsumed.quantity.value, + adjustment: quantityConsumed, }, children: [] }); @@ -263,6 +275,7 @@ async function spendResources(prop, actionContext) { CreatureProperties.update(prop._id, { $inc: { usesUsed: 1 } }, { + //@ts-expect-error no typings for collection 2 selector selector: prop }); if (!prop.silent) actionContext.addLog({ @@ -277,8 +290,9 @@ async function spendResources(prop, actionContext) { recalculateCalculation(attConsumed.quantity, actionContext); if (!attConsumed.quantity?.value) return; + const quantityConsumed = +attConsumed.quantity.value; if (!attConsumed.variableName) return; - let stat = actionContext.scope[attConsumed.variableName]; + const stat = actionContext.scope[attConsumed.variableName]; if (!stat) { spendLog.push(attConsumed.variableName + ': ' + ' not found'); return; @@ -289,10 +303,10 @@ async function spendResources(prop, actionContext) { value: attConsumed.quantity.value, actionContext, }); - if (attConsumed.quantity.value > 0) { - spendLog.push(stat.name + ': ' + attConsumed.quantity.value); - } else if (attConsumed.quantity.value < 0) { - gainLog.push(stat.name + ': ' + -attConsumed.quantity.value); + if (quantityConsumed > 0) { + spendLog.push(stat.name + ': ' + quantityConsumed); + } else if (quantityConsumed < 0) { + gainLog.push(stat.name + ': ' + -quantityConsumed); } }); @@ -313,20 +327,3 @@ async function spendResources(prop, actionContext) { inline: true, }); } - -function hasAncestorRelationship(a, b) { - let top, bottom; - if (a.ancestors.length === b.ancestors.length) { - // Can't be ancestors of one another if they have the same number of ancestors - return false; - } else if (a.ancestors.length > b.ancestors.length) { - // longer ancestor list goes on the bottom - top = b; - bottom = a; - } else { - top = a; - bottom = b; - } - const expectedAncestorPosition = top.ancestors.length; - return bottom.ancestors[expectedAncestorPosition]?.id === top._id; -} diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js b/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js index 5ca17972..e14265d5 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAdjustment.js @@ -1,11 +1,11 @@ -import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; -import recalculateCalculation from './shared/recalculateCalculation.js'; -import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; +import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; +import recalculateCalculation from './shared/recalculateCalculation'; +import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; export default function applyAdjustment(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; + const prop = node.doc const damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; if (!prop.amount) { diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js index 727097e1..bf415327 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js @@ -1,15 +1,14 @@ -import applyProperty from '../applyProperty.js'; -import recalculateCalculation from './shared/recalculateCalculation.js'; -import rollDice from '/imports/parser/rollDice.js'; -import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import getUserInput from '/imports/api/engine/actions/getUserInput'; +import applyProperty from '../applyProperty'; +import recalculateCalculation from './shared/recalculateCalculation'; +import rollDice from '/imports/parser/rollDice'; +import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; export default async function applyBranch(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); const scope = actionContext.scope; const targets = actionContext.targets; - const prop = node.node; + const prop = node.doc switch (prop.branchType) { case 'if': recalculateCalculation(prop.condition, actionContext); diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js index 57fd8d25..6455835a 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js @@ -1,24 +1,22 @@ import { - setLineageOfDocs, - renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { setDocToLastOrder } from '/imports/api/parenting/order.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; -import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; + renewDocIds, +} from '/imports/api/parenting/parentingFunctions'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex'; +import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey'; import { get } from 'lodash'; -import resolve, { map, toString } from '/imports/parser/resolve.js'; -import accessor from '/imports/parser/parseTree/accessor.js'; -import logErrors from './shared/logErrors.js'; -import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js'; -import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; -import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js'; +import resolve, { map, toString } from '/imports/parser/resolve'; +import accessor from '/imports/parser/parseTree/accessor'; +import logErrors from './shared/logErrors'; +import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs'; +import cyrb53 from '/imports/api/engine/computation/utility/cyrb53'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; +import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX'; +import recalculateInlineCalculations from './shared/recalculateInlineCalculations'; export default function applyBuff(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; + const prop = node.doc let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; // Mark the buff as dirty for recalculation @@ -26,7 +24,7 @@ export default function applyBuff(node, actionContext) { // Then copy the descendants of the buff to the targets let propList = [prop]; - function addChildrenToPropList(children, { skipCrystalize } = {}) { + function addChildrenToPropList(children, { skipCrystalize } = { skipCrystalize: false }) { children.forEach(child => { if (skipCrystalize) child.node._skipCrystalize = true; propList.push(child.node); @@ -41,13 +39,20 @@ export default function applyBuff(node, actionContext) { crystalizeVariables({ propList, actionContext }); } - let oldParent = { - id: prop.parent.id, - collection: prop.parent.collection, - }; buffTargets.forEach(target => { + const targetPropList = EJSON.clone(propList); + // Move the properties to the target by replacing the old subtree parent and root with the ' + // target id + renewDocIds({ + docArray: targetPropList, + idMap: { + [prop.parentId]: target._id, + [prop.root.id]: target._id, + }, + collectionMap: { [prop.root.collection]: 'creatures' } + }); // Apply the buff - copyNodeListToTarget(propList, target, oldParent); + CreatureProperties.batchInsert(targetPropList); //Log the buff let logValue = prop.description?.value @@ -82,23 +87,6 @@ export default function applyBuff(node, actionContext) { // Don't apply the children of the buff, they get copied to the target instead } -function copyNodeListToTarget(propList, target, oldParent) { - let ancestry = [{ collection: 'creatures', id: target._id }]; - setLineageOfDocs({ - docArray: propList, - newAncestry: ancestry, - oldParent, - }); - renewDocIds({ - docArray: propList, - }); - setDocToLastOrder({ - collection: CreatureProperties, - doc: propList[0], - }); - CreatureProperties.batchInsert(propList); -} - /** * Replaces all variables with their resolved values * except variables of the form `~target.thing.total` become `thing.total` diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js index a9915bdf..3b69c230 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBuffRemover.js @@ -1,17 +1,17 @@ import { findLast, difference, intersection, filter } from 'lodash'; -import applyProperty from '../applyProperty.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import { getProperyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { softRemove } from '/imports/api/parenting/softRemove.js'; -import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; -import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; +import applyProperty from '../applyProperty'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; +import { getPropertyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { softRemove } from '/imports/api/parenting/softRemove'; +import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags'; +import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; export default function applyBuffRemover(node, actionContext) { // Apply triggers applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; + const prop = node.doc // Log Name if (prop.name && !prop.silent) { @@ -21,7 +21,7 @@ export default function applyBuffRemover(node, actionContext) { // Remove buffs if (prop.targetParentBuff) { // Remove nearest ancestor buff - const ancestors = getProperyAncestors(actionContext.creature._id, prop._id); + const ancestors = getPropertyAncestors(actionContext.creature._id, prop._id); const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff'); if (!nearestBuff) { actionContext.addLog({ diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index 9bda5d10..0b6a022f 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js @@ -1,21 +1,21 @@ import { some, intersection, difference, remove, includes } from 'lodash'; -import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; -import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js'; -import resolve, { Context, toString } from '/imports/parser/resolve.js'; -import logErrors from './shared/logErrors.js'; -import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js' -import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; +import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; +import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs'; +import resolve, { Context, toString } from '/imports/parser/resolve'; +import logErrors from './shared/logErrors'; +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation' +import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty'; import { getPropertiesOfType -} from '/imports/api/engine/loadCreatures.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; -import applySavingThrow from '/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js'; +} from '/imports/api/engine/loadCreatures'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; +import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags'; +import applySavingThrow from '/imports/api/engine/actions/applyPropertyByType/applySavingThrow'; export default function applyDamage(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; + const prop = node.doc const scope = actionContext.scope; // Skip if there is no parse node to work with @@ -167,7 +167,7 @@ export default function applyDamage(node, actionContext) { creatureId: target._id, content: [{ name, - value: `Recieved **${damageDealt}** ${suffix}`, + value: `Received **${damageDealt}** ${suffix}`, }], } }); diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyFolder.js b/app/imports/api/engine/actions/applyPropertyByType/applyFolder.js index ea216685..19f2c5f2 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyFolder.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyFolder.js @@ -1,5 +1,5 @@ -import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; +import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; export default function applyFolder(node, actionContext) { // Apply triggers diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js b/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js index 3bc3b24e..9f25fd7c 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyItemAsAmmo.js @@ -1,14 +1,14 @@ -import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js'; -import applyProperty from '../applyProperty.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; -import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; +import { getPropertyDescendants } from '/imports/api/engine/loadCreatures'; +import applyProperty from '../applyProperty'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; +import { docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions'; +import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity'; export default function applyItemAsAmmo(node, actionContext) { // The item node should come without children, since it is not part of the original action tree - const prop = node.node; + const prop = node.doc // Get all the item's descendant properties - const properties = getPropertyDecendants(actionContext.creature._id, prop._id); + const properties = getPropertyDescendants(actionContext.creature._id, prop._id); properties.sort((a, b) => a.order - b.order); const propertyForest = nodeArrayToTree(properties); diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyNote.js b/app/imports/api/engine/actions/applyPropertyByType/applyNote.js index b7cb6c13..6c29ffaa 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyNote.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyNote.js @@ -1,10 +1,10 @@ -import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js'; -import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; +import recalculateInlineCalculations from './shared/recalculateInlineCalculations'; +import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; -export default async function applyNote(node, actionContext) { - await applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; +export default function applyNote(node, actionContext) { + applyNodeTriggers(node, 'before', actionContext); + const prop = node.doc // Log Name, summary let content = { name: prop.name }; diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js index 5edf2613..63185db0 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js @@ -1,12 +1,12 @@ -import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; -import logErrors from './shared/logErrors.js'; -import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js'; -import resolve, { toString } from '/imports/parser/resolve.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; +import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; +import logErrors from './shared/logErrors'; +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation'; +import resolve, { toString } from '/imports/parser/resolve'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; export default function applyRoll(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; + const prop = node.doc if (prop.roll?.calculation) { const logValue = []; diff --git a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js index ae2f878e..17269482 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js @@ -1,14 +1,14 @@ -import rollDice from '/imports/parser/rollDice.js'; -import recalculateCalculation from './shared/recalculateCalculation.js'; -import applyProperty from '../applyProperty.js'; -import numberToSignedString from '/imports/api/utility/numberToSignedString.js'; -import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js'; +import rollDice from '/imports/parser/rollDice'; +import recalculateCalculation from './shared/recalculateCalculation'; +import applyProperty from '../applyProperty'; +import numberToSignedString from '/imports/api/utility/numberToSignedString'; +import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; +import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck'; export default function applySavingThrow(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; + const prop = node.doc const originalTargets = actionContext.targets; let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js b/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js index 47ec3e08..41222e0f 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyToggle.js @@ -1,10 +1,10 @@ -import recalculateCalculation from './shared/recalculateCalculation.js'; -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js'; +import recalculateCalculation from './shared/recalculateCalculation'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; +import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren'; export default function applyToggle(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); - const prop = node.node; + const prop = node.doc recalculateCalculation(prop.condition, actionContext); if (prop.condition?.value) { return applyChildren(node, actionContext); diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js b/app/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js index 765409be..4d0aae6b 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js @@ -1,5 +1,5 @@ -import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import applyProperty from '/imports/api/engine/actions/applyProperty.js'; +import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers'; +import applyProperty from '/imports/api/engine/actions/applyProperty'; export default async function applyChildren(node, actionContext) { applyNodeTriggers(node, 'after', actionContext); diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js index ba9f330f..4220cd92 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js @@ -1,13 +1,12 @@ -import logErrors from './logErrors.js'; -import { Context, toPrimitiveOrString } from '/imports/parser/resolve.js'; +import logErrors from './logErrors'; +import { toPrimitiveOrString } from '/imports/parser/resolve'; import { aggregateCalculationEffects, aggregateCalculationProficiencies, resolveCalculationNode, -} from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js'; +} from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation'; import { getSingleProperty } from '/imports/api/engine/loadCreatures'; -import resolve from '/imports/parser/resolve.js'; -import { getEffectiveActionScope } from '/imports/api/engine/actions/Actions'; +import resolve from '/imports/parser/resolve'; // TODO move this whole file to Actions.ts // Redo the work of imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js index 07e3afd3..a7d12852 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js @@ -1,7 +1,7 @@ -import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.js'; -import recalculateCalculation from './recalculateCalculation.js' +import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations'; +import recalculateCalculation from './recalculateCalculation' -export default function recalculateInlineCalculations(inlineCalcObj, action) { +export default function recalculateInlineCalculations(inlineCalcObj, actionContext) { // Skip if there are no calculations if (!inlineCalcObj?.inlineCalculations?.length) return; // Recalculate each calculation with the current scope diff --git a/app/imports/api/engine/actions/applyTriggers.testFn.js b/app/imports/api/engine/actions/applyTriggers.testFn.js index f2955afb..ad484232 100644 --- a/app/imports/api/engine/actions/applyTriggers.testFn.js +++ b/app/imports/api/engine/actions/applyTriggers.testFn.js @@ -1,5 +1,5 @@ -import { triggerMatchTags } from '/imports/api/engine/actions/applyTriggers.js'; -import clean from '/imports/api/engine/computation/utility/cleanProp.testFn.js'; +import { triggerMatchTags } from '/imports/api/engine/actions/applyTriggers'; +import clean from '/imports/api/engine/computation/utility/cleanProp.testFn'; import { assert } from 'chai'; export default function () { diff --git a/app/imports/api/engine/actions/applyTriggers.js b/app/imports/api/engine/actions/applyTriggers.ts similarity index 85% rename from app/imports/api/engine/actions/applyTriggers.js rename to app/imports/api/engine/actions/applyTriggers.ts index e7d842a9..b4fac8da 100644 --- a/app/imports/api/engine/actions/applyTriggers.js +++ b/app/imports/api/engine/actions/applyTriggers.ts @@ -1,13 +1,13 @@ -import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js'; -import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js'; -import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js'; -import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; -import applyProperty from '/imports/api/engine/actions/applyProperty.js'; +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation'; +import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations'; +import { getPropertyDescendants } from '/imports/api/engine/loadCreatures'; +import { TreeNode, docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions'; +import applyProperty from '/imports/api/engine/actions/applyProperty'; import { difference, intersection } from 'lodash'; -import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; +import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags'; -export async function applyNodeTriggers(node, timing, actionContext) { - const prop = node.node; +export function applyNodeTriggers(node: TreeNode, timing, actionContext) { + const prop = node.doc; const type = prop.type; const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing]; if (triggers) { @@ -68,7 +68,7 @@ export async function applyTrigger(trigger, prop, actionContext) { if (!trigger.silent) actionContext.addLog(content); // Get all the trigger's properties and apply them - const properties = getPropertyDecendants(actionContext.creature._id, trigger._id); + const properties = getPropertyDescendants(actionContext.creature._id, trigger._id); properties.sort((a, b) => a.order - b.order); const propertyForest = nodeArrayToTree(properties); for (const node of propertyForest) { diff --git a/app/imports/api/engine/actions/doAction.js b/app/imports/api/engine/actions/doAction.js index 74db6ef4..ea4720c0 100644 --- a/app/imports/api/engine/actions/doAction.js +++ b/app/imports/api/engine/actions/doAction.js @@ -1,15 +1,15 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; +import { docsToForest } from '/imports/api/parenting/parentingFunctions'; import { - getProperyAncestors, getPropertyDecendants -} from '/imports/api/engine/loadCreatures.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import applyProperty from './applyProperty.js'; -import ActionContext from '/imports/api/engine/actions/ActionContext.js'; + getPropertyAncestors, getPropertyDescendants +} from '/imports/api/engine/loadCreatures'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import applyProperty from './applyProperty'; +import ActionContext from '/imports/api/engine/actions/ActionContext'; const doAction = new ValidatedMethod({ name: 'creatureProperties.doAction', @@ -47,10 +47,10 @@ const doAction = new ValidatedMethod({ async run({ actionId, targetIds = [], scope, invocationId }) { console.log('do Action running'); // Get action context - let action = CreatureProperties.findOne(actionId); - const creatureId = action.ancestors[0].id; - - const actionContext = new ActionContext(creatureId, targetIds, this, invocationId); + const action = CreatureProperties.findOne(actionId); + if (!action) throw new Meteor.Error('not-found', 'The action was not found'); + const creatureId = action.root.id; + const actionContext = new ActionContext(creatureId, targetIds, this); // Check permissions assertEditPermission(actionContext.creature, this.userId); @@ -58,10 +58,10 @@ const doAction = new ValidatedMethod({ assertEditPermission(target, this.userId); }); - const ancestors = getProperyAncestors(creatureId, action._id); + const ancestors = getPropertyAncestors(creatureId, action._id); ancestors.sort((a, b) => a.order - b.order); - const properties = getPropertyDecendants(creatureId, action._id); + const properties = getPropertyDescendants(creatureId, action._id); properties.push(action); properties.sort((a, b) => a.order - b.order); @@ -86,7 +86,7 @@ export async function doActionWork({ }) { // get the docs const ancestorScope = getAncestorScope(ancestors); - const propertyForest = nodeArrayToTree(properties); + const propertyForest = docsToForest(properties); if (propertyForest.length !== 1) { throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`); } @@ -106,7 +106,7 @@ export async function doActionWork({ // Assumes ancestors are in tree order already function getAncestorScope(ancestors) { - let scope = {}; + const scope = {}; ancestors.forEach(prop => { scope[`#${prop.type}`] = prop; }); diff --git a/app/imports/api/engine/actions/doAction.test.js b/app/imports/api/engine/actions/doAction.test.js index b9fdfa23..8b28545c 100644 --- a/app/imports/api/engine/actions/doAction.test.js +++ b/app/imports/api/engine/actions/doAction.test.js @@ -1,10 +1,10 @@ -import '/imports/api/simpleSchemaConfig.js'; -//import testTypes from './testTypes/index.js'; -import applyTriggers from '/imports/api/engine/actions/applyTriggers.testFn.js'; -import { doActionWork } from './doAction.js'; -import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import '/imports/api/simpleSchemaConfig'; +//import testTypes from './testTypes/index'; +import applyTriggers from '/imports/api/engine/actions/applyTriggers.testFn'; +import { doActionWork } from './doAction'; +import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import Creatures from '/imports/api/creature/creatures/Creatures'; function cleanProp(prop) { let schema = CreatureProperties.simpleSchema(prop); diff --git a/app/imports/api/engine/actions/doCastSpell.js b/app/imports/api/engine/actions/doCastSpell.js index 4ea03630..cdcdb53c 100644 --- a/app/imports/api/engine/actions/doCastSpell.js +++ b/app/imports/api/engine/actions/doCastSpell.js @@ -1,15 +1,15 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; import { - getProperyAncestors, getPropertyDecendants -} from '/imports/api/engine/loadCreatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import { doActionWork } from '/imports/api/engine/actions/doAction.js'; -import ActionContext from '/imports/api/engine/actions/ActionContext.js'; + getPropertyAncestors, getPropertyDescendants +} from '/imports/api/engine/loadCreatures'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; +import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty'; +import { doActionWork } from '/imports/api/engine/actions/doAction'; +import ActionContext from '/imports/api/engine/actions/ActionContext'; const doAction = new ValidatedMethod({ name: 'creatureProperties.doCastSpell', @@ -48,7 +48,7 @@ const doAction = new ValidatedMethod({ run({ spellId, slotId, ritual, targetIds = [], scope = {} }) { // Get action context let spell = CreatureProperties.findOne(spellId); - const creatureId = spell.ancestors[0].id; + const creatureId = spell.root.id; const actionContext = new ActionContext(creatureId, targetIds, this); // Check permissions @@ -57,10 +57,10 @@ const doAction = new ValidatedMethod({ assertEditPermission(target, this.userId); }); - const ancestors = getProperyAncestors(creatureId, spell._id); + const ancestors = getPropertyAncestors(creatureId, spell._id); ancestors.sort((a, b) => a.order - b.order); - const properties = getPropertyDecendants(creatureId, spell._id); + const properties = getPropertyDescendants(creatureId, spell._id); properties.push(spell); properties.sort((a, b) => a.order - b.order); diff --git a/app/imports/api/engine/actions/doCheck.js b/app/imports/api/engine/actions/doCheck.js index 69343c78..d95a5c93 100644 --- a/app/imports/api/engine/actions/doCheck.js +++ b/app/imports/api/engine/actions/doCheck.js @@ -1,12 +1,12 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import rollDice from '/imports/parser/rollDice.js'; -import numberToSignedString from '/imports/api/utility/numberToSignedString.js'; -import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -import ActionContext from '/imports/api/engine/actions/ActionContext.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; +import rollDice from '/imports/parser/rollDice'; +import numberToSignedString from '/imports/api/utility/numberToSignedString'; +import { applyTriggers } from '/imports/api/engine/actions/applyTriggers'; +import ActionContext from '/imports/api/engine/actions/ActionContext'; import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation'; import { getSingleProperty } from '/imports/api/engine/loadCreatures'; @@ -26,7 +26,8 @@ const doCheck = new ValidatedMethod({ }, run({ propId, scope }) { const prop = CreatureProperties.findOne(propId); - const creatureId = prop.ancestors[0].id; + if (!prop) throw new Meteor.Error('not-found', 'The property was not found'); + const creatureId = prop.root.id; const actionContext = new ActionContext(creatureId, [creatureId], this); Object.assign(actionContext.scope, scope); actionContext.scope[`#${prop.type}`] = prop; diff --git a/app/imports/api/engine/actions/index.js b/app/imports/api/engine/actions/index.js index 5cd7a8b4..91e94fcf 100644 --- a/app/imports/api/engine/actions/index.js +++ b/app/imports/api/engine/actions/index.js @@ -1,2 +1,2 @@ -import './doCastSpell.js'; -import './doCheck.js'; +import './doCastSpell'; +import './doCheck'; diff --git a/app/imports/api/engine/computation/CreatureComputation.ts b/app/imports/api/engine/computation/CreatureComputation.ts index 75ec51b3..2f252927 100644 --- a/app/imports/api/engine/computation/CreatureComputation.ts +++ b/app/imports/api/engine/computation/CreatureComputation.ts @@ -1,6 +1,6 @@ import { EJSON } from 'meteor/ejson'; import createGraph, { Graph } from 'ngraph.graph'; -import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; +import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags'; interface CreatureProperty { _id: string; diff --git a/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js b/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.ts similarity index 58% rename from app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js rename to app/imports/api/engine/computation/buildComputation/computeInactiveStatus.ts index 982f1c60..f698d6f0 100644 --- a/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js +++ b/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.ts @@ -1,7 +1,10 @@ -import walkDown from '/imports/api/engine/computation/utility/walkdown.js'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import walkDown from '/imports/api/engine/computation/utility/walkdown'; +import { TreeNode } from '/imports/api/parenting/parentingFunctions'; +import { isSpell } from '/imports/api/properties/Spells'; -export default function computeInactiveStatus(node) { - const prop = node.node; +export default function computeInactiveStatus(node: TreeNode): void { + const prop = node.doc; if (!isActive(prop)) { // Mark prop inactive due to self prop.inactive = true; @@ -10,22 +13,21 @@ export default function computeInactiveStatus(node) { if (!childrenActive(prop)) { // Mark children as inactive due to ancestor walkDown(node.children, child => { - child.node.inactive = true; - child.node.deactivatedByAncestor = true; + child.doc.inactive = true; + child.doc.deactivatedByAncestor = true; }); } } -function isActive(prop) { +function isActive(prop: CreatureProperty): boolean { if (prop.disabled) return false; - switch (prop.type) { - // Unprepared spells are inactive - case 'spell': return !!prop.prepared || !!prop.alwaysPrepared; - default: return true; + if (isSpell(prop)) { + return !!prop.prepared || !!prop.alwaysPrepared; } + return true; } -function childrenActive(prop) { +function childrenActive(prop): boolean { // Children of disabled properties are always inactive if (prop.disabled) return false; switch (prop.type) { diff --git a/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js b/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js index e9e4a287..e7485c11 100644 --- a/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js +++ b/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js @@ -3,11 +3,11 @@ * before `spacesLeft` can be computed */ export default function computeSlotQuantityFilled(node, dependencyGraph) { - let slot = node.node; + let slot = node.doc; if (slot.type !== 'propertySlot') return; slot.totalFilled = 0; node.children.forEach(child => { - let childProp = child.node; + let childProp = child.doc; dependencyGraph.addLink(slot._id, childProp._id, 'slotFill'); if ( Number.isFinite(childProp.slotQuantityFilled) diff --git a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js index 3d9bf564..aca928f7 100644 --- a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js @@ -1,8 +1,8 @@ -import walkDown from '/imports/api/engine/computation/utility/walkdown.js'; -import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies.js'; +import walkDown from '/imports/api/engine/computation/utility/walkdown'; +import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies'; export default function computeToggleDependencies(node, dependencyGraph, computation, forest) { - const prop = node.node; + const prop = node.doc // Only for toggles if (prop.type !== 'toggle') return; @@ -11,12 +11,12 @@ export default function computeToggleDependencies(node, dependencyGraph, computa getEffectTagTargets(prop, computation).forEach(targetId => { const target = forest.nodeIndex[targetId]; if (!target) return; - target.node._computationDetails.toggleAncestors.push(prop); - dependencyGraph.addLink(target.node._id, prop._id, 'toggle'); + target.doc._computationDetails.toggleAncestors.push(prop); + dependencyGraph.addLink(target.doc._id, prop._id, 'toggle'); walkDown(target.children, child => { // The child nodes depend on the toggle - child.node._computationDetails.toggleAncestors.push(prop); - dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); + child.doc._computationDetails.toggleAncestors.push(prop); + dependencyGraph.addLink(child.doc._id, prop._id, 'toggle'); }); }); } @@ -26,7 +26,7 @@ export default function computeToggleDependencies(node, dependencyGraph, computa walkDown(node.children, child => { // The child nodes depend on the toggle - child.node._computationDetails.toggleAncestors.push(prop); - dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); + child.doc._computationDetails.toggleAncestors.push(prop); + dependencyGraph.addLink(child.doc._id, prop._id, 'toggle'); }); } diff --git a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js index de4a931a..5e3601e7 100644 --- a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js @@ -1,5 +1,4 @@ -import findAncestorByType from '/imports/api/engine/computation/utility/findAncestorByType.js'; -import { traverse } from '/imports/parser/resolve.js'; +import { traverse } from '/imports/parser/resolve'; export default function linkCalculationDependencies(dependencyGraph, prop, { propsById }) { prop._computationDetails.calculations.forEach(calcObj => { @@ -57,3 +56,14 @@ function getAncestorProp(type, memo, prop, propsById) { return ancestorProp; } } + +function findAncestorByType(prop, type, propsById) { + if (!prop || !prop.parentId) return; + let parentProp = prop; + while (parentProp) { + parentProp = propsById[parentProp.parentId]; + if (parentProp?.type === type) { + return parentProp; + } + } +} diff --git a/app/imports/api/engine/computation/buildComputation/linkInventory.js b/app/imports/api/engine/computation/buildComputation/linkInventory.js index 7961e4df..faa2674c 100644 --- a/app/imports/api/engine/computation/buildComputation/linkInventory.js +++ b/app/imports/api/engine/computation/buildComputation/linkInventory.js @@ -10,7 +10,7 @@ export default function linkInventory(forest, dependencyGraph) { while (stack.length) { const top = stack[stack.length - 1]; - const prop = top.node; + const prop = top.doc; if (prop._computationDetails.inventoryChildrenVisited) { if (prop.type === 'container') containerStack.pop(); stack.pop(); @@ -18,7 +18,7 @@ export default function linkInventory(forest, dependencyGraph) { } else { // Add all containers to the stack when we first visit them if (prop.type === 'container') { - containerStack.push(top.node); + containerStack.push(top.doc); } // Push children onto the stack and mark this as children are visited stack.push(...top.children); diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index 1c8cb208..5558f062 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -1,9 +1,9 @@ -import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; -import { prettifyParseError, parse } from '/imports/parser/parser.js'; -import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; +import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX'; +import { prettifyParseError, parse } from '/imports/parser/parser'; +import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey'; import { get, set, unset } from 'lodash'; -import errorNode from '/imports/parser/parseTree/error.js'; -import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; +import errorNode from '/imports/parser/parseTree/error'; +import cyrb53 from '/imports/api/engine/computation/utility/cyrb53'; export default function parseCalculationFields(prop, schemas) { discoverInlineCalculationFields(prop, schemas); diff --git a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js index d87fe3b3..f0813250 100644 --- a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js +++ b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js @@ -1,4 +1,4 @@ -import applyFnToKey from '../utility/applyFnToKey.js'; +import applyFnToKey from '../utility/applyFnToKey'; import { unset } from 'lodash'; export default function removeSchemaFields(schemas, prop) { diff --git a/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js index 248d34a9..c4119e2d 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js @@ -1,9 +1,11 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; export default function () { let computation = buildComputationFromProps(testProperties); + const bySelf = (propId, note) => assertDeactivatedBySelf(computation, propId, note); const byAncestor = (propId, note) => assertDeactivatedByAncestor(computation, propId, note); const active = (propId, note) => assertActive(computation, propId, note); @@ -51,66 +53,68 @@ var testProperties = [ clean({ _id: 'itemUnequippedId', type: 'item', - ancestors: [{ id: 'charId' }], + parentId: 'charId', }), clean({ _id: 'itemUnequippedChildId', type: 'folder', - ancestors: [{ id: 'charId' }, { id: 'itemUnequippedId' }], + parentId: 'itemUnequippedId', }), clean({ _id: 'itemEquippedId', type: 'item', equipped: true, - ancestors: [{ id: 'charId' }], + parentId: 'charId', }), clean({ _id: 'itemEquippedChildId', type: 'folder', - ancestors: [{ id: 'charId' }, { id: 'itemEquippedId' }], + parentId: 'itemEquippedId', }), // Spells clean({ _id: 'spellPreparedId', type: 'spell', - ancestors: [{ id: 'charId' }], + parentId: 'charId', prepared: true, }), clean({ _id: 'spellPreparedChildId', type: 'folder', - ancestors: [{ id: 'charId' }, { id: 'spellPreparedId' }], + parentId: 'spellPreparedId', }), clean({ _id: 'spellAlwaysPreparedId', type: 'spell', - ancestors: [{ id: 'charId' }], + parentId: 'charId', alwaysPrepared: true, }), clean({ _id: 'spellAlwaysPreparedChildId', type: 'folder', - ancestors: [{ id: 'charId' }, { id: 'spellAlwaysPreparedId' }], + parentId: 'spellAlwaysPreparedId', }), clean({ _id: 'spellUnpreparedId', type: 'spell', - ancestors: [{ id: 'charId' }], + parentId: 'charId', }), clean({ _id: 'spellUnpreparedChildId', type: 'folder', - ancestors: [{ id: 'charId' }, { id: 'spellUnpreparedId' }], + parentId: 'spellUnpreparedId', }), // Notes clean({ _id: 'NoteId', type: 'note', - ancestors: [{ id: 'charId' }], + parentId: 'charId', }), clean({ _id: 'NoteChildId', type: 'folder', - ancestors: [{ id: 'charId' }, { id: 'NoteId' }], + parentId: 'NoteId', }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js index b2c80f9a..5ee0ee23 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js @@ -1,6 +1,7 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; export default function () { const computation = buildComputationFromProps(testProperties); @@ -13,7 +14,6 @@ var testProperties = [ clean({ _id: 'slotId', type: 'propertySlot', - ancestors: [{ id: 'charId' }], }), // Children clean({ @@ -21,16 +21,18 @@ var testProperties = [ type: 'folder', slotQuantityFilled: 3, slotFillerType: 'item', - ancestors: [{ id: 'charId' }, { id: 'slotId' }], + parentId: 'slotId', }), clean({ _id: 'slotChildId', type: 'item', - ancestors: [{ id: 'charId' }, { id: 'slotId' }], + parentId: 'slotId', }), clean({ _id: 'slotGrandchildId', type: 'effect', - ancestors: [{ id: 'charId' }, { id: 'slotId' }, { id: 'slotChildId' }], + parentId: 'slotChildId', }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js index 268ea2ef..5ffe3fb3 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js @@ -1,8 +1,9 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); const hasLink = computation.dependencyGraph.hasLink; assert.include( @@ -37,38 +38,37 @@ var testProperties = [ _id: 'enabledToggleId', type: 'toggle', enabled: true, - ancestors: [{id: 'charId'}], }), clean({ _id: 'disabledToggleId', type: 'toggle', disabled: true, - ancestors: [{id: 'charId'}], }), clean({ _id: 'conditionToggleId', type: 'toggle', - ancestors: [{id: 'charId'}], }), // Children clean({ _id: 'enabledToggleChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'enabledToggleId'}], + parentId: 'enabledToggleId', }), clean({ _id: 'disabledToggleChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'disabledToggleId'}], + parentId: 'disabledToggleId', }), clean({ _id: 'conditionToggleChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'conditionToggleId'}], + parentId: 'conditionToggleId', }), clean({ _id: 'conditionToggleGrandChildId', type: 'folder', - ancestors: [{id: 'charId'}, {id: 'conditionToggleId'}, {id: 'conditionToggleChildId'}], + parentId: 'conditionToggleChildId', }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js index 29fd101a..60c5eb22 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js @@ -1,8 +1,9 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); const hasLink = computation.dependencyGraph.hasLink; const prop = (id) => computation.propsById[id]; @@ -32,7 +33,6 @@ var testProperties = [ clean({ _id: 'spellListId', type: 'spellList', - ancestors: [{id: 'charId'}], }), clean({ _id: 'childId', @@ -40,7 +40,7 @@ var testProperties = [ description: { text: 'DC {#spellList.dc} save or suck' }, - ancestors: [{id: 'charId'}, {id: 'spellListId'}], + parentId: 'spellListId', }), clean({ _id: 'grandchildId', @@ -48,7 +48,7 @@ var testProperties = [ dc: { calculation: '#spellList.dc + strength + wisdom.modifier' }, - ancestors: [{id: 'charId'}, {id: 'spellListId'}, {id: 'childId'}], + parentId: 'childId', }), clean({ _id: 'strengthId', @@ -57,6 +57,7 @@ var testProperties = [ baseValue: { calculation: '15 + ', }, - ancestors: [{id: 'charId'}], }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js index cb22544a..e412c8fa 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js @@ -1,8 +1,9 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); const hasLink = computation.dependencyGraph.hasLink; @@ -62,28 +63,28 @@ var testProperties = [ type: 'item', equipped: true, attuned: true, - ancestors: [{id: 'charId'}], }), clean({ _id: 'containerId', type: 'container', carried: true, - ancestors: [{id: 'charId'}], }), clean({ _id: 'childContainerId', type: 'container', carried: true, - ancestors: [{id: 'charId'}, {id: 'containerId'}], - }), - clean({ - _id: 'childItemId', - type: 'item', - ancestors: [{id: 'charId'}, {id: 'containerId'}], + parentId: 'containerId', }), clean({ _id: 'grandchildItemId', type: 'item', - ancestors: [{id: 'charId'}, {id: 'containerId'}, {id: 'childContainerId'}], + parentId: 'childContainerId', + }), + clean({ + _id: 'childItemId', + type: 'item', + parentId: 'containerId', }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js b/app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js index b8f9d4f1..8b1d5f1d 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js @@ -1,8 +1,8 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import clean from '../../utility/cleanProp.testFn.js'; +import clean from '../../utility/cleanProp.testFn'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); const getLink = computation.dependencyGraph.hasLink; const getNode = computation.dependencyGraph.getNode; @@ -22,6 +22,5 @@ var testProperties = [ _id: 'strengthId', type: 'attribute', variableName: 'strength', - ancestors: [{id: 'charId'}], }), ]; diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index e9dbb625..2437d24e 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -1,19 +1,19 @@ -import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; +import { docsToForest } from '/imports/api/parenting/parentingFunctions'; import { DenormalisedOnlyCreaturePropertySchema as denormSchema } - from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { getProperties, getCreature, getVariables } from '/imports/api/engine/loadCreatures.js'; -import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; -import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; -import linkInventory from './buildComputation/linkInventory.js'; -import walkDown from './utility/walkdown.js'; -import parseCalculationFields from './buildComputation/parseCalculationFields.js'; -import computeInactiveStatus from './buildComputation/computeInactiveStatus.js'; -import computeToggleDependencies from './buildComputation/computeToggleDependencies.js'; -import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js'; -import linkTypeDependencies from './buildComputation/linkTypeDependencies.js'; -import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js'; -import CreatureComputation from './CreatureComputation.ts'; -import removeSchemaFields from './buildComputation/removeSchemaFields.js'; + from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { getProperties, getCreature, getVariables } from '/imports/api/engine/loadCreatures'; +import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex'; +import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex'; +import linkInventory from './buildComputation/linkInventory'; +import walkDown from './utility/walkdown'; +import parseCalculationFields from './buildComputation/parseCalculationFields'; +import computeInactiveStatus from './buildComputation/computeInactiveStatus'; +import computeToggleDependencies from './buildComputation/computeToggleDependencies'; +import linkCalculationDependencies from './buildComputation/linkCalculationDependencies'; +import linkTypeDependencies from './buildComputation/linkTypeDependencies'; +import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled'; +import CreatureComputation from './CreatureComputation'; +import removeSchemaFields from './buildComputation/removeSchemaFields'; /** * Store index of properties @@ -86,7 +86,7 @@ export function buildComputationFromProps(properties, creature, variables) { }); // Get all the properties as trees based on their ancestors - let forest = nodeArrayToTree(properties); + let forest = docsToForest(properties); // Walk the property trees computing things that need to be inherited walkDown(forest, node => { computeInactiveStatus(node); diff --git a/app/imports/api/engine/computation/buildCreatureComputation.test.js b/app/imports/api/engine/computation/buildCreatureComputation.test.js index 33b3830b..2a1485ef 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.test.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.test.js @@ -1,16 +1,16 @@ -import '/imports/api/simpleSchemaConfig.js'; -import { buildComputationFromProps } from './buildCreatureComputation.js'; +import '/imports/api/simpleSchemaConfig'; +import { buildComputationFromProps } from './buildCreatureComputation'; import { assert } from 'chai'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import computeInactiveStatus from './buildComputation/tests/computeInactiveStatus.testFn.js'; -import computeSlotQuantityFilled from './buildComputation/tests/computeSlotQuantityFilled.testFn.js'; -import computeToggleDependencies from './buildComputation/tests/computeToggleDependencies.testFn.js'; -import linkCalculationDependencies from './buildComputation/tests/linkCalculationDependencies.testFn.js'; -import linkInventory from './buildComputation/tests/linkInventory.testFn.js'; -import linkTypeDependencies from './buildComputation/tests/linkTypeDependencies.testFn.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import computeInactiveStatus from './buildComputation/tests/computeInactiveStatus.testFn'; +import computeSlotQuantityFilled from './buildComputation/tests/computeSlotQuantityFilled.testFn'; +import computeToggleDependencies from './buildComputation/tests/computeToggleDependencies.testFn'; +import linkCalculationDependencies from './buildComputation/tests/linkCalculationDependencies.testFn'; +import linkInventory from './buildComputation/tests/linkInventory.testFn'; +import linkTypeDependencies from './buildComputation/tests/linkTypeDependencies.testFn'; -describe('buildComputation', function(){ - it('Builds something at all', function(){ +describe('buildComputation', function () { + it('Builds something at all', function () { let computation = buildComputationFromProps(testProperties); assert.exists(computation); }); @@ -37,7 +37,7 @@ var testProperties = [ }), ]; -function clean(prop){ +function clean(prop) { let schema = CreatureProperties.simpleSchema(prop); return schema.clean(prop); } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType.js b/app/imports/api/engine/computation/computeComputation/computeByType.js index 6d6e8c53..a7adfc3e 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType.js @@ -1,14 +1,13 @@ -import _variable from './computeByType/computeVariable.js'; -import action from './computeByType/computeAction.js'; -import attribute from './computeByType/computeAttribute.js'; -import skill from './computeByType/computeSkill.js'; -import pointBuy from './computeByType/computePointBuy.js'; -import propertySlot from './computeByType/computeSlot.js'; -import container from './computeByType/computeContainer.js'; -import spellList from './computeByType/computeSpellList.js'; -import toggle from './computeByType/computeToggle.js'; -import trigger from './computeByType/computeTrigger.js' -import _calculation from './computeByType/computeCalculation.js'; +import _variable from './computeByType/computeVariable'; +import action from './computeByType/computeAction'; +import attribute from './computeByType/computeAttribute'; +import skill from './computeByType/computeSkill'; +import pointBuy from './computeByType/computePointBuy'; +import propertySlot from './computeByType/computeSlot'; +import container from './computeByType/computeContainer'; +import spellList from './computeByType/computeSpellList'; +import toggle from './computeByType/computeToggle'; +import _calculation from './computeByType/computeCalculation'; export default Object.freeze({ _variable, diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js index 9e067c0d..d49f0cd4 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -1,8 +1,8 @@ -import call from '/imports/parser/parseTree/call.js'; -import constant from '/imports/parser/parseTree/constant.js'; -import operator from '/imports/parser/parseTree/operator.js'; -import parenthesis from '/imports/parser/parseTree/parenthesis.js'; -import resolve, { toPrimitiveOrString } from '/imports/parser/resolve.js'; +import call from '/imports/parser/parseTree/call'; +import constant from '/imports/parser/parseTree/constant'; +import operator from '/imports/parser/parseTree/operator'; +import parenthesis from '/imports/parser/parseTree/parenthesis'; +import resolve, { toPrimitiveOrString } from '/imports/parser/resolve'; export default function computeCalculation(computation, node) { const calcObj = node.data; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeContainer.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeContainer.js index 0258692f..c77611cf 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeContainer.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeContainer.js @@ -1,5 +1,5 @@ -import aggregate from './computeVariable/aggregate/index.js'; -import { safeStrip } from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js'; +import aggregate from './computeVariable/aggregate/index'; +import { safeStrip } from '/imports/api/engine/computation/utility/stripFloatingPointOddities'; export default function computeContainer(computation, node) { if (!node.data) node.data = {}; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js b/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js index f39bfaa6..c2e3da59 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computePointBuy.js @@ -1,5 +1,5 @@ import { has } from 'lodash'; -import resolveCalculationNode from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js'; +import { resolveCalculationNode } from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation'; export default function computePointBuy(computation, node) { const prop = node.data; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js index 92c52756..19e06a33 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js @@ -1,12 +1,11 @@ -import aggregate from './computeVariable/aggregate/index.js'; -import computeVariableAsAttribute from './computeVariable/computeVariableAsAttribute.js'; -import computeVariableAsSkill from './computeVariable/computeVariableAsSkill.js'; -import computeVariableAsConstant from './computeVariable/computeVariableAsConstant.js'; -import computeVariableAsClass from './computeVariable/computeVariableAsClass.js'; -import computeVariableAsToggle from './computeVariable/computeVariableAsToggle.js'; -import computeImplicitVariable from './computeVariable/computeImplicitVariable.js'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import { computedInlineCalculationField } from '/imports/api/properties/subSchemas/inlineCalculationField'; +import aggregate from './computeVariable/aggregate/index'; +import computeVariableAsAttribute from './computeVariable/computeVariableAsAttribute'; +import computeVariableAsSkill from './computeVariable/computeVariableAsSkill'; +import computeVariableAsConstant from './computeVariable/computeVariableAsConstant'; +import computeVariableAsClass from './computeVariable/computeVariableAsClass'; +import computeVariableAsToggle from './computeVariable/computeVariableAsToggle'; +import computeImplicitVariable from './computeVariable/computeImplicitVariable'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; export default function computeVariable(computation, node) { const scope = computation.scope; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js index 7b63db25..ebd6a989 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js @@ -12,7 +12,7 @@ export default function aggregateDefinition({ node, linkedNode, link }) { !definingProp || prop.type !== 'pointBuyRow' && ( definingProp.type === 'pointBuyRow' || - prop.order > definingProp.order + prop.left > definingProp.left ) ) { // override the current defining prop diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEventDefinition.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEventDefinition.js index f36cf5b9..8e1dfda6 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEventDefinition.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEventDefinition.js @@ -10,7 +10,7 @@ export default function aggregateEventDefinition({ node, linkedNode, link }) { // Find the last defining event if ( !definingEvent || - prop.order > definingEvent.order + prop.left > definingEvent.left ) { // override the current defining prop if (definingEvent) definingEvent.overridden = true; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/index.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/index.js index 3a617494..c0e32be6 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/index.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/index.js @@ -1,10 +1,10 @@ -import definition from './aggregateDefinition.js'; -import damageMultiplier from './aggregateDamageMultiplier.js'; -import effect from './aggregateEffect.js'; -import eventDefinition from './aggregateEventDefinition.js'; -import proficiency from './aggregateProficiency.js'; -import classLevel from './aggregateClassLevel.js'; -import inventory from './aggregateInventory.js'; +import definition from './aggregateDefinition'; +import damageMultiplier from './aggregateDamageMultiplier'; +import effect from './aggregateEffect'; +import eventDefinition from './aggregateEventDefinition'; +import proficiency from './aggregateProficiency'; +import classLevel from './aggregateClassLevel'; +import inventory from './aggregateInventory'; export default Object.freeze({ classLevel, diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js index 13dcc7f2..bc757c85 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js @@ -1,61 +1,61 @@ -import getAggregatorResult from './getAggregatorResult.js'; +import getAggregatorResult from './getAggregatorResult'; /* * Variables with effects, proficiencies, or damage multipliers but no defining * properties are added to the scope as implicit variables */ - export default function computeImplicitVariable(node){ - const prop = {}; +export default function computeImplicitVariable(node) { + const prop = {}; // Combine damage multipliers - if (node.data.immunity){ + if (node.data.immunity) { prop.immunity = node.data.immunity; prop.immunities = node.data.immunities; } - if (node.data.resistance){ + if (node.data.resistance) { prop.resistance = node.data.resistance; prop.resistances = node.data.resistances; } - if (node.data.vulnerability){ + if (node.data.vulnerability) { prop.vulnerability = node.data.vulnerability; prop.vulnerabilities = node.data.vulnerabilities; } - const result = getAggregatorResult(node); - if (result !== undefined){ - prop.value = result; - } - if (node.data.proficiency !== undefined){ - prop.proficiency = node.data.proficiency; - } + const result = getAggregatorResult(node); + if (result !== undefined) { + prop.value = result; + } + if (node.data.proficiency !== undefined) { + prop.proficiency = node.data.proficiency; + } - // denormalise class level aggregator - let classLevelAgg = node.data.classLevelAggregator; - if (classLevelAgg){ - prop.level = classLevelAgg.level; - } + // denormalise class level aggregator + let classLevelAgg = node.data.classLevelAggregator; + if (classLevelAgg) { + prop.level = classLevelAgg.level; + } - // denormalise the effect aggregator fields - const aggregator = node.data.effectAggregator; - if (aggregator){ - if (aggregator.advantage && !aggregator.disadvantage){ - prop.advantage = 1; - } else if (aggregator.disadvantage && !aggregator.advantage){ - prop.advantage = -1; - } else { - prop.advantage = 0; - } - // Passive bonus - prop.passiveBonus = aggregator.passiveAdd; - // conditional benefits - prop.conditionalBenefits = aggregator.conditional; - // Roll bonuses - prop.rollBonus = aggregator.rollBonus; - // Forced to fail - prop.fail = aggregator.fail; - // Rollbonus - prop.rollBonuses = aggregator.rollBonus; - } + // denormalise the effect aggregator fields + const aggregator = node.data.effectAggregator; + if (aggregator) { + if (aggregator.advantage && !aggregator.disadvantage) { + prop.advantage = 1; + } else if (aggregator.disadvantage && !aggregator.advantage) { + prop.advantage = -1; + } else { + prop.advantage = 0; + } + // Passive bonus + prop.passiveBonus = aggregator.passiveAdd; + // conditional benefits + prop.conditionalBenefits = aggregator.conditional; + // Roll bonuses + prop.rollBonus = aggregator.rollBonus; + // Forced to fail + prop.fail = aggregator.fail; + // Rollbonus + prop.rollBonuses = aggregator.rollBonus; + } - return prop; - } + return prop; +} diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js index 70b16186..0252490a 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -1,4 +1,4 @@ -import getAggregatorResult from './getAggregatorResult.js'; +import getAggregatorResult from './getAggregatorResult'; export default function computeVariableAsAttribute(computation, node, prop) { let result = getAggregatorResult(node) || 0; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js index 898d2c5e..a441160e 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js @@ -1,6 +1,6 @@ -import { parse } from '/imports/parser/parser.js'; +import { parse } from '/imports/parser/parser'; -export default function computeVariableAsConstant(computation, node, prop){ +export default function computeVariableAsConstant(computation, node, prop) { let string = prop.calculation; if (!string) return; let parseNode; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js index 8a260810..d57d3692 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -1,4 +1,4 @@ -import aggregate from './aggregate/index.js'; +import aggregate from './aggregate/index'; export default function computeVariableAsSkill(computation, node, prop) { // Skills are based on some ability Modifier diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsToggle.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsToggle.js index 0c3c41a9..f546426a 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsToggle.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsToggle.js @@ -1,6 +1,6 @@ -import getAggregatorResult from './getAggregatorResult.js'; +import getAggregatorResult from './getAggregatorResult'; -export default function computeVariableAsToggle(computation, node, prop){ +export default function computeVariableAsToggle(computation, node, prop) { let result = getAggregatorResult(node, prop) || 0; prop.value = !!result || !!prop.enabled || !!prop.condition?.value; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js index 9e8b7ebb..5d3519ce 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js @@ -1,4 +1,4 @@ -import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js'; +import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities'; export default function getAggregatorResult(node) { // Work out the base value as the greater of the deining stat value diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js index 47c78f49..a915d5cd 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js @@ -1,9 +1,9 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); @@ -33,7 +33,6 @@ var testProperties = [ clean({ _id: 'actionId', type: 'action', - ancestors: [{id: 'charId'}], summary: { text: 'test summary {1 + 2} without referencing anything {3 + 4}', }, @@ -61,14 +60,17 @@ var testProperties = [ calculation: 'nonExistantProperty + 7', }, usesUsed: 5, + left: 1, + right: 2, }), clean({ _id: 'rolledDescriptionId', type: 'action', - ancestors: [{id: 'charId'}], summary: { text: 'test roll gets compiled {4 + (2 + 2)} properly', }, + left: 3, + right: 4, }), clean({ _id: 'numItemsConumedId', @@ -77,6 +79,8 @@ var testProperties = [ baseValue: { calculation: '3', }, + left: 5, + right: 6, }), clean({ _id: 'numResourceConumedId', @@ -85,6 +89,8 @@ var testProperties = [ baseValue: { calculation: '4', }, + left: 7, + right: 8, }), clean({ _id: 'resourceVarId', @@ -94,6 +100,8 @@ var testProperties = [ baseValue: { calculation: '9', }, + left: 9, + right: 10, }), clean({ _id: 'inlineRefResourceId', @@ -102,6 +110,8 @@ var testProperties = [ baseValue: { calculation: '1 + 5', }, + left: 11, + right: 12, }), clean({ _id: 'arrowId', @@ -110,5 +120,7 @@ var testProperties = [ quantity: 27, icon: 'itemIcon', color: 'itemColor', + left: 13, + right: 14, }), ]; diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js index 32a3c7f9..51d717de 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js @@ -1,7 +1,7 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; export default function () { const computation = buildComputationFromProps(testProperties); @@ -27,6 +27,8 @@ var testProperties = [ _id: 'emptyId', type: 'attribute', attributeType: 'ability', + left: 1, + right: 2, }), clean({ _id: 'noVariableNameId', @@ -35,6 +37,8 @@ var testProperties = [ baseValue: { calculation: '8' }, + left: 3, + right: 4, }), clean({ _id: 'strengthId', @@ -44,6 +48,8 @@ var testProperties = [ baseValue: { calculation: '12' }, + left: 5, + right: 6, }), clean({ _id: 'overriddenDexId', @@ -54,6 +60,8 @@ var testProperties = [ baseValue: { calculation: '15' }, + left: 7, + right: 8, }), clean({ _id: 'dexterityId', @@ -64,6 +72,8 @@ var testProperties = [ baseValue: { calculation: '15' }, + left: 9, + right: 10, }), clean({ _id: 'constitutionId', @@ -73,6 +83,8 @@ var testProperties = [ baseValue: { calculation: '21' }, + left: 11, + right: 12, }), clean({ _id: 'referencesDexId', @@ -81,6 +93,8 @@ var testProperties = [ baseValue: { calculation: 'dexterity.modifier + 2' }, + left: 13, + right: 14, }), clean({ _id: 'hitDiceId', @@ -91,6 +105,8 @@ var testProperties = [ baseValue: { calculation: '4' }, + left: 15, + right: 16, }), clean({ _id: 'parseErrorId', @@ -100,5 +116,7 @@ var testProperties = [ baseValue: { calculation: '12 +' }, + left: 17, + right: 18, }), ]; diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js index 2c6bdc8f..e1151fa1 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js @@ -1,9 +1,10 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); const scope = id => computation.scope[id]; @@ -20,41 +21,37 @@ var testProperties = [ type: 'class', variableName: 'wizard', classType: 'startingClass', - ancestors: [{id: 'charId'}], }), clean({ _id: 'rangerId', type: 'class', variableName: 'ranger', classType: 'multiClass', - ancestors: [{id: 'charId'}], }), clean({ _id: 'wiz1Id', type: 'classLevel', variableName: 'wizard', level: 1, - ancestors: [{id: 'charId'}], }), clean({ _id: 'wiz2Id', type: 'classLevel', variableName: 'wizard', level: 2, - ancestors: [{id: 'charId'}], }), clean({ _id: 'wiz4Id', type: 'classLevel', variableName: 'wizard', level: 4, - ancestors: [{id: 'charId'}], }), clean({ _id: 'rang1Id', type: 'classLevel', variableName: 'ranger', level: 1, - ancestors: [{id: 'charId'}], }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js index 94fd414e..13b8f77e 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js @@ -1,9 +1,10 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); const prop = id => computation.propsById[id]; @@ -23,6 +24,7 @@ var testProperties = [ baseValue: { calculation: 'arrayConstant[3]', }, - ancestors: [{id: 'charId'}], }), ]; + +applyNestedSetProperties(testProperties); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js index ee777627..ed71ad98 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js @@ -1,9 +1,9 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); const scope = id => computation.scope[id]; diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js index 357e7c51..83ca46b3 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js @@ -1,7 +1,7 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; export default function () { const computation = buildComputationFromProps(testProperties); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js index 13b76e4a..e1893fe5 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js @@ -1,9 +1,10 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties, compareOrder } from '/imports/api/parenting/parentingFunctions'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); const prop = id => computation.propsById[id]; @@ -13,9 +14,8 @@ export default function(){ assert.equal(scope('valueEquipment'), 3); assert.equal(scope('itemsAttuned'), 1); - - assert.equal(prop('childContainerId').carriedWeight, 69); - assert.equal(prop('childContainerId').contentsWeight, 69); + assert.equal(prop('childContainerId').carriedWeight, 69, 'Calculates container carried weight correctly'); + assert.equal(prop('childContainerId').contentsWeight, 69, 'Calculates container contents weight correctly'); assert.equal(scope('weightCarried'), 104); assert.equal(scope('valueCarried'), 129); @@ -32,7 +32,6 @@ var testProperties = [ attuned: true, weight: 2, value: 3, - ancestors: [{id: 'charId'}], }), clean({ _id: 'containerId', @@ -40,22 +39,13 @@ var testProperties = [ carried: true, weight: 5, value: 7, - ancestors: [{id: 'charId'}], - }), - clean({ - _id: 'childContainerId', - type: 'container', - carried: true, - weight: 11, - value: 13, - ancestors: [{id: 'charId'}, {id: 'containerId'}], }), clean({ _id: 'childItemId', type: 'item', weight: 17, value: 19, - ancestors: [{id: 'charId'}, {id: 'containerId'}], + parentId: 'containerId', }), clean({ _id: 'grandchildItemId', @@ -63,6 +53,16 @@ var testProperties = [ weight: 23, // 69 total value: 29, // 87 total quantity: 3, - ancestors: [{id: 'charId'}, {id: 'containerId'}, {id: 'childContainerId'}], + parentId: 'childContainerId', + }), + clean({ + _id: 'childContainerId', + type: 'container', + carried: true, + weight: 11, + value: 13, + parentId: 'containerId', }), ]; +applyNestedSetProperties(testProperties); +testProperties.sort(compareOrder); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js index c27b4e86..7f427160 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js @@ -1,10 +1,12 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; +import { applyNestedSetProperties, compareOrder } from '/imports/api/parenting/parentingFunctions'; export default function () { const computation = buildComputationFromProps(testProperties); + const hasLink = computation.dependencyGraph.hasLink; computeCreatureComputation(computation); const prop = id => computation.propsById[id]; assert.equal( @@ -38,7 +40,6 @@ var testProperties = [ clean({ _id: 'actionId', type: 'action', - ancestors: [{ id: 'charId' }], attackRoll: { calculation: 'strength.modifier', }, @@ -48,7 +49,6 @@ var testProperties = [ _id: 'profBonusId', type: 'attribute', variableName: 'proficiencyBonus', - ancestors: [{ id: 'charId' }], baseValue: { calculation: '13' }, @@ -62,3 +62,5 @@ var testProperties = [ targetTags: ['martial weapon'] }), ]; +applyNestedSetProperties(testProperties); +testProperties.sort(compareOrder); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js index 2557296a..a8d3e325 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js @@ -1,9 +1,9 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); const prop = id => computation.propsById[id]; diff --git a/app/imports/api/engine/computation/computeComputation/tests/index.js b/app/imports/api/engine/computation/computeComputation/tests/index.js index 87ae6f07..32b8dda8 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/index.js +++ b/app/imports/api/engine/computation/computeComputation/tests/index.js @@ -1,14 +1,12 @@ -import computeAction from './computeAction.testFn.js'; -import computeAttribute from './computeAttribute.testFn.js'; -import computeCalculations from './computeCalculations.testFn.js'; -import computeClasses from './computeClasses.testFn.js'; -import computeConstants from './computeConstants.testFn.js'; -import computeInventory from './computeInventory.testFn.js'; -import computeDamageMultipliers from './computeDamageMultipliers.testFn.js'; -import computeEffects from './computeEffects.testFn.js'; -import computeSkills from './computeSkills.testFn.js'; -import computePointBuys from './computePointBuys.testFn.js'; -import computeProficiencies from './computeProficiencies.testFn.js'; +import computeAction from './computeAction.testFn'; +import computeAttribute from './computeAttribute.testFn'; +import computeClasses from './computeClasses.testFn'; +import computeConstants from './computeConstants.testFn'; +import computeInventory from './computeInventory.testFn'; +import computeDamageMultipliers from './computeDamageMultipliers.testFn'; +import computeEffects from './computeEffects.testFn'; +import computeSkills from './computeSkills.testFn'; +import computeProficiencies from './computeProficiencies.testFn'; export default [{ text: 'Computes actions', diff --git a/app/imports/api/engine/computation/computeCreatureComputation.js b/app/imports/api/engine/computation/computeCreatureComputation.js index 1ffe58be..15369790 100644 --- a/app/imports/api/engine/computation/computeCreatureComputation.js +++ b/app/imports/api/engine/computation/computeCreatureComputation.js @@ -1,7 +1,7 @@ -import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles.js'; -import computeByType from '/imports/api/engine/computation/computeComputation/computeByType.js'; -import embedInlineCalculations from './utility/embedInlineCalculations.js'; -import { removeEmptyCalculations } from './buildComputation/parseCalculationFields.js'; +import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles'; +import computeByType from '/imports/api/engine/computation/computeComputation/computeByType'; +import embedInlineCalculations from './utility/embedInlineCalculations'; +import { removeEmptyCalculations } from './buildComputation/parseCalculationFields'; import path from 'ngraph.path'; export default function computeCreatureComputation(computation) { diff --git a/app/imports/api/engine/computation/computeCreatureComputation.test.js b/app/imports/api/engine/computation/computeCreatureComputation.test.js index f64e6e7f..45018d3f 100644 --- a/app/imports/api/engine/computation/computeCreatureComputation.test.js +++ b/app/imports/api/engine/computation/computeCreatureComputation.test.js @@ -1,11 +1,11 @@ -import computeCreatureComputation from './computeCreatureComputation.js'; -import { buildComputationFromProps } from './buildCreatureComputation.js'; +import computeCreatureComputation from './computeCreatureComputation'; +import { buildComputationFromProps } from './buildCreatureComputation'; import { assert } from 'chai'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import computeTests from './computeComputation/tests/index.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import computeTests from './computeComputation/tests/index'; -describe('Compute compuation', function(){ - it('Computes something at all', function(){ +describe('Compute compuation', function () { + it('Computes something at all', function () { let computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); assert.exists(computation); @@ -28,7 +28,7 @@ var testProperties = [ }), ]; -function clean(prop){ +function clean(prop) { let schema = CreatureProperties.simpleSchema(prop); return schema.clean(prop); } diff --git a/app/imports/api/engine/computation/utility/applyFnToKey.test.js b/app/imports/api/engine/computation/utility/applyFnToKey.test.js index 5e1b3a3c..4c6680cb 100644 --- a/app/imports/api/engine/computation/utility/applyFnToKey.test.js +++ b/app/imports/api/engine/computation/utility/applyFnToKey.test.js @@ -1,9 +1,9 @@ -import applyFnToKey from './applyFnToKey.js'; +import applyFnToKey from './applyFnToKey'; import { assert } from 'chai'; import { get } from 'lodash'; -describe('apply function to key', function(){ - it('uses a basic key correctly', function(){ +describe('apply function to key', function () { + it('uses a basic key correctly', function () { let obj = getStartingObject(); applyFnToKey(obj, 'fox.name', (doc, key) => { assert.equal(obj, doc); @@ -11,7 +11,7 @@ describe('apply function to key', function(){ assert.equal(get(doc, key), 'foxy'); }); }); - it('uses a single nested key correctly', function(){ + it('uses a single nested key correctly', function () { let obj = getStartingObject(); let foxSounds = []; applyFnToKey(obj, 'fox.sound.$', (doc, key) => { @@ -21,7 +21,7 @@ describe('apply function to key', function(){ assert.include(foxSounds, 'tjoef'); assert.include(foxSounds, 'kek'); }); - it('uses a double nested key correctly', function(){ + it('uses a double nested key correctly', function () { let obj = getStartingObject(); let birdSounds = []; applyFnToKey(obj, 'birds.$.sound.$', (doc, key) => { @@ -33,7 +33,7 @@ describe('apply function to key', function(){ }); }); -function getStartingObject(){ +function getStartingObject() { return { fox: { name: 'foxy', @@ -48,7 +48,7 @@ function getStartingObject(){ sound: [ 'koer', ] - },{ + }, { name: 'parrot', sound: [ 'hello', diff --git a/app/imports/api/engine/computation/utility/cleanProp.testFn.js b/app/imports/api/engine/computation/utility/cleanProp.testFn.js index 1c845150..32bbbfe3 100644 --- a/app/imports/api/engine/computation/utility/cleanProp.testFn.js +++ b/app/imports/api/engine/computation/utility/cleanProp.testFn.js @@ -1,6 +1,9 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; -export default function cleanProp(prop){ +export default function cleanProp(prop) { + if (!prop.root) { + prop.root = { collection: 'creatures', id: 'testCreature' } + } let schema = CreatureProperties.simpleSchema(prop); return schema.clean(prop); } diff --git a/app/imports/api/engine/computation/utility/embedInlineCalculations.js b/app/imports/api/engine/computation/utility/embedInlineCalculations.js index 096cf921..6cdfc63b 100644 --- a/app/imports/api/engine/computation/utility/embedInlineCalculations.js +++ b/app/imports/api/engine/computation/utility/embedInlineCalculations.js @@ -1,6 +1,6 @@ -import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; +import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX'; -export default function embedInlineCalculations(inlineCalcObj){ +export default function embedInlineCalculations(inlineCalcObj) { const string = inlineCalcObj.text; const calculations = inlineCalcObj.inlineCalculations; if (!string || !calculations) return; diff --git a/app/imports/api/engine/computation/utility/findAncestorByType.js b/app/imports/api/engine/computation/utility/findAncestorByType.js deleted file mode 100644 index 492cc6d0..00000000 --- a/app/imports/api/engine/computation/utility/findAncestorByType.js +++ /dev/null @@ -1,10 +0,0 @@ -export default function findAncestorByType(prop, type, propsById){ - if (!prop || !prop.ancestors) return; - let ancestor; - for (let i = prop.ancestors.length - 1; i >= 0; i--){ - ancestor = propsById[prop.ancestors[i].id]; - if (ancestor && ancestor.type === type){ - return ancestor; - } - } -} diff --git a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js index 0548fedd..21a7b9fd 100644 --- a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js +++ b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor' import { EJSON } from 'meteor/ejson'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex'; export default function writeAlteredProperties(computation) { let bulkWriteOperations = []; diff --git a/app/imports/api/engine/computation/writeComputation/writeErrors.js b/app/imports/api/engine/computation/writeComputation/writeErrors.js index 66002515..f3088fcb 100644 --- a/app/imports/api/engine/computation/writeComputation/writeErrors.js +++ b/app/imports/api/engine/computation/writeComputation/writeErrors.js @@ -1,9 +1,9 @@ -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; -export default function(creatureId, errors = []){ - if (errors.length){ - Creatures.update(creatureId, {$set: {computeErrors: errors}}); +export default function (creatureId, errors = []) { + if (errors.length) { + Creatures.update(creatureId, { $set: { computeErrors: errors } }); } else { - Creatures.update(creatureId, {$unset: {computeErrors: 1}}); + Creatures.update(creatureId, { $unset: { computeErrors: 1 } }); } } diff --git a/app/imports/api/engine/computation/writeComputation/writeScope.js b/app/imports/api/engine/computation/writeComputation/writeScope.js index 7f708dd3..1c2473e4 100644 --- a/app/imports/api/engine/computation/writeComputation/writeScope.js +++ b/app/imports/api/engine/computation/writeComputation/writeScope.js @@ -1,5 +1,5 @@ -import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import Creatures from '/imports/api/creature/creatures/Creatures'; import { EJSON } from 'meteor/ejson'; export default function writeScope(creatureId, computation) { @@ -27,7 +27,6 @@ export default function writeScope(creatureId, computation) { // Remove large properties that aren't likely to be accessed delete scope[key].parent; - delete scope[key].ancestors; // Remove empty keys for (const subKey in scope[key]) { diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js index b6dba147..ca3e7cc3 100644 --- a/app/imports/api/engine/computeCreature.js +++ b/app/imports/api/engine/computeCreature.js @@ -1,8 +1,8 @@ -import buildCreatureComputation from './computation/buildCreatureComputation.js'; -import computeCreatureComputation from './computation/computeCreatureComputation.js'; -import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties.js'; -import writeScope from './computation/writeComputation/writeScope.js'; -import writeErrors from './computation/writeComputation/writeErrors.js'; +import buildCreatureComputation from './computation/buildCreatureComputation'; +import computeCreatureComputation from './computation/computeCreatureComputation'; +import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties'; +import writeScope from './computation/writeComputation/writeScope'; +import writeErrors from './computation/writeComputation/writeErrors'; export default function computeCreature(creatureId) { if (Meteor.isClient) return; diff --git a/app/imports/api/engine/loadCreatures.js b/app/imports/api/engine/loadCreatures.ts similarity index 73% rename from app/imports/api/engine/loadCreatures.js rename to app/imports/api/engine/loadCreatures.ts index 40b9793b..21e339ae 100644 --- a/app/imports/api/engine/loadCreatures.js +++ b/app/imports/api/engine/loadCreatures.ts @@ -1,16 +1,18 @@ import { debounce } from 'lodash'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureProperties, { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; import computeCreature from './computeCreature'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; const COMPUTE_DEBOUNCE_TIME = 100; // ms -export const loadedCreatures = new Map(); // creatureId => {creature, properties, etc.} +export const loadedCreatures: Map = new Map(); // creatureId => {creature, properties, etc.} +// TODO: migrate to nested sets -export function loadCreature(creatureId, subscription) { +export function loadCreature(creatureId: string, subscription: Tracker.Computation) { if (!creatureId) throw 'creatureId is required'; let creature = loadedCreatures.get(creatureId); - if (loadedCreatures.has(creatureId)) { + if (creature) { creature.subs.add(subscription); } else { creature = new LoadedCreature(subscription, creatureId); @@ -32,75 +34,67 @@ function unloadCreature(creatureId, subscription) { } } -export function getSingleProperty(creatureId, propertyId) { - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); - const property = creature.properties.get(propertyId); - const cloneProp = EJSON.clone(property); - return cloneProp; +export function getSingleProperty(creatureId: string, propertyId: string) { + const creature = loadedCreatures.get(creatureId) + const property = creature?.properties.get(propertyId); + if (property) { + return EJSON.clone(property); } // console.time(`Cache miss on creature properties: ${creatureId}`) const prop = CreatureProperties.findOne({ _id: propertyId, - 'ancestors.id': creatureId, + 'root.id': creatureId, 'removed': { $ne: true }, - }, { - sort: { order: 1 }, }); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); return prop; } export function getProperties(creatureId) { - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); + const creature = loadedCreatures.get(creatureId); + if (creature) { const props = Array.from(creature.properties.values()); - const cloneProps = EJSON.clone(props); - return cloneProps + return EJSON.clone(props); } // console.time(`Cache miss on creature properties: ${creatureId}`) const props = CreatureProperties.find({ - 'ancestors.id': creatureId, + 'root.id': creatureId, 'removed': { $ne: true }, }, { - sort: { order: 1 }, + sort: { left: 1 }, }).fetch(); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); return props; } export function getPropertiesOfType(creatureId, propType) { - if (loadedCreatures.has(creatureId)) { - const creature = loadedCreatures.get(creatureId); - const props = [] + const creature = loadedCreatures.get(creatureId); + if (creature) { + const props: CreatureProperty[] = [] for (const prop of creature.properties.values()) { if (prop.type === propType) { props.push(prop); } } - const cloneProps = EJSON.clone(props); - return cloneProps + return EJSON.clone(props); } // console.time(`Cache miss on creature properties: ${creatureId}`) const props = CreatureProperties.find({ - 'ancestors.id': creatureId, + 'root.id': creatureId, 'removed': { $ne: true }, 'type': propType, }, { - sort: { order: 1 }, + sort: { left: 1 }, }).fetch(); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); return props; } export function getCreature(creatureId) { - if (loadedCreatures.has(creatureId)) { - const loadedCreature = loadedCreatures.get(creatureId); - const creature = loadedCreature.creature; - if (creature) { - const cloneCreature = EJSON.clone(creature); - return cloneCreature; - } + const loadedCreature = loadedCreatures.get(creatureId); + const loadedCreatureDoc = loadedCreature?.creature; + if (loadedCreatureDoc) { + return EJSON.clone(loadedCreatureDoc); } // console.time(`Cache miss on Creature: ${creatureId}`); const creature = Creatures.findOne(creatureId); @@ -109,13 +103,10 @@ export function getCreature(creatureId) { } export function getVariables(creatureId) { - if (loadedCreatures.has(creatureId)) { - const loadedCreature = loadedCreatures.get(creatureId); - const variables = loadedCreature.variables; - if (variables) { - const cloneVariables = EJSON.clone(variables); - return cloneVariables; - } + const loadedCreature = loadedCreatures.get(creatureId); + const loadedVariables = loadedCreature?.variables; + if (loadedVariables) { + return EJSON.clone(loadedVariables); } // console.time(`Cache miss on variables: ${creatureId}`); const variables = CreatureVariables.findOne({ _creatureId: creatureId }); @@ -131,49 +122,44 @@ export function replaceLinkedVariablesWithProps(variables) { } } -export function getProperyAncestors(creatureId, propertyId) { +export function getPropertyAncestors(creatureId: string, propertyId: string) { const prop = getSingleProperty(creatureId, propertyId); if (!prop) return []; - const ancestorIds = []; - prop.ancestors.forEach(ref => { - if (ref.collection === 'creatureProperties') { - ancestorIds.push(ref.id); - } - }); - if (loadedCreatures.has(creatureId)) { + const loadedCreature = loadedCreatures.get(creatureId); + if (loadedCreature) { // Get the ancestor properties from the cache - const creature = loadedCreatures.get(creatureId); - const props = []; - ancestorIds.forEach(id => { - const prop = creature.properties.get(id); - if (prop) { - props.push(prop); - } - }); - const cloneProps = EJSON.clone(props); - return cloneProps + const props: CreatureProperty[] = []; + let currentProp: CreatureProperty | undefined = prop; + // Iterate through parent chain to get all linked ancestors + while (currentProp?.parentId) { + currentProp = getSingleProperty(creatureId, currentProp.parentId); + if (currentProp) props.push(currentProp); + } + return EJSON.clone(props); } else { // Fetch from database return CreatureProperties.find({ - _id: { $in: ancestorIds }, + ...getFilter.ancestors(prop), removed: { $ne: true }, }, { - sort: { order: 1 }, + sort: { left: 1 } }).fetch(); } } -export function getPropertyDecendants(creatureId, propertyId) { +export function getPropertyDescendants(creatureId, propertyId) { const property = getSingleProperty(creatureId, propertyId); if (!property) return []; - // This prop will always appear at the same position in the ancestor array - // of its decendants, so only check there - const expectedAncestorPostition = property.ancestors.length; if (loadedCreatures.has(creatureId)) { const creature = loadedCreatures.get(creatureId); - const props = []; + if (!creature) return []; + const props: CreatureProperty[] = []; + // Loop through all properties and find ones that match the nested set condition for (const prop of creature.properties.values()) { - if (prop.ancestors[expectedAncestorPostition]?.id === propertyId) { + if ( + prop.left > property.left + && prop.right < property.right + ) { props.push(prop); } } @@ -181,10 +167,10 @@ export function getPropertyDecendants(creatureId, propertyId) { return cloneProps } else { return CreatureProperties.find({ - 'ancestors.id': propertyId, + ...getFilter.descendants(property), removed: { $ne: true }, }, { - sort: { order: 1 }, + sort: { left: 1 }, }).fetch(); } } @@ -221,14 +207,21 @@ export function getPropertyChildren(creatureId, property) { } class LoadedCreature { + subs: Set; + propertyObserver: Meteor.LiveQueryHandle; + creatureObserver: Meteor.LiveQueryHandle; + variablesObserver: Meteor.LiveQueryHandle; + properties: Map; + creature: any; + variables: any; + constructor(sub, creatureId) { + const self = this; // This may be called from a subscription, but we don't want the observers // to be destroyed with it, so use a non-reactive context to observe // the required documents - const self = this; Tracker.nonreactive(() => { self.subs = new Set([sub]); - const compute = debounce(Meteor.bindEnvironment(() => { computeCreature(creatureId); }), COMPUTE_DEBOUNCE_TIME); @@ -269,8 +262,8 @@ class LoadedCreature { self.changeCreature(id, fields); if (fields.dirty) compute(); }, - removed(id) { - self.removeCreature(id); + removed() { + self.removeCreature(); }, }); @@ -287,8 +280,8 @@ class LoadedCreature { changed(id, fields) { self.changeVariables(id, fields); }, - removed(id) { - self.removeVariables(id); + removed() { + self.removeVariables(); }, }); }); @@ -331,7 +324,7 @@ class LoadedCreature { } static changeDoc(doc, fields) { if (!doc) return; - for (let key in fields) { + for (const key in fields) { if (key === undefined) { delete doc[key]; } else { diff --git a/app/imports/api/files/UserImages.js b/app/imports/api/files/UserImages.js index 057c48a0..51cf9030 100644 --- a/app/imports/api/files/UserImages.js +++ b/app/imports/api/files/UserImages.js @@ -1,8 +1,8 @@ let createS3FilesCollection; if (Meteor.isServer) { - createS3FilesCollection = require('/imports/api/files/server/s3FileStorage.js').createS3FilesCollection + createS3FilesCollection = require('/imports/api/files/server/s3FileStorage').createS3FilesCollection } else { - createS3FilesCollection = require('/imports/api/files/client/s3FileStorage.js').createS3FilesCollection + createS3FilesCollection = require('/imports/api/files/client/s3FileStorage').createS3FilesCollection } const UserImages = createS3FilesCollection({ diff --git a/app/imports/api/getModifierFields.js b/app/imports/api/getModifierFields.js index f26e42af..57548ede 100644 --- a/app/imports/api/getModifierFields.js +++ b/app/imports/api/getModifierFields.js @@ -1,8 +1,8 @@ -import MONGO_OPERATORS from '/imports/constants/MONGO_OPERATORS.js'; +import MONGO_OPERATORS from '/imports/constants/MONGO_OPERATORS'; -const hasAny = function(values){ - for (let value of values){ - if (this.has(value)){ +const hasAny = function (values) { + for (let value of values) { + if (this.has(value)) { return true; } } @@ -11,11 +11,11 @@ const hasAny = function(values){ // Returns a Set of fields the modifier changes // The set has been extended with the "hasAny" function -export default function getModifierFields (modifier) { +export default function getModifierFields(modifier) { let fields = new Set(); - for (let operator of MONGO_OPERATORS){ - if (modifier[operator]) for (let field in modifier[operator]){ + for (let operator of MONGO_OPERATORS) { + if (modifier[operator]) for (let field in modifier[operator]) { const indexOfDot = field.indexOf('.'); if (indexOfDot !== -1) { field = field.substring(0, indexOfDot); diff --git a/app/imports/api/icons/Icons.js b/app/imports/api/icons/Icons.js index 072d2467..eb2f8fe1 100644 --- a/app/imports/api/icons/Icons.js +++ b/app/imports/api/icons/Icons.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let Icons = new Mongo.Collection('icons'); diff --git a/app/imports/api/library/Libraries.js b/app/imports/api/library/Libraries.js index 1cbb723e..bdf2475b 100644 --- a/app/imports/api/library/Libraries.js +++ b/app/imports/api/library/Libraries.js @@ -1,12 +1,13 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import SharingSchema from '/imports/api/sharing/SharingSchema.js'; -import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; -import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import { getUserTier } from '/imports/api/users/patreon/tiers.js' -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import SharingSchema from '/imports/api/sharing/SharingSchema'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin'; +import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { getUserTier } from '/imports/api/users/patreon/tiers' +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; /** * Libraries are trees of library nodes where each node represents a character @@ -160,7 +161,7 @@ const removeLibrary = new ValidatedMethod({ export function removeLibaryWork(libraryId) { Libraries.remove(libraryId); - LibraryNodes.remove({ 'ancestors.id': libraryId }); + LibraryNodes.remove(getFilter.descendantsOfRoot(libraryId)); } export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, updateLibraryShowInMarket, removeLibrary }; diff --git a/app/imports/api/library/LibraryCollections.js b/app/imports/api/library/LibraryCollections.js index aff9fe20..b09912db 100644 --- a/app/imports/api/library/LibraryCollections.js +++ b/app/imports/api/library/LibraryCollections.js @@ -1,11 +1,11 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import SharingSchema from '/imports/api/sharing/SharingSchema.js'; -import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; -import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; -import { getUserTier } from '/imports/api/users/patreon/tiers.js' -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import SharingSchema from '/imports/api/sharing/SharingSchema'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin'; +import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions'; +import { getUserTier } from '/imports/api/users/patreon/tiers' +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; /** * LibraryCollections are groups of libraries that are subscribed together at once diff --git a/app/imports/api/library/LibraryNodes.js b/app/imports/api/library/LibraryNodes.js index 39264fb6..a55bd74f 100644 --- a/app/imports/api/library/LibraryNodes.js +++ b/app/imports/api/library/LibraryNodes.js @@ -3,20 +3,20 @@ import { Mongo } from 'meteor/mongo'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js'; -import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema.js'; -import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js'; -import Libraries from '/imports/api/library/Libraries.js'; -import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { softRemove } from '/imports/api/parenting/softRemove.js'; -import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js'; -import { storedIconsSchema } from '/imports/api/icons/Icons.js'; -import '/imports/api/library/methods/index.js'; -import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import { restore } from '/imports/api/parenting/softRemove.js'; -import { getAncestry } from '/imports/api/parenting/parenting.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; +import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema'; +import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema'; +import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex'; +import Libraries from '/imports/api/library/Libraries'; +import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; +import { softRemove } from '/imports/api/parenting/softRemove'; +import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema'; +import { storedIconsSchema } from '/imports/api/icons/Icons'; +import '/imports/api/library/methods/index'; +import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import { restore } from '/imports/api/parenting/softRemove'; +import { fetchDocByRef, getAncestry } from '/imports/api/parenting/parentingFunctions'; +import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; let LibraryNodes = new Mongo.Collection('libraryNodes'); @@ -122,7 +122,7 @@ for (let key in propertySchemasIndex) { function getLibrary(node) { if (!node) throw new Meteor.Error('No node provided'); - let library = Libraries.findOne(node.ancestors[0].id); + let library = Libraries.findOne(node.root.id); if (!library) throw new Meteor.Error('Library does not exist'); return library; } @@ -148,22 +148,20 @@ const insertNode = new ValidatedMethod({ }, run({ libraryNode, parentRef }) { // get the new ancestry - let { parentDoc, ancestors } = getAncestry({ parentRef }); + const parentDoc = fetchDocByRef(parentRef); // Check permission to edit let root; if (parentRef.collection === 'libraries') { root = parentDoc; } else if (parentRef.collection === 'libraryNodes') { - root = Libraries.findOne(parentDoc.ancestors[0].id); + root = Libraries.findOne(parentDoc.root.id); + libraryNode.parentId = parentRef.id; } else { throw `${parentRef.collection} is not a valid parent collection` } assertEditPermission(root, this.userId); - // Set the ancestry of the library node - libraryNode.parent = parentRef; - libraryNode.ancestors = ancestors; // Remove its ID if it came with one to force a random one to be generated // server-side delete libraryNode._id; @@ -178,10 +176,7 @@ const insertNode = new ValidatedMethod({ } // Tree structure changed by insert, reorder the tree - reorderDocs({ - collection: LibraryNodes, - ancestorId: root._id, - }); + rebuildNestedSets(LibraryNodes, root._id); // Return the id of the inserted node return nodeId; @@ -198,6 +193,8 @@ const updateLibraryNode = new ValidatedMethod({ case 'order': case 'parent': case 'ancestors': + case 'parentId': + case 'root': return false; } }, @@ -299,7 +296,7 @@ const restoreLibraryNode = new ValidatedMethod({ let node = LibraryNodes.findOne(_id); assertNodeEditPermission(node, this.userId); // Do work - restore({ _id, collection: LibraryNodes }); + restore(LibraryNodes, _id); } }); diff --git a/app/imports/api/library/getCreatureLibraryIds.js b/app/imports/api/library/getCreatureLibraryIds.js index a951a042..14b6c5e7 100644 --- a/app/imports/api/library/getCreatureLibraryIds.js +++ b/app/imports/api/library/getCreatureLibraryIds.js @@ -1,5 +1,5 @@ -import LibraryCollections from '/imports/api/library/LibraryCollections.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import LibraryCollections from '/imports/api/library/LibraryCollections'; +import Creatures from '/imports/api/creature/creatures/Creatures'; import getUserLibraryIds from './getUserLibraryIds'; import { intersection, union } from 'lodash'; diff --git a/app/imports/api/library/getUserLibraryIds.js b/app/imports/api/library/getUserLibraryIds.js index 55f7498d..ed347b2e 100644 --- a/app/imports/api/library/getUserLibraryIds.js +++ b/app/imports/api/library/getUserLibraryIds.js @@ -1,5 +1,5 @@ -import LibraryCollections from '/imports/api/library/LibraryCollections.js'; -import Libraries from '/imports/api/library/Libraries.js'; +import LibraryCollections from '/imports/api/library/LibraryCollections'; +import Libraries from '/imports/api/library/Libraries'; import { union } from 'lodash'; export default function getUserLibraryIds(userId) { diff --git a/app/imports/api/library/methods/copyLibraryNodeTo.js b/app/imports/api/library/methods/copyLibraryNodeTo.js index ea9ce06f..5c703253 100644 --- a/app/imports/api/library/methods/copyLibraryNodeTo.js +++ b/app/imports/api/library/methods/copyLibraryNodeTo.js @@ -1,23 +1,24 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import SimpleSchema from 'simpl-schema'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; import { assertDocCopyPermission, assertDocEditPermission -} from '/imports/api/sharing/sharingPermissions.js'; +} from '/imports/api/sharing/sharingPermissions'; import { setLineageOfDocs, - renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; + renewDocIds, + getFilter +} from '/imports/api/parenting/parentingFunctions'; +import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; +import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; var snackbar; if (Meteor.isClient) { snackbar = require( - '/imports/client/ui/components/snackbars/SnackbarQueue.js' + '/imports/client/ui/components/snackbars/SnackbarQueue' ).snackbar } @@ -46,12 +47,14 @@ const copyLibraryNodeTo = new ValidatedMethod({ ); } const libraryNode = LibraryNodes.findOne(_id); + if (!libraryNode) throw new Meteor.Error('not-found', 'Library node was not found'); + const parentDoc = fetchDocByRef(parent); assertDocCopyPermission(libraryNode, this.userId); assertDocEditPermission(parentDoc, this.userId); let decendants = LibraryNodes.find({ - 'ancestors.id': _id, + ...getFilter.descendants(libraryNode), removed: { $ne: true }, }, { limit: DUPLICATE_CHILDREN_LIMIT + 1, @@ -69,28 +72,17 @@ const copyLibraryNodeTo = new ValidatedMethod({ const nodes = [libraryNode, ...decendants]; - const newAncestry = parentDoc.ancestors || []; - newAncestry.push(parent); - // re-map all the ancestors - setLineageOfDocs({ - docArray: nodes, - newAncestry, - oldParent: libraryNode.parent, - }); - // Give the docs new IDs without breaking internal references renewDocIds({ docArray: nodes }); // Order the root node - libraryNode.order = (parentDoc.order || 0) + 0.5; + libraryNode.left = Number.MAX_SAFE_INTEGER - 1; + libraryNode.right = Number.MAX_SAFE_INTEGER; LibraryNodes.batchInsert(nodes); // Tree structure changed by inserts, reorder the tree - reorderDocs({ - collection: LibraryNodes, - ancestorId: parent.collection === 'libraries' ? parent.id : parentDoc.ancestors[0].id, - }); + rebuildNestedSets(LibraryNodes, parentDoc.root.id); }, }); diff --git a/app/imports/api/library/methods/duplicateLibraryNode.js b/app/imports/api/library/methods/duplicateLibraryNode.js index eef9f10c..0983f97b 100644 --- a/app/imports/api/library/methods/duplicateLibraryNode.js +++ b/app/imports/api/library/methods/duplicateLibraryNode.js @@ -1,18 +1,19 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import SimpleSchema from 'simpl-schema'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; +import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions'; import { setLineageOfDocs, - renewDocIds -} from '/imports/api/parenting/parenting.js'; -import { reorderDocs } from '/imports/api/parenting/order.js'; + renewDocIds, + getFilter +} from '/imports/api/parenting/parentingFunctions'; +import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; var snackbar; if (Meteor.isClient) { snackbar = require( - '/imports/client/ui/components/snackbars/SnackbarQueue.js' + '/imports/client/ui/components/snackbars/SnackbarQueue' ).snackbar } @@ -33,6 +34,8 @@ const duplicateLibraryNode = new ValidatedMethod({ }, run({ _id }) { let libraryNode = LibraryNodes.findOne(_id); + if (!libraryNode) throw new Meteor.Error('not-found', 'Library node was not found'); + assertDocEditPermission(libraryNode, this.userId); let randomSrc = DDP.randomStream('duplicateLibraryNode'); @@ -40,7 +43,7 @@ const duplicateLibraryNode = new ValidatedMethod({ libraryNode._id = libraryNodeId; let nodes = LibraryNodes.find({ - 'ancestors.id': _id, + ...getFilter.descendants(libraryNode), removed: { $ne: true }, }, { limit: DUPLICATE_CHILDREN_LIMIT + 1, @@ -56,16 +59,6 @@ const duplicateLibraryNode = new ValidatedMethod({ } } - // re-map all the ancestors - setLineageOfDocs({ - docArray: nodes, - newAncestry: [ - ...libraryNode.ancestors, - { id: libraryNodeId, collection: 'libraryNodes' } - ], - oldParent: { id: _id, collection: 'libraryNodes' }, - }); - // Give the docs new IDs without breaking internal references const allNodes = [libraryNode, ...nodes]; renewDocIds({ docArray: allNodes }); @@ -76,10 +69,7 @@ const duplicateLibraryNode = new ValidatedMethod({ LibraryNodes.batchInsert(allNodes); // Tree structure changed by inserts, reorder the tree - reorderDocs({ - collection: LibraryNodes, - ancestorId: libraryNode.ancestors[0].id, - }); + rebuildNestedSets(LibraryNodes, libraryNode.root.id); return libraryNodeId; }, diff --git a/app/imports/api/library/methods/getDefaultSlotFiller.js b/app/imports/api/library/methods/getDefaultSlotFiller.js index aeb5917b..fe8deb0e 100644 --- a/app/imports/api/library/methods/getDefaultSlotFiller.js +++ b/app/imports/api/library/methods/getDefaultSlotFiller.js @@ -11,8 +11,8 @@ export default function getDefaultSlotFiller(slot) { type: slotType, libraryTags: slot.slotTags || [], name: 'Custom ' + slot.name || 'slot filler', - parent: { collection: 'creatureProperties', id: slot._id }, - ancestors: [...slot.ancestors, { collection: 'creatureProperties', id: slot._id }], + parentId: slot._id, + root: { ...slot.root }, }; return filler; } diff --git a/app/imports/api/library/methods/index.js b/app/imports/api/library/methods/index.js index e67eec8a..ebc207ae 100644 --- a/app/imports/api/library/methods/index.js +++ b/app/imports/api/library/methods/index.js @@ -1,3 +1,3 @@ -import '/imports/api/library/methods/copyLibraryNodeTo.js'; -import '/imports/api/library/methods/duplicateLibraryNode.js'; -import '/imports/api/library/methods/updateReferenceNode.js'; +import '/imports/api/library/methods/copyLibraryNodeTo'; +import '/imports/api/library/methods/duplicateLibraryNode'; +import '/imports/api/library/methods/updateReferenceNode'; diff --git a/app/imports/api/library/methods/updateReferenceNode.js b/app/imports/api/library/methods/updateReferenceNode.js index aa2a5407..f0303ec0 100644 --- a/app/imports/api/library/methods/updateReferenceNode.js +++ b/app/imports/api/library/methods/updateReferenceNode.js @@ -1,12 +1,12 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import SimpleSchema from 'simpl-schema'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import LibraryNodes from '/imports/api/library/LibraryNodes'; import { assertDocEditPermission, assertViewPermission, -} from '/imports/api/sharing/sharingPermissions.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; +} from '/imports/api/sharing/sharingPermissions'; +import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; const updateReferenceNode = new ValidatedMethod({ name: 'libraryNodes.updateReferenceNode', @@ -45,8 +45,8 @@ function updateReferenceNodeWork(node, userId) { try { doc = fetchDocByRef(node.ref); if (doc.removed) throw 'Property has been deleted'; - if (doc.ancestors[0].id !== node.ancestors[0].id) { - library = fetchDocByRef(doc.ancestors[0]); + if (doc.root.id !== node.root.id) { + library = fetchDocByRef(doc.root); assertViewPermission(library, userId) } } catch (e) { diff --git a/app/imports/api/parenting/ChildSchema.js b/app/imports/api/parenting/ChildSchema.js deleted file mode 100644 index f4787bcc..00000000 --- a/app/imports/api/parenting/ChildSchema.js +++ /dev/null @@ -1,40 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; - -const RefSchema = new SimpleSchema({ - id: { - type: String, - regEx: SimpleSchema.RegEx.Id, - // TODO: Rather than indexing this field, index `ancestors.0.id` to only - // index the root of the ancestor heirarchy to significantly reduce - // index size and improve performance - // All queries on an ancestor document need to target `ancestors.0.id` first - // before targeting a younger ancestor - index: 1 - }, - collection: { - type: String, - max: STORAGE_LIMITS.collectionName, - }, -}); - -let ChildSchema = new SimpleSchema({ - order: { - type: Number, - }, - parent: { - type: RefSchema, - optional: true, - }, - ancestors: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.ancestorCount, - }, - 'ancestors.$': { - type: RefSchema, - }, -}); - -export default ChildSchema; -export { RefSchema }; diff --git a/app/imports/api/parenting/ChildSchema.ts b/app/imports/api/parenting/ChildSchema.ts new file mode 100644 index 00000000..7955496a --- /dev/null +++ b/app/imports/api/parenting/ChildSchema.ts @@ -0,0 +1,86 @@ +import SimpleSchema from 'simpl-schema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; + +export interface Reference { + collection: string, + id: string, +} + +export interface TreeDoc { + _id: string, + root: Reference, + parentId?: string, + left: number, + right: number, +} + +const RefSchema = new SimpleSchema({ + id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + collection: { + type: String, + max: STORAGE_LIMITS.collectionName, + }, +}); + +const ChildSchema = new SimpleSchema({ + root: { + type: Object, + }, + 'root.id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + 'root.collection': { + type: String, + max: STORAGE_LIMITS.collectionName, + }, + // Parent id of a document in the same collection + // Undefined parent id implies the root is the parent + parentId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, + /** + * The tree structure goes as follows where the numbering follows a counterclockwise depth first + * path around the tree. The canonical structure comes from the root and parentId references, + * while the left and right numbering is used to optimize ancestor queries. + * + * Left can be used as the canonical ordering of properties in an expanded tree folder view. + * + * 1 Books 12 + * ┃ + * 2 Programming 11 + * ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 10 + * ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 7 8 dbm 9 + */ + left: { + type: Number, + index: 1, + // Default to absolutely last with space for right + defaultValue: Number.MAX_SAFE_INTEGER - 1, + }, + right: { + type: Number, + index: 1, + // Default to zero children, so right = left + 1 + defaultValue: Number.MAX_SAFE_INTEGER, + } +}); + +export const treeDocFields = { + _id: 1, + root: 1, + parentId: 1, + left: 1, + right: 1, +} + +export default ChildSchema; +export { RefSchema }; diff --git a/app/imports/api/parenting/fetchDocByRef.js b/app/imports/api/parenting/fetchDocByRef.js deleted file mode 100644 index dfa485dc..00000000 --- a/app/imports/api/parenting/fetchDocByRef.js +++ /dev/null @@ -1,12 +0,0 @@ -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; - -const docNotFoundError = function({id, collection}){ - throw new Meteor.Error('document-not-found', - `No document could be found with id: ${id} in ${collection}` - ); -}; - -export default function fetchDocByRef({id, collection}, options){ - return getCollectionByName(collection).findOne(id, options) || - docNotFoundError({id, collection}); -} diff --git a/app/imports/api/parenting/getCollectionByName.js b/app/imports/api/parenting/getCollectionByName.js deleted file mode 100644 index bc9fe4e0..00000000 --- a/app/imports/api/parenting/getCollectionByName.js +++ /dev/null @@ -1,11 +0,0 @@ -const collectionDoesntExistError = function(collectionName){ - throw new Meteor.Error('bad-collection-reference', - `Parent references collection ${collectionName}, which does not exist` - ); -}; - -const getCollectionByName = function(name){ - return Mongo.Collection.get(name) || collectionDoesntExistError(name); -}; - -export default getCollectionByName; diff --git a/app/imports/api/parenting/getDescendantsInDepthFirstOrder.js b/app/imports/api/parenting/getDescendantsInDepthFirstOrder.js deleted file mode 100644 index 9010d51b..00000000 --- a/app/imports/api/parenting/getDescendantsInDepthFirstOrder.js +++ /dev/null @@ -1,27 +0,0 @@ -import nodesToTree from '/imports/api/parenting/nodesToTree.js'; - -export default function getDescendantsInDepthFirstOrder({ - collection, - ancestorId, - filter, - options = {fields: {order: 1, ancestors: 1}}, -}){ - let forest = nodesToTree({collection, ancestorId, filter, options}); - let orderMemo = getDocsInDepthFirstOrder(forest); - return orderMemo; -} - -export function getDocsInDepthFirstOrder(forest){ - let docs = []; - forest.forEach(node => { - addNodeAndTraverse(node, docs) - }); - return docs; -} - -function addNodeAndTraverse(node, docs){ - docs.push(node.node); - node.children.forEach(child => { - addNodeAndTraverse(child, docs) - }); -} diff --git a/app/imports/api/parenting/nodesToTree.js b/app/imports/api/parenting/nodesToTree.js deleted file mode 100644 index 86fbe1c1..00000000 --- a/app/imports/api/parenting/nodesToTree.js +++ /dev/null @@ -1,122 +0,0 @@ -import { union, difference, sortBy, findLast, intersection } from 'lodash'; - -export function nodeArrayToTree(nodes) { - // Store a dict and list of all the nodes - let nodeIndex = {}; - let nodeList = []; - nodes.forEach(node => { - let treeNode = { - node: node, - children: [], - }; - nodeIndex[node._id] = treeNode; - nodeList.push(treeNode); - }); - // Create a forest of trees - let forest = []; - // Either the node is a child of its nearest found ancestor, or in the forest as a root - nodeList.forEach(treeNode => { - let ancestorInForest = findLast( - treeNode.node.ancestors, - ancestor => !!nodeIndex[ancestor.id] - ); - if (ancestorInForest) { - nodeIndex[ancestorInForest.id].children.push(treeNode); - } else { - forest.push(treeNode); - } - }); - forest.nodeIndex = nodeIndex; - return forest; -} - -// Fetch the documents from a collection, and return the tree of those documents -export default function nodesToTree({ - collection, ancestorId, filter, options = {}, - includeFilteredDocAncestors = false, includeFilteredDocDescendants = false -}) { - // Setup the filter - let collectionFilter = { - 'ancestors.id': ancestorId, - 'removed': { $ne: true }, - }; - if (filter) { - collectionFilter = { - ...collectionFilter, - ...filter, - } - } - // Set up the options - let collectionSort = { - order: 1 - }; - if (options && options.sort) { - collectionSort = { - ...collectionSort, - ...options.sort, - } - } - let collectionOptions = { - sort: collectionSort, - } - if (options) { - collectionOptions = { - ...collectionOptions, - ...options, - } - } - // Find all the nodes that match the filter - let docs = collection.find(collectionFilter, collectionOptions).map(doc => { - if (!filter) return doc; - // Mark the nodes that were found by the custom filter - doc._matchedDocumentFilter = true; - return doc; - }); - let ancestors = []; - let ancestorIds = []; - let docIds = []; - if (filter && (includeFilteredDocAncestors || includeFilteredDocDescendants)) { - docIds = docs.map(doc => doc._id) - } - if (filter && includeFilteredDocAncestors) { - // Add all ancestor ids to an array - docs.forEach(doc => { - ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id)); - }); - // Get all the docs that are also ancestors and mark them - docs.forEach(doc => { - if (ancestorIds.includes(doc._id)) { - doc._ancestorOfMatchedDocument = true; - } - }); - // Remove the ancestor IDs of docs we have already found - ancestorIds = difference(ancestorIds, docIds); - // Get the ancestor docs from the collection, don't worry about `removed` docs, - // if their descendant was not removed, neither are they - ancestors = collection.find({ _id: { $in: ancestorIds } }).map(doc => { - // Mark that the nodes are ancestors of the found nodes - doc._ancestorOfMatchedDocument = true; - return doc; - }); - } - let descendants = []; - if (filter && includeFilteredDocDescendants) { - let exludeIds = union(ancestorIds, docIds); - descendants = collection.find({ - '_id': { $nin: exludeIds }, - 'ancestors.id': { $in: docIds }, - 'removed': { $ne: true }, - }).map(doc => { - // Mark that the nodes are descendants of the found nodes - doc._descendantOfMatchedDocument = true; - return doc; - }); - } - let nodes = sortBy([ - ...ancestors, - ...docs, - ...descendants - ], 'order'); - // Find all the nodes - return nodeArrayToTree(nodes); -} diff --git a/app/imports/api/parenting/order.js b/app/imports/api/parenting/order.js deleted file mode 100644 index c94e814b..00000000 --- a/app/imports/api/parenting/order.js +++ /dev/null @@ -1,156 +0,0 @@ -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import getDescendantsInDepthFirstOrder from '/imports/api/parenting/getDescendantsInDepthFirstOrder.js' - -// Docs keep track of their depth-first order amongst their entire ancestor tree -export function compareOrder(docA, docB){ - // < 0 if A comes before B - // = 0 if A and B are the same order - // > 0 if B comes before A - - // They must share a root ancestor to be meaningfully sorted - if (docA.ancestors[0].id !== docB.ancestors[0].id){ - return 0; - } else { - return docA.order - docB.order; - } -} - -export function getHighestOrder({collection, ancestorId}){ - const highestOrderedDoc = collection.findOne({ - 'ancestors.id': ancestorId, - }, { - fields: {order: 1}, - sort: {order: -1}, - }); - return highestOrderedDoc ? highestOrderedDoc.order : -1; -} - -export function setDocToLastOrder({collection, doc}){ - doc.order = getHighestOrder({ - collection, - ancestorId: doc.ancestors[0].id, - }) + 1; -} - -// update the order of a doc, and shift the related docs around to suit the new -// order -function cheapUpdateDocOrder({docRef, order}){ - let doc = fetchDocByRef(docRef, {fields: { - order: 1, - parent: 1, - }}); - let collection = getCollectionByName(docRef.collection); - const currentOrder = doc.order; - if (currentOrder === order){ - return; - } else { - // First move the documents that are in the way - let inBetweenSelector, increment; - if (order > currentOrder){ - // Move in-between docs backward - inBetweenSelector = { - $gt: currentOrder, - $lte: order - }; - increment = -1; - } else if (order < currentOrder){ - // Move in-between docs forward - inBetweenSelector = { - $lt: currentOrder, - $gte: order - }; - increment = 1; - } - collection.update({ - 'ancestors.id': doc.ancestors[0].id, - order: inBetweenSelector, - }, { - $inc: {order: increment}, - }, { - multi: true, - selector: {type: 'any'}, - }); - // Then move the document itself - collection.update(doc._id, {$set: {order}}, {selector: {type: 'any'}}); - } -} - -export function cheapRemovedDocAtOrder({collection, doc}){ - // Decrement the order of all docs after the removed doc - collection.update({ - 'ancestors.id': doc.ancestors[0].id, - order: {$gt: doc.order}, - }, { - $inc: {order: -1}, - }, { - multi: true, - selector: {type: 'any'}, - }); -} - -export function cheapInsertedDocAtOrder({collection, ancestorId, order}){ - // Increment the order of all docs after the inserted doc - collection.update({ - 'ancestors.id': ancestorId, - order: {$gte: order}, - }, { - $inc: {order: 1}, - }, { - multi: true, - selector: {type: 'any'}, - }); -} - -// Update the order a single doc and re-order the entire related doc list -// with the change -export function safeUpdateDocOrder({docRef, order}){ - let collection = getCollectionByName(docRef.collection); - // Put the new doc half a step in front of its new order - // to ensure it's in front of whichever doc was there before - collection.update(docRef.id, { - $set: {order} - }, { - selector: {type: 'any'} - }); - // reorder all related docs so that order is back to being a continous - // set of whole numbers - let movedDoc = fetchDocByRef(docRef, {fields: {ancestors: 1}}); - let ancestorId = movedDoc.ancestors[0].id; - reorderDocs({collection, ancestorId}); -} - -export function reorderDocs({collection, ancestorId}){ - let orderedDocs = getDescendantsInDepthFirstOrder({collection, ancestorId}); - let bulkWrite = []; - orderedDocs.forEach((doc, index) => { - if (doc.order !== index){ - bulkWrite.push({ - updateOne : { - filter: {_id: doc._id}, - update: {$set: {order: index}}, - }, - }); - } - }); - if (Meteor.isServer && bulkWrite.length){ - collection.rawCollection().bulkWrite( - bulkWrite, - {ordered : false}, - function(e){ - if (e) { - console.error('Bulk write failed: '); - console.error(e); - } - } - ); - } else { - bulkWrite.forEach(op => { - collection.update( - op.updateOne.filter, - op.updateOne.update, - {selector: {type: 'any'}} - ); - }); - } -} diff --git a/app/imports/api/parenting/organizeMethods.js b/app/imports/api/parenting/organizeMethods.js deleted file mode 100644 index 54d5f1ba..00000000 --- a/app/imports/api/parenting/organizeMethods.js +++ /dev/null @@ -1,130 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import { union } from 'lodash'; -import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { updateParent } from '/imports/api/parenting/parenting.js'; -import { reorderDocs, safeUpdateDocOrder } from '/imports/api/parenting/order.js'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; -import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; - -const organizeDoc = new ValidatedMethod({ - name: 'organize.organizeDoc', - validate: new SimpleSchema({ - docRef: RefSchema, - parentRef: RefSchema, - order: { - type: Number, - // Should end in 0.5 to place it reliably between two existing documents - }, - skipRecompute: { - type: Boolean, - optional: true, - }, - skipClient: { - type: Boolean, - optional: true, - }, - }).validator(), - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 5, - timeInterval: 5000, - }, - run({ docRef, parentRef, order, skipRecompute, skipClient }) { - if (skipClient && this.isSimulation) { - return; - } - let doc = fetchDocByRef(docRef); - let collection = getCollectionByName(docRef.collection); - // The user must be able to edit both the doc and its parent to move it - // successfully - assertDocEditPermission(doc, this.userId); - let parent = fetchDocByRef(parentRef); - assertDocEditPermission(parent, this.userId); - - // Change the doc's parent - updateParent({ docRef, parentRef }); - // Change the doc's order to be a half step ahead of its target location - collection.update(doc._id, { $set: { order } }, { selector: { type: 'any' } }); - - // Reorder both ancestors' documents - let oldAncestorId = doc.ancestors[0].id; - reorderDocs({ collection, ancestorId: oldAncestorId }); - - let newAncestorId = getRootId(parent); - if (newAncestorId !== oldAncestorId) { - reorderDocs({ collection, ancestorId: newAncestorId }); - } - - // Figure out which creatures need to be recalculated after this move - let docCreatures = getCreatureAncestors(doc); - let parentCreatures = getCreatureAncestors(parent); - if (!skipRecompute) { - let creaturesToRecompute = union(docCreatures, parentCreatures); - // Mark the creatures for recompute - Creatures.update({ - _id: { $in: creaturesToRecompute } - }, { - $set: { dirty: true }, - }); - } - }, -}); - -const reorderDoc = new ValidatedMethod({ - name: 'organize.reorderDoc', - validate: new SimpleSchema({ - docRef: RefSchema, - order: { - type: Number, - // Should end in 0.5 to place it reliably between two existing documents - }, - }).validator(), - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 5, - timeInterval: 5000, - }, - run({ docRef, order }) { - let doc = fetchDocByRef(docRef); - assertDocEditPermission(doc, this.userId); - safeUpdateDocOrder({ docRef, order }); - // Recompute the affected creatures - const ancestors = getCreatureAncestors(doc); - if (ancestors.length) { - Creatures.update({ - _id: { $in: ancestors } - }, { - $set: { dirty: true }, - }); - } - }, -}); - -function getRootId(doc) { - if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) { - return doc.ancestors[0].id; - } else { - return doc._id; - } -} - -function getCreatureAncestors(doc) { - let ids = []; - if (doc.type === 'pc' || doc.type === 'npc' || doc.type === 'monster') { - ids.push(doc._id); - } - if (doc.ancestors) { - doc.ancestors.forEach(ancestorRef => { - if (ancestorRef.collection === 'creatures') { - ids.push(ancestorRef.id); - } - }); - } - return ids; -} - -export { organizeDoc, reorderDoc }; diff --git a/app/imports/api/parenting/parenting.js b/app/imports/api/parenting/parenting.js deleted file mode 100644 index 23e241ae..00000000 --- a/app/imports/api/parenting/parenting.js +++ /dev/null @@ -1,238 +0,0 @@ -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import { flatten } from 'lodash'; - -const generalParents = [ - 'attribute', - 'buff', - 'classLevel', - 'feature', - 'folder', - 'root', - 'item', - 'spell', -]; - -// Which types are allowed as parents for other types -const allowedParenting = { - folder: [...generalParents, 'container'], - rollResult: ['roll', 'rollResult'], - container: ['root', 'folder'], - item: ['root', 'container', 'folder'], -}; - -const allParentTypes = new Set(flatten(Object.values(allowedParenting))); - -export function canBeParent(type) { - return true; - //TODO until there is a good reason to disallow certain parenting options, - // this should just let the user do whatever - return type && allParentTypes.has(type); -} - -export function getAllowedParents({ childType }) { - return allowedParenting[childType] || generalParents; -} - -export function isParentAllowed({ parentType = 'root', childType }) { - return true; - //TODO until there is a good reason to disallow certain parenting options, - // this should just let the user do whatever - if (!childType) throw 'childType is required'; - let allowedParents = getAllowedParents({ childType }); - return allowedParents.includes(parentType); -} - -export function fetchParent({ id, collection }) { - return fetchDocByRef({ id, collection }); -} - -export function fetchChildren({ collection, parentId, filter = {}, options = { sort: { order: 1 } } }) { - filter['parent.id'] = parentId; - let children = []; - children.push( - ...collection.find({ - 'parent.id': parentId - }, options).fetch() - ); - return children; -} - -export function updateChildren({ collection, parentId, filter = {}, modifier, options = {} }) { - filter['parent.id'] = parentId; - options.multi = true; - collection.update(filter, modifier, options); -} - -export function fetchDescendants({ collection, ancestorId, filter = {}, options }) { - filter['ancestors.id'] = ancestorId; - let descendants = []; - descendants.push(...collection.find(filter, options).fetch()); - return descendants; -} - -export function updateDescendants({ collection, ancestorId, filter = {}, modifier, options = {} }) { - filter['ancestors.id'] = ancestorId; - options.multi = true; - options.selector = { type: 'any' }; - collection.update(filter, modifier, options); -} - -export function forEachDescendant({ collection, ancestorId, filter = {}, options }, callback) { - filter['ancestors.id'] = ancestorId; - collection.find(filter, options).forEach(callback); -} - -// 1 database read -export function getAncestry({ parentRef, inheritedFields = {} }) { - let parentDoc = fetchDocByRef(parentRef, { fields: inheritedFields }); - let parent = { ...parentRef }; - for (let field in inheritedFields) { - if (inheritedFields[field]) { - parent[field] = parentDoc[field]; - } - } - - // Ancestors is [...parent's ancestors, parent ref] - let ancestors = parentDoc.ancestors || []; - ancestors.push(parent); - - return { parentDoc, parent, ancestors }; -} - -export function setLineageOfDocs({ docArray, oldParent, newAncestry }) { - const newParent = newAncestry[newAncestry.length - 1]; - docArray.forEach(doc => { - if (doc.parent.id === oldParent.id) { - doc.parent = newParent; - } - let oldAncestors = doc.ancestors; - let oldParentIndex = oldAncestors.findIndex(a => a.id === oldParent.id); - if (oldParentIndex === -1) return; - doc.ancestors = [...newAncestry, ...oldAncestors.slice(oldParentIndex + 1)]; - }); -} - -/** - * Give documents new random ids and transform their references. - * Transform collections of re-IDed docs according to the collection map - */ -export function renewDocIds({ docArray, collectionMap, idMap = {} }) { - // idMap is a map of {oldId: newId} - // Get a random generator that's consistent on client and server - let randomSrc = DDP.randomStream('renewDocIds'); - - // Replaces all the ids of a sub-document array with new random ids - function replaceIds(arr) { - arr?.forEach?.(obj => { - obj._id = randomSrc.id(); - }); - } - - docArray.forEach(doc => { - // Give new ids and map the changes as {oldId: newId} - let oldId = doc._id; - // Respect the existing map if a document appears in the array twice - let newId = idMap[oldId] || randomSrc.id(); - doc._id = newId; - idMap[oldId] = newId; - - // Replace ids of sub-properties that might have _id fields - replaceIds(doc.resources?.conditions); - replaceIds(doc.resources?.attributesConsumed); - replaceIds(doc.resources?.itemsConsumed); - replaceIds(doc.extraTags); - replaceIds(doc.values); - }); - - // Remap all references using the new IDs - const remapReference = ref => { - if (idMap[ref.id]) { - ref.id = idMap[ref.id]; - ref.collection = collectionMap && collectionMap[ref.collection] || ref.collection; - } - } - docArray.forEach(doc => { - remapReference(doc.parent); - doc.ancestors.forEach(remapReference); - }); -} - -export function updateParent({ docRef, parentRef }) { - let collection = getCollectionByName(docRef.collection); - let oldDoc = fetchDocByRef(docRef, { - fields: { - parent: 1, - ancestors: 1, - type: 1, - } - }); - let updateOptions = { selector: { type: 'any' } }; - - // Skip if we aren't changing the parent id - if (oldDoc.parent.id === parentRef.id) return; - - // Get the parent and its ancestry - let { parentDoc, parent, ancestors } = getAncestry({ parentRef }); - - // Check that the doc isn't its own ancestor - ancestors.forEach(ancestor => { - if (docRef.id === ancestor.id) { - throw new Meteor.Error('invalid parenting', - 'A doc can\'t be its own ancestor') - } - }); - - // If the doc and its parent are in the same collection, apply the allowed - // parent rules based on type - if (docRef.collection === parentRef.collection) { - let parentAllowed = isParentAllowed({ - parentType: parentDoc.type, - childType: oldDoc.type - }); - if (!parentAllowed) { - throw new Meteor.Error('invalid parenting', - `Can't make ${oldDoc.type} a child of ${parentDoc.type}`) - } - } - - // update the document's parenting - collection.update(docRef.id, { - $set: { parent, ancestors } - }, updateOptions); - - // Remove the old ancestors from the descendants - updateDescendants({ - collection, - ancestorId: docRef.id, - modifier: { - $pullAll: { - ancestors: oldDoc.ancestors, - } - }, - options: updateOptions, - }); - - // Add the new ancestors to the descendants - updateDescendants({ - collection, - ancestorId: docRef.id, - modifier: { - $push: { - ancestors: { - $each: ancestors, - $position: 0, - }, - } - }, - options: updateOptions, - }); -} - -export function getName(doc) { - if (doc.name) return name; - var i = doc.ancestors.length; - while (i--) { - if (doc.ancestors[i].name) return doc.ancestors[i].name; - } -} diff --git a/app/imports/api/parenting/parentingFunctions.test.ts b/app/imports/api/parenting/parentingFunctions.test.ts new file mode 100644 index 00000000..f9900200 --- /dev/null +++ b/app/imports/api/parenting/parentingFunctions.test.ts @@ -0,0 +1,186 @@ +import { docsToForest, calculateNestedSetOperations, getFilter } from '/imports/api/parenting/parentingFunctions' +import { TreeDoc } from '/imports/api/parenting/ChildSchema'; +import { assert } from 'chai'; + +function doc(_id, left, right, parentId?): TreeDoc { + return { _id, root: { id: 'root', collection: 'col' }, left, right, parentId }; +} + +function op(_id, left, right) { + return { + updateOne: { + filter: { _id }, + update: { $set: { left, right } }, + }, + }; +} + +describe('Parenting with nested sets', function () { + /** + * Test the following structure + * + * 1 Books 12 + * ┃ + * 2 Programming 11 + * ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 10 + * ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 7 8 dbm 9 + */ + it('Takes a set of documents and builds the forest', function () { + const docArray: Array = [ + doc('Books', 1, 12), + doc('Programming', 2, 11), + doc('Languages', 3, 4), + doc('Databases', 5, 10), + doc('MongoDB', 6, 7), + doc('dbm', 8, 9), + ]; + const forest = docsToForest(docArray); + assert.deepEqual(forest, [ + { + doc: doc('Books', 1, 12), children: [ + { + doc: doc('Programming', 2, 11), children: [ + { doc: doc('Languages', 3, 4), children: [] }, + { + doc: doc('Databases', 5, 10), children: [ + { doc: doc('MongoDB', 6, 7), children: [] }, + { doc: doc('dbm', 8, 9), children: [] }, + ] + } + ] + } + ] + } + ]); + }); + it('Can recalculate left and right for docs with set parents', function () { + const docArray = [ + doc('Books', 71, 33, undefined), + doc('Programming', 72, 33, 'Books'), + doc('Languages', 73, 33, 'Programming'), + doc('Databases', 74, 33, 'Programming'), + doc('MongoDB', 75, 33, 'Databases'), + doc('dbm', 76, 33, 'Databases'), + ]; + const ops = calculateNestedSetOperations(docArray); + assert.deepEqual(ops, [ + op('Books', 1, 12), + op('Programming', 2, 11), + op('Languages', 3, 4), + op('Databases', 5, 10), + op('MongoDB', 6, 7), + op('dbm', 8, 9), + ]); + }); + it('Can recalculate left and right for docs with set parents in random order', function () { + const docArray = [ + doc('MongoDB', 71, 33, 'Databases'), + doc('Programming', 72, 33, 'Books'), + doc('Languages', 73, 33, 'Programming'), + doc('Books', 74, 33, undefined), + doc('Databases', 75, 33, 'Programming'), + doc('dbm', 76, 33, 'Databases'), + ]; + const ops = calculateNestedSetOperations(docArray); + assert.deepEqual(ops, [ + op('Books', 1, 12), + op('Programming', 2, 11), + op('Languages', 3, 4), + op('Databases', 5, 10), + op('MongoDB', 6, 7), + op('dbm', 8, 9), + ]); + }); +}); + +describe('Document tree filters can fetch other documents based on their position in the tree', function () { + // Add the docs to a new collection + /** + * Test the following structure + * + * 1 Books 12 + * ┃ + * 2 Programming 11 + * ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 10 + * ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 7 8 dbm 9 + */ + const treeCollection: Mongo.Collection = new Mongo.Collection('treeDocs'); + treeCollection.remove({}); + [ + doc('Books', 1, 12, undefined), + doc('Programming', 2, 11, 'Books'), + doc('Languages', 3, 4, 'Programming'), + doc('Databases', 5, 10, 'Programming'), + doc('MongoDB', 6, 7, 'Databases'), + doc('dbm', 8, 9, 'Databases'), + ].map(doc => { + return treeCollection.insert(doc); + }); + const docs: TreeDoc[] = treeCollection.find({}).fetch(); + + it('Can filter ancestors', async function () { + const ancestorIds: { [id: string]: string[] } = {}; + docs.forEach(doc => { + ancestorIds[doc._id] = treeCollection.find( + getFilter.ancestors(doc) + ).map(doc => doc._id); + }); + assert.isEmpty(ancestorIds['Books'], 'Books has no ancestors'); + assert.sameMembers(ancestorIds['Programming'], ['Books']); + assert.sameMembers(ancestorIds['Languages'], ['Books', 'Programming']); + assert.sameMembers(ancestorIds['Databases'], ['Books', 'Programming']); + assert.sameMembers(ancestorIds['MongoDB'], ['Books', 'Programming', 'Databases']); + assert.sameMembers(ancestorIds['dbm'], ['Books', 'Programming', 'Databases']); + }); + + it('Can filter descendants', async function () { + const descendantIds: { [id: string]: string[] } = {}; + docs.forEach(doc => { + descendantIds[doc._id] = treeCollection.find( + getFilter.descendants(doc) + ).map(doc => doc._id); + }); + assert.isEmpty(descendantIds['MongoDB'], 'MongoDB has no descendants'); + assert.isEmpty(descendantIds['dbm'], 'dbm has no descendants'); + assert.isEmpty(descendantIds['Languages'], 'Languages has no descendants'); + assert.sameMembers(descendantIds['Databases'], ['dbm', 'MongoDB']); + assert.sameMembers(descendantIds['Programming'], ['dbm', 'MongoDB', 'Languages', 'Databases']); + assert.sameMembers(descendantIds['Books'], [ + 'dbm', 'MongoDB', 'Languages', 'Databases', 'Programming' + ]); + }); + + it('Can filter children', async function () { + const childrenIds: { [id: string]: string[] } = {}; + docs.forEach(doc => { + childrenIds[doc._id] = treeCollection.find( + getFilter.children(doc) + ).map(doc => doc._id); + }); + assert.sameMembers(childrenIds['Books'], ['Programming']); + assert.sameMembers(childrenIds['Programming'], ['Languages', 'Databases']); + assert.isEmpty(childrenIds['Languages'], 'Languages has no children'); + assert.sameMembers(childrenIds['Databases'], ['dbm', 'MongoDB']); + assert.isEmpty(childrenIds['MongoDB'], 'MongoDB has no children'); + assert.isEmpty(childrenIds['dbm'], 'dbm has no children'); + }); + + it('Can filter parents', async function () { + const parentIds: { [id: string]: string[] } = {}; + docs.forEach(doc => { + parentIds[doc._id] = treeCollection.find( + getFilter.parent(doc) + ).map(doc => doc._id); + }); + assert.isEmpty(parentIds['Books'], 'Books has no parent'); + assert.sameMembers(parentIds['Programming'], ['Books']); + assert.sameMembers(parentIds['Languages'], ['Programming']); + assert.sameMembers(parentIds['Databases'], ['Programming']); + assert.sameMembers(parentIds['MongoDB'], ['Databases']); + assert.sameMembers(parentIds['dbm'], ['Databases']); + }); +}); diff --git a/app/imports/api/parenting/parentingFunctions.ts b/app/imports/api/parenting/parentingFunctions.ts new file mode 100644 index 00000000..af81ccef --- /dev/null +++ b/app/imports/api/parenting/parentingFunctions.ts @@ -0,0 +1,589 @@ +import { chain, reverse } from 'lodash'; +import { TreeDoc, treeDocFields, Reference } from '/imports/api/parenting/ChildSchema'; + +export function getCollectionByName(name: string): Mongo.Collection { + const collection = Mongo.Collection.get(name) + if (!collection) { + throw new Meteor.Error('bad-collection-reference', + `Parent references collection ${name}, which does not exist` + ); + } + return collection; +} + +function assertDocFound(doc) { + if (!doc) { + throw new Meteor.Error('document-not-found', + `No document could be found with id: ${ref.id} in ${ref.collection}` + ); + } +} + +export function fetchDocByRefAsync(ref: Reference, options?: Mongo.Options): Promise { + const doc = getCollectionByName(ref.collection).findOneAsync(ref.id, options); + assertDocFound(doc); + return doc; +} + +export function fetchDocByRef(ref: Reference, options?: Mongo.Options): TreeDoc { + const doc: TreeDoc = getCollectionByName(ref.collection).findOne(ref.id, options); + assertDocFound(doc); + return doc; +} + +export interface TreeNode { + doc: T, + children: TreeNode[] +} + +/** + * + * @param nodes An array of documents that share a common root. Must already be sorted by `.left` in ascending order + * @returns An array of tree nodes that each contain a document and its children. Children are + * assigned based on the nearest ancestor included in the input, which may or may not be their + * actual direct parents + */ +export function docsToForest(docs: Array): TreeNode[] { + if (!docs.length) return []; + const forest: TreeNode[] = []; + const ancestorStack: TreeNode[] = []; + let currentAncestor: TreeNode | undefined; + docs.forEach(doc => { + const node: TreeNode = { + doc, + children: [], + }; + // Remove ancestors from the stack until we find one that contains the current document + // Ancestor contains document if ancestor.left < doc.left and ancestor.right > doc.right + // ancestor.left < doc.left is ensured already, because we sorted by doc.left + while (currentAncestor && currentAncestor.doc.right < doc.left) { + currentAncestor = ancestorStack.pop(); + } + // Add this child to its place in the forest, either as a child of the ancestor or as the root + // of a new tree + if (currentAncestor) { + currentAncestor.children.push(node); + } else { + forest.push(node); + } + // Move the last ancestor onto the stack and make this node the new one + if (currentAncestor) ancestorStack.push(currentAncestor); + currentAncestor = node; + }); + return forest; +} + +/** + * Fetch the documents from a collection, and return the tree of those documents, potentially + * including their ancestors or descendants as required + * @param param options + * @returns An array of tree nodes that each contain a document and its children. Children are + * assigned based on the nearest ancestor included in the input, which may or may not be their + * actual direct parents + */ +type FilteredDoc = { + _descendantOfMatchedDocument?: boolean, + _matchedDocumentFilter?: boolean, + _ancestorOfMatchedDocument?: boolean, +} & TreeDoc; + +export async function filterToForest( + collection: Mongo.Collection, + rootId: string, + filter: Mongo.Selector, + { + options = >{}, + includeFilteredDocAncestors = false, + includeFilteredDocDescendants = false + } = {} +): Promise[]> { + // Setup the filter + let collectionFilter = { + 'root.id': rootId, + 'removed': { $ne: true }, + }; + if (filter) { + collectionFilter = { + ...collectionFilter, + ...filter, + } + } + // Set up the options + let collectionSort = { + left: 1 + }; + if (options && options.sort) { + collectionSort = { + ...collectionSort, + ...options.sort, + } + } + let collectionOptions: Mongo.Options = { + sort: collectionSort, + } + if (options) { + collectionOptions = { + ...collectionOptions, + ...options, + } + } + // Find all the docs that match the filter + const docs: TreeDoc[] = await collection.find(collectionFilter, collectionOptions) + .mapAsync(doc => { + if (!filter) return doc; + // Mark the docs that were found by the custom filter + doc._matchedDocumentFilter = true; + return doc; + }); + + // Get the doc ancestors + let ancestors: object[] = []; + if (filter && includeFilteredDocAncestors) { + ancestors = await collection.find(getFilter.ancestorsOfAll(docs), collectionOptions).mapAsync(doc => { + // Mark that the nodes are ancestors of the found nodes + doc._ancestorOfMatchedDocument = true; + return doc; + }); + } + + // Get the doc descendants + let descendants: FilteredDoc[] = []; + if (filter && includeFilteredDocDescendants) { + descendants = await collection.find({ + 'removed': { $ne: true }, + ...getFilter.descendantsOfAll(docs), + }).mapAsync((doc: FilteredDoc) => { + // Mark that the nodes are descendants of the found nodes + doc._descendantOfMatchedDocument = true; + return doc; + }); + } + const nodes = chain([ + ancestors, + docs, + descendants + ]).uniqBy('_id') + .sortBy('left') + .value(); + // Find all the nodes + return docsToForest(nodes); +} + +type ForestAndOrphans = { forest: TreeNode[], orphanIds: string[] } +/** + * Takes a complete set of documents and builds a forest using just their `.parentIds` + * Uses `.left` for sibling order within a parent only. + * Orphans whose direct parents can't be found are collected separately + * @param docs An array of all document that share a common root already sorted by `.left` in + * ascending order + * @returns forest: An array of tree nodes that each contain a document and its children. + * orphans: an array of the same, but their parents weren't in the input array + */ +export function docsToForestByParentId(docs: TreeDoc[]): ForestAndOrphans { + // Collect all the docs in a dict by id + const nodesById = <{ [_id: string]: TreeNode }>{}; + docs.forEach(doc => { + nodesById[doc._id] = { doc, children: [] }; + }); + // Assign the docs to their parent or the forest or orphanage + const forest: TreeNode[] = []; + const orphanIds: string[] = []; + docs.forEach(doc => { + const node = nodesById[doc._id]; + if (!doc.parentId) { + // Root is parent + forest.push(node); + } else if (nodesById[doc.parentId]) { + // Parent is found + nodesById[doc.parentId].children.push(node); + } else { + // Parent is missing, unset it, and store orphan + node.doc.parentId = undefined; + orphanIds.push(node.doc._id); + forest.push(node); + } + }); + return { forest, orphanIds }; +} + +export const getFilter = { + /** + * + * @param doc A document or array of documents that share a root + * @returns A query filter that finds all the ancestors of the doc(s) + */ + ancestors(doc: TreeDoc) { + return { + 'root.id': doc.root.id, + left: { $lt: doc.left }, + right: { $gt: doc.right }, + }; + }, + ancestorsOfAll(docs: Array) { + // The ancestors of no documents is a query that returns nothing + if (docs.length === 0) { + return { _id: '' }; + } + // Fallback to the simpler filter for a single document + if (docs.length === 1) { + return getFilter.ancestors(docs[0]); + } + // Build a filter that selects all ancestors + const filter = { + 'root.id': docs[0].root.id, + $or: [], + }; + docs.forEach(doc => { + filter.$or.push({ + left: { $lt: doc.left }, + right: { $gt: doc.right }, + }); + }); + return filter; + }, + descendantsOfRoot(rootId: string) { + return { + 'root.id': rootId, + } + }, + /** + * @param rootIds a non-empty array of ids + */ + descendantsOfAllRoots(rootIds: string[]) { + if (!rootIds.length) throw 'rootIds can\'t be empty'; + return { + 'root.id': { $in: rootIds }, + }; + }, + descendants(doc: TreeDoc) { + return { + 'root.id': doc.root.id, + left: { $gt: doc.left }, + right: { $lt: doc.right }, + }; + }, + descendantsOfAll(docs: Array) { + // The descendants of no documents is a query that returns nothing + if (docs.length === 0) { + return { _id: '' }; + } + // Fallback to the simpler filter for a single document + if (docs.length === 1) { + return getFilter.descendants(docs[0]); + } + // Build a filter that selects all descendants + const filter = { + 'root.id': docs[0].root.id, + $or: <{ + left: { $gt: number }, + right: { $lt: number }, + }[]>[], + }; + docs.forEach(doc => { + filter.$or.push({ + left: { $gt: doc.left }, + right: { $lt: doc.right }, + }); + }); + return filter; + }, + children(doc: TreeDoc) { + return { + 'root.id': doc.root.id, + parentId: doc._id, + }; + }, + parent(doc: TreeDoc) { + return { + _id: doc.parentId, + }; + }, +} + +/** + * Give documents new random ids and transform their references. + * Transform collections of re-IDed docs according to the collection map + */ +export function renewDocIds({ docArray, collectionMap = {}, idMap = {} }) { + // idMap is a map of {oldId: newId} + // Get a random generator that's consistent on client and server + const randomSrc = DDP.randomStream('renewDocIds'); + + // Give new ids and map the changes as {oldId: newId} + docArray.forEach(doc => { + const oldId = doc._id; + const newId = idMap[oldId] || randomSrc.id(); + doc._id = newId; + idMap[oldId] = newId; + }); + + // Get the id from the map if it exists, leave unchanged otherwise + const remap = id => idMap[id] || id + + // If there are references by id that need to be maintained when copying from + // a library, here is where we would update them + docArray.forEach(doc => { + // Remap the root and parent ids + doc.root.id = remap(doc.root.id); + doc.root.collection = collectionMap[doc.root.collection] || doc.root.collection; + doc.parentId = remap(doc.parentId); + + // Remap itemIds of items selected as ammo + doc.resource?.itemsConsumed?.forEach(itemConsumed => { + itemConsumed.itemId = remap(itemConsumed.itemId); + }); + }); +} + +/** + * Changes the doc to be a child of the parent, and then rebuilds the nested sets of the roots + * of both doc and parent + * @param doc The doc to move + * @param parent The new parent of the doc + * @param collection + * @returns + */ +export async function changeParent(doc: TreeDoc, parent: TreeDoc, collection: Mongo.Collection, order?: number) { + // Skip if we aren't changing the parent id + if (doc.parentId === parent._id) return; + + // Store the original roots + const rootChange = doc.root.id !== parent.root.id; + + // Check that the doc isn't becoming its own ancestor + if (parent.left > doc.left && parent.right < doc.right) { + throw new Meteor.Error('invalid parenting', 'A doc can\'t be its own ancestor'); + } + + // update the document's parenting and root if necessary + const update: Mongo.Modifier = { + $set: { parentId: parent._id } + }; + if (rootChange && update.$set) { + update.$set.root = parent.root; + } + if (order && update.$set) { + update.$set.left = order; + } + await collection.updateAsync(doc._id, update); + + // Rebuild the nested sets of everything on the root document(s) + rebuildNestedSets(collection, doc.root.id); + if (rootChange) { + rebuildNestedSets(collection, parent.root.id); + } +} + +export function compareOrder(docA, docB) { + // < 0 if A comes before B + // = 0 if A and B are the same order + // > 0 if B comes before A + + // They must share a root ancestor to be meaningfully sorted + if (docA.root.id !== docB.root.id) { + return 0; + } else { + return docA.left - docB.left; + } +} + +/** + * Determine if two properties have an ancestor relationship, returns true if A is an ancestor of B + * or B is an ancestor of A + */ +export function hasAncestorRelationship(propA: TreeDoc, propB: TreeDoc): boolean { + // If they don't share a root, they can't share an ancestor relationship + if (propA.root.id !== propB.root.id) { + return false; + } + // Return if there is an ancestor relationship in either direction + return isAncestor(propA, propB) || isAncestor(propB, propA); +} + +/** + * Returns true if A is a direct ancestor of B, assuming their roots are equal + */ +export function isAncestor(propA: TreeDoc, propB: TreeDoc): boolean { + return propA.left < propB.left && propA.right > propB.right; +} + +/** + * @deprecated Just set left to Number.MAX_SAFE_INTEGER instead + */ +export function setDocToLastOrder(collection: Mongo.Collection, doc: TreeDoc) { + doc.left = Number.MAX_SAFE_INTEGER; +} + +export async function rebuildNestedSets(collection: Mongo.Collection, rootId: string) { + const docs = collection.find({ + 'root.id': rootId, + removed: { $ne: true } + }, { + fields: treeDocFields, + sort: { + //Reverse sorting so that arrays can be used as stacks with the first item on top + left: 1, + }, + }).fetch(); + + const operations = calculateNestedSetOperations(docs); + + await writeBulkOperations(collection, operations); +} + +/** Calculates the operations needed to make a tree of nested sets + * Warning: Will reverse the order of docs! + * Walk around the tree numbering left on the way down and right on the way up like so: + * + * 1 Books 12 + * ┃ + * 2 Programming 11 + * ┏━━━━━━━━┻━━━━━━━━━┓ + * 3 Languages 4 5 Databases 10 + * ┏━━━━━━━┻━━━━━━━┓ + * 6 MongoDB 7 8 dbm 9 + * + * + * @param docs + * @returns + */ +export function calculateNestedSetOperations(docs: TreeDoc[]) { + const { forest: stack, orphanIds } = docsToForestByParentId(reverse(docs)); + const removeMissingParentsOp = orphanIds.length ? { + updateMany: { + filter: { _id: { $in: orphanIds } }, + update: { $unset: { parentId: 1 } }, + } + } : undefined; + const visitedNodes = new Set(); + const visitedChildren = new Set(); + const opsById: { [_id: string]: any } = {} + let count = 1; + + while (stack.length) { + const top = stack[stack.length - 1]; + if (visitedNodes.has(top)) { + // We've arrived at this node again for some reason, this shouldn't happen + console.log('visited already, parent loop maybe?') + stack.pop(); + } else if (visitedChildren.has(top)) { + // We've arrived at this node after visiting the children, + // we must be on the way up, mark the right number + visitedNodes.add(top); + stack.pop(); + if (top.doc.right !== count) { + opsById[top.doc._id].updateOne.update.$set.right = count; + } + count += 1; + } else { + // We're arriving at this node for the first time + // We must be on the way down, mark the left number and go visit the children + visitedChildren.add(top); + stack.push(...top.children); + if (top.doc.left !== count) { + opsById[top.doc._id] = { + updateOne: { + filter: { _id: top.doc._id }, + update: { $set: { left: count } }, + } + }; + } + count += 1; + } + } + + const operations = [...Object.values(opsById)]; + if (removeMissingParentsOp) operations.push(removeMissingParentsOp); + return operations; +} + +/** + * Same as calculateNestedSetOperations, but applies the ops to the properties + * Mostly used to create testing documents. + * @param docs + * @returns + */ +export function applyNestedSetProperties(docs: TreeDoc[]) { + // Walk around the tree numbering left on the way down and right on the way up like so: + const { forest: stack, orphanIds } = docsToForestByParentId(reverse([...docs])); + + const visitedNodes = new Set(); + const visitedChildren = new Set(); + let count = 1; + + while (stack.length) { + const top = stack[stack.length - 1]; + if (orphanIds.includes(top.doc._id)) { + delete top.doc.parentId; + } + if (visitedNodes.has(top)) { + // We've arrived at this node again for some reason, this shouldn't happen + console.log('visited already, parent loop maybe?') + stack.pop(); + } else if (visitedChildren.has(top)) { + // We've arrived at this node after visiting the children, + // we must be on the way up, mark the right number + visitedNodes.add(top); + stack.pop(); + if (top.doc.right !== count) { + top.doc.right = count; + } + count += 1; + } else { + // We're arriving at this node for the first time + // We must be on the way down, mark the left number and go visit the children + visitedChildren.add(top); + stack.push(...top.children); + if (top.doc.left !== count) { + top.doc.left = count; + } + count += 1; + } + } +} + +/** + * Write some number of bulk operations to the collection, uses a bulk write on the server + * and iterates through regular updates on the client + * Resolves once all writes have completed + * @param collection The collection to write to + * @param operations An array of bulk operations to write + * @returns Promise + */ +async function writeBulkOperations(collection: Mongo.Collection, operations) { + if (Meteor.isServer && operations.length) { + return new Promise((resolve, reject) => { + collection.rawCollection().bulkWrite( + operations, + { ordered: false }, + function (e) { + if (e) { + reject(e); + } else { + resolve(undefined); + } + } + ); + }); + } else { + const promises = operations.map(op => { + if (op.updateOne) { + return collection.updateAsync( + op.updateOne.filter, + op.updateOne.update, + { selector: { type: 'any' } } + ); + } else if (op.updateMany) { + return collection.updateAsync( + op.updateMany.filter, + op.updateMany.update, + { + selector: { type: 'any' }, + multi: true, + }, + ) + } + }); + return Promise.all(promises); + } +} diff --git a/app/imports/api/parenting/softRemove.js b/app/imports/api/parenting/softRemove.js deleted file mode 100644 index 099abfc2..00000000 --- a/app/imports/api/parenting/softRemove.js +++ /dev/null @@ -1,74 +0,0 @@ -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import { updateDescendants } from '/imports/api/parenting/parenting.js'; - -export function softRemove({_id, collection}){ - let removalDate = new Date(); - if (typeof collection === 'string') { - collection = getCollectionByName(collection); - } - // Remove this document - collection.update( - _id, { - $set: { - removed: true, - removedAt: removalDate, - }, - $unset: { - removedWith: 1, - } - }, { - selector: {type: 'any'}, - }, - ); - // Remove all the descendants that have not yet been removed, and set them to be - // removed with this document - updateDescendants({ - collection, - ancestorId: _id, - filter: {removed: {$ne: true}}, - modifier: {$set: { - removed: true, - removedAt: removalDate, - removedWith: _id, - }}, - }); -} - -const restoreError = function(){ - throw new Meteor.Error('restore-failed', - 'Could not restore this document, maybe it was removed by a parent?' - ); -}; - -export function restore({ _id, collection, extraUpdates}){ - if (typeof collection === 'string') { - collection = getCollectionByName(collection); - } - const update = { - $unset: { - removed: 1, - removedAt: 1, - }, - ...extraUpdates - } - - let numUpdated = collection.update({ - _id, - removedWith: {$exists: false} - }, update , { - selector: {type: 'any'}, - },); - if (numUpdated === 0) restoreError(); - updateDescendants({ - collection, - ancestorId: _id, - filter: { - removedWith: _id, - }, - modifier: { $unset: { - removed: 1, - removedAt: 1, - removedWith: 1, - }}, - }); -} diff --git a/app/imports/api/parenting/softRemove.ts b/app/imports/api/parenting/softRemove.ts new file mode 100644 index 00000000..10c49523 --- /dev/null +++ b/app/imports/api/parenting/softRemove.ts @@ -0,0 +1,90 @@ +import { getCollectionByName, getFilter } from '/imports/api/parenting/parentingFunctions'; +import { TreeDoc } from '/imports/api/parenting/ChildSchema'; + +export async function softRemove(collection: Mongo.Collection | string, doc?: TreeDoc | string) { + const removalDate = new Date(); + if (typeof collection === 'string') { + collection = getCollectionByName(collection); + } + if (typeof doc === 'string') { + doc = await collection.findOneAsync(doc); + } + if (!doc) { + throw new Meteor.Error('not found', 'The document to remove was not found'); + } + // Remove this document + const removeDocPromise = collection.updateAsync( + doc._id, + { + $set: { + removed: true, + removedAt: removalDate, + }, + $unset: { + removedWith: 1, + } + }, { + selector: { type: 'any' }, + }, + ); + // Remove all the descendants that have not yet been removed, and set them to be + // removed with this document + const removeDescendantsPromise = collection.updateAsync({ + ...getFilter.descendants(doc), + removed: { $ne: true }, + }, { + $set: { + removed: true, + removedAt: removalDate, + removedWith: doc._id, + } + }, { + selector: { type: 'any' }, + multi: true, + }); + return Promise.all([removeDocPromise, removeDescendantsPromise]); +} + +const restoreError = function () { + throw new Meteor.Error('restore-failed', + 'Could not restore this document, maybe it was removed by a parent?' + ); +}; + +export async function restore(collection: Mongo.Collection | string, doc: TreeDoc | string, extraUpdates?) { + if (typeof collection === 'string') { + collection = getCollectionByName(collection); + } + if (typeof doc === 'string') { + const foundDoc = await collection.findOneAsync(doc) + if (!foundDoc) { + throw new Meteor.Error('not found', 'The document to remove was not found'); + } + doc = foundDoc; + } + const numUpdated: number = await collection.updateAsync({ + _id: doc._id, + removedWith: { $exists: false } + }, { + $unset: { + removed: 1, + removedAt: 1, + }, + ...extraUpdates + }, { + selector: { type: 'any' }, + }); + if (numUpdated === 0) restoreError(); + return collection.updateAsync({ + removedWith: doc._id, + }, { + $unset: { + removed: 1, + removedAt: 1, + removedWith: 1, + } + }, { + selector: { type: 'any' }, + multi: true, + }); +} diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.ts similarity index 79% rename from app/imports/api/properties/Actions.js rename to app/imports/api/properties/Actions.ts index 163d938d..0f7c6510 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.ts @@ -1,13 +1,60 @@ import SimpleSchema from 'simpl-schema'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import { storedIconsSchema } from '/imports/api/icons/Icons.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import { storedIconsSchema } from '/imports/api/icons/Icons'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { InlineCalculation } from '/imports/api/properties/subSchemas/inlineCalculationField'; +import { CalculatedField } from '/imports/api/properties/subSchemas/computedField'; + +export interface Action extends ActionBase { + type: 'action' +} + +export interface ActionBase extends CreatureProperty { + name?: string + summary?: InlineCalculation + description?: InlineCalculation + actionType: 'action' | 'bonus' | 'attack' | 'reaction' | 'free' | 'long' | 'event' + variableName?: string + target: 'self' | 'singleTarget' | 'multipleTargets' + attackRoll?: CalculatedField + uses?: CalculatedField + usesUsed?: number + reset?: string + silent?: boolean + insufficientResources?: boolean + usesLeft?: number + overridden?: boolean + // Resources + resources: { + itemsConsumed: { + _id: string + tag?: string + itemName?: string + quantity?: CalculatedField + itemId?: string + available?: number + }[] + attributesConsumed: { + _id: string + variableName?: string + quantity?: CalculatedField + available?: number + statName?: string + }[] + conditions?: { + _id: string, + condition?: CalculatedField + conditionNote?: string, + }[] + } +} /* * Actions are things a character can do */ -let ActionSchema = createPropertySchema({ +const ActionSchema = createPropertySchema({ name: { type: String, optional: true, diff --git a/app/imports/api/properties/Adjustments.js b/app/imports/api/properties/Adjustments.js index 6b2607f5..8521063c 100644 --- a/app/imports/api/properties/Adjustments.js +++ b/app/imports/api/properties/Adjustments.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; const AdjustmentSchema = createPropertySchema({ // The roll that determines how much to change the attribute diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index 33fa4ca5..0bac605a 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -1,7 +1,7 @@ import SimpleSchema from 'simpl-schema'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; /* * Attributes are numbered stats of a character diff --git a/app/imports/api/properties/Branches.js b/app/imports/api/properties/Branches.js index 1677b87b..fc68edb2 100644 --- a/app/imports/api/properties/Branches.js +++ b/app/imports/api/properties/Branches.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let BranchSchema = createPropertySchema({ branchType: { diff --git a/app/imports/api/properties/BuffRemovers.js b/app/imports/api/properties/BuffRemovers.js index b96466cb..8adce152 100644 --- a/app/imports/api/properties/BuffRemovers.js +++ b/app/imports/api/properties/BuffRemovers.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let BuffRemoverSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Buffs.js b/app/imports/api/properties/Buffs.js index 386c402b..dab5594b 100644 --- a/app/imports/api/properties/Buffs.js +++ b/app/imports/api/properties/Buffs.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let BuffSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/ClassLevels.js b/app/imports/api/properties/ClassLevels.js index 5fd8a702..30c75cb5 100644 --- a/app/imports/api/properties/ClassLevels.js +++ b/app/imports/api/properties/ClassLevels.js @@ -1,7 +1,7 @@ import SimpleSchema from 'simpl-schema'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; const ClassLevelSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Classes.js b/app/imports/api/properties/Classes.js index e6e6471f..8242b1bc 100644 --- a/app/imports/api/properties/Classes.js +++ b/app/imports/api/properties/Classes.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; // Classes are like slots, except they only take class levels and enforce that // lower levels are taken before higher levels @@ -40,7 +40,7 @@ let ClassSchema = createPropertySchema({ 'extraTags.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, - autoValue(){ + autoValue() { if (!this.isSet) return Random.id(); } }, @@ -74,7 +74,7 @@ const ComputedOnlyClassSchema = createPropertySchema({ type: 'computedOnlyField', optional: true, }, - + // Denormalised fields level: { type: SimpleSchema.Integer, diff --git a/app/imports/api/properties/Constants.js b/app/imports/api/properties/Constants.js index ded0f090..9b70bfca 100644 --- a/app/imports/api/properties/Constants.js +++ b/app/imports/api/properties/Constants.js @@ -1,12 +1,12 @@ import SimpleSchema from 'simpl-schema'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; import { parse, prettifyParseError, -} from '/imports/parser/parser.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import resolve, { Context, traverse } from '/imports/parser/resolve.js'; +} from '/imports/parser/parser'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import resolve, { Context, traverse } from '/imports/parser/resolve'; /* * Constants are primitive values that can be used elsewhere in computations diff --git a/app/imports/api/properties/Containers.js b/app/imports/api/properties/Containers.js index 6936bd15..f840a0bf 100644 --- a/app/imports/api/properties/Containers.js +++ b/app/imports/api/properties/Containers.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let ContainerSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/DamageMultipliers.js b/app/imports/api/properties/DamageMultipliers.js index 3c75fbbf..c76f104f 100644 --- a/app/imports/api/properties/DamageMultipliers.js +++ b/app/imports/api/properties/DamageMultipliers.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; /* * DamageMultipliers are multipliers that affect how much damage is taken from diff --git a/app/imports/api/properties/Damages.js b/app/imports/api/properties/Damages.js index b5510444..34ee997c 100644 --- a/app/imports/api/properties/Damages.js +++ b/app/imports/api/properties/Damages.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import { SavingThrowSchema, ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import { SavingThrowSchema, ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows'; const DamageSchema = createPropertySchema({ // The roll that determines how much to damage the attribute diff --git a/app/imports/api/properties/Effects.js b/app/imports/api/properties/Effects.js index 77eadbeb..347a4000 100644 --- a/app/imports/api/properties/Effects.js +++ b/app/imports/api/properties/Effects.js @@ -1,7 +1,7 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema'; /* * Effects are reason-value attached to skills and abilities diff --git a/app/imports/api/properties/Features.js b/app/imports/api/properties/Features.js index 91fc95b6..e4b467c1 100644 --- a/app/imports/api/properties/Features.js +++ b/app/imports/api/properties/Features.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let FeatureSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Folders.js b/app/imports/api/properties/Folders.js index 626b07ed..96bc2840 100644 --- a/app/imports/api/properties/Folders.js +++ b/app/imports/api/properties/Folders.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; // Folders organize a character sheet into a tree, particularly to group things // like 'race' and 'background' diff --git a/app/imports/api/properties/Items.js b/app/imports/api/properties/Items.ts similarity index 84% rename from app/imports/api/properties/Items.js rename to app/imports/api/properties/Items.ts index b4bb7319..02e0e0c9 100644 --- a/app/imports/api/properties/Items.js +++ b/app/imports/api/properties/Items.ts @@ -1,6 +1,14 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; + +export interface Item extends CreatureProperty { + type: 'item' + name?: string + plural?: string + quantity: number +} const ItemSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Notes.js b/app/imports/api/properties/Notes.js index 1e738b08..4aa66a6e 100644 --- a/app/imports/api/properties/Notes.js +++ b/app/imports/api/properties/Notes.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let NoteSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/PointBuys.js b/app/imports/api/properties/PointBuys.js index fcbb8f6e..5c45aab4 100644 --- a/app/imports/api/properties/PointBuys.js +++ b/app/imports/api/properties/PointBuys.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; /* * PointBuys are reason-value attached to skills and abilities diff --git a/app/imports/api/properties/Proficiencies.js b/app/imports/api/properties/Proficiencies.js index cec32217..fe48b01b 100644 --- a/app/imports/api/properties/Proficiencies.js +++ b/app/imports/api/properties/Proficiencies.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema'; let ProficiencySchema = new SimpleSchema({ name: { diff --git a/app/imports/api/properties/References.js b/app/imports/api/properties/References.js index d6ece6e6..31459cdf 100644 --- a/app/imports/api/properties/References.js +++ b/app/imports/api/properties/References.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let ReferenceSchema = new SimpleSchema({ ref: { diff --git a/app/imports/api/properties/Rolls.js b/app/imports/api/properties/Rolls.js index 7a2c5eeb..b97dc498 100644 --- a/app/imports/api/properties/Rolls.js +++ b/app/imports/api/properties/Rolls.js @@ -1,7 +1,7 @@ import SimpleSchema from 'simpl-schema'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; /** * Rolls are children to actions or other rolls, they are triggered with 0 or diff --git a/app/imports/api/properties/SavingThrows.js b/app/imports/api/properties/SavingThrows.js index d0045187..d53c7273 100644 --- a/app/imports/api/properties/SavingThrows.js +++ b/app/imports/api/properties/SavingThrows.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; // These are the rolls made when saves are called for // For the saving throw bonus or proficiency, see ./Skills.js diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index 4f677417..dc24ca92 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; -import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema'; /* * Skills are anything that results in a modifier to be added to a D20 diff --git a/app/imports/api/properties/Slots.js b/app/imports/api/properties/Slots.js index 310ba198..fd41c36c 100644 --- a/app/imports/api/properties/Slots.js +++ b/app/imports/api/properties/Slots.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let SlotSchema = createPropertySchema({ name: { @@ -37,7 +37,7 @@ let SlotSchema = createPropertySchema({ 'extraTags.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, - autoValue(){ + autoValue() { if (!this.isSet) return Random.id(); } }, diff --git a/app/imports/api/properties/SpellLists.js b/app/imports/api/properties/SpellLists.js index 285832eb..e99c6ff5 100644 --- a/app/imports/api/properties/SpellLists.js +++ b/app/imports/api/properties/SpellLists.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; let SpellListSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Spells.js b/app/imports/api/properties/Spells.ts similarity index 73% rename from app/imports/api/properties/Spells.js rename to app/imports/api/properties/Spells.ts index 575c9b5e..d378f2ff 100644 --- a/app/imports/api/properties/Spells.js +++ b/app/imports/api/properties/Spells.ts @@ -1,6 +1,30 @@ -import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js'; +import { ActionBase, ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions'; import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; + +export interface Spell extends ActionBase { + name?: string + type: 'spell' + alwaysPrepared?: boolean + prepared?: boolean + castWithoutSpellSlots?: boolean + hasAttackRoll?: boolean + castingTime?: string + range?: string + duration?: string + verbal?: boolean + somatic?: boolean + concentration?: boolean + material?: string + ritual?: boolean + level?: number + school?: string +} + +export function isSpell(prop: CreatureProperty): prop is Spell { + return prop.type === 'spell'; +} const magicSchools = [ 'abjuration', diff --git a/app/imports/api/properties/Toggles.js b/app/imports/api/properties/Toggles.js index c5babdc6..e52f6e49 100644 --- a/app/imports/api/properties/Toggles.js +++ b/app/imports/api/properties/Toggles.js @@ -1,7 +1,7 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema'; const ToggleSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Triggers.js b/app/imports/api/properties/Triggers.js index 09d777e2..1dea7daa 100644 --- a/app/imports/api/properties/Triggers.js +++ b/app/imports/api/properties/Triggers.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; const eventOptions = { doActionProperty: 'Do action', diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index 89cb21e4..a70304ed 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -1,32 +1,32 @@ import SimpleSchema from 'simpl-schema'; -import { ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js'; -import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustments.js'; -import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js'; -import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js'; -import { ComputedOnlyBuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js'; -import { ComputedOnlyBranchSchema } from '/imports/api/properties/Branches.js'; -import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes.js'; -import { ComputedOnlyClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; -import { ComputedOnlyConstantSchema } from '/imports/api/properties/Constants.js'; -import { ComputedOnlyContainerSchema } from '/imports/api/properties/Containers.js'; -import { ComputedOnlyDamageSchema } from '/imports/api/properties/Damages.js'; -import { ComputedOnlyDamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js'; -import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects.js'; -import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features.js'; -import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders.js'; -import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js'; -import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js'; -import { ComputedOnlyPointBuySchema } from '/imports/api/properties/PointBuys.js'; -import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies.js'; -import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References.js'; -import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js'; -import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; -import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js'; -import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots.js'; -import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js'; -import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js'; -import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js'; -import { ComputedOnlyTriggerSchema } from '/imports/api/properties/Triggers.js'; +import { ComputedOnlyActionSchema } from '/imports/api/properties/Actions'; +import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustments'; +import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes'; +import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs'; +import { ComputedOnlyBuffRemoverSchema } from '/imports/api/properties/BuffRemovers'; +import { ComputedOnlyBranchSchema } from '/imports/api/properties/Branches'; +import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes'; +import { ComputedOnlyClassLevelSchema } from '/imports/api/properties/ClassLevels'; +import { ComputedOnlyConstantSchema } from '/imports/api/properties/Constants'; +import { ComputedOnlyContainerSchema } from '/imports/api/properties/Containers'; +import { ComputedOnlyDamageSchema } from '/imports/api/properties/Damages'; +import { ComputedOnlyDamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers'; +import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects'; +import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features'; +import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders'; +import { ComputedOnlyItemSchema } from '/imports/api/properties/Items'; +import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes'; +import { ComputedOnlyPointBuySchema } from '/imports/api/properties/PointBuys'; +import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies'; +import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References'; +import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls'; +import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows'; +import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills'; +import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots'; +import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells'; +import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists'; +import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles'; +import { ComputedOnlyTriggerSchema } from '/imports/api/properties/Triggers'; const propertySchemasIndex = { action: ComputedOnlyActionSchema, diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index b6adaaa2..775aa504 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -1,32 +1,32 @@ import SimpleSchema from 'simpl-schema'; -import { ComputedActionSchema } from '/imports/api/properties/Actions.js'; -import { ComputedAdjustmentSchema } from '/imports/api/properties/Adjustments.js'; -import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js'; -import { ComputedBuffSchema } from '/imports/api/properties/Buffs.js'; -import { ComputedBuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js'; -import { ComputedBranchSchema } from '/imports/api/properties/Branches.js'; -import { ComputedClassSchema } from '/imports/api/properties/Classes.js'; -import { ComputedClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; -import { ConstantSchema } from '/imports/api/properties/Constants.js'; -import { ComputedContainerSchema } from '/imports/api/properties/Containers.js'; -import { ComputedDamageSchema } from '/imports/api/properties/Damages.js'; -import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js'; -import { ComputedEffectSchema } from '/imports/api/properties/Effects.js'; -import { ComputedFeatureSchema } from '/imports/api/properties/Features.js'; -import { ComputedFolderSchema } from '/imports/api/properties/Folders.js'; -import { ComputedItemSchema } from '/imports/api/properties/Items.js'; -import { ComputedNoteSchema } from '/imports/api/properties/Notes.js'; -import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys.js'; -import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; -import { ReferenceSchema } from '/imports/api/properties/References.js'; -import { ComputedRollSchema } from '/imports/api/properties/Rolls.js'; -import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; -import { ComputedSkillSchema } from '/imports/api/properties/Skills.js'; -import { ComputedSlotSchema } from '/imports/api/properties/Slots.js'; -import { ComputedSpellSchema } from '/imports/api/properties/Spells.js'; -import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js'; -import { ComputedToggleSchema } from '/imports/api/properties/Toggles.js'; -import { ComputedTriggerSchema } from '/imports/api/properties/Triggers.js'; +import { ComputedActionSchema } from '/imports/api/properties/Actions'; +import { ComputedAdjustmentSchema } from '/imports/api/properties/Adjustments'; +import { ComputedAttributeSchema } from '/imports/api/properties/Attributes'; +import { ComputedBuffSchema } from '/imports/api/properties/Buffs'; +import { ComputedBuffRemoverSchema } from '/imports/api/properties/BuffRemovers'; +import { ComputedBranchSchema } from '/imports/api/properties/Branches'; +import { ComputedClassSchema } from '/imports/api/properties/Classes'; +import { ComputedClassLevelSchema } from '/imports/api/properties/ClassLevels'; +import { ConstantSchema } from '/imports/api/properties/Constants'; +import { ComputedContainerSchema } from '/imports/api/properties/Containers'; +import { ComputedDamageSchema } from '/imports/api/properties/Damages'; +import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers'; +import { ComputedEffectSchema } from '/imports/api/properties/Effects'; +import { ComputedFeatureSchema } from '/imports/api/properties/Features'; +import { ComputedFolderSchema } from '/imports/api/properties/Folders'; +import { ComputedItemSchema } from '/imports/api/properties/Items'; +import { ComputedNoteSchema } from '/imports/api/properties/Notes'; +import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys'; +import { ProficiencySchema } from '/imports/api/properties/Proficiencies'; +import { ReferenceSchema } from '/imports/api/properties/References'; +import { ComputedRollSchema } from '/imports/api/properties/Rolls'; +import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows'; +import { ComputedSkillSchema } from '/imports/api/properties/Skills'; +import { ComputedSlotSchema } from '/imports/api/properties/Slots'; +import { ComputedSpellSchema } from '/imports/api/properties/Spells'; +import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists'; +import { ComputedToggleSchema } from '/imports/api/properties/Toggles'; +import { ComputedTriggerSchema } from '/imports/api/properties/Triggers'; const propertySchemasIndex = { action: ComputedActionSchema, diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index ed0a3a22..4f56a373 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -1,32 +1,32 @@ import SimpleSchema from 'simpl-schema'; -import { ActionSchema } from '/imports/api/properties/Actions.js'; -import { AdjustmentSchema } from '/imports/api/properties/Adjustments.js'; -import { AttributeSchema } from '/imports/api/properties/Attributes.js'; -import { BuffSchema } from '/imports/api/properties/Buffs.js'; -import { BuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js'; -import { BranchSchema } from '/imports/api/properties/Branches.js'; -import { ClassSchema } from '/imports/api/properties/Classes.js'; -import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; -import { ConstantSchema } from '/imports/api/properties/Constants.js'; -import { DamageSchema } from '/imports/api/properties/Damages.js'; -import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js'; -import { EffectSchema } from '/imports/api/properties/Effects.js'; -import { FeatureSchema } from '/imports/api/properties/Features.js'; -import { FolderSchema } from '/imports/api/properties/Folders.js'; -import { NoteSchema } from '/imports/api/properties/Notes.js'; -import { PointBuySchema } from '/imports/api/properties/PointBuys.js'; -import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; -import { ReferenceSchema } from '/imports/api/properties/References.js'; -import { RollSchema } from '/imports/api/properties/Rolls.js'; -import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; -import { SkillSchema } from '/imports/api/properties/Skills.js'; -import { SlotSchema } from '/imports/api/properties/Slots.js'; -import { SpellListSchema } from '/imports/api/properties/SpellLists.js'; -import { SpellSchema } from '/imports/api/properties/Spells.js'; -import { ToggleSchema } from '/imports/api/properties/Toggles.js'; -import { TriggerSchema } from '/imports/api/properties/Triggers.js'; -import { ContainerSchema } from '/imports/api/properties/Containers.js'; -import { ItemSchema } from '/imports/api/properties/Items.js'; +import { ActionSchema } from '/imports/api/properties/Actions'; +import { AdjustmentSchema } from '/imports/api/properties/Adjustments'; +import { AttributeSchema } from '/imports/api/properties/Attributes'; +import { BuffSchema } from '/imports/api/properties/Buffs'; +import { BuffRemoverSchema } from '/imports/api/properties/BuffRemovers'; +import { BranchSchema } from '/imports/api/properties/Branches'; +import { ClassSchema } from '/imports/api/properties/Classes'; +import { ClassLevelSchema } from '/imports/api/properties/ClassLevels'; +import { ConstantSchema } from '/imports/api/properties/Constants'; +import { DamageSchema } from '/imports/api/properties/Damages'; +import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers'; +import { EffectSchema } from '/imports/api/properties/Effects'; +import { FeatureSchema } from '/imports/api/properties/Features'; +import { FolderSchema } from '/imports/api/properties/Folders'; +import { NoteSchema } from '/imports/api/properties/Notes'; +import { PointBuySchema } from '/imports/api/properties/PointBuys'; +import { ProficiencySchema } from '/imports/api/properties/Proficiencies'; +import { ReferenceSchema } from '/imports/api/properties/References'; +import { RollSchema } from '/imports/api/properties/Rolls'; +import { SavingThrowSchema } from '/imports/api/properties/SavingThrows'; +import { SkillSchema } from '/imports/api/properties/Skills'; +import { SlotSchema } from '/imports/api/properties/Slots'; +import { SpellListSchema } from '/imports/api/properties/SpellLists'; +import { SpellSchema } from '/imports/api/properties/Spells'; +import { ToggleSchema } from '/imports/api/properties/Toggles'; +import { TriggerSchema } from '/imports/api/properties/Triggers'; +import { ContainerSchema } from '/imports/api/properties/Containers'; +import { ItemSchema } from '/imports/api/properties/Items'; const propertySchemasIndex = { action: ActionSchema, diff --git a/app/imports/api/properties/subSchemas/ErrorSchema.js b/app/imports/api/properties/subSchemas/ErrorSchema.js index 41635874..636281e6 100644 --- a/app/imports/api/properties/subSchemas/ErrorSchema.js +++ b/app/imports/api/properties/subSchemas/ErrorSchema.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; const ErrorSchema = new SimpleSchema({ message: { diff --git a/app/imports/api/properties/subSchemas/InlineComputationSchema.js b/app/imports/api/properties/subSchemas/InlineComputationSchema.js index 14a33e75..a52120c2 100644 --- a/app/imports/api/properties/subSchemas/InlineComputationSchema.js +++ b/app/imports/api/properties/subSchemas/InlineComputationSchema.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; const InlineComputationSchema = new SimpleSchema({ // The part between bracers {} diff --git a/app/imports/api/properties/subSchemas/RollDetailsSchema.js b/app/imports/api/properties/subSchemas/RollDetailsSchema.js index 44f39e5e..9ea3f279 100644 --- a/app/imports/api/properties/subSchemas/RollDetailsSchema.js +++ b/app/imports/api/properties/subSchemas/RollDetailsSchema.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; const RollDetailsSchema = new SimpleSchema({ number: { diff --git a/app/imports/api/properties/subSchemas/RollResultsSchema.js b/app/imports/api/properties/subSchemas/RollResultsSchema.js index 8aea32e9..b15746f3 100644 --- a/app/imports/api/properties/subSchemas/RollResultsSchema.js +++ b/app/imports/api/properties/subSchemas/RollResultsSchema.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import ResultsSchema from '/imports/api/properties/subSchemas/ResultsSchema.js'; +import ResultsSchema from '/imports/api/properties/subSchemas/ResultsSchema'; let RollResultsSchema = new SimpleSchema({ _id: { diff --git a/app/imports/api/properties/subSchemas/TagTargetingSchema.js b/app/imports/api/properties/subSchemas/TagTargetingSchema.js index 8bbe1ddb..ea33d758 100644 --- a/app/imports/api/properties/subSchemas/TagTargetingSchema.js +++ b/app/imports/api/properties/subSchemas/TagTargetingSchema.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; const TagTargetingSchema = new SimpleSchema({ // True when targeting by tags instead of stats diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.ts similarity index 93% rename from app/imports/api/properties/subSchemas/computedField.js rename to app/imports/api/properties/subSchemas/computedField.ts index 9e1197e2..cba0c0da 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.ts @@ -1,6 +1,16 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; + +export interface CalculatedField { + calculation?: string, + value?: string | number, + effectIds?: string[], + parseNode?: any, + parseError?: any, + hash?: number, + errors?: any[], +} // Get schemas that apply fields directly so they can be gracefully extended // because {type: Schema} fields can't be extended diff --git a/app/imports/api/properties/subSchemas/createPropertySchema.js b/app/imports/api/properties/subSchemas/createPropertySchema.js index a866295c..2d2d6b11 100644 --- a/app/imports/api/properties/subSchemas/createPropertySchema.js +++ b/app/imports/api/properties/subSchemas/createPropertySchema.js @@ -1,17 +1,17 @@ import { inlineCalculationFieldToCompute, computedOnlyInlineCalculationField, -} from '/imports/api/properties/subSchemas/inlineCalculationField.js'; +} from '/imports/api/properties/subSchemas/inlineCalculationField'; import { fieldToCompute, computedOnlyField, -} from '/imports/api/properties/subSchemas/computedField.js'; +} from '/imports/api/properties/subSchemas/computedField'; import SimpleSchema from 'simpl-schema'; // Search through the schema for keys whose type is 'fieldToCompute' etc. // replace the type with Object and attach extend the schema with // the required fields to make the computation work -export default function createPropertySchema(definition){ +export default function createPropertySchema(definition) { const computationFields = { inlineCalculationFieldToCompute: [], computedOnlyInlineCalculationField: [], @@ -20,18 +20,18 @@ export default function createPropertySchema(definition){ }; const computedKeys = Object.keys(computationFields); - for (let key in definition){ + for (let key in definition) { const def = definition[key]; - if (computedKeys.includes(def.type)){ + if (computedKeys.includes(def.type)) { computationFields[def.type].push(key); applyDefaultCalculationValue(definition, key); def.type = Object; - if (!def.optional){ + if (!def.optional) { console.warn( `computed field: '${key}' of '${def.type}' is expected to be optional` ); } - if (def.removeBeforeCompute){ + if (def.removeBeforeCompute) { console.warn( `computed field: '${key}' of '${def.type}' should not be removed before computation` ) @@ -58,12 +58,12 @@ export default function createPropertySchema(definition){ return schema } -function applyDefaultCalculationValue(definition, key){ +function applyDefaultCalculationValue(definition, key) { const def = definition[key]; if ( def.type === 'computedOnlyField' || def.type === 'computedOnlyInlineCalculationField' - ){ + ) { // don't apply defaults to computed only fields // because it would add the calculation field which should only appear // on the fields to compute @@ -72,12 +72,12 @@ function applyDefaultCalculationValue(definition, key){ let defaultValue = def.defaultValue; if (!defaultValue) return; let calcField; - if (def.type === 'fieldToCompute'){ + if (def.type === 'fieldToCompute') { calcField = key + '.calculation' } else { calcField = key + '.text' } - if (definition[calcField]){ + if (definition[calcField]) { definition[calcField].defaultValue = defaultValue; } else { definition[calcField] = { diff --git a/app/imports/api/properties/subSchemas/inlineCalculationField.js b/app/imports/api/properties/subSchemas/inlineCalculationField.ts similarity index 86% rename from app/imports/api/properties/subSchemas/inlineCalculationField.js rename to app/imports/api/properties/subSchemas/inlineCalculationField.ts index 22e37654..c50ea24d 100644 --- a/app/imports/api/properties/subSchemas/inlineCalculationField.js +++ b/app/imports/api/properties/subSchemas/inlineCalculationField.ts @@ -1,10 +1,18 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; +import { CalculatedField } from './computedField'; + +export interface InlineCalculation { + text?: string, + hash?: number, + value?: string, + inlineCalculations: CalculatedField[], +} // Get schemas that apply fields directly so they can be gracefully extended // because {type: Schema} fields can't be extended -function inlineCalculationFieldToCompute(field){ +function inlineCalculationFieldToCompute(field) { return new SimpleSchema({ // The object should already be set, but set again just in case [field]: { @@ -20,7 +28,7 @@ function inlineCalculationFieldToCompute(field){ }); } -function computedOnlyInlineCalculationField(field){ +function computedOnlyInlineCalculationField(field) { return new SimpleSchema({ // The object should already be set, but set again just in case [field]: { @@ -30,9 +38,8 @@ function computedOnlyInlineCalculationField(field){ }, // a hash of the text to see if the current cached values need to be updated [`${field}.hash`]: { - type: String, + type: Number, optional: true, - max: STORAGE_LIMITS.inlineCalculationField, }, [`${field}.value`]: { type: String, @@ -90,7 +97,7 @@ function computedOnlyInlineCalculationField(field){ }); } -function computedInlineCalculationField(field){ +function computedInlineCalculationField(field) { return inlineCalculationFieldToCompute(field).extend( computedOnlyInlineCalculationField(field) ) diff --git a/app/imports/api/sharing/SharingSchema.js b/app/imports/api/sharing/SharingSchema.js index 629f90bb..695d9674 100644 --- a/app/imports/api/sharing/SharingSchema.js +++ b/app/imports/api/sharing/SharingSchema.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import '/imports/api/sharing/sharing.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import '/imports/api/sharing/sharing'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; let SharingSchema = new SimpleSchema({ owner: { diff --git a/app/imports/api/sharing/sharing.js b/app/imports/api/sharing/sharing.js index 7062ff40..89222dbf 100644 --- a/app/imports/api/sharing/sharing.js +++ b/app/imports/api/sharing/sharing.js @@ -1,11 +1,10 @@ import SimpleSchema from 'simpl-schema'; -import { assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; +import { assertOwnership } from '/imports/api/sharing/sharingPermissions'; +import { getCollectionByName, fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; +import { RefSchema } from '/imports/api/parenting/ChildSchema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { getUserTier } from '/imports/api/users/patreon/tiers.js'; +import { getUserTier } from '/imports/api/users/patreon/tiers'; const setPublic = new ValidatedMethod({ name: 'sharing.setPublic', diff --git a/app/imports/api/sharing/sharingPermissions.js b/app/imports/api/sharing/sharingPermissions.js index c01675f4..8c600f92 100644 --- a/app/imports/api/sharing/sharingPermissions.js +++ b/app/imports/api/sharing/sharingPermissions.js @@ -1,5 +1,5 @@ import { includes } from 'lodash'; -import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; +import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; function assertIdValid(userId) { if (!userId || typeof userId !== 'string') { @@ -98,8 +98,8 @@ export function assertCopyPermission(doc, userId) { function getRoot(doc) { assertdocExists(doc); - if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) { - return fetchDocByRef(doc.ancestors[0]); + if (doc.root) { + return fetchDocByRef(doc.root); } else { return doc; } diff --git a/app/imports/api/tabletop/Messages.js b/app/imports/api/tabletop/Messages.js index 634f32d9..07cd270a 100644 --- a/app/imports/api/tabletop/Messages.js +++ b/app/imports/api/tabletop/Messages.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import Tabletops, { assertUserInTabletop } from '/imports/api/tabletop/Tabletops.js'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import Tabletops, { assertUserInTabletop } from '/imports/api/tabletop/Tabletops'; let Messages = new Mongo.Collection('messages'); diff --git a/app/imports/api/tabletop/Tabletops.js b/app/imports/api/tabletop/Tabletops.js index 5c81986c..21456580 100644 --- a/app/imports/api/tabletop/Tabletops.js +++ b/app/imports/api/tabletop/Tabletops.js @@ -48,8 +48,8 @@ let TabletopSchema = new SimpleSchema({ Tabletops.attachSchema(TabletopSchema); -import '/imports/api/tabletop/methods/removeTabletop.js'; -import '/imports/api/tabletop/methods/insertTabletop.js'; -import '/imports/api/tabletop/methods/addCreaturesToTabletop.js'; +import '/imports/api/tabletop/methods/removeTabletop'; +import '/imports/api/tabletop/methods/insertTabletop'; +import '/imports/api/tabletop/methods/addCreaturesToTabletop'; export default Tabletops; diff --git a/app/imports/api/tabletop/methods/addCreaturesToTabletop.js b/app/imports/api/tabletop/methods/addCreaturesToTabletop.js index b17b953b..babea992 100644 --- a/app/imports/api/tabletop/methods/addCreaturesToTabletop.js +++ b/app/imports/api/tabletop/methods/addCreaturesToTabletop.js @@ -1,10 +1,10 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertUserInTabletop } from './shared/tabletopPermissions.js'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; -import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import { assertUserInTabletop } from './shared/tabletopPermissions'; +import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import Creatures from '/imports/api/creature/creatures/Creatures'; const addCreaturesToTabletop = new ValidatedMethod({ @@ -30,23 +30,23 @@ const addCreaturesToTabletop = new ValidatedMethod({ timeInterval: 5000, }, - run({tabletopId, creatureIds}) { + run({ tabletopId, creatureIds }) { if (!this.userId) { throw new Meteor.Error('tabletops.addCreatures.denied', - 'You need to be logged in to remove a tabletop'); + 'You need to be logged in to remove a tabletop'); } assertUserHasPaidBenefits(this.userId); assertUserInTabletop(tabletopId, this.userId); assertAdmin(this.userId); Creatures.update({ - _id: {$in: creatureIds}, + _id: { $in: creatureIds }, $or: [ - {writers: this.userId}, - {owner: this.userId}, + { writers: this.userId }, + { owner: this.userId }, ], }, { - $set: {tabletop: tabletopId}, + $set: { tabletop: tabletopId }, }, { multi: true, }); diff --git a/app/imports/api/tabletop/methods/insertTabletop.js b/app/imports/api/tabletop/methods/insertTabletop.js index 62ad2156..c213f04e 100644 --- a/app/imports/api/tabletop/methods/insertTabletop.js +++ b/app/imports/api/tabletop/methods/insertTabletop.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Tabletops from '../Tabletops.js'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; -import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js'; +import Tabletops from '../Tabletops'; +import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; const insertTabletop = new ValidatedMethod({ diff --git a/app/imports/api/tabletop/methods/removeTabletop.js b/app/imports/api/tabletop/methods/removeTabletop.js index 4636eb4b..d3ac2d33 100644 --- a/app/imports/api/tabletop/methods/removeTabletop.js +++ b/app/imports/api/tabletop/methods/removeTabletop.js @@ -1,11 +1,11 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Tabletops from '../Tabletops.js'; -import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; -import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js'; -import { assertUserIsTabletopOwner } from './shared/tabletopPermissions.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import Tabletops from '../Tabletops'; +import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; +import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers'; +import { assertUserIsTabletopOwner } from './shared/tabletopPermissions'; +import Creatures from '/imports/api/creature/creatures/Creatures'; const removeTabletop = new ValidatedMethod({ diff --git a/app/imports/api/tabletop/methods/shared/tabletopPermissions.js b/app/imports/api/tabletop/methods/shared/tabletopPermissions.js index 2f14543d..3a233c95 100644 --- a/app/imports/api/tabletop/methods/shared/tabletopPermissions.js +++ b/app/imports/api/tabletop/methods/shared/tabletopPermissions.js @@ -1,25 +1,25 @@ -import Tabletops from '../../Tabletops.js'; +import Tabletops from '../../Tabletops'; -export function assertUserInTabletop(tabletopId, userId){ +export function assertUserInTabletop(tabletopId, userId) { let tabletop = Tabletops.findOne(tabletopId); - if (!tabletop){ + if (!tabletop) { throw new Meteor.Error('Tabletop does not exist', - 'No tabletop could be found for the given tabletop id'); + 'No tabletop could be found for the given tabletop id'); } - if (tabletop.gameMaster !== userId && !tabletop.players.includes(userId)){ + if (tabletop.gameMaster !== userId && !tabletop.players.includes(userId)) { throw new Meteor.Error('Not in tabletop', - 'The user is not the gamemaster or a player in the given tabletop'); + 'The user is not the gamemaster or a player in the given tabletop'); } } -export function assertUserIsTabletopOwner(tabletopId, userId){ +export function assertUserIsTabletopOwner(tabletopId, userId) { let tabletop = Tabletops.findOne(tabletopId); - if (!tabletop){ + if (!tabletop) { throw new Meteor.Error('Tabletop does not exist', - 'No tabletop could be found for the given tabletop id'); + 'No tabletop could be found for the given tabletop id'); } - if (tabletop.gameMaster !== userId){ + if (tabletop.gameMaster !== userId) { throw new Meteor.Error('Not the owner', - 'The user is not the owner of the given tabletop'); + 'The user is not the owner of the given tabletop'); } } diff --git a/app/imports/api/users/Invites.js b/app/imports/api/users/Invites.js index af98adf2..19799b92 100644 --- a/app/imports/api/users/Invites.js +++ b/app/imports/api/users/Invites.js @@ -1,9 +1,9 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { getUserTier } from '/imports/api/users/patreon/tiers.js'; +import { getUserTier } from '/imports/api/users/patreon/tiers'; -let Invites= new Mongo.Collection('invites'); +let Invites = new Mongo.Collection('invites'); let InviteSchema = new SimpleSchema({ inviter: { @@ -34,13 +34,13 @@ let InviteSchema = new SimpleSchema({ }, }); -if (Meteor.isServer){ - Accounts.onLogin(function({user}){ +if (Meteor.isServer) { + Accounts.onLogin(function ({ user }) { alignInvitesWithPatreonTier(user); }); } -function alignInvitesWithPatreonTier(user){ +function alignInvitesWithPatreonTier(user) { const tier = getUserTier(user); let availableInvites = tier.invites; let currentlyFundedInvites = []; @@ -48,7 +48,7 @@ function alignInvitesWithPatreonTier(user){ Invites.find({ inviter: user._id }).forEach(invite => { - if (invite.isFunded){ + if (invite.isFunded) { currentlyFundedInvites.push(invite); } else { currenltyUnfundedInvites.push(invite); @@ -63,23 +63,23 @@ function alignInvitesWithPatreonTier(user){ currenltyUnfundedInvites.sort((a, b) => b.dateConfirmed - a.dateConfirmed); // Defund or delete excess invites - while (currentlyFundedInvites.length > availableInvites){ + while (currentlyFundedInvites.length > availableInvites) { let inviteToDefund = currentlyFundedInvites.pop(); - if (inviteToDefund.invitee){ - Invites.update(inviteToDefund._id, {$set: {isFunded: false}}); + if (inviteToDefund.invitee) { + Invites.update(inviteToDefund._id, { $set: { isFunded: false } }); } else { Invites.remove(inviteToDefund._id); } } // Fund unfunded invites or insert new ones - while (currentlyFundedInvites.length < availableInvites){ - if (currenltyUnfundedInvites.length){ + while (currentlyFundedInvites.length < availableInvites) { + if (currenltyUnfundedInvites.length) { let inviteToFund = currenltyUnfundedInvites.pop(); currentlyFundedInvites.push(inviteToFund); - Invites.update(inviteToFund._id, {$set: {isFunded: true}}); + Invites.update(inviteToFund._id, { $set: { isFunded: true } }); } else { - let inviteId = Invites.insert({inviter: user._id, isFunded: true}); - currentlyFundedInvites.push({_id: inviteId}); + let inviteId = Invites.insert({ inviter: user._id, isFunded: true }); + currentlyFundedInvites.push({ _id: inviteId }); } } } @@ -97,17 +97,17 @@ const getInviteToken = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({inviteId}) { + run({ inviteId }) { let invite = Invites.findOne(inviteId); if (this.userId !== invite.inviter) { throw new Meteor.Error('Invites.methods.getToken.denied', - 'You need to be the inviter of the invite to create a token'); + 'You need to be the inviter of the invite to create a token'); } - if (invite.inviteToken){ + if (invite.inviteToken) { return invite.inviteToken; } else { let inviteToken = Random.id(5); - Invites.update(inviteId, {$set: {inviteToken}}) + Invites.update(inviteId, { $set: { inviteToken } }) return inviteToken; } }, @@ -125,32 +125,32 @@ const acceptInviteToken = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({inviteToken}) { + run({ inviteToken }) { if (!this.userId) { throw new Meteor.Error('Invites.methods.acceptToken.denied', - 'You need to be the logged in to accept a token'); + 'You need to be the logged in to accept a token'); } if (Meteor.isClient) return; - let invite = Invites.findOne({inviteToken}); - if (!invite){ + let invite = Invites.findOne({ inviteToken }); + if (!invite) { throw new Meteor.Error('Invites.methods.acceptToken.notFound', - 'No invite could be found for this link, maybe it has already been claimed'); + 'No invite could be found for this link, maybe it has already been claimed'); } // If the invitee is already filled, fix unexpected case by deleting the token - if (invite.invitee){ + if (invite.invitee) { Invites.update(invite._id, { - $unset: {inviteToken: 1} + $unset: { inviteToken: 1 } }); throw new Meteor.Error('Invites.methods.acceptToken.alreadyAccepted', - 'This invite has already been claimed'); + 'This invite has already been claimed'); } - if (this.userId === invite.inviter){ + if (this.userId === invite.inviter) { throw new Meteor.Error('Invites.methods.acceptToken.ownToken', - 'You can\'t accept your own invite'); + 'You can\'t accept your own invite'); } Invites.update(invite._id, { - $set: {invitee: this.userId}, - $unset: {inviteToken: 1}, + $set: { invitee: this.userId }, + $unset: { inviteToken: 1 }, }); }, }); @@ -168,28 +168,28 @@ const revokeInvite = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({inviteId}) { + run({ inviteId }) { if (!this.userId) { throw new Meteor.Error('Invites.methods.revokeInvite.denied', - 'You need to be the logged in to revoke a token'); + 'You need to be the logged in to revoke a token'); } if (Meteor.isClient) return; let invite = Invites.findOne(inviteId); - if (!invite){ + if (!invite) { throw new Meteor.Error('Invites.methods.revokeInvite.notFound', - 'No invite could be found for this id'); + 'No invite could be found for this id'); } if (this.userId !== invite.inviter) { throw new Meteor.Error('Invites.methods.revokeInvite.denied', - 'You are not the owner of this invite'); + 'You are not the owner of this invite'); } // If the invitee is empty, the token has already been revoked - if (!invite.invitee){ + if (!invite.invitee) { return; } Invites.update(invite._id, { - $unset: {invitee: 1, dateConfirmed: 1}, + $unset: { invitee: 1, dateConfirmed: 1 }, }); }, }); diff --git a/app/imports/api/users/Users.js b/app/imports/api/users/Users.js index b27edfca..b219eeba 100644 --- a/app/imports/api/users/Users.js +++ b/app/imports/api/users/Users.js @@ -1,12 +1,12 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Libraries from '/imports/api/library/Libraries.js'; -import LibraryCollections from '/imports/api/library/LibraryCollections.js'; -import '/imports/api/users/methods/deleteMyAccount.js'; -import '/imports/api/users/methods/addEmail.js'; -import '/imports/api/users/methods/removeEmail.js'; -import '/imports/api/users/methods/updateFileStorageUsed.js'; +import Libraries from '/imports/api/library/Libraries'; +import LibraryCollections from '/imports/api/library/LibraryCollections'; +import '/imports/api/users/methods/deleteMyAccount'; +import '/imports/api/users/methods/addEmail'; +import '/imports/api/users/methods/removeEmail'; +import '/imports/api/users/methods/updateFileStorageUsed'; import { some } from 'lodash'; const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || []; const defaultLibraryCollections = process.env.DEFAULT_LIBRARY_COLLECTIONS && process.env.DEFAULT_LIBRARY_COLLECTIONS.split(',') || []; diff --git a/app/imports/api/users/methods/deleteMyAccount.js b/app/imports/api/users/methods/deleteMyAccount.js index 5c555855..c452583c 100644 --- a/app/imports/api/users/methods/deleteMyAccount.js +++ b/app/imports/api/users/methods/deleteMyAccount.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Libraries, { removeLibaryWork } from '/imports/api/library/Libraries.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js'; +import Libraries, { removeLibaryWork } from '/imports/api/library/Libraries'; +import Creatures from '/imports/api/creature/creatures/Creatures'; +import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature'; Meteor.users.deleteMyAccount = new ValidatedMethod({ name: 'users.deleteMyAccount', diff --git a/app/imports/api/users/methods/updateFileStorageUsed.js b/app/imports/api/users/methods/updateFileStorageUsed.js index 4fad2feb..b72a55e2 100644 --- a/app/imports/api/users/methods/updateFileStorageUsed.js +++ b/app/imports/api/users/methods/updateFileStorageUsed.js @@ -1,7 +1,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; -import UserImages from '/imports/api/files/UserImages.js'; +import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; +import UserImages from '/imports/api/files/UserImages'; const fileCollections = [ArchiveCreatureFiles, UserImages]; const updateFileStorageUsed = new ValidatedMethod({ @@ -29,7 +29,7 @@ export default updateFileStorageUsed; export function updateFileStorageUsedWork(userId) { if (!userId) { throw new Meteor.Error('idRequired', - 'No user ID was provided to update file storage used') + 'No user ID was provided to update file storage used') } let sum = 0; @@ -51,7 +51,7 @@ export function incrementFileStorageUsed(userId, amount) { throw new Meteor.Error('idRequired', 'No user ID was provided to update file storage used') } - + const user = Meteor.users.findOne(userId); if (!user) { throw new Meteor.Error('noUser', 'User not found'); diff --git a/app/imports/api/users/patreon/tiers.js b/app/imports/api/users/patreon/tiers.js index edd1d44a..e5f5d2e9 100644 --- a/app/imports/api/users/patreon/tiers.js +++ b/app/imports/api/users/patreon/tiers.js @@ -1,6 +1,6 @@ import { findLast } from 'lodash'; -import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js'; -import Invites from '/imports/api/users/Invites.js'; +import getEntitledCents from '/imports/api/users/patreon/getEntitledCents'; +import Invites from '/imports/api/users/Invites'; const patreonDisabled = !!Meteor.settings?.public?.disablePatreon; const TIERS = Object.freeze([ @@ -80,14 +80,14 @@ const PATREON_DISABLED_TIER = Object.freeze({ paidBenefits: true, }); -export function getTierByEntitledCents(entitledCents = 0){ +export function getTierByEntitledCents(entitledCents = 0) { if (patreonDisabled) return PATREON_DISABLED_TIER; return findLast(TIERS, tier => entitledCents >= tier.minimumEntitledCents); } -export function getUserTier(user){ +export function getUserTier(user) { if (!user) throw 'user must be provided'; - if (typeof user === 'string'){ + if (typeof user === 'string') { user = Meteor.users.findOne(user, { fields: { 'services.patreon': 1, @@ -99,19 +99,19 @@ export function getUserTier(user){ const entitledCents = getEntitledCents(user); const tier = getTierByEntitledCents(entitledCents); if (tier.paidBenefits) return tier; - let invite = Invites.findOne({invitee: user._id, isFunded: true}); - if (invite){ + let invite = Invites.findOne({ invitee: user._id, isFunded: true }); + if (invite) { return GUEST_TIER; } else { return tier; } } -export function assertUserHasPaidBenefits(user){ +export function assertUserHasPaidBenefits(user) { let tier = getUserTier(user); - if (!tier.paidBenefits){ + if (!tier.paidBenefits) { throw new Meteor.Error('no paid benefits', - `The ${tier.name} tier does not have the required benefits`); + `The ${tier.name} tier does not have the required benefits`); } } diff --git a/app/imports/api/users/patreon/updatePatreonOnLogin.js b/app/imports/api/users/patreon/updatePatreonOnLogin.js index cdff000e..f00d739e 100644 --- a/app/imports/api/users/patreon/updatePatreonOnLogin.js +++ b/app/imports/api/users/patreon/updatePatreonOnLogin.js @@ -1,11 +1,11 @@ -import updatePatreonDetails from '/imports/api/users/patreon/updatePatreonDetails.js'; +import updatePatreonDetails from '/imports/api/users/patreon/updatePatreonDetails'; const ONE_DAY = 24 * 60 * 60 * 1000; -Accounts.onLogin(({user}) => { +Accounts.onLogin(({ user }) => { let patreon = user.services && user.services.patreon; - if (patreon){ + if (patreon) { const timeSinceIdentityUpdate = new Date() - patreon.lastUpdatedIdentity; - if (timeSinceIdentityUpdate > ONE_DAY){ + if (timeSinceIdentityUpdate > ONE_DAY) { updatePatreonDetails(user); } } diff --git a/app/imports/client/serviceWorker.js b/app/imports/client/serviceWorker.js index 5caa9669..44a2d393 100644 --- a/app/imports/client/serviceWorker.js +++ b/app/imports/client/serviceWorker.js @@ -1,5 +1,5 @@ Meteor.startup(() => { - navigator.serviceWorker.register('/sw.js') - .then() - .catch(error => console.log('ServiceWorker registration failed: ', error)); + navigator.serviceWorker.register('/sw') + .then() + .catch(error => console.log('ServiceWorker registration failed: ', error)); }); diff --git a/app/imports/client/ui/components/CoinValue.vue b/app/imports/client/ui/components/CoinValue.vue index d5ccc0f8..21ea3de3 100644 --- a/app/imports/client/ui/components/CoinValue.vue +++ b/app/imports/client/ui/components/CoinValue.vue @@ -19,7 +19,7 @@