From f63d2ad254c8f6d6728a3e01e590084817097b66 Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:28:20 +0200 Subject: [PATCH] Began migration of queries to nested sets --- .../archive/methods/archiveCreatureToFile.js | 5 +- .../archive/methods/verifyArchiveSafety.js | 10 +- .../methods/copyPropertyToLibrary.js | 38 +++---- .../methods/damageProperty.js | 2 +- .../methods/duplicateProperty.js | 19 ++-- .../creatureProperties/methods/flipToggle.js | 2 +- .../methods/getParentRefByTag.js | 3 +- .../methods/getSlotFillFilter.js | 11 +- .../methods/getSlotFillFilter.test.js | 4 +- .../methods/updateCreatureProperty.js | 4 +- .../recomputeCreaturesByProperty.js | 7 +- .../creatures/defaultCharacterProperties.js | 29 ++--- .../creatures/methods/removeCreature.js | 3 +- .../creatures/methods/restCreature.js | 5 +- .../mixins/propagateInheritanceUpdateMixin.js | 59 ---------- app/imports/api/docs/Docs.js | 101 +++--------------- .../applyPropertyByType/applyAction.ts | 19 +--- app/imports/api/engine/actions/doCastSpell.js | 2 +- app/imports/api/engine/actions/doCheck.js | 3 +- app/imports/api/library/Libraries.js | 3 +- app/imports/api/library/LibraryNodes.js | 16 +-- .../api/library/methods/copyLibraryNodeTo.js | 21 ++-- .../library/methods/duplicateLibraryNode.js | 22 ++-- .../library/methods/getDefaultSlotFiller.js | 4 +- .../library/methods/updateReferenceNode.js | 4 +- .../api/parenting/parentingFunctions.ts | 34 ++++++ app/imports/api/parenting/softRemove.ts | 2 +- app/imports/api/sharing/sharingPermissions.js | 4 +- .../client/ui/components/tree/TreeNode.vue | 11 +- .../ui/creature/buildTree/BuildTreeNode.vue | 8 +- .../characterSheetTabs/ActionsTab.vue | 8 +- .../character/characterSheetTabs/BuildTab.vue | 12 ++- .../characterSheetTabs/FeaturesTab.vue | 8 +- .../characterSheetTabs/InventoryTab.vue | 33 +++--- .../characterSheetTabs/JournalTab.vue | 28 +++-- .../characterSheetTabs/SpellsTab.vue | 40 +++---- .../character/characterSheetTabs/StatsTab.vue | 10 +- .../CharacterSheetPrinted.vue | 10 +- .../PrintedInventory.vue | 23 ++-- app/imports/client/ui/docs/DocEditForm.vue | 5 +- app/imports/migrations/server/dbv3/dbv3.ts | 2 + 41 files changed, 242 insertions(+), 392 deletions(-) delete mode 100644 app/imports/api/creature/mixins/propagateInheritanceUpdateMixin.js diff --git a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js index 469657b4..e10d29cd 100644 --- a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js +++ b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js @@ -9,11 +9,12 @@ 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) { // Build the archive document const creature = Creatures.findOne(creatureId); - const properties = CreatureProperties.find({ 'ancestors.id': creatureId }).fetch(); + const properties = CreatureProperties.find({ ...getFilter.descendantsOfRoot(creatureId) }).fetch(); const experiences = Experiences.find({ creatureId }).fetch(); const logs = CreatureLogs.find({ creatureId }).fetch(); let archiveCreature = { @@ -68,7 +69,7 @@ const archiveCreatureToFile = new ValidatedMethod({ async run({ creatureId }) { assertOwnership(creatureId, this.userId); if (Meteor.isServer) { - archiveCreature(creatureId, this.userId); + archiveCreature(creatureId); } else { removeCreatureWork(creatureId); } diff --git a/app/imports/api/creature/archive/methods/verifyArchiveSafety.js b/app/imports/api/creature/archive/methods/verifyArchiveSafety.js index 5b46d7ed..fffb5a73 100644 --- a/app/imports/api/creature/archive/methods/verifyArchiveSafety.js +++ b/app/imports/api/creature/archive/methods/verifyArchiveSafety.js @@ -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/creatureProperties/methods/copyPropertyToLibrary.js b/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js index 14ff9151..69f072ec 100644 --- a/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js +++ b/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js @@ -10,8 +10,8 @@ import { assertCopyPermission } from '/imports/api/sharing/sharingPermissions'; import { - setLineageOfDocs, - getAncestry, + fetchDocByRef, + getFilter, renewDocIds } from '/imports/api/parenting/parentingFunctions'; import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; @@ -40,20 +40,20 @@ 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 rebuildNestedSets(LibraryNodes, rootLibrary._id); @@ -63,7 +63,7 @@ const copyPropertyToLibrary = new ValidatedMethod({ }, }); -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({ @@ -83,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 }, }); @@ -105,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, @@ -119,11 +112,8 @@ function insertNodeFromProperty(propId, ancestors, order, method) { }); // Order the root node - if (order === undefined) { - rebuildNestedSets(LibraryNodes, prop.root.id); - } else { - prop.order = order; - } + prop.left = Number.MAX_SAFE_INTEGER - 1; + prop.right = Number.MAX_SAFE_INTEGER; // Clean the props props = cleanProps(props); @@ -135,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) { @@ -155,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 555a35fe..eaa3fa85 100644 --- a/app/imports/api/creature/creatureProperties/methods/damageProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/damageProperty.js @@ -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 diff --git a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js index 3cfe331e..ad5665c3 100644 --- a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js @@ -5,7 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; import { - setLineageOfDocs, + getFilter, renewDocIds } from '/imports/api/parenting/parentingFunctions'; import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; @@ -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; diff --git a/app/imports/api/creature/creatureProperties/methods/flipToggle.js b/app/imports/api/creature/creatureProperties/methods/flipToggle.js index 29d97f32..c97b3ac9 100644 --- a/app/imports/api/creature/creatureProperties/methods/flipToggle.js +++ b/app/imports/api/creature/creatureProperties/methods/flipToggle.js @@ -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 bdcb4c10..1c664692 100644 --- a/app/imports/api/creature/creatureProperties/methods/getParentRefByTag.js +++ b/app/imports/api/creature/creatureProperties/methods/getParentRefByTag.js @@ -1,8 +1,9 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; export default function getParentRefByTag(creatureId, tag) { let prop = CreatureProperties.findOne({ - 'ancestors.id': creatureId, + ...getFilter.descendantsOfRoot(creatureId), removed: { $ne: true }, inactive: { $ne: true }, tags: tag, 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 a237b3c1..cd8f1c5a 100644 --- a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js +++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js @@ -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/updateCreatureProperty.js b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js index b1f067fc..01ebb388 100644 --- a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js @@ -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 085ddf07..06fe42c7 100644 --- a/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js +++ b/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js @@ -2,11 +2,8 @@ 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); - } - } + computeCreature.call(property.root.id); } diff --git a/app/imports/api/creature/creatures/defaultCharacterProperties.js b/app/imports/api/creature/creatures/defaultCharacterProperties.js index fd0a8b28..dc94cd96 100644 --- a/app/imports/api/creature/creatures/defaultCharacterProperties.js +++ b/app/imports/api/creature/creatures/defaultCharacterProperties.js @@ -5,7 +5,6 @@ export default function defaultCharacterProperties(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', @@ -17,31 +16,35 @@ export default function defaultCharacterProperties(creatureId) { 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/removeCreature.js b/app/imports/api/creature/creatures/methods/removeCreature.js index 3f6d7279..646a6bc8 100644 --- a/app/imports/api/creature/creatures/methods/removeCreature.js +++ b/app/imports/api/creature/creatures/methods/removeCreature.js @@ -7,10 +7,11 @@ 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 }); + CreatureProperties.remove(getFilter.descendantsOfRoot(creatureId)); CreatureLogs.remove({ creatureId }); Experiences.remove({ creatureId }); } diff --git a/app/imports/api/creature/creatures/methods/restCreature.js b/app/imports/api/creature/creatures/methods/restCreature.js index f8a5ddd7..f281bbf2 100644 --- a/app/imports/api/creature/creatures/methods/restCreature.js +++ b/app/imports/api/creature/creatures/methods/restCreature.js @@ -7,6 +7,7 @@ import { union } from 'lodash'; 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/mixins/propagateInheritanceUpdateMixin.js b/app/imports/api/creature/mixins/propagateInheritanceUpdateMixin.js deleted file mode 100644 index 9fd239f8..00000000 --- a/app/imports/api/creature/mixins/propagateInheritanceUpdateMixin.js +++ /dev/null @@ -1,59 +0,0 @@ -import { - updateChildren, - updateDescendants, -} from '/imports/api/parenting/parentingFunctions'; -import { inheritedFields } from '/imports/api/parenting/ChildSchema'; -import MONGO_OPERATORS from '/imports/constants/MONGO_OPERATORS'; - -// 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/docs/Docs.js b/app/imports/api/docs/Docs.js index fcbd042f..1ab8f86f 100644 --- a/app/imports/api/docs/Docs.js +++ b/app/imports/api/docs/Docs.js @@ -9,53 +9,11 @@ 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 { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; -import { getAncestry } from '/imports/api/parenting/parentingFunctions'; +import { getFilter, rebuildNestedSets } 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, @@ -105,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 () { @@ -161,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; @@ -184,7 +111,7 @@ const insertDoc = new ValidatedMethod({ } const docId = Docs.insert(doc); - rebuildNestedSets(Docs, 'root'); + rebuildNestedSets(Docs); return docId; }, }); @@ -222,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); - } - rebuildNestedSets(Docs, 'root'); + rebuildNestedSets(Docs); return updates; }, }); @@ -278,7 +201,7 @@ const softRemoveDoc = new ValidatedMethod({ run({ _id }) { assertDocsEditPermission(this.userId); softRemove({ _id, collection: Docs }); - rebuildNestedSets(Docs, 'root'); + rebuildNestedSets(Docs); } }); @@ -294,8 +217,8 @@ const restoreDoc = new ValidatedMethod({ }, run({ _id }) { assertDocsEditPermission(this.userId); - restore({ _id, collection: Docs }); - rebuildNestedSets(Docs, 'root'); + restore('docs', _id); + rebuildNestedSets(Docs); } }); diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.ts b/app/imports/api/engine/actions/applyPropertyByType/applyAction.ts index 6977142c..02b674c0 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.ts +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.ts @@ -8,7 +8,7 @@ import { damagePropertyWork } from '/imports/api/creature/creatureProperties/met 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 } from '/imports/api/parenting/parentingFunctions'; +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'; @@ -327,20 +327,3 @@ function spendResources(prop: Action, 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/doCastSpell.js b/app/imports/api/engine/actions/doCastSpell.js index 022000bb..cdcdb53c 100644 --- a/app/imports/api/engine/actions/doCastSpell.js +++ b/app/imports/api/engine/actions/doCastSpell.js @@ -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 diff --git a/app/imports/api/engine/actions/doCheck.js b/app/imports/api/engine/actions/doCheck.js index c20a5250..8ee1096f 100644 --- a/app/imports/api/engine/actions/doCheck.js +++ b/app/imports/api/engine/actions/doCheck.js @@ -25,7 +25,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/library/Libraries.js b/app/imports/api/library/Libraries.js index 25cf4ae9..bdf2475b 100644 --- a/app/imports/api/library/Libraries.js +++ b/app/imports/api/library/Libraries.js @@ -7,6 +7,7 @@ import { assertEditPermission, assertOwnership } from '/imports/api/sharing/shar 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/LibraryNodes.js b/app/imports/api/library/LibraryNodes.js index 0ef34863..a55bd74f 100644 --- a/app/imports/api/library/LibraryNodes.js +++ b/app/imports/api/library/LibraryNodes.js @@ -15,7 +15,7 @@ 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 { getAncestry } from '/imports/api/parenting/parentingFunctions'; +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; @@ -195,6 +193,8 @@ const updateLibraryNode = new ValidatedMethod({ case 'order': case 'parent': case 'ancestors': + case 'parentId': + case 'root': return false; } }, @@ -296,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/methods/copyLibraryNodeTo.js b/app/imports/api/library/methods/copyLibraryNodeTo.js index 6d672064..5c703253 100644 --- a/app/imports/api/library/methods/copyLibraryNodeTo.js +++ b/app/imports/api/library/methods/copyLibraryNodeTo.js @@ -9,7 +9,8 @@ import { } from '/imports/api/sharing/sharingPermissions'; import { setLineageOfDocs, - renewDocIds + renewDocIds, + getFilter } from '/imports/api/parenting/parentingFunctions'; import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions'; @@ -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,26 +72,16 @@ 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 - // TODO: rebuild tree nested sets - rebuildNestedSets(LibraryNodes, parentDoc.root.id); }, }); diff --git a/app/imports/api/library/methods/duplicateLibraryNode.js b/app/imports/api/library/methods/duplicateLibraryNode.js index 7e026ac1..0983f97b 100644 --- a/app/imports/api/library/methods/duplicateLibraryNode.js +++ b/app/imports/api/library/methods/duplicateLibraryNode.js @@ -5,7 +5,8 @@ import LibraryNodes from '/imports/api/library/LibraryNodes'; import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions'; import { setLineageOfDocs, - renewDocIds + renewDocIds, + getFilter } from '/imports/api/parenting/parentingFunctions'; import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; @@ -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/updateReferenceNode.js b/app/imports/api/library/methods/updateReferenceNode.js index d796a5af..f0303ec0 100644 --- a/app/imports/api/library/methods/updateReferenceNode.js +++ b/app/imports/api/library/methods/updateReferenceNode.js @@ -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/parentingFunctions.ts b/app/imports/api/parenting/parentingFunctions.ts index fad6a6ba..af81ccef 100644 --- a/app/imports/api/parenting/parentingFunctions.ts +++ b/app/imports/api/parenting/parentingFunctions.ts @@ -241,6 +241,20 @@ export const getFilter = { }); 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, @@ -373,6 +387,26 @@ export function compareOrder(docA, docB) { } } +/** + * 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 */ diff --git a/app/imports/api/parenting/softRemove.ts b/app/imports/api/parenting/softRemove.ts index e08cf7ba..10c49523 100644 --- a/app/imports/api/parenting/softRemove.ts +++ b/app/imports/api/parenting/softRemove.ts @@ -51,7 +51,7 @@ const restoreError = function () { ); }; -export async function restore(collection: Mongo.Collection | string, doc: TreeDoc | string, extraUpdates) { +export async function restore(collection: Mongo.Collection | string, doc: TreeDoc | string, extraUpdates?) { if (typeof collection === 'string') { collection = getCollectionByName(collection); } diff --git a/app/imports/api/sharing/sharingPermissions.js b/app/imports/api/sharing/sharingPermissions.js index dcea4605..8c600f92 100644 --- a/app/imports/api/sharing/sharingPermissions.js +++ b/app/imports/api/sharing/sharingPermissions.js @@ -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/client/ui/components/tree/TreeNode.vue b/app/imports/client/ui/components/tree/TreeNode.vue index 288ff106..82f63802 100644 --- a/app/imports/client/ui/components/tree/TreeNode.vue +++ b/app/imports/client/ui/components/tree/TreeNode.vue @@ -86,6 +86,7 @@ import { canBeParent } from '/imports/api/parenting/parentingFunctions'; import { getPropertyIcon } from '/imports/constants/PROPERTIES'; import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue'; +import { isAncestor } from '/imports/api/parenting/parentingFunctions'; import { some } from 'lodash'; export default { @@ -121,9 +122,9 @@ export default { }, data() { return { - expanded: this.startExpanded || this.node._ancestorOfMatchedDocument || - some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) || - false, + expanded: this.startExpanded || + this.node._ancestorOfMatchedDocument || + isAncestor(this.node, this.selectedNode), } }, computed: { @@ -152,8 +153,8 @@ export default { this.expanded = !!value || some(this.selectedNode?.ancestors, ref => ref.id === this.node._id); }, - 'selectedNode.ancestors'(value) { - this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded; + 'selectedNode.parentId'() { + this.expanded = isAncestor(this.node, this.selectedNode) || this.expanded; }, }, beforeCreate() { diff --git a/app/imports/client/ui/creature/buildTree/BuildTreeNode.vue b/app/imports/client/ui/creature/buildTree/BuildTreeNode.vue index 9d85e712..e72b1a59 100644 --- a/app/imports/client/ui/creature/buildTree/BuildTreeNode.vue +++ b/app/imports/client/ui/creature/buildTree/BuildTreeNode.vue @@ -131,6 +131,7 @@ import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue' import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty'; import restoreProperty from '/imports/api/creature/creatureProperties/methods/restoreProperty'; import getPropertyTitle from '/imports/client/ui/properties/shared/getPropertyTitle'; +import { isAncestor } from '/imports/api/parenting/parentingFunctions'; export default { name: 'BuildTreeNode', @@ -233,11 +234,10 @@ export default { }, watch: { 'node._ancestorOfMatchedDocument'(value){ - this.expanded = !!value || - some(this.selectedNode?.ancestors, ref => ref.id === this.node._id); + this.expanded = !!value || isAncestor(this.node, this.selectedNode); }, - 'selectedNode.ancestors'(value){ - this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded; + 'selectedNode.parentId'(){ + this.expanded = isAncestor(this.node, this.selectedNode) || this.expanded; }, }, beforeCreate() { diff --git a/app/imports/client/ui/creature/character/characterSheetTabs/ActionsTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/ActionsTab.vue index 88c5c4cf..d9da510d 100644 --- a/app/imports/client/ui/creature/character/characterSheetTabs/ActionsTab.vue +++ b/app/imports/client/ui/creature/character/characterSheetTabs/ActionsTab.vue @@ -40,6 +40,7 @@ import ColumnLayout from '/imports/client/ui/components/ColumnLayout.vue'; import ActionCard from '/imports/client/ui/properties/components/actions/ActionCard.vue'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; export default { components: { @@ -56,10 +57,11 @@ export default { data() { return { tabName: 'actions', }}, + // @ts-ignore Meteor isn't defined on vue meteor: { actions() { const folderIds = CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'folder', groupStats: true, hideStatsGroup: true, @@ -68,9 +70,7 @@ export default { }, { fields: { _id: 1 } }).map(folder => folder._id); return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - }, + ...getFilter.descendantsOfRoot(this.creatureId), 'parent.id': { $nin: folderIds, }, diff --git a/app/imports/client/ui/creature/character/characterSheetTabs/BuildTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/BuildTab.vue index 3bc2c376..7fa9e1f3 100644 --- a/app/imports/client/ui/creature/character/characterSheetTabs/BuildTab.vue +++ b/app/imports/client/ui/creature/character/characterSheetTabs/BuildTab.vue @@ -211,6 +211,7 @@ import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue' import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty'; import getPropertyTitle from '/imports/client/ui/properties/shared/getPropertyTitle'; import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; function traverse(tree, callback, parents = []){ tree.forEach(node => { @@ -271,6 +272,7 @@ export default { return this.hiddenSlots.length + this.hiddenPointBuys.length; }, }, + // @ts-ignore Meteor isn't defined on vue meteor: { creature(){ return Creatures.findOne(this.creatureId); @@ -281,7 +283,7 @@ export default { hiddenPointBuys() { return CreatureProperties.find({ type: 'pointBuy', - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), ignored: true, pointsLeft: {$ne: 0}, removed: {$ne: true}, @@ -291,7 +293,7 @@ export default { hiddenSlots(){ return CreatureProperties.find({ type: 'propertySlot', - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), ignored: true, $and: [ { @@ -313,7 +315,7 @@ export default { }, classProperties(){ return CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'class', removed: {$ne: true}, inactive: {$ne: true}, @@ -324,7 +326,7 @@ export default { classLevels() { const classVariableNames = this.classProperties.map(c => c.variableName) return CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'classLevel', variableName: {$nin: classVariableNames}, removed: {$ne: true}, @@ -335,7 +337,7 @@ export default { }, slotBuildTree(){ const slots = CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: {$in: ['propertySlot', 'pointBuy']}, $or: [ {'slotCondition.value': {$nin: [false, 0, '']}}, diff --git a/app/imports/client/ui/creature/character/characterSheetTabs/FeaturesTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/FeaturesTab.vue index 1edde5a8..c2a0d007 100644 --- a/app/imports/client/ui/creature/character/characterSheetTabs/FeaturesTab.vue +++ b/app/imports/client/ui/creature/character/characterSheetTabs/FeaturesTab.vue @@ -36,6 +36,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import ColumnLayout from '/imports/client/ui/components/ColumnLayout.vue'; import FeatureCard from '/imports/client/ui/properties/components/features/FeatureCard.vue'; import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; export default { components: { @@ -54,10 +55,11 @@ export default { tabName: 'features', }; }, + // @ts-ignore Meteor isn't defined on vue meteor: { features() { const folderIds = CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'folder', groupStats: true, hideStatsGroup: true, @@ -66,9 +68,7 @@ export default { }, { fields: { _id: 1 } }).map(folder => folder._id); return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - }, + ...getFilter.descendantsOfRoot(this.creatureId), 'parent.id': { $nin: folderIds, }, diff --git a/app/imports/client/ui/creature/character/characterSheetTabs/InventoryTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/InventoryTab.vue index 06feb2bc..296df786 100644 --- a/app/imports/client/ui/creature/character/characterSheetTabs/InventoryTab.vue +++ b/app/imports/client/ui/creature/character/characterSheetTabs/InventoryTab.vue @@ -118,6 +118,7 @@ import CoinValue from '/imports/client/ui/components/CoinValue.vue'; import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities'; import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; export default { components: { @@ -140,10 +141,11 @@ export default { tabName: 'inventory', }; }, + // @ts-ignore Meteor isn't defined on vue meteor: { folderIds() { return CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'folder', groupStats: true, hideStatsGroup: true, @@ -153,9 +155,7 @@ export default { }, containers() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - }, + ...getFilter.descendantsOfRoot(this.creatureId), 'parent.id': { $nin: this.folderIds, }, @@ -164,7 +164,7 @@ export default { inactive: { $ne: true }, }, { sort: { order: 1 }, - }); + }).fetch(); }, creature() { return Creatures.findOne(this.creatureId, { @@ -179,11 +179,9 @@ export default { }, containersWithoutAncestorContainers() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - $nin: this.containerIds, - }, - 'parent.id': { + ...getFilter.descendantsOfRoot(this.creatureId), + $not: getFilter.descendantsOfAll(this.containers), + parentId: { $nin: this.folderIds, }, type: 'container', @@ -195,11 +193,9 @@ export default { }, carriedItems() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - $nin: this.containerIds, - }, - 'parent.id': { + ...getFilter.descendantsOfRoot(this.creatureId), + $not: getFilter.descendantsOfAll(this.containers), + parentId: { $nin: this.folderIds, }, type: 'item', @@ -213,9 +209,7 @@ export default { }, equippedItems() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - }, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'item', equipped: true, removed: { $ne: true }, @@ -246,9 +240,6 @@ export default { }, }, computed: { - containerIds() { - return this.containers.map(container => container._id); - }, weightCarried() { return stripFloatingPointOddities( this.variables && diff --git a/app/imports/client/ui/creature/character/characterSheetTabs/JournalTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/JournalTab.vue index 3cc9b33b..d8b32e63 100644 --- a/app/imports/client/ui/creature/character/characterSheetTabs/JournalTab.vue +++ b/app/imports/client/ui/creature/character/characterSheetTabs/JournalTab.vue @@ -39,6 +39,7 @@ import NoteCard from '/imports/client/ui/properties/components/persona/NoteCard. import CreatureSummary from '/imports/client/ui/creature/character/CreatureSummary.vue'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; export default { components: { @@ -58,11 +59,12 @@ export default { tabName: 'journal', }; }, + // @ts-ignore Meteor isn't defined on vue meteor: { notes() { // Get all the notes that aren't children of group folders or of other displayed notes const folderIds = CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'folder', groupStats: true, hideStatsGroup: true, @@ -71,27 +73,23 @@ export default { }, { fields: { _id: 1 } }).map(folder => folder._id); const noteFilter = { - 'ancestors.id': { - $eq: this.creatureId, - }, - 'parent.id': { + ...getFilter.descendantsOfRoot(this.creatureId), + 'parentId': { $nin: folderIds, }, type: 'note', removed: { $ne: true }, inactive: { $ne: true }, }; - const noteIds = CreatureProperties.find(noteFilter, { - sort: { order: 1 }, - fields: {_id: 1}, - }).map(note => note._id); - - noteFilter['ancestors.id'] = { - $eq: this.creatureId, - $nin: noteIds, - } + const allNotes = CreatureProperties.find(noteFilter, { + sort: { left: 1 }, + }); - return CreatureProperties.find(noteFilter, { + return CreatureProperties.find({ + ...noteFilter, + ...getFilter.descendantsOfRoot(this.creatureId), + $not: getFilter.descendantsOfAll(allNotes), + }, { sort: {order: 1}, }); }, diff --git a/app/imports/client/ui/creature/character/characterSheetTabs/SpellsTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/SpellsTab.vue index 4c98d840..ff68b24b 100644 --- a/app/imports/client/ui/creature/character/characterSheetTabs/SpellsTab.vue +++ b/app/imports/client/ui/creature/character/characterSheetTabs/SpellsTab.vue @@ -55,6 +55,7 @@ import SpellListCard from '/imports/client/ui/properties/components/spells/Spell import SpellList from '/imports/client/ui/properties/components/spells/SpellList.vue'; import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin'; import SpellSlotCard from '/imports/client/ui/properties/components/attributes/SpellSlotCard.vue'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; export default { components: { @@ -76,10 +77,11 @@ export default { tabName: 'spells', } }, + // @ts-ignore Meteor isn't defined on vue meteor: { folderIds() { return CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'folder', groupStats: true, hideStatsGroup: true, @@ -89,7 +91,7 @@ export default { }, hasSpellSlots() { return !!CreatureProperties.findOne({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), inactive: { $ne: true }, removed: { $ne: true }, overridden: { $ne: true }, @@ -100,10 +102,8 @@ export default { }, spellSlots() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - }, - 'parent.id': { + ...getFilter.descendantsOfRoot(this.creatureId), + 'parentId': { $nin: this.folderIds, }, inactive: { $ne: true }, @@ -121,22 +121,20 @@ export default { }, spellLists() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - }, - 'parent.id': { + ...getFilter.descendantsOfRoot(this.creatureId), + 'parentId': { $nin: this.folderIds, }, type: 'spellList', removed: { $ne: true }, inactive: { $ne: true }, }, { - sort: { order: 1 } - }); + sort: { left: 1 } + }).fetch(); }, hasSpells() { return !!CreatureProperties.findOne({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'spell', removed: { $ne: true }, inactive: { $ne: true }, @@ -144,11 +142,9 @@ export default { }, spellsWithoutList() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - $nin: this.spellListIds, - }, - 'parent.id': { + ...getFilter.descendantsOfRoot(this.creatureId), + $not: getFilter.descendantsOfAll(this.spellLists), + parentId: { $nin: this.folderIds, }, type: 'spell', @@ -164,11 +160,9 @@ export default { }, spellListsWithoutAncestorSpellLists() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - $nin: this.spellListIds, - }, - 'parent.id': { + ...getFilter.descendantsOfRoot(this.creatureId), + $not: getFilter.descendantsOfAll(this.spellLists), + parentId: { $nin: this.folderIds, }, type: 'spellList', diff --git a/app/imports/client/ui/creature/character/characterSheetTabs/StatsTab.vue b/app/imports/client/ui/creature/character/characterSheetTabs/StatsTab.vue index 3e5017aa..e2e819e1 100644 --- a/app/imports/client/ui/creature/character/characterSheetTabs/StatsTab.vue +++ b/app/imports/client/ui/creature/character/characterSheetTabs/StatsTab.vue @@ -411,6 +411,7 @@ import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue' import FolderGroupCard from '/imports/client/ui/properties/components/folders/FolderGroupCard.vue'; import { get, set, uniqBy } from 'lodash'; import { docsToForest } from '/imports/api/parenting/parentingFunctions'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; function walkDown(forest, callback){ let stack = [...forest]; @@ -516,12 +517,13 @@ export default { return uniqBy(conditionals, '_id'); }, }, + // @ts-ignore Meteor isn't defined on vue meteor: { properties() { const creature = this.creature; if (!creature) return; const folderIds = CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'folder', groupStats: true, hideStatsGroup: true, @@ -530,8 +532,8 @@ export default { }, { fields: { _id: 1 } }).map(folder => folder._id); const filter = { - 'ancestors.id': this.creatureId, - 'parent.id': { + ...getFilter.descendantsOfRoot(this.creatureId), + parentId: { $nin: folderIds, }, $or: [ @@ -585,7 +587,7 @@ export default { toggles() { return CreatureProperties.find({ type: 'toggle', - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), removed: { $ne: true }, deactivatedByAncestor: { $ne: true }, deactivatedByToggle: { $ne: true }, diff --git a/app/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue b/app/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue index 26433b04..47fd6bcf 100644 --- a/app/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue +++ b/app/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue @@ -91,6 +91,7 @@ import PrintedSpells from '/imports/client/ui/creature/character/printedCharacte import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; import QrcodeVue from 'qrcode.vue' +import { getFilter } from '/imports/api/parenting/parentingFunctions'; export default { components: { @@ -138,6 +139,7 @@ export default { ].sort((a, b) => a.order - b.order); }, }, + // @ts-ignore reactiveProvide isn't defined on vue reactiveProvide: { name: 'context', include: ['creatureId', 'editPermission'], @@ -182,7 +184,7 @@ export default { race() { if (this.variables?.race?.value?.valueType === 'string') return this.variables.race.value.value; const prop = CreatureProperties.findOne({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), tags: 'race', removed: { $ne: true }, inactive: { $ne: true }, @@ -194,7 +196,7 @@ export default { background() { if (this.variables?.background?.value?.valueType === 'string') return this.variables.background.value.value; const prop = CreatureProperties.findOne({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), tags: 'background', removed: { $ne: true }, inactive: { $ne: true }, @@ -205,7 +207,7 @@ export default { }, classProperties(){ return CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'class', removed: {$ne: true}, inactive: {$ne: true}, @@ -216,7 +218,7 @@ export default { classLevels() { const classVariableNames = this.classProperties.map(c => c.variableName) return CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'classLevel', variableName: {$nin: classVariableNames}, removed: {$ne: true}, diff --git a/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedInventory.vue b/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedInventory.vue index c9c063ed..ea8c7056 100644 --- a/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedInventory.vue +++ b/app/imports/client/ui/creature/character/printedCharacterSheet/PrintedInventory.vue @@ -82,6 +82,7 @@ import stripFloatingPointOddities from '/imports/api/engine/computation/utility/ import PrintedLineItem from '/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedLineItem.vue'; import PrintedContainer from '/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedContainer.vue'; import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; export default { components: { @@ -101,10 +102,11 @@ export default { organize: false, } }, + // @ts-ignore Meteor isn't defined on vue meteor: { containers() { return CreatureProperties.find({ - 'ancestors.id': this.creatureId, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'container', removed: { $ne: true }, inactive: { $ne: true }, @@ -125,10 +127,8 @@ export default { }, containersWithoutAncestorContainers() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - $nin: this.containerIds - }, + ...getFilter.descendantsOfRoot(this.creatureId), + $not: getFilter.descendantsOfAll(this.containers), type: 'container', removed: { $ne: true }, inactive: { $ne: true }, @@ -150,10 +150,8 @@ export default { }, carriedItems() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - $nin: this.containerIds - }, + ...getFilter.descendantsOfRoot(this.creatureId), + $not: getFilter.descendantsOfAll(this.containers), type: 'item', equipped: { $ne: true }, removed: { $ne: true }, @@ -165,9 +163,7 @@ export default { }, equippedItems() { return CreatureProperties.find({ - 'ancestors.id': { - $eq: this.creatureId, - }, + ...getFilter.descendantsOfRoot(this.creatureId), type: 'item', equipped: true, removed: { $ne: true }, @@ -198,9 +194,6 @@ export default { }, }, computed: { - containerIds() { - return this.containers.map(container => container._id); - }, weightCarried() { return stripFloatingPointOddities( this.variables && diff --git a/app/imports/client/ui/docs/DocEditForm.vue b/app/imports/client/ui/docs/DocEditForm.vue index a2714143..4ac78106 100644 --- a/app/imports/client/ui/docs/DocEditForm.vue +++ b/app/imports/client/ui/docs/DocEditForm.vue @@ -180,10 +180,7 @@ export default { doc: { name: 'New Doc', }, - parentRef: this.doc && { - id: this.docId, - collection: 'docs', - }, + parentId: this.docId, }, ack); }, remove({ ack } = {}) { diff --git a/app/imports/migrations/server/dbv3/dbv3.ts b/app/imports/migrations/server/dbv3/dbv3.ts index 8962a874..be72e1ca 100644 --- a/app/imports/migrations/server/dbv3/dbv3.ts +++ b/app/imports/migrations/server/dbv3/dbv3.ts @@ -11,6 +11,8 @@ Migrations.add({ migrateCollection('libraryNodes'); console.log('migrating up creature props 2 -> 3'); migrateCollection('creatureProperties'); + console.log('migrating up docs 2 -> 3'); + migrateCollection('docs'); console.log('New schema fields added, if it was done correctly remove the old fields manually'); },