Table Of Contents
Quick Fix: Basic Custom Methods
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
name: String,
email: String,
password: String,
role: { type: String, default: 'user' }
});
// Instance method - called on document instances
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
// Static method - called on the Model
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
const User = mongoose.model('User', userSchema);
// Usage
const user = await User.findByEmail('john@example.com');
const isValid = await user.comparePassword('password123');
The Problem: Advanced Custom Method Patterns
// Comprehensive custom methods
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['user', 'admin', 'moderator'], default: 'user' },
profile: {
avatar: String,
bio: String,
socialLinks: {
twitter: String,
linkedin: String
}
},
preferences: {
emailNotifications: { type: Boolean, default: true },
theme: { type: String, enum: ['light', 'dark'], default: 'light' }
},
lastLogin: Date,
isActive: { type: Boolean, default: true },
loginAttempts: { type: Number, default: 0 },
lockUntil: Date,
createdAt: { type: Date, default: Date.now }
});
// Instance Methods
userSchema.methods.getPublicProfile = function() {
return {
id: this._id,
name: this.name,
avatar: this.profile?.avatar,
bio: this.profile?.bio,
joinDate: this.createdAt
};
};
userSchema.methods.updateLastLogin = async function() {
this.lastLogin = new Date();
this.loginAttempts = 0;
this.lockUntil = undefined;
return this.save();
};
userSchema.methods.incrementLoginAttempts = async function() {
const maxAttempts = 5;
const lockoutTime = 15 * 60 * 1000; // 15 minutes
this.loginAttempts += 1;
if (this.loginAttempts >= maxAttempts) {
this.lockUntil = new Date(Date.now() + lockoutTime);
}
return this.save();
};
userSchema.methods.isLocked = function() {
return this.lockUntil && this.lockUntil > Date.now();
};
userSchema.methods.hasPermission = function(permission) {
const permissions = {
user: ['read'],
moderator: ['read', 'moderate'],
admin: ['read', 'moderate', 'admin']
};
return permissions[this.role]?.includes(permission) || false;
};
userSchema.methods.generatePasswordResetToken = function() {
const crypto = require('crypto');
const token = crypto.randomBytes(32).toString('hex');
// Store hashed version
this.passwordResetToken = crypto.createHash('sha256').update(token).digest('hex');
this.passwordResetExpires = Date.now() + 10 * 60 * 1000; // 10 minutes
return token; // Return unhashed version
};
userSchema.methods.toSafeObject = function() {
const obj = this.toObject();
delete obj.password;
delete obj.passwordResetToken;
delete obj.loginAttempts;
delete obj.lockUntil;
return obj;
};
// Static Methods
userSchema.statics.findActiveUsers = function() {
return this.find({ isActive: true });
};
userSchema.statics.findByRole = function(role) {
return this.find({ role });
};
userSchema.statics.findByEmailDomain = function(domain) {
const regex = new RegExp(`@${domain}$`, 'i');
return this.find({ email: regex });
};
userSchema.statics.createWithPassword = async function(userData) {
const salt = await bcrypt.genSalt(12);
const hashedPassword = await bcrypt.hash(userData.password, salt);
return this.create({
...userData,
password: hashedPassword
});
};
userSchema.statics.authenticate = async function(email, password) {
const user = await this.findOne({ email: email.toLowerCase() });
if (!user || !user.isActive) {
throw new Error('Invalid credentials');
}
if (user.isLocked()) {
throw new Error('Account is temporarily locked');
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
await user.incrementLoginAttempts();
throw new Error('Invalid credentials');
}
await user.updateLastLogin();
return user;
};
userSchema.statics.getStatistics = async function() {
const stats = await this.aggregate([
{
$group: {
_id: null,
totalUsers: { $sum: 1 },
activeUsers: {
$sum: { $cond: [{ $eq: ['$isActive', true] }, 1, 0] }
},
usersByRole: {
$push: '$role'
}
}
},
{
$project: {
_id: 0,
totalUsers: 1,
activeUsers: 1,
roleDistribution: {
$reduce: {
input: '$usersByRole',
initialValue: {},
in: {
$mergeObjects: [
'$$value',
{
$arrayToObject: [[{
k: '$$this',
v: { $add: [{ $ifNull: [`$$value.$$this`, 0] }, 1] }
}]]
}
]
}
}
}
}
}
]);
return stats[0] || { totalUsers: 0, activeUsers: 0, roleDistribution: {} };
};
// Query Helpers - chainable query methods
userSchema.query.byRole = function(role) {
return this.where({ role });
};
userSchema.query.active = function() {
return this.where({ isActive: true });
};
userSchema.query.withRecentLogin = function(days = 30) {
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
return this.where({ lastLogin: { $gte: cutoff } });
};
userSchema.query.orderByName = function() {
return this.sort({ name: 1 });
};
// Usage of query helpers
// User.find().byRole('admin').active().withRecentLogin(7);
// Virtuals as computed properties
userSchema.virtual('fullName').get(function() {
return this.name;
});
userSchema.virtual('daysSinceJoined').get(function() {
return Math.floor((Date.now() - this.createdAt) / (1000 * 60 * 60 * 24));
});
userSchema.virtual('isNewUser').get(function() {
return this.daysSinceJoined <= 7;
});
// Advanced method with error handling
userSchema.methods.updateProfile = async function(profileData) {
try {
// Validate social links if provided
if (profileData.socialLinks) {
if (profileData.socialLinks.twitter && !profileData.socialLinks.twitter.startsWith('@')) {
throw new Error('Twitter handle must start with @');
}
if (profileData.socialLinks.linkedin && !profileData.socialLinks.linkedin.includes('linkedin.com')) {
throw new Error('Invalid LinkedIn URL');
}
}
// Update profile fields
Object.assign(this.profile, profileData);
return await this.save();
} catch (error) {
throw new Error(`Profile update failed: ${error.message}`);
}
};
// Method with external API integration
userSchema.methods.sendNotificationEmail = async function(subject, message) {
if (!this.preferences.emailNotifications) {
return false;
}
try {
// Integrate with email service
const emailService = require('../services/emailService');
await emailService.send({
to: this.email,
subject: subject,
body: message
});
return true;
} catch (error) {
console.error('Failed to send notification email:', error);
return false;
}
};
// Batch operation static method
userSchema.statics.updatePreferences = async function(userIds, preferences) {
return this.updateMany(
{ _id: { $in: userIds } },
{ $set: { preferences } }
);
};
// Method factory for creating dynamic methods
function createRoleChecker(role) {
return function() {
return this.role === role;
};
}
userSchema.methods.isAdmin = createRoleChecker('admin');
userSchema.methods.isModerator = createRoleChecker('moderator');
userSchema.methods.isRegularUser = createRoleChecker('user');
// Async static method with complex logic
userSchema.statics.findSimilarUsers = async function(userId, limit = 5) {
const user = await this.findById(userId);
if (!user) throw new Error('User not found');
// Find users with similar interests, same role, etc.
const similarUsers = await this.find({
_id: { $ne: userId },
role: user.role,
isActive: true
})
.limit(limit)
.select('name email profile.avatar');
return similarUsers;
};
// Method with caching
const cache = new Map();
userSchema.statics.getCachedStatistics = async function(ttl = 300000) { // 5 minutes
const cacheKey = 'user_statistics';
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const stats = await this.getStatistics();
cache.set(cacheKey, {
data: stats,
timestamp: Date.now()
});
return stats;
};
// Method chaining helper
userSchema.methods.chain = function() {
const self = this;
return {
updateLastLogin: () => {
self.lastLogin = new Date();
return this;
},
activate: () => {
self.isActive = true;
return this;
},
setRole: (role) => {
self.role = role;
return this;
},
save: () => self.save()
};
};
// Usage: await user.chain().updateLastLogin().activate().save();
const User = mongoose.model('User', userSchema);
// Post schema with custom methods
const postSchema = new mongoose.Schema({
title: String,
content: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
published: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now }
});
postSchema.methods.like = async function(userId) {
if (!this.likes.includes(userId)) {
this.likes.push(userId);
return this.save();
}
return this;
};
postSchema.methods.unlike = async function(userId) {
this.likes = this.likes.filter(id => !id.equals(userId));
return this.save();
};
postSchema.methods.isLikedBy = function(userId) {
return this.likes.some(id => id.equals(userId));
};
postSchema.statics.findPublished = function() {
return this.find({ published: true });
};
postSchema.query.byAuthor = function(authorId) {
return this.where({ author: authorId });
};
const Post = mongoose.model('Post', postSchema);
Custom Mongoose methods solve "code reusability", "business logic organization", and "query abstraction" issues. Instance methods operate on documents, static methods on models, query helpers chain filters. Keep methods focused and testable. Alternative: service layer patterns, repository patterns, GraphQL resolvers.
Share this article
Add Comment
No comments yet. Be the first to comment!