Some changes to how parenting and ordering of docs interface

This commit is contained in:
Stefan Zermatten
2019-07-30 14:48:49 +02:00
parent 438f128641
commit cbdd72e09b
5 changed files with 96 additions and 179 deletions

View File

@@ -8,37 +8,10 @@ import getModifierFields from '/imports/api/getModifierFields.js';
let LibraryNodes = new Mongo.Collection('libraryNodes'); let LibraryNodes = new Mongo.Collection('libraryNodes');
const RefSchema = new SimpleSchema({
id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1
},
});
let LibraryNodeSchema = schema({ let LibraryNodeSchema = schema({
name: {
type: String,
optional: true,
},
libraryNodeType: { libraryNodeType: {
type: String, type: String,
allowedValues: Object.keys(librarySchemas), allowedValues: Object.keys(librarySchemas),
},
order: {
type: SimpleSchema.Integer,
index: true,
},
parent: {
type: RefSchema,
},
// ancestors[0] should be the library to check for permission
ancestors: {
type: Array,
defaultValue: [],
},
'ancestors.$': {
type: RefSchema,
}, },
}); });

View File

@@ -1,18 +1,6 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import schema from '/imports/api/schema.js'; import schema from '/imports/api/schema.js';
const inhertitedFieldsSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
enabled: {
type: Boolean,
optional: true,
index: 1,
},
});
const RefSchema = new SimpleSchema({ const RefSchema = new SimpleSchema({
id: { id: {
type: String, type: String,
@@ -24,9 +12,11 @@ const RefSchema = new SimpleSchema({
}, },
}); });
RefSchema.extend(inhertitedFieldsSchema);
let ChildSchema = schema({ let ChildSchema = schema({
order: {
type: Number,
min: 0,
},
parent: { parent: {
type: RefSchema, type: RefSchema,
optional: true, optional: true,
@@ -40,7 +30,4 @@ let ChildSchema = schema({
}, },
}); });
const inheritedFields = new Set(inhertitedFieldsSchema.objectKeys());
export default ChildSchema; export default ChildSchema;
export { inheritedFields };

View File

@@ -1,8 +1,52 @@
import SimpleSchema from 'simpl-schema'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
export function getHighestOrder({collection, rootAncestor}){ // Docs keep track of their order amongst their siblings and keep a copy of the
// order of their ancestors. Order is first compared between oldest non-shared
// ancestors, then by ancestors before children, then between order of siblings.
export function compareOrder(docA, docB){
// < 0 if A comes before B
// = 0 if A and B are the same order
// > 0 if B comes before A
// Documents are equal order to themselves
if (docA._id && docB._id && docA._id === docB._id){
return 0;
}
// If they are siblings, just compare order
if (docA.parent.id === docB.parent.id){
return docA.order - docB.order;
}
// They must share a root ancestor to be meaningfully sorted
if (docA.ancestors[0].id !== docB.ancestors[0].id){
return 0;
}
// Go through their ancestors after the root, and find the first order
// difference
let i, difference;
const length = Math.min(docA.ancestors.length, docB.ancestors.length);
for (i = 1; i < length; i++){
difference = docA.ancestors[i].order - docB.ancestors[i].order;
if (difference){
return difference;
} else if (docA.ancestors[i].id !== docB.ancestors[i].id) {
throw new Meteor.Error('Sibling order clash',
'Sibling docs share the same order, sort failed');
}
}
// We haven't returned yet, all ancestors up to this point are shared and one
// doc has no more ancestors implying one is an ancestor of the other,
// return the difference in their ancestor list lengths, shorter comes first
return docA.ancestors.length - docB.ancestors.length
}
export function getHighestOrder({collection, parentId}){
const highestOrderedDoc = collection.findOne({ const highestOrderedDoc = collection.findOne({
'ancestors.0': rootAncestor, 'parent.id': parentId,
}, { }, {
fields: {order: 1}, fields: {order: 1},
sort: {order: -1}, sort: {order: -1},
@@ -13,17 +57,22 @@ export function getHighestOrder({collection, rootAncestor}){
export function setDocToLastOrder({collection, doc}){ export function setDocToLastOrder({collection, doc}){
doc.order = getHighestOrder({ doc.order = getHighestOrder({
collection, collection,
rootAncestor: doc.ancestors[0], parentId: doc.parent.id,
}) + 1; }) + 1;
} }
export function setDocOrder({collection, doc, order}){ export function updateOrder({docRef, order}){
let doc = fetchDocByRef(docRef, {fields: {
order: 1,
parent: 1,
}});
let collection = getCollectionByName(docRef.collection);
const currentOrder = doc.order; const currentOrder = doc.order;
if (currentOrder === order){ if (currentOrder === order){
return; return;
} else { } else {
// Move the document to its new order // Move the document to its new order
collection.update(doc._id, {$set: {order}}); docRef.collection.update(doc._id, {$set: {order}});
let inBetweenSelector, increment; let inBetweenSelector, increment;
if (order > currentOrder){ if (order > currentOrder){
// Move in-between docs backward // Move in-between docs backward
@@ -41,8 +90,8 @@ export function setDocOrder({collection, doc, order}){
increment = 1; increment = 1;
} }
collection.update({ collection.update({
'parent.id': doc.parent.id,
order: {$and: inBetweenSelector}, order: {$and: inBetweenSelector},
rootAncestor: doc.ancestors[0],
}, { }, {
$inc: {order: increment}, $inc: {order: increment},
}, { }, {
@@ -51,10 +100,10 @@ export function setDocOrder({collection, doc, order}){
} }
} }
export function reorderDocs({collection, rootAncestor}){ export function reorderDocs({collection, parentId}){
let bulkWrite = []; let bulkWrite = [];
collection.find({ collection.find({
'ancestors.0': rootAncestor, 'parent.id': parentId,
}, { }, {
fields: {order: 1}, fields: {order: 1},
sort: {order: 1} sort: {order: 1}

View File

@@ -0,0 +1,6 @@
import { updateParent } from '/imports/api/parenting/parenting.js';
export default function organizeDoc({docRef, parentRef, order}){
updateParent({docRef, parentRef});
updateOrder({docRef, order})
};

View File

@@ -2,83 +2,57 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
// n = collections.length
let collections = [];
export function registerCollection(collectionName){
collections.push(collectionName);
}
// 1 database hit to get the parent by reference
export function fetchParent({id, collection}){ export function fetchParent({id, collection}){
return fetchDocByRef({id, collection}); return fetchDocByRef({id, collection});
} }
// n database hits to get the children by parent id export function fetchChildren({ collection, parentId, filter = {}, options = {sort: {order: 1}} }){
export function fetchChildren({parentId, filter = {}, options}){
filter["parent.id"] = parentId; filter["parent.id"] = parentId;
let children = []; let children = [];
collections.forEach(collection => { children.push(
children.push( ...collection.find({
...collection.find({ "parent.id": parentId
"parent.id": parentId }, options).fetch()
}, options).fetch() );
);
});
return children; return children;
} }
// n database hits to update the decendents export function updateChildren({collection, parentId, filter = {}, modifier, options={}}){
export function updateChildren({parentId, filter = {}, modifier, options={}}){
filter["parent.id"] = parentId; filter["parent.id"] = parentId;
options.multi = true; options.multi = true;
collections.forEach(collection => { collection.update(filter, modifier, options);
collection.update(filter, modifier, options);
});
} }
// n database hits to fetch the decendents by ancestor id, in no particular order export function fetchDecendents({ collection, ancestorId, filter = {}, options}){
export function fetchDecendents({ancestorId, filter = {}, options}){
filter["ancestors.id"] = ancestorId; filter["ancestors.id"] = ancestorId;
let decendents = []; let decendents = [];
collections.forEach(collection => { decendents.push(...collection.find(filter, options).fetch());
decendents.push(...collection.find(filter, options).fetch());
});
return decendents; return decendents;
} }
// n database hits to update the decendents export function updateDecendents({collection, ancestorId, filter = {}, modifier, options={}}){
export function updateDecendents({ancestorId, filter = {}, modifier, options={}}){
filter["ancestors.id"] = ancestorId; filter["ancestors.id"] = ancestorId;
options.multi = true; options.multi = true;
collections.forEach(collection => { collection.update(filter, modifier, options);
collection.update(filter, modifier, options);
});
} }
// n database hits to get decendents to act on export function forEachDecendent({collection, ancestorId, filter = {}, options}, callback){
export function forEachDecendent({ancestorId, filter = {}, options}, callback){
filter["ancestors.id"] = ancestorId; filter["ancestors.id"] = ancestorId;
collections.forEach(collection => { collection.find(filter, options).forEach(callback);
collection.find(filter, options).forEach(callback);
});
} }
// 1 database read // 1 database read
// TODO generalise for all inheritedFields export function getAncestry({parentRef, inheritedFields = {}}){
export function getAncestry({id, collection}){ // Ancestry must include ancestors
// Get the parent ref inheritedFields.ancestors = 1;
let parentDoc = fetchDocByRef({id, collection}, {fields: {
name: 1, let parentDoc = fetchDocByRef(parentRef, {fields: inheritedFields});
enabled: 1, let parent = { ...parentRef};
ancestors: 1, for (let field in inheritedFields){
}}); if (inheritedFields[field]){
let parent = { parent[field] = parentDoc[field];
id, }
collection, }
name: parentDoc.name,
enabled: parentDoc.enabled,
};
// Ancestors is [...parent's ancestors, parent ref] // Ancestors is [...parent's ancestors, parent ref]
let ancestors = parentDoc.ancestors || []; let ancestors = parentDoc.ancestors || [];
@@ -87,77 +61,7 @@ export function getAncestry({id, collection}){
return {parent, ancestors}; return {parent, ancestors};
} }
export function setDocAncestryMixin(methodOptions){ export function updateParent({docRef, parentRef}){
// Extend the method's schema to require the needed properties
// This mixin should come before simpleschema mixin
methodOptions.schema = new SimpleSchema({
parent: {
type: Object,
optional: true,
},
'parent.id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
'parent.collection': {
type: String,
},
}).extend(methodOptions.schema);
// Change the doc's ancestry before running
let runFunc = methodOptions.run;
methodOptions.run = function(doc, ...rest){
// If the doc's parent doesn't exist, set it to the character
if (!doc.parent && doc.charId) {
doc.parent = {id: doc.charId, collection: 'creatures'};
}
let {parent, ancestors} = getAncestry(doc.parent);
doc.parent = parent;
doc.ancestors = ancestors;
return runFunc.call(this, doc, ...rest);
};
return methodOptions;
}
function ensureAncestryContainsId(ancestors, id){
if (!id){
throw new Meteor.Error('ancestor-check-failed',
`Expected charId, got ${id}`
);
}
if (!ancestors){
throw new Meteor.Error('ancestor-check-failed',
`Expected ancestors array, got ${ancestors}`
);
}
for (let ancestor of ancestors){
if (ancestor.id === id){
return;
}
}
throw new Meteor.Error('ancestor-check-failed',
`Ancestors did not contain id: ${id}`
);
}
export function ensureAncestryContainsCharIdMixin(methodOptions){
// Extend the method's schema to require the needed properties
// This mixin should come before simpleSchemaMixin
methodOptions.schema = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).extend(methodOptions.schema);
let runFunc = methodOptions.run;
methodOptions.run = function({charId, ancestors}){
ensureAncestryContainsId(ancestors, charId);
return runFunc.apply(this, arguments);
};
return methodOptions;
}
export function updateParent(docRef, parentRef){
let collection = getCollectionByName(docRef.collection); let collection = getCollectionByName(docRef.collection);
let oldDoc = fetchDocByRef(docRef, {fields: { let oldDoc = fetchDocByRef(docRef, {fields: {
parent: 1, parent: 1,
@@ -168,7 +72,7 @@ export function updateParent(docRef, parentRef){
if (oldDoc.parent.id === parentRef.id) return; if (oldDoc.parent.id === parentRef.id) return;
// update the document's parenting // update the document's parenting
let {parent, ancestors} = getAncestry(parentRef); let {parent, ancestors} = getAncestry({parentRef});
collection.update(docRef.id, {$set: {parent, ancestors}}); collection.update(docRef.id, {$set: {parent, ancestors}});
// Remove the old ancestors from the decendents // Remove the old ancestors from the decendents
@@ -191,9 +95,7 @@ export function updateParent(docRef, parentRef){
}); });
} }
// TODO these rely on hard coding inherited fields // TODO move these functions to character properties collection
// the inherited fields should only appear on the childChema, nowhere else
// Move these somewhere appropriate
export function findEnabled(collection, query, options){ export function findEnabled(collection, query, options){
query.enabled = true; query.enabled = true;
query['ancestors.$.enabled'] = {$not: false}; query['ancestors.$.enabled'] = {$not: false};