'use strict';
const assert = require('assert');
const mongoose = require('../../');
const start = require('../common');
describe('validation docs', function() {
let db;
const Schema = mongoose.Schema;
before(function() {
db = mongoose.createConnection(start.uri, {
minPoolSize: 1,
maxPoolSize: 1
});
});
beforeEach(() => db.deleteModel(/Vehicle/));
after(async function() {
await db.close();
});
* Before we get into the specifics of validation syntax, please keep the following rules in mind:
*
* - Validation is defined in the [SchemaType](./schematypes.html)
* - Validation is [middleware](./middleware.html). Mongoose registers validation as a `pre('save')` hook on every schema by default.
* - You can disable automatic validation before save by setting the [validateBeforeSave](./guide.html#validateBeforeSave) option
* - You can manually run validation using `doc.validate(callback)` or `doc.validateSync()`
* - You can manually mark a field as invalid (causing validation to fail) by using [`doc.invalidate(...)`](./api/document.html#document_Document-invalidate)
* - Validators are not run on undefined values. The only exception is the [`required` validator](./api/schematype.html#schematype_SchemaType-required).
* - Validation is asynchronously recursive; when you call [Model#save](./api/model.html#model_Model-save), sub-document validation is executed as well. If an error occurs, your [Model#save](./api/model.html#model_Model-save) callback receives it
* - Validation is customizable
*/
it('Validation', async function() {
const schema = new Schema({
name: {
type: String,
required: true
}
});
const Cat = db.model('Cat', schema);
const cat = new Cat();
let error;
try {
await cat.save();
} catch (err) {
error = err;
}
assert.equal(error.errors['name'].message,
'Path `name` is required.');
error = cat.validateSync();
assert.equal(error.errors['name'].message,
'Path `name` is required.');
});
* Mongoose has several built-in validators.
*
* - All [SchemaTypes](/docs/schematypes.html) have the built-in [required](./api/schematype.html#schematype_SchemaType-required) validator. The required validator uses the [SchemaType's `checkRequired()` function](./api/schematype.html#schematype_SchemaType-checkRequired) to determine if the value satisfies the required validator.
* - [Numbers](/docs/schematypes.html#numbers) have [`min` and `max`](./schematypes.html#number-validators) validators.
* - [Strings](/docs/schematypes.html#strings) have [`enum`, `match`, `minLength`, and `maxLength`](./schematypes.html#string-validators) validators.
*
* Each of the validator links above provide more information about how to enable them and customize their error messages.
*/
it('Built-in Validators', function() {
const breakfastSchema = new Schema({
eggs: {
type: Number,
min: [6, 'Too few eggs'],
max: 12
},
bacon: {
type: Number,
required: [true, 'Why no bacon?']
},
drink: {
type: String,
enum: ['Coffee', 'Tea'],
required: function() {
return this.bacon > 3;
}
}
});
const Breakfast = db.model('Breakfast', breakfastSchema);
const badBreakfast = new Breakfast({
eggs: 2,
bacon: 0,
drink: 'Milk'
});
let error = badBreakfast.validateSync();
assert.equal(error.errors['eggs'].message,
'Too few eggs');
assert.ok(!error.errors['bacon']);
assert.equal(error.errors['drink'].message,
'`Milk` is not a valid enum value for path `drink`.');
badBreakfast.bacon = 5;
badBreakfast.drink = null;
error = badBreakfast.validateSync();
assert.equal(error.errors['drink'].message, 'Path `drink` is required.');
badBreakfast.bacon = null;
error = badBreakfast.validateSync();
assert.equal(error.errors['bacon'].message, 'Why no bacon?');
});
* You can configure the error message for individual validators in your schema. There are two equivalent
* ways to set the validator error message:
*
* - Array syntax: `min: [6, 'Must be at least 6, got {VALUE}']`
* - Object syntax: `enum: { values: ['Coffee', 'Tea'], message: '{VALUE} is not supported' }`
*
* Mongoose also supports rudimentary templating for error messages.
* Mongoose replaces `{VALUE}` with the value being validated.
*/
it('Custom Error Messages', function() {
const breakfastSchema = new Schema({
eggs: {
type: Number,
min: [6, 'Must be at least 6, got {VALUE}'],
max: 12
},
drink: {
type: String,
enum: {
values: ['Coffee', 'Tea'],
message: '{VALUE} is not supported'
}
}
});
db.deleteModel(/Breakfast/);
const Breakfast = db.model('Breakfast', breakfastSchema);
const badBreakfast = new Breakfast({
eggs: 2,
drink: 'Milk'
});
const error = badBreakfast.validateSync();
assert.equal(error.errors['eggs'].message,
'Must be at least 6, got 2');
assert.equal(error.errors['drink'].message, 'Milk is not supported');
});
* A common gotcha for beginners is that the `unique` option for schemas
* is *not* a validator. It's a convenient helper for building [MongoDB unique indexes](https://www.mongodb.com/docs/manual/core/index-unique/).
* See the [FAQ](/docs/faq.html) for more information.
*/
it('The `unique` Option is Not a Validator', async function() {
const uniqueUsernameSchema = new Schema({
username: {
type: String,
unique: true
}
});
const U1 = db.model('U1', uniqueUsernameSchema);
const U2 = db.model('U2', uniqueUsernameSchema);
this.timeout(5000);
const dup = [{ username: 'Val' }, { username: 'Val' }];
await U1.create(dup).catch(err => {
err?.message;
});
await U2.init();
await U2.create(dup).catch(error => {
assert.ok(error);
assert.ok(!error.errors);
assert.ok(error.message.indexOf('duplicate key error') !== -1);
});
});
* If the built-in validators aren't enough, you can define custom validators
* to suit your needs.
*
* Custom validation is declared by passing a validation function.
* You can find detailed instructions on how to do this in the
* [`SchemaType#validate()` API docs](./api/schematype.html#schematype_SchemaType-validate).
*/
it('Custom Validators', function() {
const userSchema = new Schema({
phone: {
type: String,
validate: {
validator: function(v) {
return /\d{3}-\d{3}-\d{4}/.test(v);
},
message: props => `${props.value} is not a valid phone number!`
},
required: [true, 'User phone number required']
}
});
const User = db.model('user', userSchema);
const user = new User();
let error;
user.phone = '555.0123';
error = user.validateSync();
assert.equal(error.errors['phone'].message,
'555.0123 is not a valid phone number!');
user.phone = '';
error = user.validateSync();
assert.equal(error.errors['phone'].message,
'User phone number required');
user.phone = '201-555-0123';
error = user.validateSync();
assert.equal(error, null);
});
* Custom validators can also be asynchronous. If your validator function
* returns a promise (like an `async` function), mongoose will wait for that
* promise to settle. If the returned promise rejects, or fulfills with
* the value `false`, Mongoose will consider that a validation error.
*/
it('Async Custom Validators', async function() {
const userSchema = new Schema({
name: {
type: String,
validate: () => Promise.reject(new Error('Oops!'))
},
email: {
type: String,
validate: {
validator: () => Promise.resolve(false),
message: 'Email validation failed'
}
}
});
const User = db.model('User', userSchema);
const user = new User();
user.email = 'test@test.co';
user.name = 'test';
let error;
try {
await user.validate();
} catch (err) {
error = err;
}
assert.ok(error);
assert.equal(error.errors['name'].message, 'Oops!');
assert.equal(error.errors['email'].message, 'Email validation failed');
});
* Errors returned after failed validation contain an `errors` object
* whose values are `ValidatorError` objects. Each
* [ValidatorError](./api/error-validation-js.html#error-validation-js) has `kind`, `path`,
* `value`, and `message` properties.
* A ValidatorError also may have a `reason` property. If an error was
* thrown in the validator, this property will contain the error that was
* thrown.
*/
it('Validation Errors', async function() {
const toySchema = new Schema({
color: String,
name: String
});
const validator = function(value) {
return /red|white|gold/i.test(value);
};
toySchema.path('color').validate(validator,
'Color `{VALUE}` not valid', 'Invalid color');
toySchema.path('name').validate(function(v) {
if (v !== 'Turbo Man') {
throw new Error('Need to get a Turbo Man for Christmas');
}
return true;
}, 'Name `{VALUE}` is not valid');
const Toy = db.model('Toy', toySchema);
const toy = new Toy({ color: 'Green', name: 'Power Ranger' });
let error;
try {
await toy.save();
} catch (err) {
error = err;
}
assert.equal(error.errors.color.message, 'Color `Green` not valid');
assert.equal(error.errors.color.kind, 'Invalid color');
assert.equal(error.errors.color.path, 'color');
assert.equal(error.errors.color.value, 'Green');
assert.equal(error.errors.name.message,
'Need to get a Turbo Man for Christmas');
assert.equal(error.errors.name.value, 'Power Ranger');
assert.equal(error.errors.name.reason.message,
'Need to get a Turbo Man for Christmas');
assert.equal(error.name, 'ValidationError');
});
* 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. That means your custom validators may assume `v` is `null`,
* `undefined`, or an instance of the type specified in your schema.
*/
it('Cast Errors', function() {
const vehicleSchema = new mongoose.Schema({
numWheels: { type: Number, max: 18 }
});
const Vehicle = db.model('Vehicle', vehicleSchema);
const doc = new Vehicle({ numWheels: 'not a number' });
const err = doc.validateSync();
err.errors['numWheels'].name;
err.errors['numWheels'].message;
assert.equal(err.errors['numWheels'].name, 'CastError');
assert.match(
err.errors['numWheels'].message,
/^Cast to Number failed for value "not a number" \(type string\) at path "numWheels"/
);
});
it('Cast Error Message Overwrite', function() {
const vehicleSchema = new mongoose.Schema({
numWheels: {
type: Number,
cast: '{VALUE} is not a number'
}
});
const Vehicle = db.model('Vehicle', vehicleSchema);
const doc = new Vehicle({ numWheels: 'pie' });
const err = doc.validateSync();
err.errors['numWheels'].name;
err.errors['numWheels'].message;
assert.equal(err.errors['numWheels'].name, 'CastError');
assert.equal(err.errors['numWheels'].message,
'"pie" is not a number');
db.deleteModel(/Vehicle/);
});
it('Cast Error Message Function Overwrite', function() {
const vehicleSchema = new mongoose.Schema({
numWheels: {
type: Number,
cast: [null, (value, path, model, kind) => `"${value}" is not a number`]
}
});
const Vehicle = db.model('Vehicle', vehicleSchema);
const doc = new Vehicle({ numWheels: 'pie' });
const err = doc.validateSync();
err.errors['numWheels'].name;
err.errors['numWheels'].message;
assert.equal(err.errors['numWheels'].name, 'CastError');
assert.equal(err.errors['numWheels'].message,
'"pie" is not a number');
db.deleteModel(/Vehicle/);
});
it('Global SchemaType Validation', async function() {
mongoose.Schema.Types.String.set('validate', v => v == null || v.length > 0);
const userSchema = new Schema({
name: String,
email: String
});
db.deleteModel(/User/);
const User = db.model('User', userSchema);
const user = new User({ name: '', email: '' });
const err = await user.validate().then(() => null, err => err);
err.errors['name'];
err.errors['email'];
assert.ok(err);
assert.equal(err.errors['name'].name, 'ValidatorError');
assert.equal(err.errors['email'].name, 'ValidatorError');
delete mongoose.Schema.Types.String.defaultOptions;
});
* Defining validators on nested objects in mongoose is tricky, because
* nested objects are not fully fledged paths.
*/
it('Required Validators On Nested Objects', function() {
let personSchema = new Schema({
name: {
first: String,
last: String
}
});
assert.throws(function() {
personSchema.path('name').required(true);
}, /Cannot.*'required'/);
const nameSchema = new Schema({
first: String,
last: String
});
personSchema = new Schema({
name: {
type: nameSchema,
required: true
}
});
const Person = db.model('Person', personSchema);
const person = new Person();
const error = person.validateSync();
assert.ok(error.errors['name']);
});
* In the above examples, you learned about document validation. Mongoose also
* supports validation for [`update()`](/docs/api/query.html#query_Query-update),
* [`updateOne()`](/docs/api/query.html#query_Query-updateOne),
* [`updateMany()`](/docs/api/query.html#query_Query-updateMany),
* and [`findOneAndUpdate()`](/docs/api/query.html#query_Query-findOneAndUpdate) operations.
* Update validators are off by default - you need to specify
* the `runValidators` option.
*
* To turn on update validators, set the `runValidators` option for
* `update()`, `updateOne()`, `updateMany()`, or `findOneAndUpdate()`.
* Be careful: update validators are off by default because they have several
* caveats.
*/
it('Update Validators', async function() {
const toySchema = new Schema({
color: String,
name: String
});
const Toy = db.model('Toys', toySchema);
Toy.schema.path('color').validate(function(value) {
return /red|green|blue/i.test(value);
}, 'Invalid color');
const opts = { runValidators: true };
let error;
try {
await Toy.updateOne({}, { color: 'not a color' }, opts);
} catch (err) {
error = err;
}
assert.equal(error.errors.color.message, 'Invalid color');
});
* There are a couple of key differences between update validators and
* document validators. In the color validation function above, `this` refers
* to the document being validated when using document validation.
* However, when running update validators, the document being updated
* may not be in the server's memory, so by default the value of `this` is
* not defined.
*/
it('Update Validators and `this`', async function() {
const toySchema = new Schema({
color: String,
name: String
});
toySchema.path('color').validate(function(value) {
if (this.get('name') && this.get('name').toLowerCase().indexOf('red') !== -1) {
return value === 'red';
}
return true;
});
const Toy = db.model('ActionFigure', toySchema);
const toy = new Toy({ color: 'green', name: 'Red Power Ranger' });
let error = toy.validateSync();
assert.ok(error.errors['color']);
const update = { color: 'green', name: 'Red Power Ranger' };
const opts = { runValidators: true };
error = null;
try {
await Toy.updateOne({}, update, opts);
} catch (err) {
error = err;
}
assert.ok(error);
});
* The other key difference is that update validators only run on the paths
* specified in the update. For instance, in the below example, because
* 'name' is not specified in the update operation, update validation will
* succeed.
*
* When using update validators, `required` validators **only** fail when
* you try to explicitly `$unset` the key.
*/
it('Update Validators Only Run On Updated Paths', async function() {
const kittenSchema = new Schema({
name: { type: String, required: true },
age: Number
});
const Kitten = db.model('Kitten', kittenSchema);
const update = { color: 'blue' };
const opts = { runValidators: true };
await Kitten.updateOne({}, update, opts);
const unset = { $unset: { name: 1 } };
const err = await Kitten.updateOne({}, unset, opts).then(() => null, err => err);
assert.ok(err);
assert.ok(err.errors['name']);
});
* One final detail worth noting: update validators **only** run on the
* following update operators:
*
* - `$set`
* - `$unset`
* - `$push` (>= 4.8.0)
* - `$addToSet` (>= 4.8.0)
* - `$pull` (>= 4.12.0)
* - `$pullAll` (>= 4.12.0)
*
* For instance, the below update will succeed, regardless of the value of
* `number`, because update validators ignore `$inc`.
*
* Also, `$push`, `$addToSet`, `$pull`, and `$pullAll` validation does
* **not** run any validation on the array itself, only individual elements
* of the array.
*/
it('Update Validators Only Run For Some Operations', async function() {
const testSchema = new Schema({
number: { type: Number, max: 0 },
arr: [{ message: { type: String, maxlength: 10 } }]
});
testSchema.path('arr').validate(function(v) {
return v.length < 2;
});
const Test = db.model('Test', testSchema);
let update = { $inc: { number: 1 } };
const opts = { runValidators: true };
await Test.updateOne({}, update, opts);
update = { $push: [{ message: 'hello' }, { message: 'world' }] };
await Test.updateOne({}, update, opts);
});
});