Nested query with mongoose - node.js

I have three models: User, Post and Comment
var User = new Schema({
name: String,
email: String,
password: String // obviously encrypted
});
var Post = new Schema({
title: String,
author: { type: Schema.ObjectId, ref: 'User' }
});
var Comment = new Schema({
text: String,
post: { type: Schema.ObjectId, ref: 'Post' },
author: { type: Schema.ObjectId, ref: 'User' }
});
I need to get all posts in which the user has commented.
I know it should be a very simple and common use case, but right now I can't figure a way to make the query without multiple calls and manually iterating the results.
I've been thinking of adding a comments field to the Post schema (which I'd prefer to avoid) and make something like:
Post.find()
.populate({ path: 'comments', match: { author: user } })
.exec(function (err, posts) {
console.log(posts);
});
Any clues without modifying my original schemas?
Thanks

You have basically a couple of approaches to solving this.
1) Without populating. This uses promises with multiple calls. First query the Comment model for the particular user, then in the callback returned use the post ids in the comments to get the posts. You can use the promises like this:
var promise = Comment.find({ "author": userId }).select("post").exec();
promise.then(function (comments) {
var postIds = comments.map(function (c) {
return c.post;
});
return Post.find({ "_id": { "$in": postIds }).exec();
}).then(function (posts) {
// do something with the posts here
console.log(posts);
}).then(null, function (err) {
// handle error here
});
2) Using populate. Query the Comment model for a particular user using the given userId, select just the post field you want and populate it:
var query = Comment.find({ "author": userId });
query.select("post").populate("post");
query.exec(function(err, results){
console.log(results);
var posts = results.map(function (r) { return r.post; });
console.log(posts);
});

Related

Saving ALL nested documents on parent update Mongoose

My parent model looks like this:
var OrderSchema = new mongoose.Schema({
serviceNotes: {type: mongoose.Schema.Types.ObjectId, ref: 'Service'},
vehicle: {type: mongoose.Schema.Types.ObjectId, ref: 'Vehicle'}
});
The children look like this:
var VehicleSchema = new mongoose.Schema({
name: String
});
var ServiceSchema = new mongoose.Schema({
baseCost: Number
});
I am trying to find an easy solution to updating all of these documents at once. The problem is, when I call an update on an order, it does not update the nested documents. Please see the following:
exports.updateOrder = function (req, res) {
var order = req.body.order,
update = {
$set: order
};
Order.findOneAndUpdate({_id: order._id}, update, {new: true}, function (err, order) {
if (err) handleError(res);
if (!order) return res.status(404).send('Order not found.');
return res.json({order: order});
});
}
An example of req.body in this case may look like this:
{
order: {
_id: 829198218932shdbn,
serviceNotes: {
_id: 8932838nsd2sdnbd,
baseCost: 1
},
vehicle: {
_id: iu283823872378bd,
name: 'Honda'
}
}
}
The order update should also update the serviceNotes with the updated information, and the vehicle with the updated information.
The only way I have been able to update the nested documents is by calling a findOneAndUpdate on the children and updating them one by one. I am looking for a solution to just call update on the parent (order) and have the children update as well.

Mongoose NodeJS Schema with array of ref's

I know there is allot's of answers about it but still I didn't quite get the idea.
I have CourseSchema:
const CourseSchema = new Schema({
course_name: String,
course_number: {type: String, unique : true },
enrolledStudents:[{
type: mongoose.Schema.Types.ObjectId,
ref: 'Student' }]
});
And a StudentSchema:
const StudentSchema = new Schema({
first_name: String,
last_name: String,
enrolledCourses:[{
type: mongoose.Schema.Types.ObjectId,
ref: 'CourseSchema'
}]
});
I want to reffer enrolledStudents at CourseSchema with a student, and enrolledCourses at StudentSchema with a course.
router.post('/addStudentToCourse', function (req, res) {
Course.findById(req.params.courseId, function(err, course){
course.enrolledStudents.push(Student.findById(req.params.studentId, function(error, student){
student.enrolledCourses.push(course).save();
})).save();
});
});
but when posting I get an error:
TypeError: Cannot read property 'enrolledStudents' of null
Ok so after readying Query-populate I did that:
router.post('/addStudentToCourse', function (req, res) {
Course.
findOne({ _id : req.body.courseId }).
populate({
path: 'enrolledStudents'
, match: { _id : req.body.studentId }
}).
exec(function (err, course) {
if (err) return handleError(err);
console.log('The course name is %s', course.course_name);
});
});
And when i'm hitting POST on postman I get on the console:
The course name is intro for cs
but it is loading for ever and later on console I get:
POST /courses/addStudentToCourse - - ms - -
You are missing the populate instruction. For example:
see more about it here
Course.
findOne({ courseId : req.params.courseId }).
populate('enrolledStudents').
exec(function (err, course) {
if (err) return handleError(err);
console.log('The course name is %s', course.name);
});
It is working by using the ref field that "knows" how to populate withput using the push syntax. it is like a foreign key population.
Just call the populate method on the query and an array of documents will be returned in place of the original _ids. you can learn more on the internals of the populate methods in the official docs

How to populate a mongoose schema

I have the following mongoose schemas
var postTable = mongoose.Schema({
userPost:{type : String},
dateCreated: {type: Date, default: Date.now},
_replies:[{type: mongoose.Schema.Types.ObjectId, ref: 'reply_table'}],
_creator:{type: mongoose.Schema.Types.ObjectId, ref: 'User'}
});
and
var reply_table = mongoose.Schema({
userReply:{type:String},
date_created:{type:Date, default: Date.now},
_post:{type: mongoose.Schema.Types.ObjectId, ref: 'post'},
_creator:{type: mongoose.Schema.Types.ObjectId, ref: 'User'}
});
var userPost = module.exports = mongoose.model("Post",postTable);
var userReply = module.exports = mongoose.model('reply_table',reply_table);
User can create post which will be entered into the Post table and other users can comment or reply to a post which will be entered into the reply_table.
I then try to populate the the post table like this
module.exports.getPost = function (callback) {
var mysort = { dateCreated: -1 };
userPost
.find({},callback)
.sort(mysort)
.populate('_creator','username')
.populate(' _replies')
.exec(function (err,post) {
if(err) throw err;
console.log(post)
});
};
When the console prints out the post it prints the post information and a object with the user information becausei have another schema setup for users, therefore I used .populate('_creator','username')
The problem is it wont print the reply information it only prints an empty array: reply[].
I'm pretty sure I'm doing everything right. I used the following code to insert information into the reply_table
//make a reply on a post
module.exports.make_reply = function (user_id,pid,reply,callback) {
var newReply = userReply({
_creator: user_id,
_post: pid,
userReply: reply
});
newReply.save(callback);
}
I know this question is very long but does anyone have any idea of what I might be doing wrong. I only want to populate the Post schema with information from the reply_table
I finally figured out a solution to my question. What i did was i created a function to insert the reply id into the post table. It basically get the comment by its id and push a reply into the _replies array in the post table.
//Insert reply into post table
module.exports.addReply = function (id,reply) {
userPost.update({_id:id},{$push:{replies:reply}},{multi:true},function
(err,post) {
});
}
When i use the getPost function it populates the reply table
module.exports.getPost = function (callback) {
var mysort = {dateCreated: -1};
userPost
.find({}, callback)
.sort(mysort)
.populate('_creator', 'username')
.populate('replies')
.exec(function (err) {
if(err) throw err;
});
};

mongoose find a document by reference property

I have a first model Person:
var personSchema = new Schema({
firstname: String,
name: String
});
module.exports = mongoose.model('Person', personSchema);
And a second model Couple:
var coupleSchema = new Schema({
person1: [{ type: Schema.Types.ObjectId, ref: 'Person' }],
person2: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
module.exports = mongoose.model('Couple', coupleSchema);
I find a couple with a person ObjectId:
Couple.find({
'person1': req.params.objectid
})
.populate({
path: 'person1 person2'
})
.exec(function (err, couple) {
if (err)
res.send(err);
res.json(couple)
});
But I would like to find a couple by giving a firstname and not an ObjectId of a Person, something like that:
Couple.find({
'person1.firstname': "Bob"
})
.populate({
path: 'person1 person2'
})
.exec(function (err, couple) {
if (err)
res.send(err);
res.json(couple)
});
But it is always empty...
Anyway to solve this?
Thank you for any feedback.
EDIT
I just implemented the answer:
Let's see my Couple model now:
var Person = require('mongoose').model('Person');
var coupleSchema = new Schema({
person1 : [{ type: Schema.Types.ObjectId, ref: 'Person' }],
person2 : [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
coupleSchema.statics.findByUsername = function (username, callback) {
var query = this.findOne()
Person.findOne({'firstname': username}, function (error, person) {
query.where(
{person1: person._id}
).exec(callback);
})
return query
}
module.exports = mongoose.model('Couple', coupleSchema);
With this usage:
Couple.findByUsername(req.params.username, function (err, couple) {
if(err)
res.send(err);
res.json(couple);
});
That works! Thank you for your answer and edits.
In your couple model, person1 is an ObjectID (I know you know it), so it has no obviously no property .firstname.
Actually the best way to achieve this, is to find the user by it's first name, and then query the couple, with the id of the user.
This method could/should stand in the couple model as a static method (simplified code sample):
couple.statics.findByPersonFirstname = function (firstname, callback) {
var query = this.findOne()
Person.findOne({firstname: firstname}, function (error, person) {
query.where($or: [
{person1: person._id},
{person1: person._id}
]).exec(callback);
})
return query
}
Just like this exemple.
EDIT: Also note that the ref must be the _id (so you couldn't store with the first name, that would be a bad idea anyway).
Considering your edit:
Person._id is maybe a String and the reference is an ObjectId, if so, try:
{person1: mongoose.Types.ObjectId(Person._id)}
Also, your variable is person and not Person. Try to log person to see if you get something.
Finally, my code sample is really simple, don't forget to handle errors and all (see the link I gave you above, which is complete).

Mongoose - trying to do 'JOINS' in MEAN stack

I am having a hard time understanding the async nature of NodeJS.
So, I have an articles object with this schema:
var ArticleSchema = new Schema({
created: {
type: Date,
default: Date.now
},
title: {
type: String,
default: '',
trim: true,
required: 'Title cannot be blank'
},
content: {
type: String,
default: '',
trim: true
},
creator: {
type: Schema.ObjectId,
ref: 'User'
}
});
and the User schema is:
var UserSchema = new Schema({
firstName: String,
lastName: String,
...
});
The problem is when I query for all the documents like so:
exports.list = function(req, res) {
// Use the model 'find' method to get a list of articles
Article.find().sort('-created').populate('creator', 'firstName lastName fullName').exec(function(err, articles) {
if (err) {
// If an error occurs send the error message
return res.status(400).send({
message: getErrorMessage(err)
});
} else {
// Send a JSON representation of the article
res.json(articles);
}
});
};
I get all the articles back successfully, but for some reasons, the article creator is returning different results
for locally authenticated users (localStrategy) and facebook authenticated users (facebook strategy) for locally authenticated users, I get:
articles = {
creator: {
id: 123,
firstName: 'Jason',
lastName: 'Dinh'
},
...
}
for fb authenticated users, I get:
articles = {
creator: {
id: 123
},
...
}
I can't seem to get a grip on PassportJS API, so what I want to do is
iterate through articles and for each article, find the user document using the article creator ID and add the user firstName and lastName to the articles object:
for each article in articles {
User.findOne({ '_id': articles[i].creator._id }, function(err, person){
//add user firstName and lastName to article
});
}
res.json(articles);
You can probably already see the problem here... my loop finishes before the documents are returned.
Now, I know that MongoDB doesn't have any 'joins' and what I want to do is essentially return a query that 'joins' two collections. I think I'm running into problems because I don't fundamentally understand the async nature of
node.
Any help?
You can use find instead of findOne and iterate inside your callback function.
User.find({ }, function(err, personList){
for each person in personList {
for each article in articles {
if (person._id === article.creator._id) {
//add user firstName and lastName to article
}
}
}
res.json(articles);
});
UPDATE:
Considering the scenario that #roco-ctz proposed (10M users), you could set a count variable and wait for it to be equal to articles.length:
var count = 0;
for each article in articles {
User.findOne({ '_id': articles[i].creator._id }, function(err, person){
//add user firstName and lastName to article
count += 1;
});
}
while (count < articles.length) {
continue;
}
res.json(articles);

Resources