Why validation runs on field update in mongoose? - node.js

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.

Related

Cannot add property to mongoose document with findOneAndUpdate

My express app tries to record the login time of the user using Mongoose's findOneAndUpdate.
router.post('/login', passport.authenticate('local', {
failureFlash: true,
failureRedirect: '/'
}), async(req, res, next) => {
// if we're at this point in the code, the user has already logged in successfully.
console.log("successful login")
// save login time to database
const result = await User.findOneAndUpdate({ username: req.body.username }, { loginTime: Date.now() }, { new: true });
console.log(result);
return res.redirect('/battle');
})
The user document does not start out with a login time property. I'm expecting this code to insert that property for me.
The actual result is, the console shows the user document being printed out, but without any added login time property. How can I fix this so a login time property is inserted into the document? Is the only way to do it by defining a login time property in the original mongoose schema? And if so, doesn't that nullify the supposed advantage of NoSQL vs SQL in that it's supposed to allow new unexpected property types into your collections and documents?
I've found the answer for anyone who might come across the same problem. It is not at all possible to add a property to a Mongoose collection if it is not already defined in the Schema. So to fix it I added the property in the Schema.
In fact, you can add a new property that isn't defined in the schema, without modifying the schema. You need to set the flag strict to false to enable this mode. See document here.
The code below demonstrates what I said, feel free to runs it:
const mongoose = require('mongoose');
// connect to database
mongoose.connect('mongodb://localhost/test', { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false });
// define the schema
const kittySchema = new mongoose.Schema({
name: String
// this flag indicate that the shema we defined is not fixed,
// document in database can have some fields that are not defined in the schema
// which is very likely
}, { strict: false });
// compile schema to model
const Kitten = mongoose.model('Kitten', kittySchema);
test();
async function test() {
// empty the database
await Kitten.deleteMany({});
// test data
const dataObject = { name: "Kitty 1" };
const firstKitty = new Kitten(dataObject);
// save in database
await firstKitty.save();
// find the kitty from database
const firstKittyDocument = await Kitten.findOne({ name: "Kitty 1" });
console.log("Mongoose document", firstKittyDocument);
// modify the kitty, add new property doesn't exist in the schema
const firstKittyDocumentModified = await Kitten.findOneAndUpdate(
{ _id: firstKittyDocument._id },
{ $set: { age: 1 } },
{ new: true }
);
console.log("Mongoose document updated", firstKittyDocumentModified);
// note : when we log the attribute that isn't in the schema, it is undefined :)
console.log("Age ", firstKittyDocumentModified.age); // undefined
console.log("Name", firstKittyDocumentModified.name); // defined
// for that, use .toObject() method to convert Mongoose document to javascript object
const firstKittyPOJO = firstKittyDocumentModified.toObject();
console.log("Age ", firstKittyPOJO.age); // defined
console.log("Name", firstKittyPOJO.name); // defined
}
The output:
Mongoose document { _id: 60d1fd0ac3b22b4e3c69d4f2, name: 'Kitty 1', __v: 0 }
Mongoose document updated { _id: 60d1fd0ac3b22b4e3c69d4f2, name: 'Kitty 1', __v: 0, age: 1 }
Age undefined
Name Kitty 1
Age 1
Name Kitty 1

Mongoose: the isAsync option for custom validators is deprecated

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!

Custom Validation on a field that checks if field already exists and is active

I have a mongodb Collection "Users" having "Name", "Email", "Active" fields.
I want to add a validation that for every document email should be unique. However if a document is invalid i.e Active is false then the email can be accepted.
Here is the model
email: { type: String, validate: {
validator: function(v) {
return new Promise((resolve, reject)=> {
console.log("validating email")
const UserModel = mongoose.model('User');
UserModel.find({email : v, active: true}, function (err, docs)
{
if (!docs.length){
resolve();
}else{
console.log('user exists: ',v);
reject(new Error("User exists!"));
}
});
})
},
message: '{VALUE} already exists!'
}
},
name: {
type: String,
required: true
},
active: {
type: Boolean,
default: true
}
Problem is whenever i do any updation on this model then this validation is called.
So if i update the name then also this validation is called and it gives the error that email already exists.
How do I add a validation on email field so if someone adds a new entry to database or updates email it checks in database if existing user has same email id and is active?
I would first call Mongoose findOne function if the User is already registered the Mongo DB, for example;
let foundUser = await User.findOne({email});
if (!foundUser) {
// update user, create user etc.
...
}
I think it is better to not use logic inside the Mongoose document object. Maybe there is a way to achieve it but I prefer to do these validations in the code, not in the document, it is just my preference.
Also you can try making email unique as follows:
email: {
type: String,
unique: true
}
I'd use unique compound index, instead of having one more additional query to your db. Your code would look like this:
const schema = = new Schema(...);
schema.index({email: 1, active: 1}, {unique: true});
Mongo itself will reject your documents and you can catch it in your code like this:
const {MongoError} = require('mongodb'); // native driver
try {
await model.updateOne(...).exec(); // or .save()
} catch (err) {
//11000 is error code for unique constraints
if (err instanceof MongoError && err.code === 11000)
console.error('Duplicate email/active pair');
}

Sequelize - How to set association, save entity and return the saved entity with the associated entity

I am trying to create an association between an existing (user) entity and save the new entity (visit).
I've read the sequelize docs and can't see a better way of doing this than saving the first entity using async/await, then fetching it again passing include as an option. See below.
export const createVisit = async(req, res) => {
req.assert('BusinessId', 'Must pass businessId').notEmpty();
req.assert('UserId', 'Must pass customerId').notEmpty();
const visit = await new Visit({
UserId: req.body.UserId,
BusinessId: req.body.BusinessId,
redemption: false,
})
.save()
.catch((error) => {
res.status(400).send({ error });
});
const visitWithUser = await Visit.findById(visit.id, {include: [{model: User, attributes: ['firstName','lastName','facebook', 'gender','email']}]})
res.status(200).send({ visit: visitWithUser })
};
Is there a way to save the entity and get sequelize to return the saved entity along with any associations?
I think it supports this feature , as per the doc , you can do it like this :
Visit.create({
UserId: req.body.UserId,
BusinessId: req.body.BusinessId,
redemption: false,
}, {
include: [User]
}).then(function(comment) {
console.log(comment.user.id);
});
Here is the git discussion if you want to read.

Mongoose findOneAndUpdate and runValidators not working

I am having issues trying to get the 'runValidators' option to work. My user schema has an email field that has required set to true but each time a new user gets added to the database (using the 'upsert' option) and the email field is empty it does not complain:
var userSchema = new mongoose.Schema({
facebookId: {type: Number, required: true},
activated: {type: Boolean, required: true, default: false},
email: {type: String, required: true}
});
findOneAndUpdate code:
model.user.user.findOneAndUpdate(
{facebookId: request.params.facebookId},
{
$setOnInsert: {
facebookId: request.params.facebookId,
email: request.payload.email,
}
},
{upsert: true,
new: true,
runValidators: true,
setDefaultsOnInsert: true
}, function (err, user) {
if (err) {
console.log(err);
return reply(boom.badRequest(authError));
}
return reply(user);
});
I have no idea what I am doing wrong, I just followed the docs: http://mongoosejs.com/docs/validation.html
In the docs is says the following:
Note that in mongoose 4.x, update validators only run on $set and $unset operations. For instance, the below update will succeed, regardless of the value of number.
I replaced the $setOnInsert with $set but had the same result.
required validators only fail when you try to explicitly $unset the key.
This makes no sense to me but it's what the docs say.
use this plugin:
mongoose-unique-validator
When using methods like findOneAndUpdate you will need to pass this configuration object:
{ runValidators: true, context: 'query' }
ie.
User.findOneAndUpdate(
{ email: 'old-email#example.com' },
{ email: 'new-email#example.com' },
{ runValidators: true, context: 'query' },
function(err) {
// ...
}
In mongoose do same thing in two step.
Find the result using findOne() method.
Add fields and save document using Model.save().
This will update your document.
I fixed the issue by adding a pre hook for findOneAndUpdate():
ExampleSchema.pre('findOneAndUpdate', function (next) {
this.options.runValidators = true
next()
})
Then when I am using findOneAndUpdate the validation is working.
I created a plugin to validate required model properties before doing update operations in mongoose.
Plugin code here
var mongoose = require('mongoose');
var _ = require('lodash');
var s = require('underscore.string');
function validateExtra(schema, options){
schema.methods.validateRequired = function(){
var deferred = Promise.defer();
var self = this;
try {
_.forEach(this.schema.paths, function (val, key) {
if (val.isRequired && _.isUndefined(self[key])) {
throw new Error(s.humanize(key) + ' is not set and is required');
}
});
deferred.resolve();
} catch (err){
deferred.reject(err);
}
return deferred.promise;
}
}
module.exports = validateExtra;
Must be called explicitly as a method from the model, so I recommend chaining it a .then chain prior to the update call.
Plugin in use here
fuelOrderModel(postVars.fuelOrder).validateRequired()
.then(function(){
return fuelOrderModel.findOneAndUpdate({_id: postVars.fuelOrder.fuelOrderId},
postVars.fuelOrder, {runValidators: true, upsert: true,
setDefaultsOnInsert: true, new: true})
.then(function(doc) {
res.json({fuelOrderId: postVars.fuelOrder.fuelOrderId});
});
}, function(err){
global.saveError(err, 'server', req.user);
res.status(500).json(err);
});
If you want to validate with findOneAndUpdate you can not get current document but you can get this keywords's contents and in this keywords's content have "op" property so solution is this :
Note : does not matter if you use context or not. Also, don't forget to send data include both "price" and "priceDiscount" in findOneAndUpdate body.
validate: {
validator: function (value) {
if (this.op === 'findOneAndUpdate') {
console.log(this.getUpdate().$set.priceDiscount);
console.log(this.getUpdate().$set.price);
return (
this.getUpdate().$set.priceDiscount < this.getUpdate().$set.price
);
}
return value < this.price;
},
message: 'Discount price ({VALUE}) should be below regular price',
}
The reason behind this behavior is that mongoose assumes you are just going to update the document, not insert one. The only possibility of having an invalid model with upsert is therefore to perform an $unset. In other words, findOneAndUpdate would be appropriate for a PATCH endpoint.
If you want to validate the model on insert, and be able to perform a update on this endpoint too (it would be a PUT endpoint) you should use replaceOne

Resources