Migrated insert prop methods to nested sets

This commit is contained in:
ThaumRystra
2023-10-01 17:30:21 +02:00
parent fb7413dba4
commit e4590de3a7
6 changed files with 80 additions and 143 deletions

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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 => {

View File

@@ -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;
}

View File

@@ -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,
}
});

View File

@@ -11,13 +11,23 @@ export function getCollectionByName(name: string): Mongo.Collection<TreeDoc> {
return collection;
}
export function fetchDocByRef(ref: Reference, options?: Mongo.Options<object>): Promise<TreeDoc> {
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<object>): Promise<TreeDoc> {
const doc = getCollectionByName(ref.collection).findOneAsync(ref.id, options);
assertDocFound(doc);
return doc;
}
export function fetchDocByRef(ref: Reference, options?: Mongo.Options<object>): 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);
});
});
}