Mongoose findOneAndUpdate and runValidators not working - node.js

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

Related

Mongoose schema validation when updating the document

Hello, I have a mongoose schema for a slug. I want to check uniqueness of it
slug: {
type: String,
default: "",
trim: true,
validate: {
validator: async function (value) {
const user = await this.model.findOne({ slug: value });
console.log(user);
console.log(this);
if (user) {
if (this.id === user.id) {
return true;
}
return false;
}
return true;
},
message: (props) => "This slug is already in use",
},
},
this validation is working fine when inserting a new document but in updating case, I want to compare it with all other fields in the schema other than itself. how could I do that
I have also added runValidators to check validation when updating also
CMS.pre("findOneAndUpdate", function () {
this.options.runValidators = true;
});
if you can suggest a better way of checking slug uniqueness in mongoose when inserting and updating
Thanks in advance
Why are you using a validator? Why not just ensure that the slug is unique by defining an index?
const User = new Schema({
slug: {
type: String,
default: "",
trim: true,
unique: true,
}
});
You will need to catch the error though when attempting to insert an already existing user, since the the unique option is not a validator. See: How to catch the error when inserting a MongoDB document which violates an unique index?
Reference:
https://mongoosejs.com/docs/faq.html#unique-doesnt-work
https://docs.mongodb.com/manual/core/index-unique/

Document must have an _id before saving

I know this is an error that has been asked about several times on StackOverflow, but I can't seem to find the answer I'm looking for. I have a simple schema that stores an _id and a URL. The URL works fine, but when I go to create a new schema and save it, it states the above error even though I have prehooks to explicitly define the _id.
Here's the Schema code as well as the prehook:
const LinkSchema = new Schema({
_id: { type: Number },
url: { type: String, required: true }
}, {
timestamps: true,
collection: 'links'
});
LinkSchema.pre('save', function(next) {
// Before saving, increment the count in the linkEntryCount document in the counter collection and create the doc if not already made.
CounterModel.findByIdAndUpdate('linkEntryCount', { $inc: { count: 1 } }, { new: true, upsert: true, useFindAndModify: false }, function(err, counter) {
if(err) return next(err);
this._id = counter.count; // Create the previously undefined ObjectID with the +1'ed counter from linkEntryCount
next();
})
});
I've created an incrementing integer counter as per the MongoDB database - using a separate collection for counting. I've tested this and it works fine, and it even seems to assign the _id when the prehook is called. When I create an instance of the model and insert the URL, that's when the error appears. The document isn't even created.
Thanks for your help!
_id: { type: Number },
A mongodb _id isnt a number but a mongoose.Schema.Types.ObjectId
So replace that line with
_id: { type: mongoose.Schema.Types.ObjectId },
You should use _id only with
new mongoose.Types.ObjectId()
And then you can add like id: { type: Number } and use that as the counter
So I solved my question using this post.
The problem was that this was being used in the wrong context inside the CounterModel.findByIdAndUpdate() function. The code was trying to update a nonexistent field in the linkEntryCount collection.
Here's the fixed prehook:
LinkSchema.pre('save', function(next) {
var link = this;
// Before saving, increment the count in the linkEntryCount document in the counter collection and create the doc if not already made.
CounterModel.findByIdAndUpdate('linkEntryCount', { $inc: { count: 1 } }, { new: true, upsert: true, useFindAndModify: false }, function(err, counter) {
if(err) return next(err);
link._id = counter.count; // Create the previously undefined ObjectID with the +1'ed counter from linkEntryCount
next();
});
});
All I did was set the prehooks reference to this to a variable and then used that later in the findByIdAndUpdate() function.

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

Prevent field modification with Mongoose Schema

Is there any way to set a field with an "unmodifiable" setting (Such as type, required, etc.) when you define a new Mongoose Schema? This means that once a new document is created, this field can't be changed.
For example, something like this:
var userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unmodifiable: true
}
})
From version 5.6.0 of Mongoose, we can use immutable: true in schemas (exactly as the aforementioned answer on mongoose-immutable package). Typical use case is for timestamps, but in your case, with username it goes like this:
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
immutable: true
}
});
If you try to update the field, modification will be ignored by Mongoose.
Going a little further than what have been asked by OP, now with Mongoose 5.7.0 we can conditionally set the immutable property.
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
immutable: doc => doc.role !== 'ADMIN'
},
role: {
type: String,
default: 'USER',
enum: ['USER', 'MODERATOR', 'ADMIN'],
immutable: true
}
});
Sources: What's New in Mongoose 5.6.0: Immutable Properties and What's New in Mongoose 5.7: Conditional Immutability, Faster Document Arrays.
Please be aware that the documentation explicitly states that when using functions with update in their identifier/name, the 'pre' middleware is not triggered:
Although values are casted to their appropriate types when using update, the following are not applied:
- defaults
- setters
- validators
- middleware
If you need those features, use the traditional approach of first retrieving the document.
Model.findOne({ name: 'borne' }, function (err, doc) {
if (err) ..
doc.name = 'jason bourne';
doc.save(callback);
})
Therefore either go with the above way by mongooseAPI, which can trigger middleware (like 'pre' in desoares answer) or triggers your own validators e.g.:
const theOneAndOnlyName = 'Master Splinter';
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
default: theOneAndOnlyName
validate: {
validator: value => {
if(value != theOneAndOnlyName) {
return Promise.reject('{{PATH}} do not specify this field, it will be set automatically');
// message can be checked at error.errors['username'].reason
}
return true;
},
message: '{{PATH}} do not specify this field, it will be set automatically'
}
}
});
or always call any update functions (e.g. 'findByIdAndUpdate' and friends) with an additional 'options' argument in the form of { runValidators: true } e.g.:
const splinter = new User({ username: undefined });
User.findByIdAndUpdate(splinter._id, { username: 'Shredder' }, { runValidators: true })
.then(() => User.findById(splinter._id))
.then(user => {
assert(user.username === 'Shredder');
done();
})
.catch(error => console.log(error.errors['username'].reason));
You can also use the validator function in a non-standard way i.e.:
...
validator: function(value) {
if(value != theOneAndOnlyName) {
this.username = theOneAndOnlyName;
}
return true;
}
...
This does not throw a 'ValidationError' but quietly overrides the specified value. It still only does so, when using save() or update functions with specified validation option argument.
I had the same problem with field modifications.
Try https://www.npmjs.com/package/mongoose-immutable-plugin
The plugin will reject each modification-attempt on a field and it works for
Update
UpdateOne
FindOneAndUpdate
UpdateMany
Re-save
It supports array, nesting objects, etc. types of field and guards deep immutability.
Plugin also handles update-options as $set, $inc, etc.
You can do it with Mongoose only, in userSchema.pre save:
if (this.isModified('modified query')) {
return next(new Error('Trying to modify restricted data'));
}
return next();
You can use Mongoose Immutable. It's a small package you can install with the command below, it allows you to use the "immutable" property.
npm install mongoose-immutable --save
then to use it:
var userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
immutable: true
}
});
userSchema.plugin(immutablePlugin);

How to return data without _id when i use findByIdAndUpdate in mongoose?

Here is my code:
Task.findByIdAndUpdate({_id: req.params.task_id}, updateObj, {new: true}, function (err, updatedUser) {
if (err) {
return result.serverError(req, res)
}
result.success(req, res, updatedUser);
});
It will return all fields,but i don't need '__v' and '_id',how should i do?Thanks.
I think mongoose-hidden is your want.
You can also use the method in this link: MongoDB: output 'id' instead of '_id'
if you just want dismiss __v, you can use versionKey option for mogoose.
http://mongoosejs.com/docs/guide.html#versionKey
Document versioning can also be disabled by setting the versionKey to
false. DO NOT disable versioning unless you know what you are doing.
new Schema({..}, { versionKey: false });
var Thing = mongoose.model('Thing', schema);
var thing = new Thing({ name: 'no versioning please' });
thing.save(); // { name: 'no versioning please' }

Resources