Way to save associations in mongoose without doing a save dance? - node.js

Say I have this schema.
var Topic = new Schema({
owner: {
type: Schema.Types.ObjectId,
ref: 'User'
},
category: {
type: Schema.Types.ObjectId,
ref: 'Category'
},
title: String,
date: Date,
lastPost: Date,
likes: Number,
posts: [{
type: Schema.Types.ObjectId,
ref: 'Post'
}]
});
var Post = new Schema({
topic: {
type: Schema.Types.ObjectId,
ref: 'Topic'
},
body: String,
date: Date,
owner: {
type: Schema.Types.ObjectId,
ref: 'User'
}
});
If I want to save the Topic then add the Topic to the topic association on the Post and then push to the posts array on the Topic object, I have to do this weird dance.
exports.create = function (req, res) {
var data = req.body.data;
var topic = new Topic();
topic.title = data.topic.title;
topic.date = new Date();
topic.lastPost = new Date();
topic.save(function (err, topicNew) {
if (err) res.send(err, 500);
var post = new Post();
post.body = data.post.body;
post.topic = topicNew;
topicNew.posts.push(post);
topic.save(function (err, t) {
if (err) res.send(err, 500);
post.save(function (err, p) {
if (err) res.send(err, 500);
return res.json(t);
});
});
});
};
I'm not seeing anything in the documentation that would help me around this. Thanks for any help.

Instantiate both the topic and the post initially. Push the post into the topic before the first topic save. Then save the topic and if that succeeds save the post. MongoDB object IDs are created by the driver right when you do new Post() so you can save that in the topic.posts array before it's saved.
That will make your 3-step dance a 2-step dance, but in the grand scheme of things this seems essentially par for the course so I wouldn't set my expectations much lower than this. Very few useful real world routes can be implemented with a single DB command.
You can also use middleware functions as a way to get sequential async operations without nesting. You can use the req object to store intermediate results and pass them from one middleware to the next.

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.

Express: mongodb mongoose linking entities

I'm building a simple web app where a company sends out a question to its employees requesting for feedback. Still learning about mongodb. Been playing around with it all week & I'm slowly getting a good hang of it with some helpful assistance on the forums but only now I realize I have been using a flawed thought process to design the schema. I was initially using a user's response as a field in the UserSchema but I have now removed it (as commented out here) as I realized this is not a user's property but rather a variable that keeps changing (yes/no/null). I now have to create a separate AnswersSchema (I was told I'll need one but I stubbornly argued against it - saw no sense in at the time I started the project) which I have done now (correct me if it's wrongly written/thought out). My question now is how do I modify my query in the api to link all the three entities together on a save operation in the router post? Please note the save operation code shown here works but is flawed as it's for when the user has a response as one of their properties. So now only the user's name shows up on the angular front-end after I removed response on UserSchema which makes sense.
var QuestionSchema = Schema({
id : ObjectId,
title : String,
employees : [{ type: ObjectId, ref: 'User'}]
});
var UserSchema = Schema({
username : String,
//response : String,
questions : [{ type: ObjectId, ref: 'Question'}]
});
//new schema/collection I've had to create
var AnswerSchema = Schema({
response : {type :String, default:null},
question : { type: ObjectId, ref: 'Question'},
employees : [{ type: ObjectId, ref: 'User'}],
})
module.exports = mongoose.model('Question', QuestionSchema);
module.exports = mongoose.model('User', UserSchema);
module.exports = mongoose.model('Answer', AnswersSchema);
api.js
Question.findOne({ title: 'Should we buy a coffee machine?'}).exec(function(err, question) {
//example data
var user = new User([{
"username": "lindelof",
"response": "yes",
},{
"username": "bailly",
"response": "no",
},{
"username": "suzan",
"response": "yes",
}]);
question.employees = [user1._id];
user.questions = [question._id];
question.save(function(err) {
if (err) throw err;
console.log(question);
user1.save(function(err) {
if (err) throw err;
});
});
});
console.log('entry saved >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
}
UPDATE
You did the right thing by adding AnswerSchema, as it's a many to many relationship. A question can be answered by many users (employees). A user can answer many questions. Therefore, it's good to have answer as an associative collection between the two.
With this relationship in mind, you need to change your schema a little:
var QuestionSchema = Schema({
id : ObjectId,
title : String,
//employees : [{ type: ObjectId, ref: 'User'}]
});
var UserSchema = Schema({
username : String,
//response : String,
//questions : [{ type: ObjectId, ref: 'Question'}]
});
var AnswerSchema = Schema({
response : {type :String, default:null},
question : { type: ObjectId, ref: 'Question'},
employee : { type: ObjectId, ref: 'User'}, //a single employee
});
Now, to know if a certain user has answered a question already, just search Answer with his and the question's ids:
Answer.findOne({
question: questionId,
employee: userId
})
.exec(function(err, answer) {
if (err) {
} else if (!answer) {
//the employee has not answered this question yet
} else {
//answered
}
});
Lastly, your submit-answer API should expect a body that contains questionId and userId (if signed in, you can get userId from session or token also). This route updates existing answer, else creates it (for create-only use create function)
router.post('/', function(req, res) {
//req.body = {question: "594315b47ab6ecc30d5184f7", employee: "594315d82ee110d10d407f93", response: "yes"}
Answer.findOneAndUpdate({
question: req.body.question,
employee: req.body.user
},
req.body,
{
upsert: true //updates if present, else inserts
}
})
.exec(function(err, answer) {
//...
});
});

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).

Nested query with mongoose

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);
});

Resources