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;
}
}
@brian-pantano
Copy link

I find your use of tab literals disturbing!

nit aside, it'd be nice if it populated level by level with each level being run in parallel, instead of having each element of pathListString.split(" ") fetch serially.

@JonathanJonathanJonathan

Thanks a lot!

@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