start-vibing 2.0.11 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -177
- package/dist/cli.js +19 -2
- package/package.json +42 -42
- package/template/.claude/CLAUDE.md +174 -174
- package/template/.claude/agents/01-orchestration/agent-selector.md +130 -130
- package/template/.claude/agents/01-orchestration/checkpoint-manager.md +142 -142
- package/template/.claude/agents/01-orchestration/context-manager.md +138 -138
- package/template/.claude/agents/01-orchestration/error-recovery.md +182 -182
- package/template/.claude/agents/01-orchestration/orchestrator.md +114 -114
- package/template/.claude/agents/01-orchestration/parallel-coordinator.md +141 -141
- package/template/.claude/agents/01-orchestration/task-decomposer.md +121 -121
- package/template/.claude/agents/01-orchestration/workflow-router.md +114 -114
- package/template/.claude/agents/02-typescript/bun-runtime-expert.md +197 -197
- package/template/.claude/agents/02-typescript/esm-resolver.md +193 -193
- package/template/.claude/agents/02-typescript/import-alias-enforcer.md +158 -158
- package/template/.claude/agents/02-typescript/ts-generics-helper.md +183 -183
- package/template/.claude/agents/02-typescript/ts-migration-helper.md +238 -238
- package/template/.claude/agents/02-typescript/ts-strict-checker.md +180 -180
- package/template/.claude/agents/02-typescript/ts-types-analyzer.md +199 -199
- package/template/.claude/agents/02-typescript/type-definition-writer.md +187 -187
- package/template/.claude/agents/02-typescript/zod-schema-designer.md +212 -212
- package/template/.claude/agents/02-typescript/zod-validator.md +158 -158
- package/template/.claude/agents/03-testing/playwright-assertions.md +265 -265
- package/template/.claude/agents/03-testing/playwright-e2e.md +247 -247
- package/template/.claude/agents/03-testing/playwright-fixtures.md +234 -234
- package/template/.claude/agents/03-testing/playwright-multi-viewport.md +256 -256
- package/template/.claude/agents/03-testing/playwright-page-objects.md +247 -247
- package/template/.claude/agents/03-testing/test-cleanup-manager.md +248 -248
- package/template/.claude/agents/03-testing/test-data-generator.md +254 -254
- package/template/.claude/agents/03-testing/tester-integration.md +278 -278
- package/template/.claude/agents/03-testing/tester-unit.md +207 -207
- package/template/.claude/agents/03-testing/vitest-config.md +287 -287
- package/template/.claude/agents/04-docker/container-health.md +255 -255
- package/template/.claude/agents/04-docker/deployment-validator.md +225 -225
- package/template/.claude/agents/04-docker/docker-compose-designer.md +281 -281
- package/template/.claude/agents/04-docker/docker-env-manager.md +235 -235
- package/template/.claude/agents/04-docker/docker-multi-stage.md +241 -241
- package/template/.claude/agents/04-docker/dockerfile-optimizer.md +208 -208
- package/template/.claude/agents/05-database/database-seeder.md +273 -273
- package/template/.claude/agents/05-database/mongodb-query-optimizer.md +230 -230
- package/template/.claude/agents/05-database/mongoose-aggregation.md +306 -306
- package/template/.claude/agents/05-database/mongoose-index-optimizer.md +182 -182
- package/template/.claude/agents/05-database/mongoose-schema-designer.md +267 -267
- package/template/.claude/agents/06-security/auth-session-validator.md +68 -68
- package/template/.claude/agents/06-security/input-sanitizer.md +80 -80
- package/template/.claude/agents/06-security/owasp-checker.md +97 -97
- package/template/.claude/agents/06-security/permission-auditor.md +100 -100
- package/template/.claude/agents/06-security/security-auditor.md +84 -84
- package/template/.claude/agents/06-security/sensitive-data-scanner.md +83 -83
- package/template/.claude/agents/07-documentation/api-documenter.md +136 -136
- package/template/.claude/agents/07-documentation/changelog-manager.md +105 -105
- package/template/.claude/agents/07-documentation/documenter.md +76 -76
- package/template/.claude/agents/07-documentation/domain-updater.md +81 -81
- package/template/.claude/agents/07-documentation/jsdoc-generator.md +114 -114
- package/template/.claude/agents/07-documentation/readme-generator.md +135 -135
- package/template/.claude/agents/08-git/branch-manager.md +58 -58
- package/template/.claude/agents/08-git/commit-manager.md +63 -63
- package/template/.claude/agents/08-git/pr-creator.md +76 -76
- package/template/.claude/agents/09-quality/code-reviewer.md +71 -71
- package/template/.claude/agents/09-quality/quality-checker.md +67 -67
- package/template/.claude/agents/10-research/best-practices-finder.md +89 -89
- package/template/.claude/agents/10-research/competitor-analyzer.md +106 -106
- package/template/.claude/agents/10-research/pattern-researcher.md +93 -93
- package/template/.claude/agents/10-research/research-cache-manager.md +76 -76
- package/template/.claude/agents/10-research/research-web.md +98 -98
- package/template/.claude/agents/10-research/tech-evaluator.md +101 -101
- package/template/.claude/agents/11-ui-ux/accessibility-auditor.md +136 -136
- package/template/.claude/agents/11-ui-ux/design-system-enforcer.md +125 -125
- package/template/.claude/agents/11-ui-ux/skeleton-generator.md +118 -118
- package/template/.claude/agents/11-ui-ux/ui-desktop.md +132 -132
- package/template/.claude/agents/11-ui-ux/ui-mobile.md +98 -98
- package/template/.claude/agents/11-ui-ux/ui-tablet.md +110 -110
- package/template/.claude/agents/12-performance/api-latency-analyzer.md +156 -156
- package/template/.claude/agents/12-performance/bundle-analyzer.md +113 -113
- package/template/.claude/agents/12-performance/memory-leak-detector.md +137 -137
- package/template/.claude/agents/12-performance/performance-profiler.md +115 -115
- package/template/.claude/agents/12-performance/query-optimizer.md +124 -124
- package/template/.claude/agents/12-performance/render-optimizer.md +154 -154
- package/template/.claude/agents/13-debugging/build-error-fixer.md +207 -207
- package/template/.claude/agents/13-debugging/debugger.md +149 -149
- package/template/.claude/agents/13-debugging/error-stack-analyzer.md +141 -141
- package/template/.claude/agents/13-debugging/network-debugger.md +208 -208
- package/template/.claude/agents/13-debugging/runtime-error-fixer.md +181 -181
- package/template/.claude/agents/13-debugging/type-error-resolver.md +185 -185
- package/template/.claude/agents/14-validation/final-validator.md +93 -93
- package/template/.claude/agents/_backup/analyzer.md +134 -134
- package/template/.claude/agents/_backup/code-reviewer.md +279 -279
- package/template/.claude/agents/_backup/commit-manager.md +219 -219
- package/template/.claude/agents/_backup/debugger.md +280 -280
- package/template/.claude/agents/_backup/documenter.md +237 -237
- package/template/.claude/agents/_backup/domain-updater.md +197 -197
- package/template/.claude/agents/_backup/final-validator.md +169 -169
- package/template/.claude/agents/_backup/orchestrator.md +149 -149
- package/template/.claude/agents/_backup/performance.md +232 -232
- package/template/.claude/agents/_backup/quality-checker.md +240 -240
- package/template/.claude/agents/_backup/research.md +315 -315
- package/template/.claude/agents/_backup/security-auditor.md +192 -192
- package/template/.claude/agents/_backup/tester.md +566 -566
- package/template/.claude/agents/_backup/ui-ux-reviewer.md +247 -247
- package/template/.claude/config/README.md +30 -30
- package/template/.claude/config/mcp-config.json +344 -344
- package/template/.claude/config/project-config.json +53 -53
- package/template/.claude/config/quality-gates.json +46 -46
- package/template/.claude/config/security-rules.json +45 -45
- package/template/.claude/config/testing-config.json +164 -164
- package/template/.claude/hooks/SETUP.md +126 -126
- package/template/.claude/hooks/run-hook.ts +176 -176
- package/template/.claude/hooks/stop-validator.ts +914 -824
- package/template/.claude/hooks/user-prompt-submit.ts +886 -886
- package/template/.claude/scripts/mcp-quick-install.ts +151 -151
- package/template/.claude/scripts/setup-mcps.ts +651 -651
- package/template/.claude/settings.json +275 -275
- package/template/.claude/skills/bun-runtime/SKILL.md +430 -430
- package/template/.claude/skills/codebase-knowledge/domains/claude-system.md +431 -431
- package/template/.claude/skills/codebase-knowledge/domains/mcp-integration.md +295 -295
- package/template/.claude/skills/debugging-patterns/SKILL.md +485 -485
- package/template/.claude/skills/docker-patterns/SKILL.md +555 -555
- package/template/.claude/skills/git-workflow/SKILL.md +454 -454
- package/template/.claude/skills/mongoose-patterns/SKILL.md +499 -499
- package/template/.claude/skills/nextjs-app-router/SKILL.md +327 -327
- package/template/.claude/skills/performance-patterns/SKILL.md +547 -547
- package/template/.claude/skills/playwright-automation/SKILL.md +438 -438
- package/template/.claude/skills/react-patterns/SKILL.md +389 -389
- package/template/.claude/skills/research-cache/SKILL.md +222 -222
- package/template/.claude/skills/shadcn-ui/SKILL.md +511 -511
- package/template/.claude/skills/tailwind-patterns/SKILL.md +465 -465
- package/template/.claude/skills/test-coverage/SKILL.md +467 -467
- package/template/.claude/skills/trpc-api/SKILL.md +434 -434
- package/template/.claude/skills/typescript-strict/SKILL.md +367 -367
- package/template/.claude/skills/zod-validation/SKILL.md +403 -403
- package/template/CLAUDE.md +117 -117
|
@@ -1,499 +1,499 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: mongoose-patterns
|
|
3
|
-
description: Mongoose/MongoDB patterns for schema design, queries, indexes, aggregations. Use when working with MongoDB through Mongoose.
|
|
4
|
-
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Mongoose Patterns - MongoDB ODM Best Practices
|
|
8
|
-
|
|
9
|
-
## Purpose
|
|
10
|
-
|
|
11
|
-
Expert guidance for Mongoose/MongoDB:
|
|
12
|
-
|
|
13
|
-
- **Schema Design** - Proper typing and validation
|
|
14
|
-
- **Queries** - Efficient query patterns
|
|
15
|
-
- **Indexes** - Performance optimization
|
|
16
|
-
- **Aggregations** - Complex data operations
|
|
17
|
-
- **Relationships** - References vs embedding
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## Project Structure
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
server/
|
|
25
|
-
├── db/
|
|
26
|
-
│ ├── connection.ts # MongoDB connection
|
|
27
|
-
│ └── index.ts # Export db utilities
|
|
28
|
-
├── models/
|
|
29
|
-
│ ├── user.model.ts
|
|
30
|
-
│ ├── post.model.ts
|
|
31
|
-
│ └── index.ts # Export all models
|
|
32
|
-
types/
|
|
33
|
-
└── models/
|
|
34
|
-
├── user.types.ts # User interfaces
|
|
35
|
-
└── post.types.ts # Post interfaces
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## Schema Design
|
|
41
|
-
|
|
42
|
-
### Basic Schema with TypeScript
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
// types/models/user.types.ts
|
|
46
|
-
import { Types, Document } from 'mongoose';
|
|
47
|
-
|
|
48
|
-
export interface IUser {
|
|
49
|
-
email: string;
|
|
50
|
-
name: string;
|
|
51
|
-
passwordHash: string;
|
|
52
|
-
role: 'admin' | 'user' | 'guest';
|
|
53
|
-
profile?: {
|
|
54
|
-
bio?: string;
|
|
55
|
-
avatar?: string;
|
|
56
|
-
};
|
|
57
|
-
createdAt: Date;
|
|
58
|
-
updatedAt: Date;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface IUserDocument extends IUser, Document {
|
|
62
|
-
_id: Types.ObjectId;
|
|
63
|
-
comparePassword(password: string): Promise<boolean>;
|
|
64
|
-
toPublic(): Omit<IUser, 'passwordHash'>;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// server/models/user.model.ts
|
|
68
|
-
import mongoose, { Schema, Model } from 'mongoose';
|
|
69
|
-
import { IUser, IUserDocument } from '$types/models/user.types';
|
|
70
|
-
|
|
71
|
-
const userSchema = new Schema<IUserDocument>(
|
|
72
|
-
{
|
|
73
|
-
email: {
|
|
74
|
-
type: String,
|
|
75
|
-
required: [true, 'Email is required'],
|
|
76
|
-
unique: true,
|
|
77
|
-
lowercase: true,
|
|
78
|
-
trim: true,
|
|
79
|
-
validate: {
|
|
80
|
-
validator: (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
|
|
81
|
-
message: 'Invalid email format',
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
name: {
|
|
85
|
-
type: String,
|
|
86
|
-
required: [true, 'Name is required'],
|
|
87
|
-
minlength: [2, 'Name must be at least 2 characters'],
|
|
88
|
-
maxlength: [100, 'Name cannot exceed 100 characters'],
|
|
89
|
-
trim: true,
|
|
90
|
-
},
|
|
91
|
-
passwordHash: {
|
|
92
|
-
type: String,
|
|
93
|
-
required: true,
|
|
94
|
-
select: false, // Never include by default
|
|
95
|
-
},
|
|
96
|
-
role: {
|
|
97
|
-
type: String,
|
|
98
|
-
enum: ['admin', 'user', 'guest'],
|
|
99
|
-
default: 'user',
|
|
100
|
-
},
|
|
101
|
-
profile: {
|
|
102
|
-
bio: { type: String, maxlength: 500 },
|
|
103
|
-
avatar: { type: String },
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
timestamps: true,
|
|
108
|
-
toJSON: { virtuals: true },
|
|
109
|
-
toObject: { virtuals: true },
|
|
110
|
-
}
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
// Indexes
|
|
114
|
-
userSchema.index({ email: 1 }, { unique: true });
|
|
115
|
-
userSchema.index({ role: 1 });
|
|
116
|
-
userSchema.index({ createdAt: -1 });
|
|
117
|
-
|
|
118
|
-
// Instance methods
|
|
119
|
-
userSchema.methods.comparePassword = async function (password: string): Promise<boolean> {
|
|
120
|
-
return Bun.password.verify(password, this.passwordHash);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
userSchema.methods.toPublic = function () {
|
|
124
|
-
const obj = this.toObject();
|
|
125
|
-
delete obj.passwordHash;
|
|
126
|
-
return obj;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// Static methods
|
|
130
|
-
userSchema.statics.findByEmail = function (email: string) {
|
|
131
|
-
return this.findOne({ email: email.toLowerCase() });
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
export const UserModel: Model<IUserDocument> = mongoose.model('User', userSchema);
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
## Connection Management
|
|
140
|
-
|
|
141
|
-
```typescript
|
|
142
|
-
// server/db/connection.ts
|
|
143
|
-
import mongoose from 'mongoose';
|
|
144
|
-
import { logger } from '@common';
|
|
145
|
-
|
|
146
|
-
const MONGODB_URI = process.env['MONGODB_URI']!;
|
|
147
|
-
|
|
148
|
-
if (!MONGODB_URI) {
|
|
149
|
-
throw new Error('MONGODB_URI environment variable not set');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let isConnected = false;
|
|
153
|
-
|
|
154
|
-
export async function connectDB(): Promise<void> {
|
|
155
|
-
if (isConnected) {
|
|
156
|
-
logger.info('Using existing MongoDB connection');
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
await mongoose.connect(MONGODB_URI, {
|
|
162
|
-
maxPoolSize: 10,
|
|
163
|
-
serverSelectionTimeoutMS: 5000,
|
|
164
|
-
socketTimeoutMS: 45000,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
isConnected = true;
|
|
168
|
-
logger.info('Connected to MongoDB');
|
|
169
|
-
} catch (error) {
|
|
170
|
-
logger.error('MongoDB connection error:', error);
|
|
171
|
-
throw error;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export async function disconnectDB(): Promise<void> {
|
|
176
|
-
if (!isConnected) return;
|
|
177
|
-
|
|
178
|
-
await mongoose.disconnect();
|
|
179
|
-
isConnected = false;
|
|
180
|
-
logger.info('Disconnected from MongoDB');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Graceful shutdown
|
|
184
|
-
process.on('SIGINT', async () => {
|
|
185
|
-
await disconnectDB();
|
|
186
|
-
process.exit(0);
|
|
187
|
-
});
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
## Query Patterns
|
|
193
|
-
|
|
194
|
-
### Find with Typing
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
// Find one
|
|
198
|
-
const user = await UserModel.findById(id);
|
|
199
|
-
const userByEmail = await UserModel.findOne({ email });
|
|
200
|
-
|
|
201
|
-
// Find with projection
|
|
202
|
-
const users = await UserModel.find({ role: 'user' }).select('name email role').lean(); // Returns plain objects (faster)
|
|
203
|
-
|
|
204
|
-
// Find with pagination
|
|
205
|
-
const page = 1;
|
|
206
|
-
const limit = 20;
|
|
207
|
-
const users = await UserModel.find()
|
|
208
|
-
.sort({ createdAt: -1 })
|
|
209
|
-
.skip((page - 1) * limit)
|
|
210
|
-
.limit(limit)
|
|
211
|
-
.lean();
|
|
212
|
-
|
|
213
|
-
// Count
|
|
214
|
-
const total = await UserModel.countDocuments({ role: 'user' });
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Update Patterns
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
// Update one (returns updated doc)
|
|
221
|
-
const updated = await UserModel.findByIdAndUpdate(
|
|
222
|
-
id,
|
|
223
|
-
{ $set: { name: 'New Name' } },
|
|
224
|
-
{ new: true, runValidators: true }
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
// Update many
|
|
228
|
-
const result = await UserModel.updateMany({ role: 'guest' }, { $set: { role: 'user' } });
|
|
229
|
-
console.log(`Updated ${result.modifiedCount} documents`);
|
|
230
|
-
|
|
231
|
-
// Upsert
|
|
232
|
-
const user = await UserModel.findOneAndUpdate(
|
|
233
|
-
{ email },
|
|
234
|
-
{ $setOnInsert: { createdAt: new Date() }, $set: { lastLogin: new Date() } },
|
|
235
|
-
{ upsert: true, new: true }
|
|
236
|
-
);
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
### Delete Patterns
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
// Delete one
|
|
243
|
-
await UserModel.findByIdAndDelete(id);
|
|
244
|
-
|
|
245
|
-
// Delete many
|
|
246
|
-
const result = await UserModel.deleteMany({ role: 'guest' });
|
|
247
|
-
console.log(`Deleted ${result.deletedCount} documents`);
|
|
248
|
-
|
|
249
|
-
// Soft delete pattern
|
|
250
|
-
const softDeleteSchema = new Schema({
|
|
251
|
-
deletedAt: { type: Date, default: null },
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// Query middleware to exclude soft-deleted
|
|
255
|
-
userSchema.pre('find', function () {
|
|
256
|
-
this.where({ deletedAt: null });
|
|
257
|
-
});
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## Aggregation Pipeline
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
// Complex aggregation
|
|
266
|
-
const stats = await UserModel.aggregate([
|
|
267
|
-
// Match stage
|
|
268
|
-
{ $match: { createdAt: { $gte: startDate } } },
|
|
269
|
-
|
|
270
|
-
// Group stage
|
|
271
|
-
{
|
|
272
|
-
$group: {
|
|
273
|
-
_id: '$role',
|
|
274
|
-
count: { $sum: 1 },
|
|
275
|
-
avgAge: { $avg: '$age' },
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
|
|
279
|
-
// Sort stage
|
|
280
|
-
{ $sort: { count: -1 } },
|
|
281
|
-
|
|
282
|
-
// Project stage
|
|
283
|
-
{
|
|
284
|
-
$project: {
|
|
285
|
-
role: '$_id',
|
|
286
|
-
count: 1,
|
|
287
|
-
avgAge: { $round: ['$avgAge', 1] },
|
|
288
|
-
_id: 0,
|
|
289
|
-
},
|
|
290
|
-
},
|
|
291
|
-
]);
|
|
292
|
-
|
|
293
|
-
// Lookup (join)
|
|
294
|
-
const postsWithAuthors = await PostModel.aggregate([
|
|
295
|
-
{
|
|
296
|
-
$lookup: {
|
|
297
|
-
from: 'users',
|
|
298
|
-
localField: 'authorId',
|
|
299
|
-
foreignField: '_id',
|
|
300
|
-
as: 'author',
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
{ $unwind: '$author' },
|
|
304
|
-
{
|
|
305
|
-
$project: {
|
|
306
|
-
title: 1,
|
|
307
|
-
content: 1,
|
|
308
|
-
'author.name': 1,
|
|
309
|
-
'author.email': 1,
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
]);
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
---
|
|
316
|
-
|
|
317
|
-
## Indexes
|
|
318
|
-
|
|
319
|
-
### Index Types
|
|
320
|
-
|
|
321
|
-
```typescript
|
|
322
|
-
// Single field
|
|
323
|
-
userSchema.index({ email: 1 }); // Ascending
|
|
324
|
-
userSchema.index({ createdAt: -1 }); // Descending
|
|
325
|
-
|
|
326
|
-
// Compound index
|
|
327
|
-
userSchema.index({ role: 1, createdAt: -1 });
|
|
328
|
-
|
|
329
|
-
// Unique index
|
|
330
|
-
userSchema.index({ email: 1 }, { unique: true });
|
|
331
|
-
|
|
332
|
-
// Sparse index (only index docs with field)
|
|
333
|
-
userSchema.index({ nickname: 1 }, { sparse: true });
|
|
334
|
-
|
|
335
|
-
// TTL index (auto-delete after time)
|
|
336
|
-
sessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
|
337
|
-
|
|
338
|
-
// Text index (full-text search)
|
|
339
|
-
postSchema.index({ title: 'text', content: 'text' });
|
|
340
|
-
|
|
341
|
-
// Partial index
|
|
342
|
-
userSchema.index(
|
|
343
|
-
{ email: 1 },
|
|
344
|
-
{
|
|
345
|
-
partialFilterExpression: { isActive: true },
|
|
346
|
-
}
|
|
347
|
-
);
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### Check Indexes
|
|
351
|
-
|
|
352
|
-
```bash
|
|
353
|
-
# List indexes
|
|
354
|
-
db.users.getIndexes()
|
|
355
|
-
|
|
356
|
-
# Explain query
|
|
357
|
-
db.users.find({ email: "test@test.com" }).explain("executionStats")
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
---
|
|
361
|
-
|
|
362
|
-
## Relationships
|
|
363
|
-
|
|
364
|
-
### Reference (Normalized)
|
|
365
|
-
|
|
366
|
-
```typescript
|
|
367
|
-
// Post references User
|
|
368
|
-
const postSchema = new Schema({
|
|
369
|
-
title: String,
|
|
370
|
-
authorId: {
|
|
371
|
-
type: Schema.Types.ObjectId,
|
|
372
|
-
ref: 'User',
|
|
373
|
-
required: true,
|
|
374
|
-
index: true,
|
|
375
|
-
},
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// Populate
|
|
379
|
-
const post = await PostModel.findById(id).populate('authorId', 'name email');
|
|
380
|
-
|
|
381
|
-
// Virtual populate
|
|
382
|
-
userSchema.virtual('posts', {
|
|
383
|
-
ref: 'Post',
|
|
384
|
-
localField: '_id',
|
|
385
|
-
foreignField: 'authorId',
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
const user = await UserModel.findById(id).populate('posts');
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
### Embedded (Denormalized)
|
|
392
|
-
|
|
393
|
-
```typescript
|
|
394
|
-
// Comments embedded in Post
|
|
395
|
-
const postSchema = new Schema({
|
|
396
|
-
title: String,
|
|
397
|
-
comments: [
|
|
398
|
-
{
|
|
399
|
-
author: String,
|
|
400
|
-
content: String,
|
|
401
|
-
createdAt: { type: Date, default: Date.now },
|
|
402
|
-
},
|
|
403
|
-
],
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// Add comment
|
|
407
|
-
await PostModel.findByIdAndUpdate(id, {
|
|
408
|
-
$push: { comments: { author: 'John', content: 'Great post!' } },
|
|
409
|
-
});
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
---
|
|
413
|
-
|
|
414
|
-
## Middleware (Hooks)
|
|
415
|
-
|
|
416
|
-
```typescript
|
|
417
|
-
// Pre-save hook
|
|
418
|
-
userSchema.pre('save', async function (next) {
|
|
419
|
-
if (this.isModified('passwordHash')) {
|
|
420
|
-
this.passwordHash = await Bun.password.hash(this.passwordHash);
|
|
421
|
-
}
|
|
422
|
-
next();
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
// Post-save hook
|
|
426
|
-
userSchema.post('save', function (doc) {
|
|
427
|
-
logger.info(`User saved: ${doc.email}`);
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// Pre-find hook
|
|
431
|
-
userSchema.pre('find', function () {
|
|
432
|
-
// Always exclude deleted documents
|
|
433
|
-
this.where({ deletedAt: null });
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
// Error handling hook
|
|
437
|
-
userSchema.post('save', function (error: Error, doc: IUserDocument, next: () => void) {
|
|
438
|
-
if (error.name === 'MongoServerError' && (error as any).code === 11000) {
|
|
439
|
-
next(new Error('Email already exists'));
|
|
440
|
-
} else {
|
|
441
|
-
next();
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
---
|
|
447
|
-
|
|
448
|
-
## Transactions
|
|
449
|
-
|
|
450
|
-
```typescript
|
|
451
|
-
import mongoose from 'mongoose';
|
|
452
|
-
|
|
453
|
-
async function transferCredits(fromId: string, toId: string, amount: number) {
|
|
454
|
-
const session = await mongoose.startSession();
|
|
455
|
-
|
|
456
|
-
try {
|
|
457
|
-
session.startTransaction();
|
|
458
|
-
|
|
459
|
-
await UserModel.findByIdAndUpdate(fromId, { $inc: { credits: -amount } }, { session });
|
|
460
|
-
|
|
461
|
-
await UserModel.findByIdAndUpdate(toId, { $inc: { credits: amount } }, { session });
|
|
462
|
-
|
|
463
|
-
await session.commitTransaction();
|
|
464
|
-
} catch (error) {
|
|
465
|
-
await session.abortTransaction();
|
|
466
|
-
throw error;
|
|
467
|
-
} finally {
|
|
468
|
-
session.endSession();
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
---
|
|
474
|
-
|
|
475
|
-
## Agent Integration
|
|
476
|
-
|
|
477
|
-
This skill is used by:
|
|
478
|
-
|
|
479
|
-
- **mongoose-schema-designer** agent
|
|
480
|
-
- **mongoose-index-optimizer** agent
|
|
481
|
-
- **mongoose-aggregation** agent
|
|
482
|
-
- **mongodb-query-optimizer** agent
|
|
483
|
-
- **database-seeder** agent
|
|
484
|
-
|
|
485
|
-
---
|
|
486
|
-
|
|
487
|
-
## FORBIDDEN
|
|
488
|
-
|
|
489
|
-
1. **User ID from input** - Always use session/context
|
|
490
|
-
2. **No indexes on query fields** - Always index filtered fields
|
|
491
|
-
3. **Returning passwordHash** - Use `select: false`
|
|
492
|
-
4. **N+1 queries** - Use `.populate()` or aggregation
|
|
493
|
-
5. **Unbounded queries** - Always use `.limit()`
|
|
494
|
-
|
|
495
|
-
---
|
|
496
|
-
|
|
497
|
-
## Version
|
|
498
|
-
|
|
499
|
-
- **v1.0.0** - Initial implementation based on Mongoose 8.x patterns
|
|
1
|
+
---
|
|
2
|
+
name: mongoose-patterns
|
|
3
|
+
description: Mongoose/MongoDB patterns for schema design, queries, indexes, aggregations. Use when working with MongoDB through Mongoose.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Mongoose Patterns - MongoDB ODM Best Practices
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Expert guidance for Mongoose/MongoDB:
|
|
12
|
+
|
|
13
|
+
- **Schema Design** - Proper typing and validation
|
|
14
|
+
- **Queries** - Efficient query patterns
|
|
15
|
+
- **Indexes** - Performance optimization
|
|
16
|
+
- **Aggregations** - Complex data operations
|
|
17
|
+
- **Relationships** - References vs embedding
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Project Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
server/
|
|
25
|
+
├── db/
|
|
26
|
+
│ ├── connection.ts # MongoDB connection
|
|
27
|
+
│ └── index.ts # Export db utilities
|
|
28
|
+
├── models/
|
|
29
|
+
│ ├── user.model.ts
|
|
30
|
+
│ ├── post.model.ts
|
|
31
|
+
│ └── index.ts # Export all models
|
|
32
|
+
types/
|
|
33
|
+
└── models/
|
|
34
|
+
├── user.types.ts # User interfaces
|
|
35
|
+
└── post.types.ts # Post interfaces
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Schema Design
|
|
41
|
+
|
|
42
|
+
### Basic Schema with TypeScript
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// types/models/user.types.ts
|
|
46
|
+
import { Types, Document } from 'mongoose';
|
|
47
|
+
|
|
48
|
+
export interface IUser {
|
|
49
|
+
email: string;
|
|
50
|
+
name: string;
|
|
51
|
+
passwordHash: string;
|
|
52
|
+
role: 'admin' | 'user' | 'guest';
|
|
53
|
+
profile?: {
|
|
54
|
+
bio?: string;
|
|
55
|
+
avatar?: string;
|
|
56
|
+
};
|
|
57
|
+
createdAt: Date;
|
|
58
|
+
updatedAt: Date;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface IUserDocument extends IUser, Document {
|
|
62
|
+
_id: Types.ObjectId;
|
|
63
|
+
comparePassword(password: string): Promise<boolean>;
|
|
64
|
+
toPublic(): Omit<IUser, 'passwordHash'>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// server/models/user.model.ts
|
|
68
|
+
import mongoose, { Schema, Model } from 'mongoose';
|
|
69
|
+
import { IUser, IUserDocument } from '$types/models/user.types';
|
|
70
|
+
|
|
71
|
+
const userSchema = new Schema<IUserDocument>(
|
|
72
|
+
{
|
|
73
|
+
email: {
|
|
74
|
+
type: String,
|
|
75
|
+
required: [true, 'Email is required'],
|
|
76
|
+
unique: true,
|
|
77
|
+
lowercase: true,
|
|
78
|
+
trim: true,
|
|
79
|
+
validate: {
|
|
80
|
+
validator: (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
|
|
81
|
+
message: 'Invalid email format',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
name: {
|
|
85
|
+
type: String,
|
|
86
|
+
required: [true, 'Name is required'],
|
|
87
|
+
minlength: [2, 'Name must be at least 2 characters'],
|
|
88
|
+
maxlength: [100, 'Name cannot exceed 100 characters'],
|
|
89
|
+
trim: true,
|
|
90
|
+
},
|
|
91
|
+
passwordHash: {
|
|
92
|
+
type: String,
|
|
93
|
+
required: true,
|
|
94
|
+
select: false, // Never include by default
|
|
95
|
+
},
|
|
96
|
+
role: {
|
|
97
|
+
type: String,
|
|
98
|
+
enum: ['admin', 'user', 'guest'],
|
|
99
|
+
default: 'user',
|
|
100
|
+
},
|
|
101
|
+
profile: {
|
|
102
|
+
bio: { type: String, maxlength: 500 },
|
|
103
|
+
avatar: { type: String },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
timestamps: true,
|
|
108
|
+
toJSON: { virtuals: true },
|
|
109
|
+
toObject: { virtuals: true },
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Indexes
|
|
114
|
+
userSchema.index({ email: 1 }, { unique: true });
|
|
115
|
+
userSchema.index({ role: 1 });
|
|
116
|
+
userSchema.index({ createdAt: -1 });
|
|
117
|
+
|
|
118
|
+
// Instance methods
|
|
119
|
+
userSchema.methods.comparePassword = async function (password: string): Promise<boolean> {
|
|
120
|
+
return Bun.password.verify(password, this.passwordHash);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
userSchema.methods.toPublic = function () {
|
|
124
|
+
const obj = this.toObject();
|
|
125
|
+
delete obj.passwordHash;
|
|
126
|
+
return obj;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Static methods
|
|
130
|
+
userSchema.statics.findByEmail = function (email: string) {
|
|
131
|
+
return this.findOne({ email: email.toLowerCase() });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const UserModel: Model<IUserDocument> = mongoose.model('User', userSchema);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Connection Management
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// server/db/connection.ts
|
|
143
|
+
import mongoose from 'mongoose';
|
|
144
|
+
import { logger } from '@common';
|
|
145
|
+
|
|
146
|
+
const MONGODB_URI = process.env['MONGODB_URI']!;
|
|
147
|
+
|
|
148
|
+
if (!MONGODB_URI) {
|
|
149
|
+
throw new Error('MONGODB_URI environment variable not set');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let isConnected = false;
|
|
153
|
+
|
|
154
|
+
export async function connectDB(): Promise<void> {
|
|
155
|
+
if (isConnected) {
|
|
156
|
+
logger.info('Using existing MongoDB connection');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await mongoose.connect(MONGODB_URI, {
|
|
162
|
+
maxPoolSize: 10,
|
|
163
|
+
serverSelectionTimeoutMS: 5000,
|
|
164
|
+
socketTimeoutMS: 45000,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
isConnected = true;
|
|
168
|
+
logger.info('Connected to MongoDB');
|
|
169
|
+
} catch (error) {
|
|
170
|
+
logger.error('MongoDB connection error:', error);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function disconnectDB(): Promise<void> {
|
|
176
|
+
if (!isConnected) return;
|
|
177
|
+
|
|
178
|
+
await mongoose.disconnect();
|
|
179
|
+
isConnected = false;
|
|
180
|
+
logger.info('Disconnected from MongoDB');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Graceful shutdown
|
|
184
|
+
process.on('SIGINT', async () => {
|
|
185
|
+
await disconnectDB();
|
|
186
|
+
process.exit(0);
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Query Patterns
|
|
193
|
+
|
|
194
|
+
### Find with Typing
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Find one
|
|
198
|
+
const user = await UserModel.findById(id);
|
|
199
|
+
const userByEmail = await UserModel.findOne({ email });
|
|
200
|
+
|
|
201
|
+
// Find with projection
|
|
202
|
+
const users = await UserModel.find({ role: 'user' }).select('name email role').lean(); // Returns plain objects (faster)
|
|
203
|
+
|
|
204
|
+
// Find with pagination
|
|
205
|
+
const page = 1;
|
|
206
|
+
const limit = 20;
|
|
207
|
+
const users = await UserModel.find()
|
|
208
|
+
.sort({ createdAt: -1 })
|
|
209
|
+
.skip((page - 1) * limit)
|
|
210
|
+
.limit(limit)
|
|
211
|
+
.lean();
|
|
212
|
+
|
|
213
|
+
// Count
|
|
214
|
+
const total = await UserModel.countDocuments({ role: 'user' });
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Update Patterns
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Update one (returns updated doc)
|
|
221
|
+
const updated = await UserModel.findByIdAndUpdate(
|
|
222
|
+
id,
|
|
223
|
+
{ $set: { name: 'New Name' } },
|
|
224
|
+
{ new: true, runValidators: true }
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Update many
|
|
228
|
+
const result = await UserModel.updateMany({ role: 'guest' }, { $set: { role: 'user' } });
|
|
229
|
+
console.log(`Updated ${result.modifiedCount} documents`);
|
|
230
|
+
|
|
231
|
+
// Upsert
|
|
232
|
+
const user = await UserModel.findOneAndUpdate(
|
|
233
|
+
{ email },
|
|
234
|
+
{ $setOnInsert: { createdAt: new Date() }, $set: { lastLogin: new Date() } },
|
|
235
|
+
{ upsert: true, new: true }
|
|
236
|
+
);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Delete Patterns
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// Delete one
|
|
243
|
+
await UserModel.findByIdAndDelete(id);
|
|
244
|
+
|
|
245
|
+
// Delete many
|
|
246
|
+
const result = await UserModel.deleteMany({ role: 'guest' });
|
|
247
|
+
console.log(`Deleted ${result.deletedCount} documents`);
|
|
248
|
+
|
|
249
|
+
// Soft delete pattern
|
|
250
|
+
const softDeleteSchema = new Schema({
|
|
251
|
+
deletedAt: { type: Date, default: null },
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Query middleware to exclude soft-deleted
|
|
255
|
+
userSchema.pre('find', function () {
|
|
256
|
+
this.where({ deletedAt: null });
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Aggregation Pipeline
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Complex aggregation
|
|
266
|
+
const stats = await UserModel.aggregate([
|
|
267
|
+
// Match stage
|
|
268
|
+
{ $match: { createdAt: { $gte: startDate } } },
|
|
269
|
+
|
|
270
|
+
// Group stage
|
|
271
|
+
{
|
|
272
|
+
$group: {
|
|
273
|
+
_id: '$role',
|
|
274
|
+
count: { $sum: 1 },
|
|
275
|
+
avgAge: { $avg: '$age' },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// Sort stage
|
|
280
|
+
{ $sort: { count: -1 } },
|
|
281
|
+
|
|
282
|
+
// Project stage
|
|
283
|
+
{
|
|
284
|
+
$project: {
|
|
285
|
+
role: '$_id',
|
|
286
|
+
count: 1,
|
|
287
|
+
avgAge: { $round: ['$avgAge', 1] },
|
|
288
|
+
_id: 0,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
// Lookup (join)
|
|
294
|
+
const postsWithAuthors = await PostModel.aggregate([
|
|
295
|
+
{
|
|
296
|
+
$lookup: {
|
|
297
|
+
from: 'users',
|
|
298
|
+
localField: 'authorId',
|
|
299
|
+
foreignField: '_id',
|
|
300
|
+
as: 'author',
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{ $unwind: '$author' },
|
|
304
|
+
{
|
|
305
|
+
$project: {
|
|
306
|
+
title: 1,
|
|
307
|
+
content: 1,
|
|
308
|
+
'author.name': 1,
|
|
309
|
+
'author.email': 1,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
]);
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Indexes
|
|
318
|
+
|
|
319
|
+
### Index Types
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// Single field
|
|
323
|
+
userSchema.index({ email: 1 }); // Ascending
|
|
324
|
+
userSchema.index({ createdAt: -1 }); // Descending
|
|
325
|
+
|
|
326
|
+
// Compound index
|
|
327
|
+
userSchema.index({ role: 1, createdAt: -1 });
|
|
328
|
+
|
|
329
|
+
// Unique index
|
|
330
|
+
userSchema.index({ email: 1 }, { unique: true });
|
|
331
|
+
|
|
332
|
+
// Sparse index (only index docs with field)
|
|
333
|
+
userSchema.index({ nickname: 1 }, { sparse: true });
|
|
334
|
+
|
|
335
|
+
// TTL index (auto-delete after time)
|
|
336
|
+
sessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
|
337
|
+
|
|
338
|
+
// Text index (full-text search)
|
|
339
|
+
postSchema.index({ title: 'text', content: 'text' });
|
|
340
|
+
|
|
341
|
+
// Partial index
|
|
342
|
+
userSchema.index(
|
|
343
|
+
{ email: 1 },
|
|
344
|
+
{
|
|
345
|
+
partialFilterExpression: { isActive: true },
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Check Indexes
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
# List indexes
|
|
354
|
+
db.users.getIndexes()
|
|
355
|
+
|
|
356
|
+
# Explain query
|
|
357
|
+
db.users.find({ email: "test@test.com" }).explain("executionStats")
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Relationships
|
|
363
|
+
|
|
364
|
+
### Reference (Normalized)
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// Post references User
|
|
368
|
+
const postSchema = new Schema({
|
|
369
|
+
title: String,
|
|
370
|
+
authorId: {
|
|
371
|
+
type: Schema.Types.ObjectId,
|
|
372
|
+
ref: 'User',
|
|
373
|
+
required: true,
|
|
374
|
+
index: true,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Populate
|
|
379
|
+
const post = await PostModel.findById(id).populate('authorId', 'name email');
|
|
380
|
+
|
|
381
|
+
// Virtual populate
|
|
382
|
+
userSchema.virtual('posts', {
|
|
383
|
+
ref: 'Post',
|
|
384
|
+
localField: '_id',
|
|
385
|
+
foreignField: 'authorId',
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const user = await UserModel.findById(id).populate('posts');
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Embedded (Denormalized)
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// Comments embedded in Post
|
|
395
|
+
const postSchema = new Schema({
|
|
396
|
+
title: String,
|
|
397
|
+
comments: [
|
|
398
|
+
{
|
|
399
|
+
author: String,
|
|
400
|
+
content: String,
|
|
401
|
+
createdAt: { type: Date, default: Date.now },
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Add comment
|
|
407
|
+
await PostModel.findByIdAndUpdate(id, {
|
|
408
|
+
$push: { comments: { author: 'John', content: 'Great post!' } },
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Middleware (Hooks)
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// Pre-save hook
|
|
418
|
+
userSchema.pre('save', async function (next) {
|
|
419
|
+
if (this.isModified('passwordHash')) {
|
|
420
|
+
this.passwordHash = await Bun.password.hash(this.passwordHash);
|
|
421
|
+
}
|
|
422
|
+
next();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Post-save hook
|
|
426
|
+
userSchema.post('save', function (doc) {
|
|
427
|
+
logger.info(`User saved: ${doc.email}`);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Pre-find hook
|
|
431
|
+
userSchema.pre('find', function () {
|
|
432
|
+
// Always exclude deleted documents
|
|
433
|
+
this.where({ deletedAt: null });
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Error handling hook
|
|
437
|
+
userSchema.post('save', function (error: Error, doc: IUserDocument, next: () => void) {
|
|
438
|
+
if (error.name === 'MongoServerError' && (error as any).code === 11000) {
|
|
439
|
+
next(new Error('Email already exists'));
|
|
440
|
+
} else {
|
|
441
|
+
next();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Transactions
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
import mongoose from 'mongoose';
|
|
452
|
+
|
|
453
|
+
async function transferCredits(fromId: string, toId: string, amount: number) {
|
|
454
|
+
const session = await mongoose.startSession();
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
session.startTransaction();
|
|
458
|
+
|
|
459
|
+
await UserModel.findByIdAndUpdate(fromId, { $inc: { credits: -amount } }, { session });
|
|
460
|
+
|
|
461
|
+
await UserModel.findByIdAndUpdate(toId, { $inc: { credits: amount } }, { session });
|
|
462
|
+
|
|
463
|
+
await session.commitTransaction();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
await session.abortTransaction();
|
|
466
|
+
throw error;
|
|
467
|
+
} finally {
|
|
468
|
+
session.endSession();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## Agent Integration
|
|
476
|
+
|
|
477
|
+
This skill is used by:
|
|
478
|
+
|
|
479
|
+
- **mongoose-schema-designer** agent
|
|
480
|
+
- **mongoose-index-optimizer** agent
|
|
481
|
+
- **mongoose-aggregation** agent
|
|
482
|
+
- **mongodb-query-optimizer** agent
|
|
483
|
+
- **database-seeder** agent
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## FORBIDDEN
|
|
488
|
+
|
|
489
|
+
1. **User ID from input** - Always use session/context
|
|
490
|
+
2. **No indexes on query fields** - Always index filtered fields
|
|
491
|
+
3. **Returning passwordHash** - Use `select: false`
|
|
492
|
+
4. **N+1 queries** - Use `.populate()` or aggregation
|
|
493
|
+
5. **Unbounded queries** - Always use `.limit()`
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## Version
|
|
498
|
+
|
|
499
|
+
- **v1.0.0** - Initial implementation based on Mongoose 8.x patterns
|