Files
DiceCloud/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js
Stefan Zermatten e89b4946d5 Improvements to level up and slot fill
- class level refs work with level up
- Improve UI
- Fixed level up backfill repeating levels when selecting higher levels
multiple times
- Allowed user to ignore slot fill condition
- Auto load more if many disabled fillers
2023-08-02 11:27:17 +02:00

249 lines
7.2 KiB
JavaScript

import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
getAncestry,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode',
validate: new SimpleSchema({
nodeIds: {
type: Array,
max: 20,
},
'nodeIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parentRef: {
type: RefSchema,
},
order: {
type: Number,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ nodeIds, parentRef, order }) {
// get the new ancestry for the properties
let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures') {
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
// {libraryId: hasViewPermission}
//let libraryPermissionMemoir = {};
let node;
nodeIds.forEach(nodeId => {
// TODO: Check library view permission for each node before starting
node = insertPropertyFromNode(nodeId, ancestors, order);
});
// get one of the root inserted docs
let rootId = node._id;
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: rootCreature._id,
});
// Return the docId of the last property, the inserted root property
return rootId;
},
});
function insertPropertyFromNode(nodeId, ancestors, order) {
// Fetch the library node and its decendents, 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;
else {
throw new Meteor.Error(
'Insert property from library failed',
`No library document with id '${nodeId}' was found`
);
}
}
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
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];
// Convert all references into actual nodes
nodes = reifyNodeReferences(nodes);
// Refetch the root node, it might have been reified
node = nodes[0] || node;
// 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 all nodes as dirty
dirtyNodes(nodes);
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
return node;
}
function storeLibraryNodeReferences(nodes) {
nodes.forEach(node => {
if (node.libraryNodeId) return;
node.libraryNodeId = node._id;
});
}
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) {
depth += 1;
// New nodes added this function
let newNodes = [];
// Filter out the reference nodes we replace
let resultingNodes = nodes.filter(node => {
// This isn't a reference node, continue as normal
if (node.type !== 'reference') return true;
// We have gone too deep, keep the reference node as an error
if (depth >= 10) {
if (Meteor.isClient) console.warn('Reference depth limit exceeded');
node.cache = { error: 'Reference depth limit exceeded' };
return true;
}
let referencedNode
try {
referencedNode = fetchDocByRef(node.ref);
referencedNode.order = node.order;
// We are definitely replacing this node, so add it to the list
visitedRefs.add(node._id);
} catch (e) {
node.cache = { error: e.reason || e.message || e.toString() };
return true;
}
// Get all the descendants of the referenced node
let descendents = LibraryNodes.find({
'ancestors.id': referencedNode._id,
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,
});
// Filter all the looped references
addedNodes = addedNodes.filter(addedNode => {
// Add all non-reference nodes
if (addedNode.type !== 'reference') {
return true;
}
// If this exact reference has already been resolved before, filter it out
if (visitedRefs.has(addedNode._id)) {
return false;
} else {
// Otherwise mark it as visited, and keep it
visitedRefs.add(addedNode._id);
return true;
}
});
// Before renewing Ids make sure the library node reference is stored
storeLibraryNodeReferences(addedNodes);
// Give the new referenced sub-tree new ids
// The referenced node must get the id of the ref node so that the
// descendants of the ref node keep their ancestry intact
renewDocIds({
docArray: addedNodes,
idMap: { [referencedNode._id]: node._id },
});
// Reify the subtree as well with recursion
addedNodes = reifyNodeReferences(addedNodes, visitedRefs, depth);
// Store the new nodes from this inner loop without altering the array
// we are looping over
newNodes.push(...addedNodes);
});
// We are done filtering the array, we can add the new nodes to it
resultingNodes.push(...newNodes);
return resultingNodes;
}
export default insertPropertyFromLibraryNode;