Mongoose - REST API - Schema With Query to different model - node.js

I'm trying to avoid DB Callback Queries.
Assuming that you have two schemas that looks like so :
1st) User Schema
username : {type: String, unique: true},
age : {type: Number}
2nd) Activity Schema
owner: [{type: Schema.Types.ObjectId, ref: 'User'}],
city: {type: String},
date: {type: Date}
So far so good.
Now lets say you have a route to /user/:id, what you would expect is to get the username and the age, but what if I would also like to return on that route the latest activity?
EDIT: Please note that latest activity isn't a value in the database. it's calculated automatically like activity.find({owner: ObjectId(id)}).sort({date: -1}).limit(1)
What is done right now:
User.findOne({username:req.params.username}).lean().exec(function(err,userDoc)
{
if(err) return errHandler(err);
Activity.findOne({owner:userDoc.username}).sort({date:-1}).exec(function(err,EventDoc){
if(err) return errHandler(err);
userDoc.latest_activity = EventDoc._id;
res.json(userDoc);
res.end();
})
})
The problem with the snippet above is that it is hard to maintain,
What if we want to add more to this API functionality? We would end in a callback of hell of queries unless we implement Q.
We tried to look at Virtual but the issue with that is that you can't
really query inside a mongoose Virtual, since it returns a
race-condition, and you are most likely not get that document on time.
We also tried to look at populate, but we couldn't make it since the documentation on populate is super poor.
QUESTION:
Is there anyway making this more modular?
Is there any way avoiding the DB Query Callback of Hell?
For example is this sort of thing possible?
User.findOne({username:req.params.username}).lean().populate(
{path:'Event',sort:{Date:-1}, limit(1)}
).exec(function(req,res))...
Thanks!

In this case, the best way to handle it would be to add a post save hook to your Activity schema to store the most recent _id in the latest_activity path of your User schema. That way you'd always have access to the id without having to do the extra query.
ActivitySchema.post('save', function(doc) {
UserSchema.findOne({username: doc.owner}).exec(function(err, user){
if (err)
console.log(err); //do something with the error
else if (user) {
user.latest_activity = doc._id;
user.save(function(err) {
if (err)
console.log(err); //do something with the error
});
}
});
});

Inspired by #BrianShambien's answer you could go with the post save, but instead of just storing the _id on the user you store a sub doc of only the last activity. Then when you grab that user it has the last activity right there.
User Model
username : {type: String, unique: true},
age : {type: Number},
last_activity: ActivitySchema
Then you do a post save hook on your ActivitySchema
ActivitySchema.post('save', function(doc) {
UserSchema.findOne({username: doc.owner}).exec(function(err, user){
if (err) errHandler(err);
user.last_activity = doc;
user.save(function(err) {
if (err) errHandler(err);
});
});
});
**********UPDATE************
This is to include the update to the user if they are not an owner, but a particpant of the the activity.
ActivitySchema.post('save', function(doc) {
findAndUpdateUser(doc.owner, doc);
if (doc.participants) {
for (var i in doc.participants) {
findAndUpdateUser(doc.participants[i], doc);
}
}
});
var findAndUpdateUser = function (username, doc) {
UserSchema.findOne({username: username}).exec(function (err, user) {
if (err) errHandler(err);
user.last_activity = doc;
user.save(function (err) {
if (err) errHandler(err);
});
});
});

Related

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

Using $or in a FindOne query - MongoDB

UPDATE: Changed if (!result.length) to if (!result) as I'm using .findOne and that seems to be working so far. If you spot anything else that can be improved within the code snippet that would be awesome!
Just starting to learn Node JS and MongoDB (using Mongoose) so excuse me if I'm totally out of context.
Im trying to find a row in my MongoDB with the following query:
exports.findById = function(req, res) {
var id = req.params.id;
Team.findOne({'teamid':id, $or:[{'creator':req.user.id}, {userlist: { $in : [req.user.id]}}]}, function(err, result) {
if (err) console.log(err);
if (!result.length)
res.redirect('/');
else
res.render('team', { team : result.teamid });
});
};
I want to retrieve a row that has the field teamid equal to id as well as to check if the field creator is equal to req.user.id OR if req.user.id is in the userlist field. I am expecting only one result from the above query.
Note that this query works just fine, but I just need to look inside the userlist array:
Team.findOne({'teamid':id, 'creator':req.user.id}, function(err, result) {...
And finally the Team schema
var Team = new Schema({
team_name: { type: String, required: true, trim: true},
teamid: { type: String },
creator: String,
created_at: Date,
userlist: Array
});
Any help to figure out what's the problem is greatly appreciated!

How to update linked document in mongoose from parent?

I have a User model with a profile field pointing to profile model, like
UserSchema = new Schema({
name: {
type: String,
require: true,
unique: false,
},
profile:{type:Schema.Types.ObjectId, ref: 'Profile'}});
Now it is saving fine, but I want to update the User and Profile at the same time by sending an object like:
{
name : 'some name',
profile.location : 'some location'
}
my simple update code:
User.update({_id:userId},req.body,{},function(){
res.status(204).end();
});
It seems to only update the User's name, but not the linked Profile document's properties. What would be the best way to do this?
The Population (ref used in profile) is introduced into Mongoose,
Because there are no joins in MongoDB but sometimes we still want references to documents in other collections. Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s).
So it is not related to update operation for the reference document. Here are one sample codes to meet your requirement.
User.update({_id: userId},
{name: req.body.name}, function(err, u) {
if (err)
console.log(err);
else {
Profile.update({_id: u.profile},
{location: req.body.profile.locatio},
function(err) {
// error handling
})
}
});
If you are using DaftMonk's Angular Fullstack Generator, then just do this :
Install lodash npm install lodash
Include lodash in your JS file like this : var _ = require("lodash");
Finally do this :
var newUser = {};
newUser.name = "Some Name";
User.findById(userId, function(err, user){
if(err) console.log(err);
var updated = _.merge(user, newUser);
updated.save(function (err) {
if (err) console.log(err);
Profile.findById(user.profile._id, function(err, profile){
profile.location = "some location";
profile.save(function(err){
if(err) console.log(err);
console.log("Updated User and Profile");
});
});
});
});
Hope this helps.

Expressjs rest api how to deal with chaining functionality

I am building a restful API using express, mongoose and mongodb. It works all fine but I have a question about how to deal with requests that contain more functionality than just one find, delete or update in the database. My user model looks as follows:
var UserSchema = new Schema({
emailaddress: {type: String, unique: true},
firstname: String,
lastname: String,
password: String,
friends: [{type: Schema.Types.ObjectId, unique: true}]
});
As you can see is the friends array just an array of ObjectIds. These ObjectIds refer to specific users in the database. If I want to retrieve an array of a user's friends I now have to look up the user that makes the request, then find all the users that have the same id as in the friends array.
Now it looks like this:
methods.get_friends = function(req, res) {
//find user.
User.findOne({_id: req.params.id}, function(err, user, next) {
if(err) next(err);
if(user) {
console.log(user);
//find friends
User.find({_id: {$in: user.friends}}, {password: 0}).exec(function (err,
friends, next) {
if(err) next(err);
if(friends) {
res.send(friends);
};
});
}
Would it be possible to seperate the lookup of the user in a certain method and chain the methods? I saw something about middleware chaining i.e. app.get('/friends', getUser, getFriend)but would that mean that I have to alter the req object in my middleware (getUser) method and then pass it on? How would you solve this issue? Would you perhaps change the mongoose model and save all friend data (means that it could become outdated) or would you create a method getUser that returns a promise on which you would collect the friend data?
I will be grateful for all the help I can get!
Thank you in advance.
Mongoose has a feature called population which exists to help in these kinds of situations. Basically, Mongoose will perform the extra query/queries that are required to load the friends documents from the database:
User.findOne({_id: req.params.id})
.populate('friends')
.exec(function(err, user) {
...
});
This will load any related friends into user.friends (as an array).
If you want to add additional constraints (in your example, password : 0), you can do that too:
User.findOne({_id: req.params.id})
.populate({
path : 'friends'
match : { password : 0 },
})
.exec(function(err, user) {
...
});
See also this documentation.

Mongoose not saving document

I've been working on a simple update function, that looks as follows:
exports.update = function update(req, res, next){
User.findById(req.param('userId'))
.exec(function (err, user) {
if(err) return next(err);
if(!user)
return res.sendData(null, 404, lang.userNotFound);
var info = req.param('info');
// Get the update to be made, in JSON format
if(info !== Object(info)) info = JSON.parse(info);
user.info = setProperties(user.info, info);
// console.log(user) gives the updated user object
user.save(function(err, usr) {
if (err) return next(err);
// console.log(usr) shows the updated user object
res.sendData();
});
});
};
function setProperties(object, update) {
if(update!== Object(update))
object = update;
else {
for (var prop in update) {
if (object.hasOwnProperty(prop))
object[prop] = setProperties(object[prop], update[prop]);
else
object[prop] = update[prop];
}
}
return object;
}
But unfortunately, although everything seems to work, in my Database nothing changes. I do not get any errors. Could anyone shed some light on this mystery? Thanks!
For reference (if it is relevant), here is my Schema:
var UserSchema = new Schema({
createdAt: {type: Date, default: Date.now},
lastActivity: {type: Date, default: Date.now},
info : {type : Schema.Types.Mixed},
});
Ah, I found the problem. Mongo was indeed performing the save, but it didn't know that an attribute had changed.
In the end, fixing it was as simple as adding
user.markModified('info');
And then everything performs as expected. It's a shame that Mongoose does not recognize this for us. Thanks to #vinz243 for pointing me in the right direction!

Resources