Custom types and extensions
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
下一篇:性能调优与查询优化