From e4590de3a74565932b832eea6151c2b118ee7a41 Mon Sep 17 00:00:00 2001 From: ThaumRystra <9525416+ThaumRystra@users.noreply.github.com> Date: Sun, 1 Oct 2023 17:30:21 +0200 Subject: [PATCH] Migrated insert prop methods to nested sets --- .../getRootCreatureAncestor.js | 5 +- .../methods/insertProperty.js | 54 ++++------- .../methods/insertPropertyFromLibraryNode.js | 90 ++++++------------- .../api/creature/mixins/setDocToLastMixin.js | 27 ------ app/imports/api/parenting/ChildSchema.ts | 4 + .../api/parenting/parentingFunctions.ts | 43 +++++---- 6 files changed, 80 insertions(+), 143 deletions(-) delete mode 100644 app/imports/api/creature/mixins/setDocToLastMixin.js diff --git a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js index 39eadabf..e13f7685 100644 --- a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js +++ b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js @@ -1,5 +1,8 @@ import Creatures from '/imports/api/creature/creatures/Creatures'; export default function getRootCreatureAncestor(property) { - return Creatures.findOne(property.ancestors[0].id); + if (property.root?.collection !== 'creatures') { + throw 'Property does not have a root ancestor' + } + return Creatures.findOne(property.root.id); } diff --git a/app/imports/api/creature/creatureProperties/methods/insertProperty.js b/app/imports/api/creature/creatureProperties/methods/insertProperty.js index f070546c..570a301d 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/insertProperty.js @@ -4,11 +4,9 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor'; import SimpleSchema from 'simpl-schema'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; -import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; -import { getAncestry } from '/imports/api/parenting/parentingFunctions'; +import { fetchDocByRef, rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag'; import { RefSchema } from '/imports/api/parenting/ChildSchema'; -import { getHighestOrder } from '/imports/api/parenting/order'; 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,43 +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 - rebuildNestedSets(CreatureProperties, 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 8f52f2a6..f582ad29 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -7,13 +7,11 @@ 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 + renewDocIds, + fetchDocByRef, + rebuildNestedSets, + getFilter } from '/imports/api/parenting/parentingFunctions'; -import { rebuildNestedSets } from '/imports/api/parenting/parentingFunctions'; -import { setDocToLastOrder } from '/imports/api/parenting/order'; -import { fetchDocByRef } 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,34 +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 rebuildNestedSets(CreatureProperties, rootCreature._id); - // Return the docId of the last property, the inserted root property - return rootId; + + // 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', @@ -90,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]; @@ -109,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); @@ -147,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) { @@ -175,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); @@ -185,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/mixins/setDocToLastMixin.js b/app/imports/api/creature/mixins/setDocToLastMixin.js deleted file mode 100644 index 3e55b5b4..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'; - -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/parenting/ChildSchema.ts b/app/imports/api/parenting/ChildSchema.ts index 027c1db0..7955496a 100644 --- a/app/imports/api/parenting/ChildSchema.ts +++ b/app/imports/api/parenting/ChildSchema.ts @@ -63,10 +63,14 @@ const ChildSchema = new SimpleSchema({ 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, } }); diff --git a/app/imports/api/parenting/parentingFunctions.ts b/app/imports/api/parenting/parentingFunctions.ts index 223c7a53..b2ec13ac 100644 --- a/app/imports/api/parenting/parentingFunctions.ts +++ b/app/imports/api/parenting/parentingFunctions.ts @@ -11,13 +11,23 @@ export function getCollectionByName(name: string): Mongo.Collection { return collection; } -export function fetchDocByRef(ref: Reference, options?: Mongo.Options): Promise { - const doc = getCollectionByName(ref.collection).findOneAsync(ref.id, options); +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; } @@ -274,15 +284,11 @@ export const getFilter = { }, } -export function fetchParent({ id, collection }) { - return fetchDocByRef({ id, collection }); -} - /** * 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 = {} }) { +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'); @@ -295,16 +301,21 @@ export function renewDocIds({ docArray, collectionMap, idMap = {} }) { idMap[oldId] = newId; }); - // 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; - } - } + // 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 => { - remapReference(doc.parent); - remapReference(doc.root); + // 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); + }); }); }