Files
DiceCloud/app/imports/api/parenting/parenting.js

238 lines
6.4 KiB
JavaScript

import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { flatten } from 'lodash';
const generalParents = [
'attribute',
'buff',
'classLevel',
'feature',
'folder',
'root',
'item',
'spell',
];
// Which types are allowed as parents for other types
const allowedParenting = {
folder: [...generalParents, 'container'],
rollResult: ['roll', 'rollResult'],
container: ['root', 'folder'],
item: ['root', 'container', 'folder'],
};
const allParentTypes = new Set(flatten(Object.values(allowedParenting)));
export function canBeParent(type){
return type && allParentTypes.has(type);
}
export function getAllowedParents({childType}){
return allowedParenting[childType] || generalParents;
}
export function isParentAllowed({parentType = 'root', childType}){
if (!childType) throw 'childType is required';
let allowedParents = getAllowedParents({childType});
return allowedParents.includes(parentType);
}
export function fetchParent({id, collection}){
return fetchDocByRef({id, collection});
}
export function fetchChildren({ collection, parentId, filter = {}, options = {sort: {order: 1}} }){
filter["parent.id"] = parentId;
let children = [];
children.push(
...collection.find({
"parent.id": parentId
}, options).fetch()
);
return children;
}
export function updateChildren({collection, parentId, filter = {}, modifier, options={}}){
filter["parent.id"] = parentId;
options.multi = true;
collection.update(filter, modifier, options);
}
export function fetchDescendants({ collection, ancestorId, filter = {}, options}){
filter["ancestors.id"] = ancestorId;
let descendants = [];
descendants.push(...collection.find(filter, options).fetch());
return descendants;
}
export function updateDescendants({collection, ancestorId, filter = {}, modifier, options={}}){
filter["ancestors.id"] = ancestorId;
options.multi = true;
options.selector = {type: 'any'};
collection.update(filter, modifier, options);
}
export function forEachDescendant({collection, ancestorId, filter = {}, options}, callback){
filter["ancestors.id"] = ancestorId;
collection.find(filter, options).forEach(callback);
}
// 1 database read
export function getAncestry({parentRef, inheritedFields = {}}){
let parentDoc = fetchDocByRef(parentRef, {fields: inheritedFields});
let parent = { ...parentRef};
for (let field in inheritedFields){
if (inheritedFields[field]){
parent[field] = parentDoc[field];
}
}
// Ancestors is [...parent's ancestors, parent ref]
let ancestors = parentDoc.ancestors || [];
ancestors.push(parent);
return {parentDoc, parent, ancestors};
}
export function setLineageOfDocs({docArray, oldParent, newAncestry}){
//const oldParent = oldAncestry[oldAncestry.length - 1];
const newParent = newAncestry[newAncestry.length - 1];
docArray.forEach(doc => {
if(doc.parent.id === oldParent.id){
doc.parent = newParent;
}
let oldAncestors = doc.ancestors;
let oldParentIndex = oldAncestors.findIndex(a => a.id === oldParent.id);
doc.ancestors = [...newAncestry, ...oldAncestors.slice(oldParentIndex + 1)];
});
}
/**
* 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}){
// map of {oldId: newId}
let idMap = {};
// Give new ids and map the changes
docArray.forEach(doc => {
let oldId = doc._id;
let newId = Random.id();
doc._id = newId;
idMap[oldId] = newId;
});
const remapReference = ref => {
if (idMap[ref.id]){
ref.id = idMap[ref.id];
ref.collection = collectionMap[ref.collection] || ref.collection;
}
}
docArray.forEach(doc => {
remapReference(doc.parent);
doc.ancestors.forEach(remapReference);
});
}
export function updateParent({docRef, parentRef}){
let collection = getCollectionByName(docRef.collection);
let oldDoc = fetchDocByRef(docRef, {fields: {
parent: 1,
ancestors: 1,
type: 1,
}});
let updateOptions = { selector: {type: 'any'} };
// Skip if we aren't changing the parent id
if (oldDoc.parent.id === parentRef.id) return;
// Get the parent and its ancestry
let {parentDoc, parent, ancestors} = getAncestry({parentRef});
// If the doc and its parent are in the same collection, apply the allowed
// parent rules based on type
if (docRef.collection === parentRef.collection){
let parentAllowed = isParentAllowed({
parentType: parentDoc.type,
childType: oldDoc.type
});
if (!parentAllowed){
throw new Meteor.Error('invalid parenting',
`Can't make ${oldDoc.type} a child of ${parentDoc.type}`)
}
}
// update the document's parenting
collection.update(docRef.id, {
$set: {parent, ancestors}
}, updateOptions);
// Remove the old ancestors from the descendants
updateDescendants({
collection,
ancestorId: docRef.id,
modifier: {$pullAll: {
ancestors: oldDoc.ancestors,
}},
options: updateOptions,
});
// Add the new ancestors to the descendants
updateDescendants({
collection,
ancestorId: docRef.id,
modifier: {$push: {
ancestors: {
$each: ancestors,
$position: 0,
},
}},
options: updateOptions,
});
}
// TODO move these functions to character properties collection
export function findEnabled(collection, query, options){
query.enabled = true;
query['ancestors.$.enabled'] = {$not: false};
return collection.find(query, options);
}
export function getName(doc){
if (doc.name) return name;
var i = doc.ancestors.length;
while(i--) {
if (ancestors[i].name) return ancestors[i].name;
}
}
export function nodesToTree({collection, ancestorId}){
// Store a dict of all the nodes
let nodeIndex = {};
let nodeList = [];
collection.find({
'ancestors.id': ancestorId,
removed: {$ne: true},
}, {
sort: {order: 1}
}).forEach( node => {
let treeNode = {
node: node,
children: [],
};
nodeIndex[node._id] = treeNode;
nodeList.push(treeNode);
});
// Create a forest of trees
let forest = [];
// Either the node is a child of another node, or in the forest as a root
nodeList.forEach(node => {
if (nodeIndex[node.node.parent.id]){
nodeIndex[node.node.parent.id].children.push(node);
} else {
forest.push(node);
}
});
return forest;
}