add brain
This commit is contained in:
572
.brain/.agent/skills/database-optimization/mongodb/SKILL.md
Normal file
572
.brain/.agent/skills/database-optimization/mongodb/SKILL.md
Normal file
@@ -0,0 +1,572 @@
|
||||
---
|
||||
name: mongodb
|
||||
description: Work with MongoDB databases using best practices. Use when designing schemas, writing queries, building aggregation pipelines, or optimizing performance. Triggers on MongoDB, Mongoose, NoSQL, aggregation pipeline, document database, MongoDB Atlas.
|
||||
---
|
||||
|
||||
# MongoDB & Mongoose
|
||||
|
||||
Build and query MongoDB databases with best practices.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install mongodb mongoose
|
||||
```
|
||||
|
||||
### Native Driver
|
||||
```typescript
|
||||
import { MongoClient, ObjectId } from 'mongodb';
|
||||
|
||||
const client = new MongoClient(process.env.MONGODB_URI!);
|
||||
const db = client.db('myapp');
|
||||
const users = db.collection('users');
|
||||
|
||||
// Connect
|
||||
await client.connect();
|
||||
|
||||
// CRUD Operations
|
||||
await users.insertOne({ name: 'Alice', email: 'alice@example.com' });
|
||||
const user = await users.findOne({ email: 'alice@example.com' });
|
||||
await users.updateOne({ _id: user._id }, { $set: { name: 'Alice Smith' } });
|
||||
await users.deleteOne({ _id: user._id });
|
||||
```
|
||||
|
||||
### Mongoose Setup
|
||||
```typescript
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
await mongoose.connect(process.env.MONGODB_URI!, {
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
});
|
||||
|
||||
// Connection events
|
||||
mongoose.connection.on('connected', () => console.log('MongoDB connected'));
|
||||
mongoose.connection.on('error', (err) => console.error('MongoDB error:', err));
|
||||
mongoose.connection.on('disconnected', () => console.log('MongoDB disconnected'));
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
await mongoose.connection.close();
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
## Schema Design
|
||||
|
||||
### Basic Schema
|
||||
```typescript
|
||||
import mongoose, { Schema, Document, Model } from 'mongoose';
|
||||
|
||||
interface IUser extends Document {
|
||||
email: string;
|
||||
name: string;
|
||||
password: string;
|
||||
role: 'user' | 'admin';
|
||||
profile: {
|
||||
avatar?: string;
|
||||
bio?: string;
|
||||
};
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>({
|
||||
email: {
|
||||
type: String,
|
||||
required: [true, 'Email is required'],
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
match: [/^\S+@\S+\.\S+$/, 'Invalid email format'],
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
minlength: 2,
|
||||
maxlength: 100,
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false, // Never return password by default
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ['user', 'admin'],
|
||||
default: 'user',
|
||||
},
|
||||
profile: {
|
||||
avatar: String,
|
||||
bio: { type: String, maxlength: 500 },
|
||||
},
|
||||
}, {
|
||||
timestamps: true, // Adds createdAt, updatedAt
|
||||
toJSON: {
|
||||
transform(doc, ret) {
|
||||
delete ret.password;
|
||||
delete ret.__v;
|
||||
return ret;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Indexes
|
||||
userSchema.index({ email: 1 });
|
||||
userSchema.index({ createdAt: -1 });
|
||||
userSchema.index({ name: 'text', 'profile.bio': 'text' }); // Text search
|
||||
|
||||
const User: Model<IUser> = mongoose.model('User', userSchema);
|
||||
```
|
||||
|
||||
### Embedded Documents vs References
|
||||
|
||||
```typescript
|
||||
// ✅ Embed when: Data is read together, doesn't grow unbounded
|
||||
const orderSchema = new Schema({
|
||||
customer: {
|
||||
name: String,
|
||||
email: String,
|
||||
address: {
|
||||
street: String,
|
||||
city: String,
|
||||
country: String,
|
||||
},
|
||||
},
|
||||
items: [{
|
||||
product: String,
|
||||
quantity: Number,
|
||||
price: Number,
|
||||
}],
|
||||
total: Number,
|
||||
});
|
||||
|
||||
// ✅ Reference when: Data is large, shared, or changes independently
|
||||
const postSchema = new Schema({
|
||||
title: String,
|
||||
content: String,
|
||||
author: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
comments: [{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Comment',
|
||||
}],
|
||||
});
|
||||
|
||||
// Populate references
|
||||
const post = await Post.findById(id)
|
||||
.populate('author', 'name email') // Select specific fields
|
||||
.populate({
|
||||
path: 'comments',
|
||||
populate: { path: 'author', select: 'name' }, // Nested populate
|
||||
});
|
||||
```
|
||||
|
||||
### Virtuals
|
||||
```typescript
|
||||
const userSchema = new Schema({
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
});
|
||||
|
||||
// Virtual field (not stored in DB)
|
||||
userSchema.virtual('fullName').get(function() {
|
||||
return `${this.firstName} ${this.lastName}`;
|
||||
});
|
||||
|
||||
// Virtual populate (for reverse references)
|
||||
userSchema.virtual('posts', {
|
||||
ref: 'Post',
|
||||
localField: '_id',
|
||||
foreignField: 'author',
|
||||
});
|
||||
|
||||
// Enable virtuals in JSON
|
||||
userSchema.set('toJSON', { virtuals: true });
|
||||
userSchema.set('toObject', { virtuals: true });
|
||||
```
|
||||
|
||||
## Query Operations
|
||||
|
||||
### Find Operations
|
||||
```typescript
|
||||
// Find with filters
|
||||
const users = await User.find({
|
||||
role: 'user',
|
||||
createdAt: { $gte: new Date('2024-01-01') },
|
||||
});
|
||||
|
||||
// Query builder
|
||||
const results = await User.find()
|
||||
.where('role').equals('user')
|
||||
.where('createdAt').gte(new Date('2024-01-01'))
|
||||
.select('name email')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(10)
|
||||
.skip(20)
|
||||
.lean(); // Return plain objects (faster)
|
||||
|
||||
// Find one
|
||||
const user = await User.findOne({ email: 'alice@example.com' });
|
||||
const userById = await User.findById(id);
|
||||
|
||||
// Exists check
|
||||
const exists = await User.exists({ email: 'alice@example.com' });
|
||||
|
||||
// Count
|
||||
const count = await User.countDocuments({ role: 'admin' });
|
||||
```
|
||||
|
||||
### Query Operators
|
||||
```typescript
|
||||
// Comparison
|
||||
await User.find({ age: { $eq: 25 } }); // Equal
|
||||
await User.find({ age: { $ne: 25 } }); // Not equal
|
||||
await User.find({ age: { $gt: 25 } }); // Greater than
|
||||
await User.find({ age: { $gte: 25 } }); // Greater or equal
|
||||
await User.find({ age: { $lt: 25 } }); // Less than
|
||||
await User.find({ age: { $lte: 25 } }); // Less or equal
|
||||
await User.find({ age: { $in: [20, 25, 30] } }); // In array
|
||||
await User.find({ age: { $nin: [20, 25] } }); // Not in array
|
||||
|
||||
// Logical
|
||||
await User.find({
|
||||
$and: [{ age: { $gte: 18 } }, { role: 'user' }],
|
||||
});
|
||||
await User.find({
|
||||
$or: [{ role: 'admin' }, { isVerified: true }],
|
||||
});
|
||||
await User.find({ age: { $not: { $lt: 18 } } });
|
||||
|
||||
// Element
|
||||
await User.find({ avatar: { $exists: true } });
|
||||
await User.find({ score: { $type: 'number' } });
|
||||
|
||||
// Array
|
||||
await User.find({ tags: 'nodejs' }); // Array contains value
|
||||
await User.find({ tags: { $all: ['nodejs', 'mongodb'] } }); // Contains all
|
||||
await User.find({ tags: { $size: 3 } }); // Array length
|
||||
await User.find({ 'items.0.price': { $gt: 100 } }); // Array index
|
||||
|
||||
// Text search
|
||||
await User.find({ $text: { $search: 'mongodb developer' } });
|
||||
|
||||
// Regex
|
||||
await User.find({ name: { $regex: /^john/i } });
|
||||
```
|
||||
|
||||
### Update Operations
|
||||
```typescript
|
||||
// Update one
|
||||
await User.updateOne(
|
||||
{ _id: userId },
|
||||
{ $set: { name: 'New Name' } }
|
||||
);
|
||||
|
||||
// Update many
|
||||
await User.updateMany(
|
||||
{ role: 'user' },
|
||||
{ $set: { isVerified: true } }
|
||||
);
|
||||
|
||||
// Find and update (returns document)
|
||||
const updated = await User.findByIdAndUpdate(
|
||||
userId,
|
||||
{ $set: { name: 'New Name' } },
|
||||
{ new: true, runValidators: true } // Return updated doc, run validators
|
||||
);
|
||||
|
||||
// Update operators
|
||||
await User.updateOne({ _id: userId }, {
|
||||
$set: { name: 'New Name' }, // Set field
|
||||
$unset: { tempField: '' }, // Remove field
|
||||
$inc: { loginCount: 1 }, // Increment
|
||||
$mul: { score: 1.5 }, // Multiply
|
||||
$min: { lowScore: 50 }, // Set if less than
|
||||
$max: { highScore: 100 }, // Set if greater than
|
||||
$push: { tags: 'new-tag' }, // Add to array
|
||||
$pull: { tags: 'old-tag' }, // Remove from array
|
||||
$addToSet: { tags: 'unique-tag' }, // Add if not exists
|
||||
});
|
||||
|
||||
// Upsert (insert if not exists)
|
||||
await User.updateOne(
|
||||
{ email: 'new@example.com' },
|
||||
{ $set: { name: 'New User' } },
|
||||
{ upsert: true }
|
||||
);
|
||||
```
|
||||
|
||||
## Aggregation Pipeline
|
||||
|
||||
### Basic Aggregation
|
||||
```typescript
|
||||
const results = await Order.aggregate([
|
||||
// Stage 1: Match
|
||||
{ $match: { status: 'completed' } },
|
||||
|
||||
// Stage 2: Group
|
||||
{ $group: {
|
||||
_id: '$customerId',
|
||||
totalOrders: { $sum: 1 },
|
||||
totalSpent: { $sum: '$total' },
|
||||
avgOrder: { $avg: '$total' },
|
||||
}},
|
||||
|
||||
// Stage 3: Sort
|
||||
{ $sort: { totalSpent: -1 } },
|
||||
|
||||
// Stage 4: Limit
|
||||
{ $limit: 10 },
|
||||
]);
|
||||
```
|
||||
|
||||
### Pipeline Stages
|
||||
```typescript
|
||||
const pipeline = [
|
||||
// $match - Filter documents
|
||||
{ $match: { createdAt: { $gte: new Date('2024-01-01') } } },
|
||||
|
||||
// $project - Shape output
|
||||
{ $project: {
|
||||
name: 1,
|
||||
email: 1,
|
||||
yearJoined: { $year: '$createdAt' },
|
||||
fullName: { $concat: ['$firstName', ' ', '$lastName'] },
|
||||
}},
|
||||
|
||||
// $lookup - Join collections
|
||||
{ $lookup: {
|
||||
from: 'orders',
|
||||
localField: '_id',
|
||||
foreignField: 'userId',
|
||||
as: 'orders',
|
||||
}},
|
||||
|
||||
// $unwind - Flatten arrays
|
||||
{ $unwind: { path: '$orders', preserveNullAndEmptyArrays: true } },
|
||||
|
||||
// $group - Aggregate
|
||||
{ $group: {
|
||||
_id: '$_id',
|
||||
name: { $first: '$name' },
|
||||
orderCount: { $sum: 1 },
|
||||
orders: { $push: '$orders' },
|
||||
}},
|
||||
|
||||
// $addFields - Add computed fields
|
||||
{ $addFields: {
|
||||
hasOrders: { $gt: ['$orderCount', 0] },
|
||||
}},
|
||||
|
||||
// $facet - Multiple pipelines
|
||||
{ $facet: {
|
||||
topCustomers: [{ $sort: { orderCount: -1 } }, { $limit: 5 }],
|
||||
stats: [{ $group: { _id: null, avgOrders: { $avg: '$orderCount' } } }],
|
||||
}},
|
||||
];
|
||||
```
|
||||
|
||||
### Analytics Examples
|
||||
```typescript
|
||||
// Sales by month
|
||||
const salesByMonth = await Order.aggregate([
|
||||
{ $match: { status: 'completed' } },
|
||||
{ $group: {
|
||||
_id: {
|
||||
year: { $year: '$createdAt' },
|
||||
month: { $month: '$createdAt' },
|
||||
},
|
||||
totalSales: { $sum: '$total' },
|
||||
orderCount: { $sum: 1 },
|
||||
}},
|
||||
{ $sort: { '_id.year': -1, '_id.month': -1 } },
|
||||
]);
|
||||
|
||||
// Top products
|
||||
const topProducts = await Order.aggregate([
|
||||
{ $unwind: '$items' },
|
||||
{ $group: {
|
||||
_id: '$items.productId',
|
||||
totalQuantity: { $sum: '$items.quantity' },
|
||||
totalRevenue: { $sum: { $multiply: ['$items.price', '$items.quantity'] } },
|
||||
}},
|
||||
{ $lookup: {
|
||||
from: 'products',
|
||||
localField: '_id',
|
||||
foreignField: '_id',
|
||||
as: 'product',
|
||||
}},
|
||||
{ $unwind: '$product' },
|
||||
{ $project: {
|
||||
name: '$product.name',
|
||||
totalQuantity: 1,
|
||||
totalRevenue: 1,
|
||||
}},
|
||||
{ $sort: { totalRevenue: -1 } },
|
||||
{ $limit: 10 },
|
||||
]);
|
||||
```
|
||||
|
||||
## Middleware (Hooks)
|
||||
|
||||
```typescript
|
||||
// Pre-save middleware
|
||||
userSchema.pre('save', async function(next) {
|
||||
if (this.isModified('password')) {
|
||||
this.password = await bcrypt.hash(this.password, 12);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Post-save middleware
|
||||
userSchema.post('save', function(doc) {
|
||||
console.log('User saved:', doc._id);
|
||||
});
|
||||
|
||||
// Pre-find middleware
|
||||
userSchema.pre(/^find/, function(next) {
|
||||
// Exclude deleted users by default
|
||||
this.find({ isDeleted: { $ne: true } });
|
||||
next();
|
||||
});
|
||||
|
||||
// Pre-aggregate middleware
|
||||
userSchema.pre('aggregate', function(next) {
|
||||
// Add match stage to all aggregations
|
||||
this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } });
|
||||
next();
|
||||
});
|
||||
```
|
||||
|
||||
## Transactions
|
||||
|
||||
```typescript
|
||||
const session = await mongoose.startSession();
|
||||
|
||||
try {
|
||||
session.startTransaction();
|
||||
|
||||
// All operations in the transaction
|
||||
const user = await User.create([{ name: 'Alice' }], { session });
|
||||
await Account.create([{ userId: user[0]._id, balance: 0 }], { session });
|
||||
await Order.updateOne({ _id: orderId }, { $set: { status: 'paid' } }, { session });
|
||||
|
||||
await session.commitTransaction();
|
||||
} catch (error) {
|
||||
await session.abortTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
session.endSession();
|
||||
}
|
||||
|
||||
// With callback
|
||||
await mongoose.connection.transaction(async (session) => {
|
||||
await User.create([{ name: 'Alice' }], { session });
|
||||
await Account.create([{ userId: user._id }], { session });
|
||||
});
|
||||
```
|
||||
|
||||
## Indexing
|
||||
|
||||
```typescript
|
||||
// Single field index
|
||||
userSchema.index({ email: 1 });
|
||||
|
||||
// Compound index
|
||||
userSchema.index({ role: 1, createdAt: -1 });
|
||||
|
||||
// Unique index
|
||||
userSchema.index({ email: 1 }, { unique: true });
|
||||
|
||||
// Partial index
|
||||
userSchema.index(
|
||||
{ email: 1 },
|
||||
{ partialFilterExpression: { isActive: true } }
|
||||
);
|
||||
|
||||
// TTL index (auto-delete after time)
|
||||
sessionSchema.index({ createdAt: 1 }, { expireAfterSeconds: 3600 });
|
||||
|
||||
// Text index for search
|
||||
postSchema.index({ title: 'text', content: 'text' });
|
||||
|
||||
// Geospatial index
|
||||
locationSchema.index({ coordinates: '2dsphere' });
|
||||
|
||||
// Check indexes
|
||||
const indexes = await User.collection.getIndexes();
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
```typescript
|
||||
// Use lean() for read-only queries
|
||||
const users = await User.find().lean();
|
||||
|
||||
// Select only needed fields
|
||||
const users = await User.find().select('name email');
|
||||
|
||||
// Use cursor for large datasets
|
||||
const cursor = User.find().cursor();
|
||||
for await (const user of cursor) {
|
||||
// Process one at a time
|
||||
}
|
||||
|
||||
// Bulk operations
|
||||
const bulkOps = [
|
||||
{ insertOne: { document: { name: 'User 1' } } },
|
||||
{ updateOne: { filter: { _id: id1 }, update: { $set: { name: 'Updated' } } } },
|
||||
{ deleteOne: { filter: { _id: id2 } } },
|
||||
];
|
||||
await User.bulkWrite(bulkOps);
|
||||
|
||||
// Explain query
|
||||
const explanation = await User.find({ role: 'admin' }).explain('executionStats');
|
||||
```
|
||||
|
||||
## MongoDB Atlas
|
||||
|
||||
```typescript
|
||||
// Atlas connection string
|
||||
const uri = 'mongodb+srv://user:password@cluster.mongodb.net/dbname?retryWrites=true&w=majority';
|
||||
|
||||
// Atlas Search (full-text search)
|
||||
const results = await Product.aggregate([
|
||||
{ $search: {
|
||||
index: 'default',
|
||||
text: {
|
||||
query: 'wireless headphones',
|
||||
path: ['name', 'description'],
|
||||
fuzzy: { maxEdits: 1 },
|
||||
},
|
||||
}},
|
||||
{ $project: {
|
||||
name: 1,
|
||||
score: { $meta: 'searchScore' },
|
||||
}},
|
||||
]);
|
||||
|
||||
// Atlas Vector Search
|
||||
const results = await Product.aggregate([
|
||||
{ $vectorSearch: {
|
||||
index: 'vector_index',
|
||||
path: 'embedding',
|
||||
queryVector: [0.1, 0.2, ...],
|
||||
numCandidates: 100,
|
||||
limit: 10,
|
||||
}},
|
||||
]);
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- **MongoDB Docs**: https://www.mongodb.com/docs/
|
||||
- **Mongoose Docs**: https://mongoosejs.com/docs/
|
||||
- **MongoDB University**: https://learn.mongodb.com/
|
||||
- **Atlas Docs**: https://www.mongodb.com/docs/atlas/
|
||||
Reference in New Issue
Block a user