The development and use of plugins
Basic Concepts of Plugins
Mongoose plugins are reusable functional modules that can add additional functionality or modify existing behavior to a Schema. By encapsulating common logic, plugins avoid code duplication, making Schema functionality extensions more modular and maintainable. Each plugin is essentially a function that takes a schema and options as parameters and can add methods, static methods, virtual properties, middleware, etc., to the schema.
// A simple plugin example
function timestampPlugin(schema, options) {
schema.add({
createdAt: Date,
updatedAt: Date
});
schema.pre('save', function(next) {
const now = new Date();
this.updatedAt = now;
if (!this.createdAt) {
this.createdAt = now;
}
next();
});
}
Plugin Development Methods
Developing a Mongoose plugin requires following a specific pattern. First, define a function that takes a schema and optional options parameters. Within the function body, you can perform various operations on the schema:
- Add Fields: Use the
schema.add()
method to add new fields. - Define Methods: Add instance methods via
schema.methods
. - Define Static Methods: Add static methods via
schema.statics
. - Add Middleware: Use
pre
andpost
hooks. - Define Virtual Properties: Use
schema.virtual()
.
function paginatePlugin(schema, options) {
const defaultOptions = {
perPage: 10,
maxPerPage: 50
};
const opts = { ...defaultOptions, ...options };
// Add static method
schema.statics.paginate = async function(query, page = 1, perPage = opts.perPage) {
const skip = (page - 1) * perPage;
const limit = Math.min(perPage, opts.maxPerPage);
const [total, items] = await Promise.all([
this.countDocuments(query),
this.find(query).skip(skip).limit(limit)
]);
return {
total,
page,
perPage: limit,
totalPages: Math.ceil(total / limit),
items
};
};
}
How to Use Plugins
Using plugins is straightforward. Simply call the plugin()
method after defining the schema. You can pass options to customize the plugin's behavior.
const mongoose = require('mongoose');
const { Schema } = mongoose;
const userSchema = new Schema({
name: String,
email: String
});
// Apply plugins
userSchema.plugin(timestampPlugin);
userSchema.plugin(paginatePlugin, { maxPerPage: 30 });
const User = mongoose.model('User', userSchema);
// Use functionality added by plugins
async function getUsers(page) {
return await User.paginate({}, page);
}
Common Plugin Development Patterns
1. Audit Log Plugin
Records document creation and modification times, along with user information.
function auditLogPlugin(schema, options) {
const userModel = options.userModel || 'User';
schema.add({
createdBy: { type: Schema.Types.ObjectId, ref: userModel },
updatedBy: { type: Schema.Types.ObjectId, ref: userModel },
createdAt: Date,
updatedAt: Date
});
schema.pre('save', function(next) {
const now = new Date();
this.updatedAt = now;
if (this.isNew) {
this.createdAt = now;
// Assume context contains current user info
if (this.$context && this.$context.userId) {
this.createdBy = this.$context.userId;
}
}
if (this.isModified() && this.$context && this.$context.userId) {
this.updatedBy = this.$context.userId;
}
next();
});
}
2. Soft Delete Plugin
Implements soft deletion instead of physically removing documents from the database.
function softDeletePlugin(schema, options) {
const deletedAtField = options.deletedAtField || 'deletedAt';
const isDeletedField = options.isDeletedField || 'isDeleted';
schema.add({
[deletedAtField]: Date,
[isDeletedField]: { type: Boolean, default: false }
});
// Add delete method
schema.methods.softDelete = function() {
this[isDeletedField] = true;
this[deletedAtField] = new Date();
return this.save();
};
// Add restore method
schema.methods.restore = function() {
this[isDeletedField] = false;
this[deletedAtField] = undefined;
return this.save();
};
// Modify query behavior to automatically filter deleted documents
schema.pre(/^find/, function() {
if (!this.getFilter()[isDeletedField]) {
this.where({ [isDeletedField]: false });
}
});
}
Advanced Plugin Techniques
1. Plugin Composition
Multiple plugins can be combined, but be mindful of execution order and potential conflicts.
const productSchema = new Schema({
name: String,
price: Number
});
productSchema.plugin(timestampPlugin);
productSchema.plugin(auditLogPlugin, { userModel: 'Admin' });
productSchema.plugin(softDeletePlugin);
2. Conditional Plugin Application
Plugins can be applied based on environment or other conditions.
if (process.env.NODE_ENV === 'development') {
const debugPlugin = require('./debug-plugin');
userSchema.plugin(debugPlugin);
}
3. Plugin Dependency Management
When plugins have dependencies, ensure they are loaded in the correct order.
function dependentPlugin(schema, options) {
// Check if the base plugin is installed
if (!schema.methods.baseMethod) {
throw new Error('dependentPlugin requires basePlugin to be installed first');
}
// Plugin implementation...
}
Practical Use Cases
1. Multilingual Support Plugin
Adds multilingual field support to documents.
function multilingualPlugin(schema, options) {
const { fields = [], languages = ['en', 'zh'] } = options;
fields.forEach(field => {
const multilingualField = {};
languages.forEach(lang => {
multilingualField[lang] = schema.path(field).instance;
});
schema.add({ [field]: multilingualField });
schema.remove(field);
// Add method to get value in current language
schema.method(`get${field.charAt(0).toUpperCase() + field.slice(1)}`, function(lang) {
return this[field][lang] || this[field][languages[0]];
});
});
}
// Usage example
const productSchema = new Schema({
name: String,
description: String
});
productSchema.plugin(multilingualPlugin, {
fields: ['name', 'description'],
languages: ['en', 'zh', 'ja']
});
2. Version Control Plugin
Implements document versioning.
function versionControlPlugin(schema, options) {
const versionSchema = new Schema({
refId: Schema.Types.ObjectId,
data: {},
version: Number,
modifiedBy: Schema.Types.ObjectId,
modifiedAt: Date
}, { timestamps: true });
const VersionModel = mongoose.model(options.versionModel || 'Version', versionSchema);
schema.pre('save', async function(next) {
if (this.isModified()) {
const versionData = this.toObject();
delete versionData._id;
delete versionData.__v;
await VersionModel.create({
refId: this._id,
data: versionData,
version: (await VersionModel.countDocuments({ refId: this._id })) + 1,
modifiedBy: this.$context?.userId
});
}
next();
});
schema.methods.getVersions = function() {
return VersionModel.find({ refId: this._id }).sort('-version');
};
schema.statics.restoreVersion = async function(id, version) {
const doc = await this.findById(id);
const versionDoc = await VersionModel.findOne({ refId: id, version });
if (!versionDoc) throw new Error('Version not found');
doc.set(versionDoc.data);
return doc.save();
};
}
Performance Optimization Considerations
When developing plugins, consider performance impacts:
- Avoid Unnecessary Middleware: Only add
pre/post
hooks when needed. - Batch Operation Optimization: Consider scenarios like
updateMany
. - Index Management: Fields added by plugins may require indexing.
- Query Optimization: Avoid degrading query performance.
function optimizedPlugin(schema) {
// Only add indexes when necessary
if (process.env.NODE_ENV === 'production') {
schema.index({ updatedAt: -1 });
}
// Optimize bulk updates
schema.pre('updateMany', function(next) {
this.update({}, { $set: { updatedAt: new Date() } });
next();
});
}
Best Practices for Testing Plugins
Writing tests for plugins is crucial for ensuring reliability:
- Isolated Testing: Test plugin functionality separately.
- Multiple Scenarios: Test with different option configurations.
- Performance Testing: Ensure no performance issues are introduced.
- Compatibility Testing: Test compatibility with other commonly used plugins.
describe('timestampPlugin', () => {
let TestModel;
before(() => {
const schema = new Schema({ name: String });
schema.plugin(timestampPlugin);
TestModel = mongoose.model('Test', schema);
});
it('should add createdAt and updatedAt fields', async () => {
const doc = await TestModel.create({ name: 'test' });
expect(doc.createdAt).to.be.instanceOf(Date);
expect(doc.updatedAt).to.be.instanceOf(Date);
});
it('should update updatedAt on save', async () => {
const doc = await TestModel.create({ name: 'test' });
const originalUpdatedAt = doc.updatedAt;
await new Promise(resolve => setTimeout(resolve, 10));
doc.name = 'updated';
await doc.save();
expect(doc.updatedAt.getTime()).to.be.greaterThan(originalUpdatedAt.getTime());
});
});
Plugin Ecosystem
Mongoose has a rich plugin ecosystem. Some popular plugins include:
- mongoose-autopopulate: Auto-populates referenced fields.
- mongoose-paginate-v2: Pagination functionality.
- mongoose-unique-validator: Uniqueness validation.
- mongoose-lean-virtuals: Includes virtual fields in lean queries.
// Example of using a third-party plugin
const uniqueValidator = require('mongoose-unique-validator');
const userSchema = new Schema({
email: { type: String, required: true, unique: true }
});
userSchema.plugin(uniqueValidator, {
message: 'Error, expected {PATH} to be unique.'
});
Advanced Patterns for Custom Plugins
For more complex scenarios, consider these advanced patterns:
- Dynamic Field Plugin: Adds fields dynamically based on configuration.
- State Machine Plugin: Implements document state transitions.
- Permission Control Plugin: Role-based field-level access control.
- Caching Plugin: Automatically caches query results.
function stateMachinePlugin(schema, options) {
const { field = 'status', states, transitions } = options;
schema.add({ [field]: { type: String, default: states.initial } });
schema.methods.canTransitionTo = function(newState) {
return transitions[this[field]].includes(newState);
};
schema.methods.transitionTo = function(newState) {
if (!this.canTransitionTo(newState)) {
throw new Error(`Invalid transition from ${this[field]} to ${newState}`);
}
this[field] = newState;
return this.save();
};
// Add state-checking methods
states.valid.forEach(state => {
schema.methods[`is${state.charAt(0).toUpperCase() + state.slice(1)}`] = function() {
return this[field] === state;
};
});
}
// Usage example
const taskSchema = new Schema({
title: String
});
taskSchema.plugin(stateMachinePlugin, {
states: {
initial: 'new',
valid: ['new', 'inProgress', 'completed', 'archived']
},
transitions: {
new: ['inProgress'],
inProgress: ['completed', 'new'],
completed: ['archived'],
archived: []
}
});
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
下一篇:自定义类型与扩展