apostrophe
Version:
The Apostrophe Content Management System.
239 lines (232 loc) • 8.61 kB
JavaScript
const joinr = module.exports = {
// Fetch a one-to-many relationship with related documents via an array
// property of your documents.
//
// If you have users and wish to bring all associated groups into a
// ._groups property based on a .groupIds array property, this is what
// you want.
//
// The first argument should be an array of documents already fetched.
//
// The second argument is the name of an array property in each of those
// documents that identifies related documents by id (for instance, groupIds).
//
// The third argument is the name of an object property in each of
// those documents that describes the relationship between the document and
// each of the related documents. This object is expected to be structured
// like this:
//
// personFields: {
// idOfPerson1: {
// jobTitle: 'Chief Cook'
// },
// idOfPerson2: {
// jobTitle: 'Chief Bottle Washer'
// }
// }
//
// If no relationship fields are needed it may be falsy.
//
// The fourth argument is the array property name in which to store the
// related documents after fetching them (for instance, _groups).
//
// The fifth argument is the function to call to fetch the related documents.
// This function will receive an array of IDs and its awaited return value
// must be an array of documents.
//
// The sixth argument is a function to transform each `_id`. This is
// used to add or rewrite the locale suffix.
//
// Afterwards the related documents will be attached directly to the items
// under the array property name specified by objectsField.
//
// *If the fieldsStorage argument is present*, the related documents
// are returned as usual, with an additional `_fields` property added to
// each, containing the custom relationship fields for that object.
//
// group._people[0].name <-- Person's name
// group._people[0]._fields.jobTitle <-- Person's job title in
// this specific department; they may have other titles in other departments
//
// Since the same person may be related to more than one group, but with
// different custom relationship fields, this method guarantees that the
// person object will be shallowly cloned so that it can have a distinct
// `_fields` property.
//
// Example:
//
// await joinr.byArray(users, 'groupIds', '_groups', async function(ids) {
// // returns a promise, as good as awaiting
// return groupsCollection.find({ groupIds: { $in: ids } }).toArray();
// });
byArray: async function(
items,
idsStorage,
fieldsStorage,
objectsField,
getter,
idMapper
) {
// This method never alters the items array itself, it alters
// the objects within it. So it is safe to reduce that array to
// its unique elements, and this simplifies calling code which does
// not have to guard against this situation.
// Note that we mean literal uniqueness (e.g. by reference), as this
// is the only time we need to avoid appending the same joined objects
// more than once.
items = [ ...new Set(items) ];
let otherIds = [];
const othersById = {};
for (const item of items) {
item[objectsField] = [];
if (joinr._has(item, idsStorage)) {
otherIds = otherIds.concat(joinr._get(item, idsStorage).map(idMapper));
}
}
if (otherIds.length) {
const others = await getter(otherIds);
// Make a lookup table of the others by id
for (const other of others) {
othersById[idMapper(other._id)] = other;
}
// Attach the others to the items
for (const item of items) {
for (const id of (joinr._get(item, idsStorage) || []).map(idMapper)) {
if (othersById[id]) {
if (fieldsStorage) {
const fieldsById = joinr._get(item, fieldsStorage) || {};
item[objectsField].push({
...othersById[id],
_fields: fieldsById[id] || {}
});
} else {
item[objectsField].push(othersById[id]);
}
}
}
}
}
},
// Perform a one-to-many relationship with related documents via an array
// property of the related documents.
//
// If you have groups and wish to bring all associated users into a
// ._users property based on a .groupIds array property of those users,
// this is what you want.
//
// The first argument should be an array of documents already fetched.
//
// The second argument is the array property in each of the related documents
// that identifies documents in your original collection (for instance,
// groupIds).
//
// The optional third argument is the name of an object property in each of
// those related documents that describes the relationship between the related
// document and each of your documents. This object is expected to be
// structured like this:
//
// personFields: {
// idOfPerson1: {
// jobTitle: 'Chief Cook'
// },
// idOfPerson2: {
// jobTitle: 'Chief Bottle Washer'
// }
// }
//
// The fourth argument is the array property name in which to store the
// related documents after fetching them (for instance, _users).
//
// The fifth argument is the function to call to fetch the related documents.
// This function will receive an array of IDs referring to documents in
// your original collection. It will be awaited, and the resolved value must
// be an array of documents.
//
// The sixth argument is a function to transform each `_id`. This is
// used to add or rewrite the locale suffix.
//
// Afterwards The related documents will be attached directly to the items
// under the property name specified by `objectsField`.
//
// *If the fieldsStorage argument is truthy*, then each related document
// gains an extra `_fields` property, containing the relationship data
// for that object. Note that the related documents are shallowly cloned to
// ensure the same document can be related to two items but with different
// relationship data.
//
// group._people[0].name <-- person's name
// group._people[0]._fields.jobTitle <-- Person's job title in
// this specific department; they may have other titles in other departments
//
// Example:
//
// await joinr.byArrayReverse(groups, 'groupIds', '_users', async function(ids) {
// // returns a promise, as good as awaiting
// return usersCollection.find({ placeIds: { $in: ids } }).toArray();
// });
byArrayReverse: async function(
items,
idsStorage,
fieldsStorage,
objectsField,
getter,
idMapper
) {
// This method never alters the items array itself, it alters
// the objects within it. So it is safe to reduce that array to
// its unique elements, and this simplifies calling code which does
// not have to guard against this situation.
// Note that we mean literal uniqueness (e.g. by reference), as this
// is the only time we need to avoid appending the same joined objects
// more than once.
items = [ ...new Set(items) ];
const itemIds = items.map(item => idMapper(item._id));
for (const item of items) {
if (!item[objectsField]) {
item[objectsField] = [];
}
}
if (itemIds.length) {
const others = await getter(itemIds);
const itemsById = {};
for (const item of items) {
itemsById[idMapper(item._id)] = item;
}
// Attach the others to the items
for (const other of others) {
for (const id of (joinr._get(other, idsStorage) || []).map(idMapper)) {
if (itemsById[id]) {
const item = itemsById[id];
if (fieldsStorage) {
const fieldsById = joinr._get(other, fieldsStorage) || {};
item[objectsField].push({
...other,
_fields: fieldsById[idMapper(item._id)] || {}
});
} else {
item[objectsField].push(other);
}
}
}
}
}
},
_has: function(o, accessor) {
return !!joinr._get(o, accessor);
},
// This supports: foo, foo.bar, foo.bar.baz (dot notation,
// like mongodb) and also passing in a custom accessor function
_get: function(o, accessor) {
let fn = accessor;
if (typeof (accessor) === 'string') {
fn = function(o) {
const keys = accessor.split(/\./);
for (const key of keys) {
o = o[key];
}
return o;
};
}
return fn(o);
}
};