247 lines
7.1 KiB
JavaScript
247 lines
7.1 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);
|
|
|
|
// 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;
|