阿里云主机折上折
  • 微信号
Current Site:Index > Custom types and extensions

Custom types and extensions

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

Custom Types and Extensions

Mongoose provides a flexible custom type mechanism, allowing developers to create specialized data types based on their needs. This capability is particularly useful when dealing with unique data structures or business logic, enabling developers to overcome the limitations of built-in types.

Basic Type Extension

The simplest way to create a custom type is by extending the mongoose.SchemaType class. The following example demonstrates creating a basic postal code type:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const SchemaType = mongoose.SchemaType;

class ZipCode extends SchemaType {
  constructor(key, options) {
    super(key, options, 'ZipCode');
  }

  cast(val) {
    if (!/^\d{6}$/.test(val)) {
      throw new Error('ZipCode must be 6 digits');
    }
    return val;
  }
}

// Register the type
mongoose.Schema.Types.ZipCode = ZipCode;

// Usage example
const addressSchema = new Schema({
  zip: { type: ZipCode }
});

Complex Custom Types

For scenarios requiring nested data handling, more complex custom types can be created. The following implements a phone number type with auto-formatting:

class PhoneNumber extends SchemaType {
  constructor(key, options) {
    super(key, options, 'PhoneNumber');
    this.countryCode = options?.countryCode || '86';
  }

  cast(val) {
    if (typeof val !== 'string') {
      throw new Error('PhoneNumber must be a string');
    }
    
    // Remove all non-digit characters
    const cleaned = val.replace(/\D/g, '');
    
    // Validate length
    if (cleaned.length < 7 || cleaned.length > 15) {
      throw new Error('Invalid phone number length');
    }
    
    // Auto-add country code
    return cleaned.startsWith(this.countryCode) 
      ? `+${cleaned}` 
      : `+${this.countryCode}${cleaned}`;
  }
}

// Usage example
const contactSchema = new Schema({
  phone: { 
    type: PhoneNumber,
    countryCode: '1'  // North American area code
  }
});

Type Validation Extension

Custom types can integrate complex validation logic. The following creates an RGB color type with validation:

class RGBColor extends SchemaType {
  constructor(key, options) {
    super(key, options, 'RGBColor');
  }

  cast(val) {
    if (typeof val === 'string') {
      // Handle #RRGGBB format
      if (/^#([0-9a-f]{3}){1,2}$/i.test(val)) {
        return val.toLowerCase();
      }
      // Handle rgb(r,g,b) format
      const rgbMatch = val.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
      if (rgbMatch) {
        const toHex = c => parseInt(c).toString(16).padStart(2, '0');
        return `#${toHex(rgbMatch[1])}${toHex(rgbMatch[2])}${toHex(rgbMatch[3])}`;
      }
    }
    throw new Error('Invalid RGB color format');
  }
}

// Usage example
const themeSchema = new Schema({
  primaryColor: { type: RGBColor }
});

Mixed Type Extension

Mongoose's Mixed type can be further extended to create dynamic types with specific behaviors:

class DynamicConfig extends mongoose.Schema.Types.Mixed {
  constructor(key, options) {
    super(key, options);
    this.allowedKeys = options?.allowedKeys || [];
  }

  cast(val) {
    if (typeof val !== 'object') {
      throw new Error('DynamicConfig must be an object');
    }
    
    // Filter disallowed keys
    return Object.keys(val).reduce((acc, key) => {
      if (this.allowedKeys.includes(key)) {
        acc[key] = val[key];
      }
      return acc;
    }, {});
  }
}

// Usage example
const appSchema = new Schema({
  config: {
    type: DynamicConfig,
    allowedKeys: ['theme', 'layout', 'features']
  }
});

Inheriting Built-in Types

Directly inheriting built-in types allows reusing existing functionality:

class TruncatedString extends mongoose.Schema.Types.String {
  constructor(key, options) {
    super(key, options);
    this.maxLength = options?.maxLength || 255;
  }

  cast(val) {
    const strVal = super.cast(val);
    return strVal.length > this.maxLength 
      ? strVal.substring(0, this.maxLength)
      : strVal;
  }
}

// Usage example
const postSchema = new Schema({
  title: { 
    type: TruncatedString,
    maxLength: 100 
  },
  content: {
    type: TruncatedString,
    maxLength: 1000
  }
});

Type Converters

Implementing type converters enables automatic data format conversion before saving:

class Timestamp extends mongoose.Schema.Types.Date {
  constructor(key, options) {
    super(key, options);
    this.precision = options?.precision || 'millisecond';
  }

  cast(val) {
    const date = super.cast(val);
    
    switch (this.precision) {
      case 'second':
        return new Date(Math.floor(date.getTime() / 1000) * 1000);
      case 'minute':
        return new Date(Math.floor(date.getTime() / 60000) * 60000);
      case 'hour':
        return new Date(Math.floor(date.getTime() / 3600000) * 3600000);
      default:
        return date;
    }
  }
}

// Usage example
const eventSchema = new Schema({
  createdAt: { 
    type: Timestamp,
    precision: 'minute',
    default: Date.now
  }
});

Virtual Type Extension

Virtual types can also be customized to implement complex calculation logic:

class GeometricMean extends mongoose.Schema.Types.Virtual {
  constructor(key, options) {
    super(key, options);
    this.fields = options?.fields || [];
  }

  get() {
    return function() {
      const values = this.fields.map(f => this[f]);
      if (values.some(v => v === undefined || v === null)) return null;
      
      const product = values.reduce((acc, val) => acc * val, 1);
      return Math.pow(product, 1 / values.length);
    };
  }
}

// Usage example
const statsSchema = new Schema({
  values: [Number]
});

statsSchema.virtual('gmean', new GeometricMean(null, {
  fields: ['values']
}));

Array Type Extension

Custom array element types can enforce specific element requirements:

class UniqueStringArray extends mongoose.Schema.Types.Array {
  constructor(key, options) {
    super(key, options);
    this.maxLength = options?.maxLength || 100;
  }

  cast(val) {
    const arr = super.cast(val);
    
    // Deduplicate
    const uniqueArr = [...new Set(arr)];
    
    // Truncate
    return uniqueArr.length > this.maxLength
      ? uniqueArr.slice(0, this.maxLength)
      : uniqueArr;
  }
}

// Usage example
const userSchema = new Schema({
  tags: {
    type: UniqueStringArray,
    maxLength: 10
  }
});

Query Condition Extension

Custom types can extend query condition handling:

class CaseInsensitiveString extends mongoose.Schema.Types.String {
  constructor(key, options) {
    super(key, options);
  }

  cast(val) {
    return super.cast(val)?.toLowerCase();
  }

  checkRequired(val) {
    return super.checkRequired(val) && val.trim().length > 0;
  }
}

// Automatically converts query conditions
const productSchema = new Schema({
  name: { type: CaseInsensitiveString, required: true }
});

// Case-insensitive queries
Product.find({ name: 'TEST' }); // Will match "test", "Test", etc.

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

如果侵犯了你的权益请来信告知我们删除。邮箱: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 ☕.