Mongoose - Return error in 'pre' middleware - node.js

How can I send a custom error message if my validation fails in schema.pre('save')? For example, if I have a chat feature, where you create a new conversation, I want to check if a conversation with the given participants already exists, so I can do:
ConversationSchema.pre('save', function(next, done) {
var that = this;
this.constructor.findOne({participants: this.participants}).then(function(conversation) {
if (conversation) {
// Send error back with the conversation object
} else {
next();
}
});
});

Pass an Error object when calling next to report the error:
ConversationSchema.pre('save', function(next, done) {
var that = this;
this.constructor.findOne({participants: this.participants}).then(function(conversation) {
if (conversation) {
var err = new Error('Conversation exists');
// Add conversation as a custom property
err.conversation = conversation;
next(err);
} else {
next();
}
});
});
Docs here.

I agree with JohnnyHK's answer except that it doesn't seem possible to add custom properties to the Error object. When receiving the error and trying to access that property the value is undefined so the solution is that you can send a custom error message but not add custom properties. My code would be something like:
ConversationSchema.pre('save', function(next) {
this.constructor.findOne({participants: this.participants}, function(err, conversation) {
if (err) next(new Error('Internal error'));
else if (conversation) next(new Error('Conversation exists'));
else next();
});
});

Related

Custom Error in Sails ORM (Waterline) callback

In my user model I have something like this:
register: function(data, next) {
User.findOne({email:data.email}).exec(function findOneUserCB(err, user) {
if (!err && user) {
return next(new Error('Email already exist.'));
}
// other things
});
}
I'm basically trying to return a custom error when the user is found but there isn't any other error from waterline.
But this doesn't work, sails complains that TypeError: Cannot call method 'toString' of undefined.
So I've tried to emulate a waterline error:
//...
var error = {
code: 'E_UNIQUE',
details: 'Invalid',
model: 'user',
invalidAttributes: {
hase: []
},
status: 400
}
return next(error);
//...
This works but it feels very hackish. Isn't it a better way to pass a custom error from within a query callback? I couldn't find any documentation about this topic
You can try something like this
register: function(data, next) {
User.findOne({email:data.email}).exec(function findOneUserCB(err, user) {
if(user){
var alreadyExists = new Error();
alreadyExists.message = require('util').format('User already exists');
alreadyExists.status = 400;
cb(alreadyExists);
}
// other things
});

Mongoose findById possibly sending Express headers?

I am trying to send an error if a condition is true using the Mongoose function findById. The problem is that Mongoose appears to be setting the res Express object and is then throwing an error when I try to set the headers myself. Here is the code:
console.log(res.headersSent); // false
Trade.findById(req.body.trade, function (err, trade) {
if (err) throw err;
// Ensure user is not making an offer on their own item
Item.findById(trade.listing, function (err, item) {
if (err) throw err;
if (req.decodedId == item.user) {
console.log(res.headersSent); // true (?)
return res.status(403).send({
success: false,
message: 'You cannot make an offer on your own item'
})
} else {
return;
}
})
And here is the stack trace for the error:
false // res.headersSent() before calling Trade.findById()
POST /api/v2/offer 200 148.799 ms - 162
true // res.headersSent() after calling Item.findById() and checking error condition
_http_outgoing.js:335
throw new Error('Can\'t set headers after they are sent.');
^
Error: Can't set headers after they are sent.
at ServerResponse.OutgoingMessage.setHeader (_http_outgoing.js:335:11)
at ServerResponse.header (/Users/Matt/Dropbox/work/TradeRate/prototype/node_modules/express/lib/response.js:700:10)
at ServerResponse.send (/Users/Matt/Dropbox/work/TradeRate/prototype/node_modules/express/lib/response.js:154:12)
at ServerResponse.json (/Users/Matt/Dropbox/work/TradeRate/prototype/node_modules/express/lib/response.js:240:15)
at ServerResponse.send (/Users/Matt/Dropbox/work/TradeRate/prototype/node_modules/express/lib/response.js:142:21)
at /Users/Matt/Dropbox/work/TradeRate/prototype/server/controllers/offers.js:48:40 // LINE THAT CONTAINS return res.status(403).send ...
at /Users/Matt/Dropbox/work/TradeRate/prototype/node_modules/mongoose/lib/query.js:1169:16
at /Users/Matt/Dropbox/work/TradeRate/prototype/node_modules/mongoose/node_modules/kareem/index.js:103:16
at process._tickCallback (node.js:355:11)
18 Jul 15:26:39 - [nodemon] app crashed - waiting for file changes before starting...
What could be causing this error? Is there aspect of the Mongoose API that sets the response headers that I'm missing?
EDIT: I added my full (updated) exported route handler in case that has some context that would make the problem more clear.
// POST /api/offer
exports.createOffer = function (req, res, next) {
console.log(res.headersSent);
Trade.findById(req.body.trade, function (err, trade) {
if (err) {
next(err);
return;
} // not good to throw from async events, let express' error handling middleware take care of it
// Ensure user is not making an offer on their own item
Item.findById(trade.listing, function (err, item) {
if (err) {
next(err);
return;
}
if (req.decodedId == item.user) {
console.log(res.headersSent); // true (?)
res.status(403).send({
success: false,
message: 'You cannot make an offer on your own item'
});
}
// all done with async stuff, pass the request long
next();
});
// If trade is expired, reject the offer
if (trade.expiresOn < Date.now()) {
res.status(403).send({
success: false,
message: 'This trade has expired and cannot accept new offers'
});
}
// Create new offer and add data
var newOffer = new Offer();
newOffer.items = req.body.items;
newOffer.trade = req.body.trade;
newOffer.save(function (err, offer) {
if (err) throw err;
});
// Add offer to items in offer
for (var i = 0; i < req.body.items.length; i++) {
Item.findById(req.body.items[i], function (err, item) {
if (err) throw err;
item.offers.push(newOffer._id);
item.save(function (err, item) {
if (err) throw err;
});
});
}
// Add offer to trade
trade.offers.push(newOffer._id);
trade.save(function (err, trade) {
if (err) throw err;
});
return res.send(newOffer);
});
};
Completely new answer, disregard my old one it was all wrong (it's been a while since I've used express).
Anyway the problem is you're calling async functions which return immediately so at the bottom there when you're calling return res.send(newOffer);, you're doing it before any of those callbacks return. So you returned before you
Check if the user is trying to create an offer on their own item
Add the new offer id to the items
Save any of those changes
Another problem is your loop there will likely fail horribly. There's no guarantee that you'll be pushing those items in order because findById and save as async, they return instantly and may be executed in any order. Plus there's no reason at all to save after every push. You need to either wait for each findById to return before continuing the loop (so you can't use a basic for loop, most likely a callback) or more correctly, just use a mongoose update query to do this all at once (you don't need to load an item to push an offer to it, just use $push)
The best way to handle all of this in express is with middleware. So change your code to this (I've added a dependency on http-errors to make error handling easier.
I'm assuming you're using the most recent version of express:
The Offer Route
var httpError = require('http-error') // needed for ezpz http errors
var express = require('express'); // needed for express.Router()
// middleware that loads the trade
function loadTrade(req, res, next) {
Trade.findById(req.body.trade, function (err, trade) {
req.trade = trade;
next(err, trade);
})
}
// middlware that checks expiration
function checkExpired(req, res, next) {
if(req.trade.expiresOn < Date.now())
next(httpError(403, 'This trade has expired and cannot accept new offers'));
else next();
}
// middleware makes sure the user isn't making an offer on their own item
function checkIsOwner(req, res, next) {
Trade.findById(req.trade.listing)
.select('user')
.exec(function(err, listing) {
if (err) next(err)
else if (listing.user == req.decodedId) next(httpError(403, 'You can not make an offer on your own item'))
else next();
})
}
// now we can create an offer
function createOffer(req, res, next) {
// req.trade was loaded and validated by our middleware
// if next(err) was called at any point this function wouldn't be called
var trade = req.trade;
Offer.create({trade: trade._id, items: req.body.items}, function (err, offer) {
if (err) {
next(err); // we only call next to trigger the error handler
return;
}
// now push the new offer id to all the items
Item.update({$in: req.body.items}, {$push: offer._id}, function (err, offer) {
if (err) next(err)
else res.json(newOffer);
})
});
}
exports.createOffer = express.Router()
.post(loadTrade)
.post(checkExpired)
.post(checkIsOwner)
.post(createOffer);
For handling errors I'd add this after you've setup all the routes (where you have your app.post('/api/v2/offer', ....) stuff:
app.use('/api/v2/*', function(err, req, res, next) {
res.status(err.status || 500).json({ success: false, message: err.message });
});
Now whenever you call next(err), this error handler will be called and send a status code and error message.

Throwing custom errors from Mongoose pre middleware and using Bluebird promises

I am using Mongoose with Bluebird promises. I am trying to throw a custom error in a validate pre middleware and have it catchable with a Bluebird catch.
Here is my pre validate method
schema.pre('validate', function(next) {
var self = this;
if (self.isNew) {
if (self.isModified('email')) {
// Check if email address on new User is a duplicate
checkForDuplicate(self, next);
}
}
});
function checkForDuplicate(model, cb) {
User.where({email: model.email}).count(function(err, count) {
if (err) return cb(err);
// If one is found, throw an error
if (count > 0) {
return cb(new User.DuplicateEmailError());
}
cb();
});
}
User.DuplicateEmailError = function () {
this.name = 'DuplicateEmailError';
this.message = 'The email used on the new user already exists for another user';
}
User.DuplicateEmailError.prototype = Error.prototype;
I am calling the save with the following in my controller
User.massAssign(request.payload).saveAsync()
.then(function(user) {
debugger;
reply(user);
})
.catch(function(err) {
debugger;
reply(err);
});
This results in the .catch() having an error that looks like this:
err: OperationalError
cause: Error
isOperational: true
message: "The email used on the new user already exists for another user"
name: "DuplicateEmailError"
stack: undefined
__proto__: OperationalError
Is there a way for me to have the custom error be what is delivered to the catch? I want tis so I can check for the error type, and have the controller respond with the appropriate message back in the response.
User.DuplicateEmailError.prototype = Error.prototype;
is wrong, it should be
User.DuplicateEmailError.prototype = Object.create(Error.prototype);
User.DuplicateEmailError.prototype.constructor = User.DuplicateEmailError;
Or better use
var util = require("util");
...
util.inherits(User.DuplicateEmailError, Error);

Trying to implement callback, or some way to wait until my get request is done. This is in node.js express

Im trying to implement some way to stop my code to redirect me before I get the response from the omdb api I am using.
My function for making a search for a movie and saving all titles in a session looks like this:
app.post('/search', isLoggedIn, function(req, res) {
function getMovies(arg, callback){
console.log('In getMovies');
console.log('searching for '+arg);
omdb.search(arg, function(err, movies) {
if(err) {
return console.error(err);
}
if(movies.length < 1) {
return console.log('No movies were found!');
}
var titles = [];
movies.forEach(function(movie) {
// If title exists in array, dont push.
if(titles.indexOf(movie.title) > -1){
console.log('skipped duplicate title of '+movie.title);
}
else{
titles.push(movie.title);
console.log('pushed '+movie.title);
}
});
// Saves the titles in a session
req.session.titles = titles;
console.log(req.session.titles);
});
// Done with the API request
callback();
}
var title = req.body.title;
getMovies(title, function() {
console.log('Done with API request, redirecting to GET SEARCH');
res.redirect('/search');
});
});
However I dont know if I implement callback in the right way, because I think there can be a problem with the api request actually executing before the callback, but not finishing before. And therefor the callback is working..
So I just want 2 things from this question. Does my callback work? And what can I do if a callback won't solve this problem?
Thankful for all answers in the right direction.
Add
callback();
To, like this
omdb.search(arg, function(err, movies) {
if (err) {
return console.error(err);
}
if (movies.length < 1) {
return console.log('No movies were found!');
}
var titles = [];
movies.forEach(function(movie) {
// If title exists in array, dont push.
if (titles.indexOf(movie.title) > -1) {
console.log('skipped duplicate title of ' + movie.title);
} else {
titles.push(movie.title);
console.log('pushed ' + movie.title);
}
});
// Saves the titles in a session
req.session.titles = titles;
callback();
});
omdb.search is asynchronous function that's why callback executed before omdb.search

Error "Undefined is not a function " Using callback node.JS

I am trying to save a new Document (user) in my MongoDb and I use callback. The code runs and goes until save the user, but after that I get an error.So I can save user. I have the following code:
function saveUser(userName, socialMediaType, socialMediaID, setDocNumber, callback){
var user;
if(socialMediaType == "fbUID"){
user = new users({
userName: userName,
userEmail: 'userEmail',
teams:[],
fbUID : socialMediaID
});
}else
if(socialMediaType =="google"){
//do the same
}
var query = {}
query["'"+ socialMediaType +"'" ] = socialMediaID
users.findOne(query, function(err, userFound){
if (err) { // err in query
log.d("Error in query FoundUser", err)
log.d("User Found", userFound)
}else
if(userFound == undefined){ //if user does not exist
user.save(function(err, user){
if(err) return console.error(err);
log.d("user saved", user);
currentSession = sessionOBJ.login(user._id, socialMediaID);
callback(currentSession,"created")
});
}else{
currentSession = sessionOBJ.login(userFound._id, socialMediaID);
callback(currentSession,"logged")
}
});
}
I call the function above through this code:
f(fbUID !== undefined){
userModelOBJ.saveUser(userName,"fbUID", fbUID, function(currentSession, status) {
res.send({"status":status,
"sessionID": currentSession.sessionID,
"expires" : currentSession.date});
});
I am getting this error :
The error is in the line :
callback(currentSession,"created")
What could be the problem?
I already did many researchers but this is a specific case.
Your saveUser() call is missing the setDocNumber argument. It looks like you're not using it in your code though, so you might be able to safely remove it. If you are using it somewhere else (that you haven't shown) then you need to do some argument checking at the top of saveUser() to support optional arguments.

Resources