Mongoose Pre hook executing infinite times - node.js

My user schema has one of the attributes as "active".
const userschema = new mongoose.Schema({
name : String,
active: {
type: Boolean,
default: true,
select: false,
},
});
Whenever I execute a query, I try to retrieve documents which has the "active" field as "false".
So I am trying to run a pre-hook before executing any query.
userSchema.pre(/^find/, function (next) {
this.find({ active: { $ne: false } });
next();
});
So this runs for any query of type find. But this doesn't seem to work. When I added an extra line
userSchema.pre(/^find/, function (next) {
console.log("inside pre");
this.find({ active: { $ne: false } });
next();
});
the output was printed infinite times and gave an error "Nodejs heap out of memory".
inside pre
inside pre
.
.
Can someone point out what's happening, and where I am going wrong?

Related

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 collection is not getting updated

I am trying to update mongoose collection name counter.
But it's not getting updated.
counter collection
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var counterSchema = new Schema({
_id: {type: String, required: true},
sequence_value: {type: Number, default: 1}
});
var Counter = module.exports = mongoose.model('Counter', counterSchema);
API
router.post('/increment',function(req, res, next){
console.log('Sequence Counter::' + getNextSequenceValue("productId"));
)};
getNextSequenceValue Method
function getNextSequenceValue(sequenceName){
var sequenceDocument = Counters.findOneAndUpdate({
query:{_id: sequenceName },
update: {$inc:{sequence_value:1}},
new:true,
upsert: true
});
console.log('Counter value::' + sequenceDocument.sequence_value);
return sequenceDocument.sequence_value;
}
But every time I hit /increment API, console.log is printing undefined.
You will always get undefined since sequenceDocument is a Promise which will later resolve with the updated document if the update operation is successful or will reject with an error if it is not. In your case the console.log statement will run before the database operation has completed. This is because findOneAndUpdate is an asynchronous operation which returns a Promise object.
The update probably fails because you are passing arguments to findOneAndUpdate in an improper manner. The function accepts the query as first argument, update operation as second and query options as third argument.
You can rewrite the getNextSequenceValue in the following way:
function getNextSequenceValue(sequenceName) {
return Counters.findOneAndUpdate(
{ // query
_id: sequenceName
},
{ $inc: { sequence_value: 1 } }, // update operation
{ // update operation options
new: true,
upsert: true
}
).then(sequenceDocument => sequenceDocument.sequence_value)
}
It will now return a Promise which will resolve with the sequence value. You can use it in your controller like this:
router.post('/increment', function(req, res, next) {
getNextSequenceValue('productId')
.then(sequenceValue => {
console.log(sequenceValue)
})
.catch(error => {
// handle possible MongoDB errors here
console.log(error)
})
})

Don't run validation on schema under certain conditions

I have a schema with several required fields. When I save a document with a published:false prop, I want to not run any validation and just save the document as is. Later, when published:true, I want to run all the validation.
I thought this would work:
MySchema.pre('validate', function(next) {
if(this._doc.published === false) {
//don't run validation
next();
}
else {
this.validate(next);
}
});
But this isn't working, it returns validation errors on required properties.
So how do I not run validation in some scenarios and run it in others? What's the most elegant way to do this?
Please try this one,
TagSchema.pre('validate', function(next) {
if (!this.published)
next();
else {
var error = new mongoose.Error.ValidationError(this);
next(error);
}
});
Test schema
var TagSchema = new mongoose.Schema({
name: {type: String, require: true},
published: Boolean,
tags: [String]
});
With published is true
var t = new Tag({
published: true,
tags: ['t1']
});
t.save(function(err) {
if (err)
console.log(err);
else
console.log('save tag successfully...');
});
Result:
{ [ValidationError: Tag validation failed]
message: 'Tag validation failed',
name: 'ValidationError',
errors: {} }
With published is false, result is
save tag successfully...

Get isModified in validator

Is there a way to check if a path has been modified in a validator? Do I need to check or do validators only run if the path was changed?
EDIT:
More specifically, I am trying to make sure an author exists before I insert an id:
var BookSchema = new Schema({
title: { type: String, required: true },
authorId: { type: Schema.Types.ObjectId, ref: 'Author' }
});
BookSchema.path('authorId').validate(function(authorId, done) {
Author.getAuthorById(authorId, function(err, author) {
if (err || !author) {
done(false);
} else {
done(true);
}
});
}, 'Invalid author, does not exist');
In this case I only want this to validate if authorId is set or if it changes. Do I need to check if changed in the function or can I assume that this validator only gets called if the authorId changes and is not null/undefined?
This makes it look like I might be able to call isModified, however I don't see that as a function on 'this'.
Mongoose validation only when changed
Yes, validators are only run if the path is changed, and they also only run if they're not undefined. Except the Required validator, which runs in both cases.

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