驗證

在深入探討驗證語法之前,請記住以下規則:

  • 驗證是在 SchemaType 中定義的。
  • 驗證是中介軟體。Mongoose 預設會在每個 schema 上將驗證註冊為 pre('save') 鉤子。
  • 驗證始終作為第一個 pre('save') 鉤子執行。這表示驗證不會在您於 pre('save') 鉤子中所做的任何變更上執行。
  • 您可以透過設定 validateBeforeSave 選項來停用儲存前的自動驗證。
  • 您可以使用 doc.validate()doc.validateSync() 手動執行驗證。
  • 您可以使用 doc.invalidate(...) 手動將欄位標記為無效(導致驗證失敗)。
  • 驗證器不會對未定義的值執行。唯一的例外是 required 驗證器
  • 當您呼叫 Model#save 時,Mongoose 也會執行子文件驗證。如果發生錯誤,您的 Model#save promise 將會 reject。
  • 驗證是可自訂的。
const schema = new Schema({
  name: {
    type: String,
    required: true
  }
});
const Cat = db.model('Cat', schema);

// This cat has no name :(
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 有幾個內建驗證器。

以上每個驗證器連結都提供了更多關於如何啟用它們以及自訂其錯誤訊息的資訊。

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?');

自訂錯誤訊息

您可以為您的 schema 中個別的驗證器設定錯誤訊息。有兩種等效的方式來設定驗證器錯誤訊息

  • 陣列語法:min: [6, '必須至少為 6,得到 {VALUE}']
  • 物件語法:enum: { values: ['Coffee', 'Tea'], message: '{VALUE} 不受支援' }

Mongoose 也支援錯誤訊息的基本範本。Mongoose 會將 {VALUE} 替換為正在驗證的值。

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

unique 選項並非驗證器

新手常犯的一個錯誤是,schema 的 unique 選項不是驗證器。它是用於建立 MongoDB 唯一索引的方便輔助工具。有關更多資訊,請參閱常見問題

const uniqueUsernameSchema = new Schema({
  username: {
    type: String,
    unique: true
  }
});
const U1 = db.model('U1', uniqueUsernameSchema);
const U2 = db.model('U2', uniqueUsernameSchema);

const dup = [{ username: 'Val' }, { username: 'Val' }];
// Race condition! This may save successfully, depending on whether
// MongoDB built the index before writing the 2 docs.
U1.create(dup).
  then(() => {
  }).
  catch(err => {
  });

// You need to wait for Mongoose to finish building the `unique`
// index before writing. You only need to build indexes once for
// a given collection, so you normally don't need to do this
// in production. But, if you drop the database between tests,
// you will need to use `init()` to wait for the index build to finish.
U2.init().
  then(() => U2.create(dup)).
  catch(error => {
    // `U2.create()` will error, but will *not* be a mongoose validation error, it will be
    // a duplicate key error.
    // See: https://masteringjs.io/tutorials/mongoose/e11000-duplicate-key
    assert.ok(error);
    assert.ok(!error.errors);
    assert.ok(error.message.indexOf('duplicate key error') !== -1);
  });

自訂驗證器

如果內建驗證器不足以滿足您的需求,您可以定義自訂驗證器。

自訂驗證是透過傳遞驗證函式來宣告的。您可以在 SchemaType#validate() API 文件中找到如何執行此操作的詳細說明。

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';
// Validation succeeds! Phone number is defined
// and fits `DDD-DDD-DDDD`
error = user.validateSync();
assert.equal(error, null);

非同步自訂驗證器

自訂驗證器也可以是非同步的。如果您的驗證器函式傳回 promise(例如 async 函式),mongoose 會等待該 promise settle。如果傳回的 promise reject,或 fulfilled 的值為 false,Mongoose 會將其視為驗證錯誤。

const userSchema = new Schema({
  name: {
    type: String,
    // You can also make a validator async by returning a promise.
    validate: () => Promise.reject(new Error('Oops!'))
  },
  email: {
    type: String,
    // There are two ways for an promise-based async validator to fail:
    // 1) If the promise rejects, Mongoose assumes the validator failed with the given error.
    // 2) If the promise resolves to `false`, Mongoose assumes the validator failed and creates an error with the given `message`.
    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 物件,其值為 ValidatorError 物件。每個 ValidatorError 都具有 kindpathvaluemessage 屬性。ValidatorError 也可能具有 reason 屬性。如果驗證器中擲回錯誤,此屬性將包含擲回的錯誤。

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

// `error` is a ValidationError object
// `error.errors.color` is a ValidatorError object
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');

// If your validator throws an exception, mongoose will use the error
// message. If your validator returns `false`,
// mongoose will use the 'Name `Power Ranger` is not valid' message.
assert.equal(error.errors.name.message,
  'Need to get a Turbo Man for Christmas');
assert.equal(error.errors.name.value, 'Power Ranger');
// If your validator threw an error, the `reason` property will contain
// the original error thrown, including the original stack trace.
assert.equal(error.errors.name.reason.message,
  'Need to get a Turbo Man for Christmas');

assert.equal(error.name, 'ValidationError');

轉換錯誤

在執行驗證器之前,Mongoose 會嘗試將值強制轉換為正確的類型。此過程稱為轉換文件。如果給定路徑的轉換失敗,error.errors 物件將包含一個 CastError 物件。

轉換會在驗證之前執行,如果轉換失敗,則不會執行驗證。這表示您的自訂驗證器可以假設 vnullundefined 或 schema 中指定的類型實例。

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; // 'CastError'
// 'Cast to Number failed for value "not a number" at path "numWheels"'
err.errors['numWheels'].message;

預設情況下,Mongoose 轉換錯誤訊息如下所示:Cast to Number failed for value "pie" at path "numWheels"。您可以在 SchemaType 上將 cast 選項設定為字串,以覆寫 Mongoose 的預設轉換錯誤訊息,如下所示。

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; // 'CastError'
// "pie" is not a number
err.errors['numWheels'].message;

Mongoose 的轉換錯誤訊息範本支援以下參數

  • {PATH}:轉換失敗的路徑
  • {VALUE}:轉換失敗的值的字串表示形式
  • {KIND}:Mongoose 嘗試轉換的類型,例如 'String''Number'

您也可以定義一個函式,Mongoose 將呼叫該函式以取得轉換錯誤訊息,如下所示。

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; // 'CastError'
// "pie" is not a number
err.errors['numWheels'].message;

全域 SchemaType 驗證

除了在個別的 schema 路徑上定義自訂驗證器之外,您還可以設定自訂驗證器以在給定 SchemaType 的每個實例上執行。例如,以下程式碼示範如何讓空字串 '' 成為所有字串路徑的無效值。

// Add a custom validator to all strings
mongoose.Schema.Types.String.set('validate', v => v == null || v > 0);

const userSchema = new Schema({
  name: String,
  email: String
});
const User = db.model('User', userSchema);

const user = new User({ name: '', email: '' });

const err = await user.validate().then(() => null, err => err);
err.errors['name']; // ValidatorError
err.errors['email']; // ValidatorError

巢狀物件上的必要驗證器

在 mongoose 中定義巢狀物件上的驗證器很棘手,因為巢狀物件不是完全成熟的路徑。

let personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
});

assert.throws(function() {
  // This throws an error, because 'name' isn't a full fledged path
  personSchema.path('name').required(true);
}, /Cannot.*'required'/);

// To make a nested object required, use a single nested schema
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']);

更新驗證器

在以上範例中,您了解了文件驗證。Mongoose 也支援 update()updateOne()updateMany()findOneAndUpdate() 操作的驗證。預設情況下會關閉更新驗證器 - 您需要指定 runValidators 選項。

若要開啟更新驗證器,請為 update()updateOne()updateMany()findOneAndUpdate() 設定 runValidators 選項。請小心:預設情況下會關閉更新驗證器,因為它們有一些注意事項。

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');

更新驗證器和 this

更新驗證器與文件驗證器之間有幾個關鍵差異。在下面的顏色驗證函式中,當使用文件驗證時,this 指的是正在驗證的文件。但是,當執行更新驗證器時,this 指的是查詢物件而不是文件。由於查詢有一個簡潔的 .get() 函式,您可以取得您想要的屬性的更新值。

const toySchema = new Schema({
  color: String,
  name: String
});

toySchema.path('color').validate(function(value) {
  // When running in `validate()` or `validateSync()`, the
  // validator can access the document using `this`.
  // When running with update validators, `this` is the Query,
  // **not** the document being updated!
  // Queries have a `get()` method that lets you get the
  // updated 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' });
// Validation failed: color: Validator failed for path `color` with value `green`
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;
}
// Validation failed: color: Validator failed for path `color` with value `green`
assert.ok(error);

更新驗證器僅在已更新的路徑上執行

另一個關鍵差異是,更新驗證器僅在更新中指定的路徑上執行。例如,在以下範例中,由於更新操作中未指定 'name',因此更新驗證將會成功。

當使用更新驗證器時,required 驗證器在您嘗試明確 $unset 索引鍵時才會失敗。

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 };
// Operation succeeds despite the fact that 'name' is not specified
await Kitten.updateOne({}, update, opts);

const unset = { $unset: { name: 1 } };
// Operation fails because 'name' is required
const err = await Kitten.updateOne({}, unset, opts).then(() => null, err => err);
assert.ok(err);
assert.ok(err.errors['name']);

更新驗證器僅針對某些操作執行

最後一個值得注意的細節:更新驗證器在以下更新運算子上執行

  • $set
  • $unset
  • $push
  • $addToSet
  • $pull
  • $pullAll

例如,無論 number 的值為何,下面的更新都會成功,因為更新驗證器會忽略 $inc

此外,$push$addToSet$pull$pullAll 驗證不會在陣列本身上執行任何驗證,僅在陣列的個別元素上執行。

const testSchema = new Schema({
  number: { type: Number, max: 0 },
  arr: [{ message: { type: String, maxlength: 10 } }]
});

// Update validators won't check this, so you can still `$push` 2 elements
// onto the array, so long as they don't have a `message` that's too long.
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 };

// There will never be a validation error here
await Test.updateOne({}, update, opts);

// This will never error either even though the array will have at
// least 2 elements.
update = { $push: [{ message: 'hello' }, { message: 'world' }] };
await Test.updateOne({}, update, opts);

接下來

現在我們已經介紹了 Validation,讓我們看看中介軟體