I'm having a spot of bother with Mongoose.js - I have a Category schema, and a Projects schema. Each category can have any number of projects, and each project has both a type and a relatedProjects array that lists projects with the same type. The relatedProjects field has to be random, and limited to 4 - but I couldn't do this with Mongoose and instead sort the entire results of relatedProjects outside of the schema using lodash.
This is the instance method I'm using:
ProjectSchema.methods.findRelatedProjects = function(callback) {
return this.model('Project')
.where('type').equals(this.type) // find projects with the same type
.where('_id').ne(this._id) // exclude this project from the result set
.select('_id')
.exec(callback);
};
This is how I fetch the relatedProjects for a single project (the promise library I'm using is when.js):
router.get('/:id', function(req, res) {
var data = {};
when(Project.findOne({ _id: req.params.id }).exec())
.then(function(project) {
data.project = project;
return node.call(data.project.findRelatedProjects.bind(data.project));
})
.then(function(relatedProjects) {
var project = data.project.toObject();
project.relatedProjects = _.first(_.shuffle(relatedProjects), 4);
res.send(project);
})
.catch(function(error) {
res.send(error);
});
});
However, when I try to populate that field after fetching an entire collection, be it Categories or Projects, I run into trouble. Either the results that get sent back are completely empty (though console.log'ing the results gives me the right data), or it comes back without the relatedProjects field set.
Here is code that returns completely empty:
router.get('/content.json', function(req, res) {
var data = {};
when(Category.find({}).lean().exec())
.then(function(categories) {
data.categories = categories;
return when.map(categories, function(category, i) {
return when(Project.find({ category: category._id }).exec())
})
.then(function(projects) {
return when.map(projects, function(project, j) {
return node.call(project.findRelatedProjects.bind(project))
.then(function(relatedProjects) {
project.relatedProjects = relatedProjects;
return project;
})
})
});
})
.then(function(categories) {
res.send({ categories: categories });
})
.catch(function(error) {
res.send(error);
});
});
It's worth noting that when.map iterates asynchronously, but I have tried looping regularly only to receive the same result.
This code returns all the data, except for the fact that each relatedProjects instance is completely empty. (console.log(relatedprojects) gives me the right data, though):
router.get('/content.json', function(req, res) {
var data = {};
when(Category.find({}).populate('projects').lean().exec())
.then(function(categories) {
data.categories = categories;
categories.forEach(function(category) {
category.projects.forEach(function(project) {
Project.findOne({ _id: project.id }, function(error, dbproject) {
dbproject.findRelatedProjects(function(error, relatedprojects) {
project.relatedProjects = relatedprojects;
})
});
});
});
res.send({categories: categories});
})
.catch(function(error) {
res.send(error);
});
});
There were a few issues. Two of them were consistent through refactoring. The first issue is that wrapping a mongoose promise with when.js was screwing things up slightly. The second issue was that the res.send was being called before the documents were populated with the correct data. This code fixed the problem:
router.get('/content.json', function(req, res) {
var data = { categories: [] };
when.map(Category.find({}).exec(), function(category, i) {
data.categories[i] = category;
data.categories[i].projects = [];
return when.map(Project.find({ category: category._id }).exec(), function(project, j) {
data.categories[i].projects[j] = project.toObject();
return node.call(project.findRelatedProjects.bind(project)).then(function(relatedProjects) {
data.categories[i].projects[j].relatedProjects = _.first(_.shuffle(relatedProjects), 4);
});
});
})
.then(function() {
res.send(data);
})
.catch(function(error) {
res.send(error);
});
});