阿里云主机折上折
  • 微信号
Current Site:Index > Custom validators and error handling

Custom validators and error handling

Author:Chuan Chen 阅读数:1496人阅读 分类: MongoDB

Basic Concepts of Custom Validators

Mongoose allows for more granular data validation through custom validators. While built-in validators like required and min are commonly used, they often lack flexibility when dealing with complex business logic. Custom validators come in two forms: synchronous and asynchronous, and can be used directly in Schema definitions.

const userSchema = new mongoose.Schema({
  phone: {
    type: String,
    validate: {
      validator: function(v) {
        return /^1[3456789]\d{9}$/.test(v);
      },
      message: props => `${props.value} is not a valid phone number`
    }
  }
});

Implementation of Synchronous Validators

Synchronous validators immediately return a boolean or error message, making them suitable for simple and fast validation. The validation function receives the current field value as a parameter and determines whether the validation passes based on the return value.

const productSchema = new mongoose.Schema({
  price: {
    type: Number,
    validate: {
      validator: function(value) {
        // Price must be greater than cost price
        return value > this.costPrice;
      },
      message: 'Selling price cannot be lower than cost price'
    }
  },
  costPrice: Number
});

When validation fails, Mongoose adds the error to the document's errors collection. Synchronous validators are automatically executed before saving the document and can be manually triggered via doc.validate().

Use Cases for Asynchronous Validators

Validations requiring database queries or network requests must use asynchronous validators. By setting the isAsync option to true, the validation function can return a Promise or use a callback function.

const orderSchema = new mongoose.Schema({
  couponCode: {
    type: String,
    validate: {
      validator: function(v) {
        return new Promise((resolve, reject) => {
          CouponModel.findOne({ code: v }, (err, coupon) => {
            if (err || !coupon) return resolve(false);
            resolve(coupon.isValid());
          });
        });
      },
      message: 'Invalid coupon code'
    }
  }
});

Asynchronous validations are automatically executed when saving a document. Note that they are not triggered by default during update operations and require explicit setting of runValidators: true.

Error Handling Mechanisms for Validation

Mongoose provides multiple ways to handle errors. When validation fails, error messages are attached to the document and can be captured via the save callback or Promise.

const newUser = new User({ phone: '123456' });
newUser.save()
  .catch(err => {
    console.log(err.errors['phone'].message); // Output specific error message
  });

For batch operations, error handling requires special attention. When using Model.updateMany(), validation failure for a single document does not prevent other documents from being updated, but an object containing error information will be returned.

Path-Level vs. Document-Level Validation

In addition to field-level validation, Mongoose also supports document-level validation. Complex logic involving multiple fields can be defined via the Schema's validate method.

const bookingSchema = new mongoose.Schema({
  startDate: Date,
  endDate: Date,
  guests: Number
});

bookingSchema.validate('datesValid', function() {
  return this.endDate > this.startDate;
}, 'End date must be later than start date');

Document-level validation is executed before saving and can access all fields of the document. This approach is suitable for scenarios requiring cross-field validation, such as date ranges or quantity limits.

Advanced Usage of Custom Error Messages

Error messages can be dynamically generated, not just static strings. Through message templates in function form, contextual information about validation failures can be included.

const inventorySchema = new mongoose.Schema({
  stock: {
    type: Number,
    validate: {
      validator: function(v) {
        return v >= 0;
      },
      message: props => `Stock value ${props.value} cannot be negative`
    }
  }
});

For multilingual applications, internationalization logic can be implemented in the message function. Error messages also support template variables, such as built-in placeholders like {PATH} and {VALUE}.

Combining Validators with Middleware

Validators are often used in combination with middleware to implement more comprehensive business logic. For example, additional validation can be performed in the pre('save') hook, or validation logs can be recorded in the post('validate') hook.

userSchema.pre('save', function(next) {
  if (this.isModified('password')) {
    if (this.password.length < 8) {
      next(new Error('Password must be at least 8 characters'));
    } else {
      next();
    }
  } else {
    next();
  }
});

This combination maintains the centralization of validation logic while handling more complex business rules. Middleware can also be used to clean or standardize data before validation.

Strategies for Testing Custom Validators

Comprehensive testing is crucial for validation logic. Test cases should cover various edge cases, including valid inputs, extreme values, and intentionally constructed invalid inputs.

describe('User Model Validation', () => {
  it('should reject invalid phone numbers', async () => {
    const user = new User({ phone: '123' });
    await expect(user.save()).to.eventually.be.rejectedWith('is not a valid phone number');
  });

  it('should accept valid phone numbers', async () => {
    const user = new User({ phone: '13800138000' });
    await expect(user.save()).to.eventually.be.fulfilled;
  });
});

Using testing frameworks like Mocha+Chai makes it easy to verify validator behavior. For asynchronous validators, ensure tests properly handle Promises or callbacks.

Performance Optimization Considerations

Complex validation logic can impact performance, especially during batch operations. Several optimization strategies are worth considering:

  1. Add caching for frequently executed validations
  2. Move CPU-intensive validations to background processes
  3. Create in-memory caches for reference data that rarely changes
const couponCache = new Map();

couponSchema.path('code').validate({
  validator: async function(code) {
    if (couponCache.has(code)) {
      return couponCache.get(code);
    }
    const valid = await checkCouponValidity(code);
    couponCache.set(code, valid);
    return valid;
  },
  message: 'Coupon has expired'
});

Reusability Patterns for Validation Logic

To avoid code duplication, common validation logic can be extracted into reusable components. Mongoose supports sharing validation logic through plugins.

// validators/phone.js
module.exports = function(schema, options) {
  schema.path('phone').validate({
    validator: function(v) {
      return /^1[3456789]\d{9}$/.test(v);
    },
    message: 'Invalid phone number format'
  });
};

// Usage in models
userSchema.plugin(require('./validators/phone'));

For more complex scenarios, validator factory functions can be created to generate different validation logic based on configuration. This approach is particularly suitable for scenarios where validation rules need to be adjusted according to different environments.

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.