Mongoose: the isAsync option for custom validators is deprecated - node.js

The Stripe Rocket Rides demo uses isAsync in a validator:
// Make sure the email has not been used.
PilotSchema.path('email').validate({
isAsync: true,
validator: function(email, callback) {
const Pilot = mongoose.model('Pilot');
// Check only when it is a new pilot or when the email has been modified.
if (this.isNew || this.isModified('email')) {
Pilot.find({ email: email }).exec(function(err, pilots) {
callback(!err && pilots.length === 0);
});
} else {
callback(true);
}
},
message: 'This email already exists. Please try to log in instead.',
});
This method throws an error with a reference
DeprecationWarning: Mongoose: the `isAsync` option for custom validators is deprecated. Make your async validators return a promise instead: https://mongoosejs.com/docs/validation.html#async-custom-validators
The MongoDB page quoted has this code:
const userSchema = new Schema({
name: {
type: String,
// You can also make a validator async by returning a promise.
validate: () => Promise.reject(new Error('Oops!'))
},
email: {
type: String,
// There are two ways for an promise-based async validator to fail:
// 1) If the promise rejects, Mongoose assumes the validator failed with the given error.
// 2) If the promise resolves to `false`, Mongoose assumes the validator failed and creates an error with the given `message`.
validate: {
validator: () => Promise.resolve(false),
message: 'Email validation failed'
}
}
});
I am new to NodeJS and I don't see how to adapt the MongoDB code to the Rocket Rides demo. Neither Implicit async custom validators (custom validators that take 2 arguments) are deprecated in mongoose >= 4.9.0 nor Mongoose custom validation for password helped.
How can I verify the uniqueness of email addresses and avoid that error?

try this, I had the same problem.
To fix it use a separate async function that you call with await.
// Make sure the email has not been used.
PilotSchema.path('email').validate({
validator: async function(v) {
return await checkMailDup(v, this);
},
message: 'This email already exists. Please try to log in instead.',
});
async function checkMailDup(v, t) {
const Pilot = mongoose.model('Pilot');
// Check only when it is a new pilot or when the email has been modified.
if (t.isNew || t.isModified('email')) {
try {
const pilots = await Pilot.find({ email: v });
return pilots.length === 0;
} catch (err) {
return false;
}
} else {
return true;
}
}
Let me know if it works.
I used the following references:
Mongoose async custom validation not working as expected
Cheers!

Related

What's the best Async/Await approach using Mongoose + Node.js?

I'd like to know if this kind of async/await approach with mongoose is correct. I still need to use .exec and then returning the promise with mongoose or I can leave things like this. Here my code snippet:
This is the user controller for example:
/* Func to update one user by id */
const updateUser = async (id, user) => {
const filter = {_id: id};
const update = {name: user.name, email: user.email};
const result = await User.findOneAndUpdate(filter, update, {new: true});
return result;
};
This is the route:
/* PATCH update user passing the id in params */
router.patch('/list/:id/update', async (req, res, next) => {
try {
const data = await usersController.updateUser(req.params.id, {
name: req.body.name,
email: req.body.email,
});
res.status(data ? 200 : 404).json({
result: data,
message: 'User updated',
});
} catch (e) {
res.status(500).json({
result: e.toString(),
});
}
});
Is this approach correct using mongoose or I need to use the async calling .exec().then().catch() after the query?
According to mongoose documentation, as far as functionality is concerned, these two are equivalent. However, they recommend using the exec because that gives you better stack traces:
const doc = await Band.findOne({ name: "Guns N' Roses" }); // works
const badId = 'this is not a valid id';
try {
await Band.findOne({ _id: badId });
} catch (err) {
// Without `exec()`, the stack trace does **not** include the
// calling code. Below is the stack trace:
//
// CastError: Cast to ObjectId failed for value "this is not a valid id" at path "_id" for model "band-promises"
// at new CastError (/app/node_modules/mongoose/lib/error/cast.js:29:11)
// at model.Query.exec (/app/node_modules/mongoose/lib/query.js:4331:21)
// at model.Query.Query.then (/app/node_modules/mongoose/lib/query.js:4423:15)
// at process._tickCallback (internal/process/next_tick.js:68:7)
err.stack;
}
try {
await Band.findOne({ _id: badId }).exec();
} catch (err) {
// With `exec()`, the stack trace includes where in your code you
// called `exec()`. Below is the stack trace:
//
// CastError: Cast to ObjectId failed for value "this is not a valid id" at path "_id" for model "band-promises"
// at new CastError (/app/node_modules/mongoose/lib/error/cast.js:29:11)
// at model.Query.exec (/app/node_modules/mongoose/lib/query.js:4331:21)
// at Context.<anonymous> (/app/test/index.test.js:138:42)
// at process._tickCallback (internal/process/next_tick.js:68:7)
err.stack;
}

Mongoose.js Unique Validation

NOTE: there's an edit at the bottom of the question:
Can I check the database for uniqueness using either a custom validator or a pre hook in a Mongoose.js model file. I am aware that I can check it in the controller, but I'd rather put it in the model file with the rest of the validators just for consistency.
I am also aware there is an npm package called mongoose-unique-validator that does this but I'm no fan of installing a library to do what should be one to five lines of code tops.
Mongoose also has a "unique" property that will throw an error if the item is not unique. But their documents clearly state this is not a validator. And the error it throws does not get routed the same as the validation errors.
Here is the relevant parts of the model file. This will check the db and if there is no dup then it creates the article but if there is a dup it throws an error but not a validation error which is what I want. If I simply return false if there is a dup it just ignores the validation and creates the duplicate article. This is no doubt related to Promises/Async. Here are the relevant Mongoose docs https://mongoosejs.com/docs/validation.html#async-custom-validators. And they talk about how the unique property is not a validator https://mongoosejs.com/docs/faq.html.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const articleSchema = new Schema({
title: {
type: String,
required: [true, "Title is required"],
// unique: true,
// isAsync: true,
validate: {
validator: function(value) {
this.constructor.findOne({title: value}, (err, article) => {
if (err || !article) {
return true;
} else {
// return false;
throw new Error('Duplicate');
}
});
},
message: (props) => `Title "${props.value}" is already in use.`
},
},
content: { type: String, required: true }
});
EDIT: I figured this out, but it only works when creating a new article, not on updates. So the question is still open but the focus is on how to get it to work on updates. On update Mongoose does not treat "this" as the document object like it does on create. Instead "this" is the request object, and "this.constructor.findOne()" throws the error "this.constructor.findOne is not a function". Here's the revised validator:
title: {
type: String,
required: [true, "Title is required"],
isAsync: true,
validate: {
validator: async function(value) {
const article = await this.constructor.findOne({title: value});
if (article) {
throw new Error(`${value} is already in use.`);
}
}
}
}
Your validator function will only run the script and it not pass any callback or promise to mongoose, so mongoose assume that the validator return true and continue the process.
According to the document, you should return promise or use callback.
Promise:
validator: function(value) {
var here = this;
return new Promise(function(resolve, reject) {
here.constructor.findOne({title: value}, (err, article) => {
if (err || !article) {
resolve(true);
} else {
resolve(false);
}
});
})
}
Callback: (need to set isAsync: true)
validator: function(value, cb) {
this.constructor.findOne({title: value}, (err, article) => {
if (err || !article) {
cb(true);
} else {
cb(false, "Content is used");
}
});
}

Why validation runs on field update in mongoose?

I know that there shouldn't be any validation on field update but it runs anyway when I try to migrate a database.
Part of the migration:
const arr = await User.find({ ban: { $exists: true } });
arr.forEach(async item => {
// this works
// await User.updateOne({ _id: item._id }, { ban: false });
// this doesn't
item.ban = false;
await item.save();
});
Part of the schema:
email: {
type: String,
validate: {
validator: email => User.doesntExist({ email }),
message: ({ value }) => `Email ${value} has already been taken`
}
}
"ValidationError: User validation failed: email: Email guest1#ex.com has already been taken"
There is an option for disabling validator to be fired in mongoose save(), which is validateBeforeSave. (since mongoose version 4.4.2)
So try to use save({ validateBeforeSave: false }) if you want to keep using save() instead of update().
You're doing it right, because as reported in mongoose documentation:
The save() function is generally the right way to update a document with Mongoose. With save(), you get full validation and middleware.
But, when you call the .save() function, all validators are called, including your user email validator:
validator: email => User.doesntExist({ email })
And in your case this is a problem, because the user being validated is already saved in the db... So, to avoid this you need to use the .update() function in order to update your users.
https://mongoosejs.com/docs/validation.html#validation
Validation is middleware. Mongoose registers validation as a pre('save') hook on every schema by default.
updateOne doesn't triggers save hook.

Mongoose helper method has no findOne method?

How can I add helper methods to find and save objects in Mongoose. A friend told me to use helper methods but I cannot get them to work after a day. I always receive errors saying that either findOne() or save() does not exist OR that next callback is undefined (when node compiles ... before I execute it):
I've tried _schema.methods, _schema.statics... nothing works...
var email = require('email-addresses'),
mongoose = require('mongoose'),
strings = require('../../utilities/common/strings'),
uuid = require('node-uuid'),
validator = require('validator');
var _schema = new mongoose.Schema({
_id: {
type: String,
trim: true,
lowercase: true,
default: uuid.v4
},
n: { // Name
type: String,
required: true,
trim: true,
lowercase: true,
unique: true,
index: true
}
});
//_schema.index({
// d: 1,
// n: 1
//}, { unique: true });
_schema.pre('save', function (next) {
if (!this.n || strings.isNullOrWhitespace(this.n)){
self.invalidate('n', 'Domain name required but not supplied');
return next(new Error('Domain name required but not supplied'));
}
var a = email.parseOneAddress('test#' + this.n);
if (!a || !a.local || !a.domain){
self.invalidate('n', 'Name is not valid domain name');
return next(new Error('Name is not valid domain name'));
}
next();
});
_schema.statics.validateForSave = function (next) {
if (!this.n || strings.isNullOrWhitespace(this.n)){
return next(new Error('Domain name required but not supplied'));
}
var a = email.parseOneAddress('test#' + this.n);
if (!a || !a.local || !a.domain){
return next(new Error('Name is not valid domain name'));
}
next();
}
_schema.statics.findUnique = function (next) {
this.validateForSave(function(err){
if (err){ return next(err); }
mongoose.model('Domain').findOne({ n: this.n }, next);
//this.findOne({ n: this.n }, next);
});
}
_schema.statics.init = function (next) {
this.findUnique(function(err){
if (err){ return next(err); }
this.save(next);
});
}
var _model = mongoose.model('Domain', _schema);
module.exports = _model;
I believe you are running into issues because of your usage with this. Every time you enter a new function this's context is changing. You can read more about this at mdn.
Additionally your callbacks aren't allowing anything to be passed into the mongoose method. For example if I was to create the most basic "save" method I I would do the following:
_schema.statics.basicCreate = function(newDocData, next) {
new _model(newDocData).save(next);
}
Now if I wanted to search the Domain collection for a unique document I would use the following:
_schema.statics.basicSearch = function(uniqueName, next) {
var query = {n: uniqueName};
_model.findOne(query, function(err, myUniqueDoc) {
if (err) return next(err);
if (!myUniqueDoc) return next(new Error("No Domain with " + uniqueName + " found"));
next(null, myNewDoc);
});
}
Mongoose has built in validations for what you are doing:
_schema.path("n").validate(function(name) {
return name.length;
}, "Domain name is required");
_schema.path("n").validate(function(name) {
var a = email.parseOneAddress("test#" + name);
if (!a || !a.local || !a.domain) {
return false;
}
return true;
}, "Name is not a valid domain name");
It returns a boolean. If false, it passes an error to the .save() callback with the stated message. For validating uniqueness:
_schema.path("n").validate(function(name, next) {
var self = this;
this.model("Domain").findOne({n: name}, function(err, domain) {
if (err) return next(err);
if (domain) {
if (self._id === domain._id) {
return next(true);
}
return next(false);
}
return next(true);
});
}, "This domain is already taken");
You're using self = this here so that you can access the document inside the findOne() callback. false is being passed to the callback if the name exists and the result that is found isn't the document itself.
I've tried _schema.methods, _schema.statics
To clarify, .statics operate on the Model, .methods operate on the document. Zane gave a good example of statics, so here is an example of methods:
_schema.methods.isDotCom = function() {
return (/.com/).test(this.n);
}
var org = new Domain({n: "stuff.org"});
var com = new Domain({n: "things.com"});
org.isDotCom(); // false
com.isDotCom(); // true
Opinion: It's neat to have mongoose do validations, but it's very easy to forget it's happening. You also may want to have some validation in one area of your app while using different validations elsewhere. I'd avoid using most of it unless you definitively know you will have to do the same thing every time and will never NOT have to do it.
Methods/statics are a different story. It's very convenient to call isDotCom() instead of writing out a regex test every time you need to check. It performs a single and simple task that saves you some typing and makes your code more readable. Using methods for boolean checks can add a ton of readability. Defining statics like findByName (Zane's basicSearch) is great when you know you're going to do a simple query like that repeatedly.
Treat Mongoose as a utility, not as core functionality.

Mongoose unique validation error type

I'm using this schema with mongoose 3.0.3 from npm:
var schema = new Schema({
_id: Schema.ObjectId,
email: {type: String, required: true, unique: true}
});
If I try to save a email that is already in db, I expect to get a ValidationError like if a required field is omitted. However this is not the case, I get a MongoError: E11000 duplicate key error index.
Which is not a validation error (happens even if I remove the unique:true).
Any idea why?
I prefer putting it in path validation mechanisms, like
UserSchema.path('email').validate(function(value, done) {
this.model('User').count({ email: value }, function(err, count) {
if (err) {
return done(err);
}
// If `count` is greater than zero, "invalidate"
done(!count);
});
}, 'Email already exists');
Then it'll just get wrapped into ValidationError and will return as first argument when you call validate or save .
I had some issues with the approved answer. Namely:
this.model('User') didn't work for me.
the callback done wasn't working properly.
I resolved those issues by:
UserSchema.path('email').validate(async (value) => {
const emailCount = await mongoose.models.User.countDocuments({email: value });
return !emailCount;
}, 'Email already exists');
I use async/await which is a personal preference because it is much neater: https://javascript.info/async-await.
Let me know if I got something wrong.
This is expected behavior
The unique: true is equivalent to setting an index in mongodb like this:
db.myCollection.ensureIndex( { "email": 1 }, { unique: true } )
To do this type of validation using Mongoose (Mongoose calls this complex validation- ie- you are not just asserting the value is a number for example), you will need to wire in to the pre-save event:
mySchema.pre("save",function(next, done) {
var self = this;
mongoose.models["User"].findOne({email : self.email},function(err, results) {
if(err) {
done(err);
} else if(results) { //there was a result found, so the email address exists
self.invalidate("email","email must be unique");
done(new Error("email must be unique"));
} else {
done();
}
});
next();
});
Simply response to json
try {
let end_study_year = new EndStudyYear(req.body);
await end_study_year.save();
res.json({
status: true,
message: 'បានរក្សាទុក!'
})
}catch (e) {
res.json({
status: false,
message: e.message.toString().includes('duplicate') ? 'ទិន្នន័យមានរួចហើយ' : e.message.split(':')[0] // check if duplicate message exist
})
}
Sorry for answering an old question. After testing I feel good to have find these answers, so I will give my experience. Both top answers are great and right, just remember that:
if your document is new, you can just validate if count is higher than 0, thats the common situation;
if your document is NOT new and has modified the unique field, you need to validate with 0 too;
if your document is NOT new and has NOT being modified, just go ahead;
Here is what I made in my code:
UserSchema.path('email').validate(async function validateDuplicatedEmail(value) {
if (!this.isNew && !this.isModified('email')) return true;
try {
const User = mongoose.model("User");
const count = await User.countDocuments({ email: value });
if (count > 0) return false;
return true;
}
catch (error) {
return false;
}
}, "Email already exists");

Resources