diff --git a/app/imports/api/library/LibraryNodes.js b/app/imports/api/library/LibraryNodes.js index ee241085..f87535ab 100644 --- a/app/imports/api/library/LibraryNodes.js +++ b/app/imports/api/library/LibraryNodes.js @@ -8,37 +8,10 @@ import getModifierFields from '/imports/api/getModifierFields.js'; let LibraryNodes = new Mongo.Collection('libraryNodes'); -const RefSchema = new SimpleSchema({ - id: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1 - }, -}); - let LibraryNodeSchema = schema({ - name: { - type: String, - optional: true, - }, libraryNodeType: { type: String, 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, }, }); diff --git a/app/imports/api/parenting/ChildSchema.js b/app/imports/api/parenting/ChildSchema.js index badd8219..15e525f0 100644 --- a/app/imports/api/parenting/ChildSchema.js +++ b/app/imports/api/parenting/ChildSchema.js @@ -1,18 +1,6 @@ import SimpleSchema from 'simpl-schema'; 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({ id: { type: String, @@ -24,9 +12,11 @@ const RefSchema = new SimpleSchema({ }, }); -RefSchema.extend(inhertitedFieldsSchema); - let ChildSchema = schema({ + order: { + type: Number, + min: 0, + }, parent: { type: RefSchema, optional: true, @@ -40,7 +30,4 @@ let ChildSchema = schema({ }, }); -const inheritedFields = new Set(inhertitedFieldsSchema.objectKeys()); - export default ChildSchema; -export { inheritedFields }; diff --git a/app/imports/api/parenting/order.js b/app/imports/api/parenting/order.js index f128aeea..3442ea04 100644 --- a/app/imports/api/parenting/order.js +++ b/app/imports/api/parenting/order.js @@ -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({ - 'ancestors.0': rootAncestor, + 'parent.id': parentId, }, { fields: {order: 1}, sort: {order: -1}, @@ -13,17 +57,22 @@ export function getHighestOrder({collection, rootAncestor}){ export function setDocToLastOrder({collection, doc}){ doc.order = getHighestOrder({ collection, - rootAncestor: doc.ancestors[0], + parentId: doc.parent.id, }) + 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; if (currentOrder === order){ return; } else { // Move the document to its new order - collection.update(doc._id, {$set: {order}}); + docRef.collection.update(doc._id, {$set: {order}}); let inBetweenSelector, increment; if (order > currentOrder){ // Move in-between docs backward @@ -41,8 +90,8 @@ export function setDocOrder({collection, doc, order}){ increment = 1; } collection.update({ + 'parent.id': doc.parent.id, order: {$and: inBetweenSelector}, - rootAncestor: doc.ancestors[0], }, { $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 = []; collection.find({ - 'ancestors.0': rootAncestor, + 'parent.id': parentId, }, { fields: {order: 1}, sort: {order: 1} diff --git a/app/imports/api/parenting/organizeDoc.js b/app/imports/api/parenting/organizeDoc.js new file mode 100644 index 00000000..427c9627 --- /dev/null +++ b/app/imports/api/parenting/organizeDoc.js @@ -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}) +}; diff --git a/app/imports/api/parenting/parenting.js b/app/imports/api/parenting/parenting.js index 04fd6a4e..24e422b3 100644 --- a/app/imports/api/parenting/parenting.js +++ b/app/imports/api/parenting/parenting.js @@ -2,83 +2,57 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; 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}){ return fetchDocByRef({id, collection}); } -// n database hits to get the children by parent id -export function fetchChildren({parentId, filter = {}, options}){ +export function fetchChildren({ collection, parentId, filter = {}, options = {sort: {order: 1}} }){ filter["parent.id"] = parentId; let children = []; - collections.forEach(collection => { - children.push( - ...collection.find({ - "parent.id": parentId - }, options).fetch() - ); - }); + children.push( + ...collection.find({ + "parent.id": parentId + }, options).fetch() + ); return children; } -// n database hits to update the decendents -export function updateChildren({parentId, filter = {}, modifier, options={}}){ +export function updateChildren({collection, parentId, filter = {}, modifier, options={}}){ filter["parent.id"] = parentId; 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({ancestorId, filter = {}, options}){ +export function fetchDecendents({ collection, ancestorId, filter = {}, options}){ filter["ancestors.id"] = ancestorId; let decendents = []; - collections.forEach(collection => { - decendents.push(...collection.find(filter, options).fetch()); - }); + decendents.push(...collection.find(filter, options).fetch()); return decendents; } -// n database hits to update the decendents -export function updateDecendents({ancestorId, filter = {}, modifier, options={}}){ +export function updateDecendents({collection, ancestorId, filter = {}, modifier, options={}}){ filter["ancestors.id"] = ancestorId; 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({ancestorId, filter = {}, options}, callback){ +export function forEachDecendent({collection, ancestorId, filter = {}, options}, callback){ filter["ancestors.id"] = ancestorId; - collections.forEach(collection => { - collection.find(filter, options).forEach(callback); - }); + collection.find(filter, options).forEach(callback); } // 1 database read -// TODO generalise for all inheritedFields -export function getAncestry({id, collection}){ - // Get the parent ref - let parentDoc = fetchDocByRef({id, collection}, {fields: { - name: 1, - enabled: 1, - ancestors: 1, - }}); - let parent = { - id, - collection, - name: parentDoc.name, - enabled: parentDoc.enabled, - }; +export function getAncestry({parentRef, inheritedFields = {}}){ + // Ancestry must include ancestors + inheritedFields.ancestors = 1; + + 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 || []; @@ -87,77 +61,7 @@ export function getAncestry({id, collection}){ return {parent, ancestors}; } -export function setDocAncestryMixin(methodOptions){ - // 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){ +export function updateParent({docRef, parentRef}){ let collection = getCollectionByName(docRef.collection); let oldDoc = fetchDocByRef(docRef, {fields: { parent: 1, @@ -168,7 +72,7 @@ export function updateParent(docRef, parentRef){ if (oldDoc.parent.id === parentRef.id) return; // update the document's parenting - let {parent, ancestors} = getAncestry(parentRef); + let {parent, ancestors} = getAncestry({parentRef}); collection.update(docRef.id, {$set: {parent, ancestors}}); // Remove the old ancestors from the decendents @@ -191,9 +95,7 @@ export function updateParent(docRef, parentRef){ }); } -// TODO these rely on hard coding inherited fields -// the inherited fields should only appear on the childChema, nowhere else -// Move these somewhere appropriate +// TODO move these functions to character properties collection export function findEnabled(collection, query, options){ query.enabled = true; query['ancestors.$.enabled'] = {$not: false};