Skip to content

Instantly share code, notes, and snippets.

@joeytwiddle
Last active December 20, 2022 15:25
Show Gist options
  • Save joeytwiddle/6129676 to your computer and use it in GitHub Desktop.
Save joeytwiddle/6129676 to your computer and use it in GitHub Desktop.
Deep population helper for mongoose
// Example usage:
// deepPopulate(blogPost, "comments comments._creator comments._creator.blogposts", {sort:{title:-1}}, callback);
// Note that the options get passed at *every* level!
// Also note that you must populate the shallower documents before the deeper ones.
function deepPopulate(doc, pathListString, options, callback) {
var listOfPathsToPopulate = pathListString.split(" ");
function doNext() {
if (listOfPathsToPopulate.length == 0) {
// Now all the things underneath the original doc should be populated. Thanks mongoose!
callback(null,doc);
} else {
var nextPath = listOfPathsToPopulate.shift();
var pathBits = nextPath.split(".");
var listOfDocsToPopulate = resolveDocumentzAtPath(doc, pathBits.slice(0,-1));
if (listOfDocsToPopulate.length > 0) {
var lastPathBit = pathBits[pathBits.length-1];
// There is an assumption here, that desendent documents which share the same path will all have the same model!
// If not, we must make a separate populate request for each doc, which could be slow.
var model = listOfDocsToPopulate[0].constructor;
var pathRequest = [{
path: lastPathBit,
options: options
}];
console.log("Populating field '"+lastPathBit+"' of "+listOfDocsToPopulate.length+" "+model.modelName+"(s)");
model.populate(listOfDocsToPopulate, pathRequest, function(err,results){
if (err) return callback(err);
//console.log("model.populate yielded results:",results);
doNext();
});
} else {
// There are no docs to populate at this level.
doNext();
}
}
}
doNext();
}
function resolveDocumentzAtPath(doc, pathBits) {
if (pathBits.length == 0) {
return [doc];
}
//console.log("Asked to resolve "+pathBits.join(".")+" of a "+doc.constructor.modelName);
var resolvedSoFar = [];
var firstPathBit = pathBits[0];
var resolvedField = doc[firstPathBit];
if (resolvedField === undefined || resolvedField === null) {
// There is no document at this location at present
} else {
if (Array.isArray(resolvedField)) {
resolvedSoFar = resolvedSoFar.concat(resolvedField);
} else {
resolvedSoFar.push(resolvedField);
}
}
//console.log("Resolving the first field yielded: ",resolvedSoFar);
var remainingPathBits = pathBits.slice(1);
if (remainingPathBits.length == 0) {
return resolvedSoFar; // A redundant check given the check at the top, but more efficient.
} else {
var furtherResolved = [];
resolvedSoFar.forEach(function(subDoc){
var deeperResults = resolveDocumentzAtPath(subDoc, remainingPathBits);
furtherResolved = furtherResolved.concat(deeperResults);
});
return furtherResolved;
}
}
@joeytwiddle
Copy link
Author

@brian-pantano I do not believe that is possible. We need to get the documents at level N before we can populate their fields at level N+1!

@buunguyen
Copy link

In case anyone's interested, I've created a more generic and robust solution in form of a Mongoose plugin. It supports multiple nested levels and subpaths, including linked documents and subdocuments. Examples:

post.deepPopulate('votes.user, comments.user.followers, ...', cb);
Post.deepPopulate(posts, 'votes.user, comments.user.followers', cb);

@joeytwiddle
Copy link
Author

joeytwiddle commented Jun 6, 2016

That's awesome buunguyen. It seems to be pretty popular. 👍

If you are every bothered by tabs on Github, there is a trick you can use: just add ?ts=2 to the URL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment