Don't run validation on schema under certain conditions - node.js

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...

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/

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

Is this Mongoose schema correct for saving strings in array?

(SOLUTION IN COMMENTS)
I'm trying to add an array of strings into my DB however each time I try i keep getting the following - i.e. it saves the entire array rather than each item separately:
Also, my mongoose schema looks like this:
var personSchema = new mongoose.Schema({
tags: [{type: String}],
createdBy: String
});
Any ideas where I'm going wrong?
Update - as requested in comments more complete code:
So in backend api:
var newPerson = new Person ({
tags: req.body.tags, //coming in as ["test", "test2"]
createdDate: new Date()
});
newPerson.save(function(err) {
if (err) {
res.json({ success: false, message: 'Error '});
} res.json({ success: true, message: 'All OK'});
});
And then in my Angular front end:
function postService($http, API, auth) {
var self = this;
self.post = function(tags) {
return $http.post(API + '/person', {
tags: JSON.stringify(tags) //note have tried just tags
})
}
}

Mongoose validation not working properly in mocha test

I am building a REST api with nodejs, using mongoose and mochajs to run some tests. I have the following scheme:
var subscriptionTypeSchema = new mongoose.Schema({
typeId : { type: Number, required: true, unique: true },
name : { type: String, required: true},
active : { type: Boolean, required: true }
});
Express route:
app.post('/1.0/subscriptiontype', subscriptiontype.create);
Controller:
exports.create = function(req, res) {
validation.subscriptionTypeValidator(req);
var errors = req.validationErrors();
if (errors) {
res.status(400).json({
errors: errors
});
} else {
var subscriptionType = new SubscriptionType();
subscriptionType.typeId = parseInt(req.body.typeId);
subscriptionType.name = req.body.name;
subscriptionType.active = req.body.active;
subscriptionType.save(function(err) {
if (err) {
var parsedError = mongooseutility.parseMongooseError(err);
res.status(400).json({
errors: [parsedError]
});
} else {
res.json({identifier: subscriptionType._id});
}
});
}
};
The mongoose utility maps the error codes to a more API friendly output (error codes 11001 and 11000 are mapped to a 'duplicate' error, as can be seen in the test).
Mocha before method:
before(function(done) {
db.connection.on('open', function() {
db.connection.db.dropDatabase(function(err) {
done();
});
});
});
I've verified that the database is dropped successfully.
The test itself makes a request using supertest. Before this test, I have a test that creates a subscription type with typeId 4 successfully, so this one should fail:
it('Should not create subscription with taken type id', function (done) {
request(app.privateapi)
.post('/1.0/subscriptiontype')
.set('Authorization', authorizationHeader)
.send({
typeId: 4,
name: 'New package',
active: 1
})
.expect(function (res) {
if (res.status !== 400) {
throw new Error('Status code was not 400');
}
var expectedResponse = { errors: [ { param: 'typeId', msg: 'duplicate' } ] };
if (JSON.stringify(res.body) !== JSON.stringify(expectedResponse)) {
throw new Error('Output was not was as expected');
}
})
.end(done);
});
Tests are invoked using grunt-simple-mocha.
This test works the first time, however when I run it a 2nd time it fails on the unique validation. A third time it works again. I've done some searching and found that it probably has something to do with a race condition while recreating indexes, so I've tried restarting mongodb before running the tests again, but that doesn't work. I've found a solution here: http://grokbase.com/t/gg/mongoose-orm/138qe75dvr/mongoose-unique-index-test-fail but I am not sure how to implement this. Any ideas?
Edit: for now I fixed it by dropping the database in an 'after' method (instead of 'before'). All the tests run fine, but it would be nice to keep the test data after the tests are done, for inspection etc...
You are not testing the creation of your tables so you can just empty your collections instead of creating the db.
Something along those lines (not tested):
beforeEach(function(done){
var models = Object.keys(mongoose.models);
var expects = models.length;
if(expects == 0) return done();
var removeCount = 1;
//maybe use async or something else but whatever
models.forEach(function(model){
model.remove({}, function(){
if(removeCount == expects){
done();
}
removeCount++;
})
});
});

Mongoose unique validation error type

I'm using this schema with mongoose 3.0.3 from npm:
var schema = new Schema({
_id: Schema.ObjectId,
email: {type: String, required: true, unique: true}
});
If I try to save a email that is already in db, I expect to get a ValidationError like if a required field is omitted. However this is not the case, I get a MongoError: E11000 duplicate key error index.
Which is not a validation error (happens even if I remove the unique:true).
Any idea why?
I prefer putting it in path validation mechanisms, like
UserSchema.path('email').validate(function(value, done) {
this.model('User').count({ email: value }, function(err, count) {
if (err) {
return done(err);
}
// If `count` is greater than zero, "invalidate"
done(!count);
});
}, 'Email already exists');
Then it'll just get wrapped into ValidationError and will return as first argument when you call validate or save .
I had some issues with the approved answer. Namely:
this.model('User') didn't work for me.
the callback done wasn't working properly.
I resolved those issues by:
UserSchema.path('email').validate(async (value) => {
const emailCount = await mongoose.models.User.countDocuments({email: value });
return !emailCount;
}, 'Email already exists');
I use async/await which is a personal preference because it is much neater: https://javascript.info/async-await.
Let me know if I got something wrong.
This is expected behavior
The unique: true is equivalent to setting an index in mongodb like this:
db.myCollection.ensureIndex( { "email": 1 }, { unique: true } )
To do this type of validation using Mongoose (Mongoose calls this complex validation- ie- you are not just asserting the value is a number for example), you will need to wire in to the pre-save event:
mySchema.pre("save",function(next, done) {
var self = this;
mongoose.models["User"].findOne({email : self.email},function(err, results) {
if(err) {
done(err);
} else if(results) { //there was a result found, so the email address exists
self.invalidate("email","email must be unique");
done(new Error("email must be unique"));
} else {
done();
}
});
next();
});
Simply response to json
try {
let end_study_year = new EndStudyYear(req.body);
await end_study_year.save();
res.json({
status: true,
message: 'បានរក្សាទុក!'
})
}catch (e) {
res.json({
status: false,
message: e.message.toString().includes('duplicate') ? 'ទិន្នន័យមានរួចហើយ' : e.message.split(':')[0] // check if duplicate message exist
})
}
Sorry for answering an old question. After testing I feel good to have find these answers, so I will give my experience. Both top answers are great and right, just remember that:
if your document is new, you can just validate if count is higher than 0, thats the common situation;
if your document is NOT new and has modified the unique field, you need to validate with 0 too;
if your document is NOT new and has NOT being modified, just go ahead;
Here is what I made in my code:
UserSchema.path('email').validate(async function validateDuplicatedEmail(value) {
if (!this.isNew && !this.isModified('email')) return true;
try {
const User = mongoose.model("User");
const count = await User.countDocuments({ email: value });
if (count > 0) return false;
return true;
}
catch (error) {
return false;
}
}, "Email already exists");

Resources