Throwing custom errors from Mongoose pre middleware and using Bluebird promises - node.js

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

Related

Perform side effects for mongoose/mongodb query

I need to query my database for users based on an array of emails and then execute a function for each result, I do this with eachAsync:
mongoose.model('User')
.find({email: {$in: ['foo#bar.com', 'bar#foo.com']}})
/* -- Run side effects before continuing -- */
.cursor()
.eachAsync((doc) => {
// do stuff
});
The problem I'm having is that I need to return a 404 status if any of the users with the given emails do not exist.
I've been looking through the mongoose docs but I can't seem to find a way of running "side effects" when working with queries. Simply "resolving" the DocumentQuery with .then doesn't work since you can't turn it into a cursor afterwards.
How can I achieve this?
You could try implementing it as shown below. I hope it helps.
// Function using async/await
getCursor: async (_, res) => {
try {
const result = []; // To hold result of cursor
const searchArray = ['foo#bar.com', 'bar#foo.com'];
let hasError = false; // to track error when email from find isn't in the array
const cursor = await mongoose.model('User').find({ email: { $in: searchArray } }).cursor();
// NOTE: Use cursor.on('data') to read the stream of data passed
cursor.on('data', (cursorChunk) => {
// NOTE: Run your side effect before continuing
if (searchArray.indexOf(cursorChunk.email) === -1) {
hasError = true;
res.status(404).json({ message: 'Resource not found!' });
} else {
// Note: Push chunk to result array if you need it
result.push(cursorChunk);
}
});
// NOTE: listen to the cursor.on('end')
cursor.on('end', () => {
// Do stuff or return result to client
if (!hasError) {
res.status(200).json({ result, success: true });
}
});
} catch (error) {
// Do error log and/or return to client
res.status(404).json({ error, message: 'Resource not found!' });
}
}

Mongoose - Return error in 'pre' middleware

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

Node / Express & Postgresql - when no rows match

Hello I am new to Postgresql and I wanted to learn how one handles 0 results as an error is thrown. Essentially I want to get a user if it doesn't exist, return null if one doesn't, and have an error handler. Below is the current code I am using. Any tips on a better way to do this are appreciated!
var options = {
// Initialization Options
promiseLib: promise
};
var pgp = require('pg-promise')(options);
var connectionString = 'postgres://localhost:5432/myDbName';
var db = pgp(connectionString);
function getUser(id) {
let user = new Promise(function(resolve, reject) {
try {
db.one('select * from users where loginName = $1', id).then(function(data) {
console.log(data);
resolve(data);
}).catch (function (e) {
console.log('error: '+e);
reject(e);
});
}
catch (e) {
console.log('error: '+e);
reject(e);
}
});
return user;
}
output in console:
error: QueryResultError {
code: queryResultErrorCode.noData
message: "No data returned from the query."
received: 0
query: "select * from users where loginName = 'someUserName'"
}
I am the author of pg-promise.
In the realm of promises one uses .then to handle all normal situations and .catch to handle all error situations.
Translated into pg-promise, which adheres to that rule, you execute a database method that resolves with results that represent all the normal situations, so anything else ends up in .catch.
Case in point, if returning one or no rows is a normal situation for your query, you should be using method oneOrNone. It is only when returning no row is an invalid situation you would use method one.
As per the API, method oneOrNone resolves with the data row found, or with null when no row found, which you can check then:
db.oneOrNone('select * from users where loginName = $1', id)
.then(user=> {
if (user) {
// user found
} else {
// user not found
}
})
.catch(error=> {
// something went wrong;
});
If, however, you have a query for which returning no data does represent an error, the proper way of checking for returning no rows would be like this:
var QRE = pgp.errors.QueryResultError;
var qrec = pgp.errors.queryResultErrorCode;
db.one('select * from users where loginName = $1', id)
.then(user=> {
// normal situation;
})
.catch(error=> {
if (error instanceof QRE && error.code === qrec.noData) {
// found no row
} else {
// something else is wrong;
}
});
Similar considerations are made when choosing method many vs manyOrNone (method any is a shorter alias for manyOrNone).
Type QueryResultError has a very friendly console output, just like all other types in the library, to give you a good idea of how to handle the situation.
In your catch handler for the query, just test for that error. Looking at pg-promise source code, a code of noData is 0. So just do something like this:
db.one('select * from users where loginName = $1', id).then(function(data) {
console.log(data);
resolve(data);
}).catch (function (e) {
if(e.code === 0){
resolve(null);
}
console.log('error: '+e);
reject(e);
});

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.

Resources