Why Mongoose doesn't validate empty document? - node.js

Let's say I have a very simple schema with a custom validation function that always returns false.
var MofoSchema = new mongoose.Schema({
name: String
});
MofoSchema.path('name').validate(function (value) {
console.log("I'm validating");
return false;
}, 'No pasaran');
mongoose.model('Mofo', MofoSchema);
Then I create a new instance of my document and I validate it:
var Mofo = mongoose.model('Mofo');
var mofo = new Mofo({name: "Tarte flambée"});
mofo.validate(function(err) {
console.log(err);
});
Perfect, the custom validator function is called and err is filled.
But now I do the same with no data:
var Mofo = mongoose.model('Mofo');
var mofo = new Mofo({});
mofo.validate(function(err) {
console.log(err);
});
The custom validator function is not called and err is undefined.
Why? I don't understand why Mongoose is not running the custom validator.
Is this behaviour by design? Is it a bug?
Should I hack a turnaround? Should I check manually for empty data before validation?
Am I doing something wrong?
PS: If you call save, the document will be saved as empty in MongoDB despite of the custom validator.

I think mongoose will only validate for existing field.
So you can use 'null' value to activate validation
var mofo = new Mofo({name: null});
For empty or undefined
var MofoSchema = new mongoose.Schema({
name: {type: String, required: true}
});
MofoSchema.path('name').validate(function (value) {...}, 'no pasaran');

You can do:
var mofo = new Mofo({ name: { type: String, default: ''} });
This will ensure you always have a value on that key to trigger validation. It also makes your schema definition easier to read.

Related

why mongoose validator not validating field type?

I am trying to validate mongoose schema, but i could not able to understand one thing which is a validation of type. I am passing number type to a string field and expecting it to fail in validation but it passes. how that is happening, can anyone please explain the logic behind this?
sample.js
const mongoose = require('mongoose');
async function validateSample(sample) {
try {
await sample.validate();
return true;
} catch (error) {
return false;
}
}
async function execMethod(){
var userSchema = new mongoose.Schema({
phone: {
type: String,
minlength: 2,
maxlength: 4,
validate: {
validator: function(v) {
return /^\d+$/.test(v);
},
message: `not a valid phone number!`
},
required: [true, 'User phone number required']
}
});
var User = mongoose.model('user', userSchema);
var validUser = new User({phone: '1234'});
var invalidUser = new User({phone: 1235});
const result = await validateSample(validUser);
const result1 = await validateSample(invalidUser);
console.log(result); // true
console.log(result1) // expected false as number type is assigned to phone. But returns true, why?
}
execMethod()
That's actually a feature of Mongoose validation.
Before running validators, Mongoose attempts to coerce values to the
correct type. This process is called casting the document. If casting fails for a given path, the error.errors object will contain a CastError object.
Casting runs before validation, and validation does not run if casting fails.
In this case, a number 1235 gets cast to a string '1235', which passed the validation just fine.
Now, there's a FR open for providing Mongoose with ability to override cast logic (either disable it completely or customize), but it's Open, not implemented.
Another way is altering casting type-wide (allowed since 5.4), like this:
mongoose.Schema.Types.String.cast(false); // prevents all casts to string
... but it might be cumbersome, as the same rule's applied to all the objects.

Change discriminator value/discriminated type in Mongoose

I've represented a class hierarchy in Mongoose via two models and a discriminator key (simple example):
var options = {discriminatorKey: 'kind'};
var UserSchema = new mongoose.Schema({
username: {type: String, index: true},
// some other fields
}, options);
// some schema methods
var User = mongoose.model('User', UserSchema);
var PowerUserSchema = new mongoose.Schema({
username: {type: String, index: true},
// some other fields
rank: {type: String}
}, options);
var PowerUser = User.discriminator('PowerUser', PowerUserSchema);
So far this works fine, however I ran into the situation, where I would like to "promote" a User to PowerUser. My initial idea was to set the "kind" property of a user and call save() on the instance, hoping that once the value is retrieved next time, the correct mongoose type will be returned:
var user = ... // retrieve user
user.kind = 'PowerUser';
user.save();
user = ... // retrieve user again
This doesn't appear to work, since the "kind" value is not saved to the instance. I came across this suggestion, which unfortunately did not update the discriminator value either.
My question now is: Am I even on the right track? Is updating the discriminator value even allowed for a situation like this, or should I better structure my data in a different way (e.g. use a single schema for both, with a "type" entry specifying what each instance is; this would have the effect that for the demotion case, no information is lost.)
Additionally, pro(de)moting a user should not break all the instances in my database where (Power)Users are referenced.
Thanks!
In the end I got this to work by doing:
var user = ... // retrieve user
var powerUser = PowerUser.hydrate(user.toObject());
powerUser.kind = 'PowerUser';
powerUser.save();
powerUser = ... // retrieve user again
On a side note, demoting a PowerUser back to User does not appear to be working that way.
have you tried using findOneAndUpdate on the Model
User.findOneAndUpdate({_id: _user._id}, {$set: {kind: "PowerUser"}, {new: true}, function (err, doc) {
should.not.exist(err);
should.exist(doc.kind);
doc.kind.should.equal('PowerUser');
done();
});
you could use a static method like this in case you also need to remove properties already set. the value new: true is to get the new modified file and strict: false so you can unset values that dont already exist on UserSchema
changes = {kind: "PowerUser"}
UserSchema.statics.switchKind = function (id, changes, callBack) {
const unset = {
rank: undefined,
someOtherField: undefined
};
return this.findOneAndUpdate({_id: id}, {$set: changes, $unset: unset}, {new: true, strict: false}, callBack);
};
As of Mongoose 6, you can modify the value of the discriminator key by using the overwriteDiscriminatorKey option:
For some reason it wasn't validating correctly for me with any of the "update" methods (e.g. findByIdAndUpdate) (bug report), so I had to do a replace, which means you need to provide the whole document:
const user = ... // retrieve user
const newUser = {
...user.toObject(), ...{ kind: 'PowerUser', name: 'New Name' }
};
User.replaceOne({ _id: user._id }, newUser, {
overwriteDiscriminatorKey: true,
runValidators: true
});
This will validate it against the PowerUser schema to make sure your document is valid.

Mongoose - inserting subdocuments

I have a user model, and a log model. The log model is a subdocument of user model. So in my user model I have:
var mongoose = require('mongoose');
var Log = require('../models/log');
var UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true
},
logsHeld: [
Log
]
});
Then in my 'Log' model I have:
var mongoose = require('mongoose');
var logSchema = new mongoose.Schema({
logComment: {
type: String,
},
});
module.exports = mongoose.model('Log', logSchema);
So upon creation of a 'user', the 'logsHeld' always begins empty. I want to know how to add subdocuments to this user model.
I've tried doing this POST method:
router.post('/createNewLog', function(req, res) {
var user = new User ({
logssHeld: [{
logComment: req.body.logComment
}]
});
user.save(function(err) {
if(err) {
req.flash('error', 'Log was not added due to error');
return res.redirect('/home');
} else {
req.flash('success', 'Log was successfully added!');
return res.redirect('/home');
}
});
});
But this doesn't work. It also includes a 'new User' line, which I don't think I need given this would be for an existing user.
You need to use the logSchema instead of the Log model as your subdocument schema in User model. You can access the schema as follows:
var mongoose = require('mongoose');
/* access the Log schema via its Model.schema property */
var LogSchema = require('../models/log').schema; // <-- access the schema with this
var UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true
},
logsHeld: [LogSchema]
});
Picking up from your comments in another answer where you are facing another issue
WriteError({"code":11000,"index":0,"errmsg":"E11000 duplicate key
error index: testDB.users.$email_1 dup key:
you are getting this because there's already a document in your users collection that has most probably a null value on the email field. Even though your schema does not explicitly specify an email field, you may have an existing old and unused unique index on users.email.
You can confirm this with
testDB.users.getIndexes()
If that is the case and manually remove the unwanted index with
testDB.users.dropIndex(<index_name_as_specified_above>)
and carry on with the POST to see if that has rectified the error, I bet my $0.02 that there is an old unused unique index in your users collection which is the main issue.
Try using logSchema which references only the subdocument schema, Log refers to the entire contents of ../models/log
var UserSchema = new mongoose.Schema({
username: {
type: String,
unique: true
},
logsHeld: [
logSchema
]
});
Documentation: http://mongoosejs.com/docs/subdocs.html
Try push to insert item in array in mongoose
var user = new User;
user.logssHeld.push({
logComment: req.body.logComment
});
user.save(function(err, doc) {
//DO whatever you want
});
see the docs here

How to cancel a mongoose query in a 'pre' hook

I am implementing some kind of caching for my 'find' queries on a certain schemas, and my cache works with the pre\post query hooks.
The question is how can I cancel the 'find' query correctly?
mySchema.pre('find', function(next){
var result = cache.Get();
if(result){
//cancel query if we have a result from cache
abort();
} else {
next();
}
});
so that this promise will be fulfilled?
Model.find({..})
.select('...')
.then(function (result) {
//We can reach here and work with the cached results
});
I was unable to find a reasonable solution to this myself for another non-caching reason but if your own specific caching method isn't too important I'd recommend you look at mongoose-cache, works well and has simple settings thanks to it's dependency: node-lru-cache, check that out for more options.
you may want to check out mongoose validators, that seems like a better way to handle controlling whether or not an object gets created.
You can create a custom validate function that will throw an error in the Model.save function, causing it to fail. Here is a code snippet from the docs:
// make sure every value is equal to "something"
function validator (val) {
return val == 'something';
}
new Schema({ name: { type: String, validate: validator }});
// with a custom error message
var custom = [validator, 'Uh oh, {PATH} does not equal "something".']
new Schema({ name: { type: String, validate: custom }});
// adding many validators at a time
var many = [
{ validator: validator, msg: 'uh oh' }
, { validator: anotherValidator, msg: 'failed' }
]
new Schema({ name: { type: String, validate: many }});
// or utilizing SchemaType methods directly:
var schema = new Schema({ name: 'string' });
schema.path('name').validate(validator, 'validation of {PATH} failed with value {VALUE}');
Found that here if you want to look into it more. Hope that helps someone!
http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate

Saving Mongoose object into two collections

Currently I have a node application which uses mongoose to save an object into a MongoDB. I am using a model similar to this:
var mongoose = require('mongoose')
, Schema = mongoose.Schema;
var RegistrationSchema = new Schema({
name:{ type: String, default: '', trim: false}
});
mongoose.model('Registration', RegistrationSchema);
Which saves my objects into a collection called registrations.
I save my registrations as such:
var registration = new Registration(data);
registration.save(function(err) {
if (err) {
return callback({
source: 'data_save',
type: 'admin',
errors: err
});
}
else {
return callback(null, data);
}
});
I would also like to save this same object when I create it, into another collection with a different name, such as registrations_new, or something to that effect. I want to duplicate this entry into the new collection. I tried to add the other collection in the connection string, which broke the mongo part entirely, I tried to create a new model called New_Registration, load that Schema and try to save it individually, but I have another issue with that. It seems that Mongoose pairs the schema with the collection, and that there really is no way to overwrite which collection it is saving to.
Anyone have any solution for this?
You can use the same schema in multiple models, so something like this works:
var RegistrationSchema = new Schema({
name:{ type: String, default: '', trim: false}
});
var Registration = mongoose.model('Registration', RegistrationSchema);
var New_Registration = mongoose.model('New_Registration', RegistrationSchema);
var registration = new Registration(data);
registration.save();
var new_registration = new New_Registration(data);
new_registration.save();

Resources