Run custom validation in mongoose update query - node.js

I have been trying to run a custom validator to check if the name entered by the user already exists in the database. Since, mongoDb treats uppercase and lowercase names as different, I created my own validator for it.
function uniqueFieldInsensitive ( modelName, field ){
return function(val, cb){
if( val && val.length ){ // if string not empty/null
var query = mongoose.models[modelName]
.where( field, new RegExp('^'+val+'$', 'i') ); // lookup the collection for somthing that looks like this field
if( !this.isNew ){ // if update, make sure we are not colliding with itself
query = query.where('_id').ne(this._id)
}
query.count(function(err,n){
// false when validation fails
cb( n < 1 )
})
} else { // raise error of unique if empty // may be confusing, but is rightful
cb( false )
}
}
}
Now, the problem is that the validator runs while saving the document in the DB but not while update.
Since, I am using mongoose version 4.x, I also tried using { runValidators: true } in my update query. That doesn't work either as the 'this' keyword in my validator is 'null' while in the case of update whereas it refers to the updated doc in the case of save.
Could you please let me know if there is something i missed or is there any other way by which I can run custom validators in update query.

Finally I found a way out to do this.
According to MongoDB documentation, it says:
First, update validators only check $set and $unset operations. Update validators will not check $push or $inc operations.
The second and most important difference lies in the fact that, in document validators, this refers to the document being updated. In the case of update validators, there is no underlying document, so this will be null in your custom validators.
Refer to : Validators for update()
So, now we are only left with calling save() instead of update() in our queries. Since, save() calls all the custom and inbuilt validators, our validator will also be called. I achieved it like this:
function(req, res, next) {
_.assign(req.libraryStep, req.body);
req.libraryStep.save().then(function(data){
res.json(data);
}).then(null, function (err) {
console.info(err);
var newErr = new errorHandler.error.ProcessingError(errorHandler.getErrorMessage(err));
next(newErr);
});
};
Notice here req.libraryStep is the document that i queried from the database. I have used lodash method assign which takes the updated json and assigns it to the existing database document.
https://lodash.com/docs#assign
I dont think this is the ideal way but as for now till Mongoose doesnt come up with supporting custom validators, we can use this to solve our problem.

This is a fairly old thread, but I wanted to update the answer for those who come across it like I did.
While you're correct about the context of this being empty in an update validator (per the docs), there is a context option you can use to set the context of this. See the docs
However, a plugin also exists that will check the uniqueness of the field you are setting: mongoose-unique-validator. I use this for checking for duplicate emails. This also has an option for case insensitivity, so I would check it out. It also does run correctly using the update command with the runValidators: true option.

Related

How to validate array length when using $push?

I'm trying to limit the amount of elements a user can add to an array field on one of my schemas. I'm currently adding the elements to the array using Schema.findOneAndUpdate(); with the $push operator.
The first thing I tried was the solution given by another answer here on StackOverflow, namely: https://stackoverflow.com/a/29418656/6502807
This solution adds a validate function to the fields in the schema definition. By setting runValidators to true, I did get the function to run with Schema.findOneAndUpdate(). It was at that moment, however, that I stumbled upon the next problem. At the end of the Validation chapter in the Mongoose docs it says:
Also, $push, $addToSet, $pull, and $pullAll validation does not run any validation on the array itself, only individual elements of the array.
So attempting to check for array length did not work when using $pull. It simply supplied the validation function with an empty array every time, regardless of its actual contents in the database.
Next thing I tried was to use a pre hook. This was without any success as well. For some reason it did not execute the hook, even with runValidators set to true. This is how I defined said hook:
Settings.pre('update', async function (next) {
if (this.messages.length > MAX_MESSAGES) {
throw new Error('Too many messages');
} else {
next();
}
});
EDIT: The reason the function did not fire was because I was using findOneAndUpdate instead of update this is fixed and the function now runs. The solution code above, however, does not work.
The schema with the array looks like this:
const Settings = new mongoose.Schema({
// A lot more fields not relevant to this question
messages: {
type: [{
type: String
}]
}
});
Another thing worth mentioning is that these update statements are used in conjunction with other options. I need the update statement to behave like an update or insert so my complete set of options looks like this:
{
runValidators: true,
setDefaultsOnInsert: true,
upsert: true,
new: true
}
When executing queries with the pre hook set like this, the array limit can be exceeded without any validation error being thrown.
At this point I'm wondering if there is any sensible way to do a max length check like this without having to do it myself outside of mongoose's abstraction layer.
I am using Mongoose 5.2.6 running on node v9.11.1 with MongoDB 4.0.0.
Any help is much appreciated!
Well if you are using latest version from mongodb and mongoose then you can use $expr operator
const udpate = await db.collection.update(
{ $expr: { $gt: [{"$size": "$messages" }, MAX_MESSAGES] }},
{ update }
)
You should be able to do that with the pre update hook. The thing is that that hook would not by default give you the update being mage so you can verify etc. You have to take it via this.getUpdate():
Settings.pre('update', async function (next) {
var preUpdate = this.getUpdate()
// now inside of the preUpdate you would have your update being made and should have the array in there on which you can check the length
});
To give you an idea in my test schema I had to do something like this on an update with a $set:
this.getUpdate().$set.books.length // gave me 2 which was correct etc
I also had no issues running and hitting the update hook at all. It looks super simple out of the mongoose docs:
AuthorSchema.pre('update', function(next) {
console.log('UPDATE hook fired!')
console.log(this.getUpdate())
next();
});

MongooseJS: Hide Specific Documents From Query

I have a User schema with a activated field of Boolean type. I want queries to only return documents which have activated: true. And I hope there is a more efficient and DRY way of doing so than adding a conditional to every find, findOne, or findById.
What would be the most effective approach?
while there may be some way to do this, it is generally a bad idea to always hide this information.
speaking from experience trying to do this with other languages and database systems, you will, at some point, want / need to load items that are not actived. but if you always and only return activated items, you'll never be able to get the list you need.
for your purposes, i would recommend creating a findActive method on your schema:
someSchema.static("findActive", function(query, cb){
// check if there is a query and callback
if (!cb){
cb = query;
query = {};
}
// set up an empty query, if there isn't one provided
if (!query) { query = {}; }
// make sure you only load activated items
query.activated = true;
// run the query
this.find(query, cb);
});
with this method, you will have a findActive method the same as findOne, but it will always filter for activated items.
MyModel.findActive(function(err, modelList){ ... });
and it optionally supports additional query filters
MyModel.findActive({some: "stuff"}, function(err, modelList){ ... });
You might want to look at Mongoose Query middleware here
Query middleware is supported for the following Model and Query
functions.
count
find
findOne
...
For example:
User.pre('find', function() {
console.log(this instanceof mongoose.Query); // true
this.activated = true;
});

Mongoose: disable empty query returning a document

When using Mongoose (with bluebird in my case, but using callbacks to illustrate), the following codes all return a document from the collection:
model.findOne({}, function(err, document) {
//returns a document
})
model.findOne(null, function(err, document) {
//returns a document
})
model.findOne([], function(err, document) {
//returns a document
})
I would like to know if and how I can disable this kind of behaviour, as it is becoming a liability to my code where I infer queries from data a user feeds into the system. Especially the null query returning a valid document worries me.
As of right now I check the input for being an non-empty, non-array, non-null object, but it's becoming a bit cumbersome at scale.
What would be the best way to exclude this behaviour?
Not sure if it is the best way to go about it, but right now I've settled on using a pre-hook on the model itself which checks for the _conditions property of the 'this' object (which I inferred from printing seems to hold the query object) to not be empty.
Inserting a self-defined object in the next functionality causes the Promise to reject in which the query was originally called from.
( _ is the underscore package)
//model.js
//model is a mongoose.Schema type in the following code
model.pre('findOne', function(next) {
var self = this
if (_.isEmpty(self._conditions)) {
next(mainErrors.malformedRequest)
} else {
next()
}
})

How do I use a pre save hook database value in Mongoose?

I want to get the value of an object before the pre-save hook and compare that to the new value. As suggested in mongoose get db value in pre-save hook and https://github.com/Automattic/mongoose/issues/2952, I did a post-init hook that copied it to a doc._original. The issue is that I'm not sure how to access this ._original in a different hook.
FieldSchema
.post('save', function (doc) {
console.log(doc._original);
});
FieldSchema
.post('init', function (doc) {
doc._original = doc.toObject();
});
I know that the doc in the post save hook is different from the doc in the post init hook, but how would I access the original?
You can only access properties on a database which you have defined in its schema. So since you probably haven't defined _original as a property in your schema, you can't access, or even set it.
One way would be to obviously define _original in your schema.
But to get and set properties not defined in your schema: use .get, and .set with {strict:false}
FieldSchema
.post('save', function (doc) {
console.log(doc.get('_original'));
});
FieldSchema
.post('init', function (doc) {
doc.set('_original', doc.toObject(), {strict: false});
});
Note the option {strict: false} passed to .set which is required because you're setting a property not defined in the schema.
update:
First thing I didn't notice before is that in your question title you want pre-save hook but in your code you actually have a post-save hook (which is what I based my answer on). I hope you know what you're doing, in that post save hook is invoked after saving the document.
But in my opinion, and from what I can understand what your original intent was from the question, I think you should actually use a pre-save hook and an async version of it (by passing next callback), so that you can .find (retrieve) the document from the database, which would be the original version since the new version hasn't yet been saved (pre-save) thus enabling you to compare without actually saving any new fields, which seems an anti-pattern to begin with.
So something like:
FieldSchema
.pre('save', function (next) {
var new_doc = this;
this.constructor // ≈ mongoose.model('…', FieldSchema).findById
.findById(this.id, function(err, original){
console.log(original);
console.log(new_doc);
next();
});
});
http://mongoosejs.com/docs/middleware.html

Mongoose getter / setters for normalizing data

I have User schema which has a username field. I would like this field to be case sensitive, so that users may register names such as BobDylan. However, I need my schema to validate new entries to check there are no duplicates, incase sensitive, such as bobdylan.
My research has taught me that I should create an additional field in the schema for storing a lower case / upper case version, so that I can easily check if it is unique. My question is, how would I achieve this with the Mongoose API?
I have tried using a set function, such as:
UserSchema.path('username_lower_case').set(function(username_lower_case) {
return this.username.toLowerCase()
});
However, this function doesn't seem to be running. I basically need to tell username_lower_case to be whatever username is, but in lower case.
One way would be to use a pre-save hook to do it.
UserSchema.pre('save', function (next) {
this.username_lower_case = this.username && this.username.toLowerCase();
next();
});
Another way would be to make username a virtual:
UserSchema.virtual('username').set(function (value) {
this.username_raw = value;
this.username_lower_case = value && value.toLowerCase();
}).get(function () {
return this.username_raw;
});

Resources