omgkit 2.2.0 → 2.3.0
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/package.json +1 -1
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,812 +1,96 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: MongoDB NoSQL database with document modeling, aggregation pipelines,
|
|
4
|
-
category: databases
|
|
5
|
-
triggers:
|
|
6
|
-
- mongodb
|
|
7
|
-
- mongo
|
|
8
|
-
- mongoose
|
|
9
|
-
- nosql
|
|
10
|
-
- document database
|
|
11
|
-
- bson
|
|
12
|
-
- aggregation
|
|
2
|
+
name: Developing with MongoDB
|
|
3
|
+
description: The agent implements MongoDB NoSQL database solutions with document modeling, aggregation pipelines, and Mongoose ODM. Use when building document-based applications, designing schemas, writing aggregations, or implementing NoSQL patterns.
|
|
13
4
|
---
|
|
14
5
|
|
|
15
|
-
# MongoDB
|
|
6
|
+
# Developing with MongoDB
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## Purpose
|
|
20
|
-
|
|
21
|
-
Build scalable document-based applications:
|
|
22
|
-
|
|
23
|
-
- Design effective document schemas
|
|
24
|
-
- Implement efficient CRUD operations
|
|
25
|
-
- Write powerful aggregation pipelines
|
|
26
|
-
- Optimize queries with proper indexing
|
|
27
|
-
- Use Mongoose ODM for type safety
|
|
28
|
-
- Handle transactions for data integrity
|
|
29
|
-
- Implement production patterns
|
|
30
|
-
|
|
31
|
-
## Features
|
|
32
|
-
|
|
33
|
-
### 1. Document Schema Design
|
|
8
|
+
## Quick Start
|
|
34
9
|
|
|
35
10
|
```typescript
|
|
36
|
-
//
|
|
37
|
-
import mongoose, { Schema, Document
|
|
38
|
-
|
|
39
|
-
// TypeScript interfaces
|
|
40
|
-
export interface IAddress {
|
|
41
|
-
street: string;
|
|
42
|
-
city: string;
|
|
43
|
-
state: string;
|
|
44
|
-
zipCode: string;
|
|
45
|
-
country: string;
|
|
46
|
-
}
|
|
11
|
+
// Schema with Mongoose
|
|
12
|
+
import mongoose, { Schema, Document } from 'mongoose';
|
|
47
13
|
|
|
48
|
-
|
|
14
|
+
interface IUser extends Document {
|
|
49
15
|
email: string;
|
|
50
|
-
|
|
51
|
-
profile: {
|
|
52
|
-
firstName: string;
|
|
53
|
-
lastName: string;
|
|
54
|
-
avatar?: string;
|
|
55
|
-
bio?: string;
|
|
56
|
-
};
|
|
57
|
-
addresses: IAddress[];
|
|
58
|
-
role: 'admin' | 'user' | 'guest';
|
|
59
|
-
isActive: boolean;
|
|
60
|
-
lastLoginAt?: Date;
|
|
16
|
+
profile: { firstName: string; lastName: string };
|
|
61
17
|
createdAt: Date;
|
|
62
|
-
updatedAt: Date;
|
|
63
|
-
|
|
64
|
-
// Instance methods
|
|
65
|
-
comparePassword(password: string): Promise<boolean>;
|
|
66
|
-
getFullName(): string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Statics interface
|
|
70
|
-
export interface IUserModel extends Model<IUser> {
|
|
71
|
-
findByEmail(email: string): Promise<IUser | null>;
|
|
72
|
-
findActiveUsers(): Promise<IUser[]>;
|
|
73
18
|
}
|
|
74
19
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
street: { type: String, required: true },
|
|
78
|
-
city: { type: String, required: true },
|
|
79
|
-
state: { type: String, required: true },
|
|
80
|
-
zipCode: { type: String, required: true },
|
|
81
|
-
country: { type: String, required: true, default: 'US' },
|
|
82
|
-
}, { _id: false });
|
|
83
|
-
|
|
84
|
-
const userSchema = new Schema<IUser, IUserModel>({
|
|
85
|
-
email: {
|
|
86
|
-
type: String,
|
|
87
|
-
required: [true, 'Email is required'],
|
|
88
|
-
unique: true,
|
|
89
|
-
lowercase: true,
|
|
90
|
-
trim: true,
|
|
91
|
-
validate: {
|
|
92
|
-
validator: (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
|
|
93
|
-
message: 'Invalid email format',
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
password: {
|
|
97
|
-
type: String,
|
|
98
|
-
required: true,
|
|
99
|
-
minlength: [8, 'Password must be at least 8 characters'],
|
|
100
|
-
select: false, // Don't include in queries by default
|
|
101
|
-
},
|
|
20
|
+
const userSchema = new Schema<IUser>({
|
|
21
|
+
email: { type: String, required: true, unique: true, lowercase: true },
|
|
102
22
|
profile: {
|
|
103
|
-
firstName: { type: String, required: true
|
|
104
|
-
lastName: { type: String, required: true
|
|
105
|
-
avatar: String,
|
|
106
|
-
bio: { type: String, maxlength: 500 },
|
|
23
|
+
firstName: { type: String, required: true },
|
|
24
|
+
lastName: { type: String, required: true },
|
|
107
25
|
},
|
|
108
|
-
|
|
109
|
-
role: {
|
|
110
|
-
type: String,
|
|
111
|
-
enum: ['admin', 'user', 'guest'],
|
|
112
|
-
default: 'user',
|
|
113
|
-
},
|
|
114
|
-
isActive: { type: Boolean, default: true },
|
|
115
|
-
lastLoginAt: Date,
|
|
116
|
-
}, {
|
|
117
|
-
timestamps: true,
|
|
118
|
-
toJSON: {
|
|
119
|
-
virtuals: true,
|
|
120
|
-
transform: (doc, ret) => {
|
|
121
|
-
delete ret.password;
|
|
122
|
-
delete ret.__v;
|
|
123
|
-
return ret;
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
});
|
|
26
|
+
}, { timestamps: true });
|
|
127
27
|
|
|
128
|
-
// Indexes
|
|
129
28
|
userSchema.index({ email: 1 });
|
|
130
|
-
|
|
131
|
-
userSchema.index({ role: 1, isActive: 1 });
|
|
132
|
-
userSchema.index({ createdAt: -1 });
|
|
133
|
-
|
|
134
|
-
// Virtual properties
|
|
135
|
-
userSchema.virtual('fullName').get(function() {
|
|
136
|
-
return `${this.profile.firstName} ${this.profile.lastName}`;
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Instance methods
|
|
140
|
-
userSchema.methods.comparePassword = async function(password: string): Promise<boolean> {
|
|
141
|
-
const bcrypt = await import('bcrypt');
|
|
142
|
-
return bcrypt.compare(password, this.password);
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
userSchema.methods.getFullName = function(): string {
|
|
146
|
-
return `${this.profile.firstName} ${this.profile.lastName}`;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
// Static methods
|
|
150
|
-
userSchema.statics.findByEmail = function(email: string) {
|
|
151
|
-
return this.findOne({ email: email.toLowerCase() });
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
userSchema.statics.findActiveUsers = function() {
|
|
155
|
-
return this.find({ isActive: true });
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Middleware (hooks)
|
|
159
|
-
userSchema.pre('save', async function(next) {
|
|
160
|
-
if (this.isModified('password')) {
|
|
161
|
-
const bcrypt = await import('bcrypt');
|
|
162
|
-
this.password = await bcrypt.hash(this.password, 12);
|
|
163
|
-
}
|
|
164
|
-
next();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
export const User = mongoose.model<IUser, IUserModel>('User', userSchema);
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### 2. CRUD Operations
|
|
171
|
-
|
|
172
|
-
```typescript
|
|
173
|
-
// src/repositories/user.repository.ts
|
|
174
|
-
import { User, IUser } from '../models/user.model';
|
|
175
|
-
import { FilterQuery, UpdateQuery, QueryOptions } from 'mongoose';
|
|
176
|
-
|
|
177
|
-
export interface PaginationOptions {
|
|
178
|
-
page?: number;
|
|
179
|
-
limit?: number;
|
|
180
|
-
sort?: Record<string, 1 | -1>;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export interface PaginatedResult<T> {
|
|
184
|
-
data: T[];
|
|
185
|
-
pagination: {
|
|
186
|
-
page: number;
|
|
187
|
-
limit: number;
|
|
188
|
-
total: number;
|
|
189
|
-
totalPages: number;
|
|
190
|
-
hasMore: boolean;
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export class UserRepository {
|
|
195
|
-
// Create
|
|
196
|
-
async create(userData: Partial<IUser>): Promise<IUser> {
|
|
197
|
-
const user = new User(userData);
|
|
198
|
-
return user.save();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async createMany(users: Partial<IUser>[]): Promise<IUser[]> {
|
|
202
|
-
return User.insertMany(users);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Read
|
|
206
|
-
async findById(id: string): Promise<IUser | null> {
|
|
207
|
-
return User.findById(id);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async findByIdWithPassword(id: string): Promise<IUser | null> {
|
|
211
|
-
return User.findById(id).select('+password');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async findByEmail(email: string): Promise<IUser | null> {
|
|
215
|
-
return User.findByEmail(email);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async findOne(filter: FilterQuery<IUser>): Promise<IUser | null> {
|
|
219
|
-
return User.findOne(filter);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async find(
|
|
223
|
-
filter: FilterQuery<IUser>,
|
|
224
|
-
options: PaginationOptions = {}
|
|
225
|
-
): Promise<PaginatedResult<IUser>> {
|
|
226
|
-
const { page = 1, limit = 20, sort = { createdAt: -1 } } = options;
|
|
227
|
-
const skip = (page - 1) * limit;
|
|
228
|
-
|
|
229
|
-
const [data, total] = await Promise.all([
|
|
230
|
-
User.find(filter).sort(sort).skip(skip).limit(limit),
|
|
231
|
-
User.countDocuments(filter),
|
|
232
|
-
]);
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
data,
|
|
236
|
-
pagination: {
|
|
237
|
-
page,
|
|
238
|
-
limit,
|
|
239
|
-
total,
|
|
240
|
-
totalPages: Math.ceil(total / limit),
|
|
241
|
-
hasMore: page * limit < total,
|
|
242
|
-
},
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Update
|
|
247
|
-
async updateById(
|
|
248
|
-
id: string,
|
|
249
|
-
update: UpdateQuery<IUser>,
|
|
250
|
-
options: QueryOptions = {}
|
|
251
|
-
): Promise<IUser | null> {
|
|
252
|
-
return User.findByIdAndUpdate(id, update, {
|
|
253
|
-
new: true,
|
|
254
|
-
runValidators: true,
|
|
255
|
-
...options,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async updateOne(
|
|
260
|
-
filter: FilterQuery<IUser>,
|
|
261
|
-
update: UpdateQuery<IUser>
|
|
262
|
-
): Promise<IUser | null> {
|
|
263
|
-
return User.findOneAndUpdate(filter, update, {
|
|
264
|
-
new: true,
|
|
265
|
-
runValidators: true,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async updateMany(
|
|
270
|
-
filter: FilterQuery<IUser>,
|
|
271
|
-
update: UpdateQuery<IUser>
|
|
272
|
-
): Promise<{ modifiedCount: number }> {
|
|
273
|
-
const result = await User.updateMany(filter, update);
|
|
274
|
-
return { modifiedCount: result.modifiedCount };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Delete
|
|
278
|
-
async deleteById(id: string): Promise<IUser | null> {
|
|
279
|
-
return User.findByIdAndDelete(id);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async deleteMany(filter: FilterQuery<IUser>): Promise<{ deletedCount: number }> {
|
|
283
|
-
const result = await User.deleteMany(filter);
|
|
284
|
-
return { deletedCount: result.deletedCount };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Soft delete
|
|
288
|
-
async softDelete(id: string): Promise<IUser | null> {
|
|
289
|
-
return this.updateById(id, { isActive: false });
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Exists check
|
|
293
|
-
async exists(filter: FilterQuery<IUser>): Promise<boolean> {
|
|
294
|
-
const result = await User.exists(filter);
|
|
295
|
-
return !!result;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Count
|
|
299
|
-
async count(filter: FilterQuery<IUser> = {}): Promise<number> {
|
|
300
|
-
return User.countDocuments(filter);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### 3. Aggregation Pipelines
|
|
306
|
-
|
|
307
|
-
```typescript
|
|
308
|
-
// src/services/analytics.service.ts
|
|
309
|
-
import { User } from '../models/user.model';
|
|
310
|
-
import { Order } from '../models/order.model';
|
|
311
|
-
|
|
312
|
-
export class AnalyticsService {
|
|
313
|
-
// User statistics by role
|
|
314
|
-
async getUserStatsByRole() {
|
|
315
|
-
return User.aggregate([
|
|
316
|
-
{ $match: { isActive: true } },
|
|
317
|
-
{
|
|
318
|
-
$group: {
|
|
319
|
-
_id: '$role',
|
|
320
|
-
count: { $sum: 1 },
|
|
321
|
-
avgAddresses: { $avg: { $size: '$addresses' } },
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
{ $sort: { count: -1 } },
|
|
325
|
-
]);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Monthly user registrations
|
|
329
|
-
async getMonthlyRegistrations(year: number) {
|
|
330
|
-
return User.aggregate([
|
|
331
|
-
{
|
|
332
|
-
$match: {
|
|
333
|
-
createdAt: {
|
|
334
|
-
$gte: new Date(`${year}-01-01`),
|
|
335
|
-
$lt: new Date(`${year + 1}-01-01`),
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
$group: {
|
|
341
|
-
_id: { $month: '$createdAt' },
|
|
342
|
-
count: { $sum: 1 },
|
|
343
|
-
},
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
$project: {
|
|
347
|
-
_id: 0,
|
|
348
|
-
month: '$_id',
|
|
349
|
-
count: 1,
|
|
350
|
-
},
|
|
351
|
-
},
|
|
352
|
-
{ $sort: { month: 1 } },
|
|
353
|
-
]);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Order revenue by product category
|
|
357
|
-
async getRevenueByCategory(startDate: Date, endDate: Date) {
|
|
358
|
-
return Order.aggregate([
|
|
359
|
-
{
|
|
360
|
-
$match: {
|
|
361
|
-
status: 'completed',
|
|
362
|
-
createdAt: { $gte: startDate, $lte: endDate },
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
{ $unwind: '$items' },
|
|
366
|
-
{
|
|
367
|
-
$lookup: {
|
|
368
|
-
from: 'products',
|
|
369
|
-
localField: 'items.productId',
|
|
370
|
-
foreignField: '_id',
|
|
371
|
-
as: 'product',
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
{ $unwind: '$product' },
|
|
375
|
-
{
|
|
376
|
-
$group: {
|
|
377
|
-
_id: '$product.category',
|
|
378
|
-
totalRevenue: { $sum: { $multiply: ['$items.quantity', '$items.price'] } },
|
|
379
|
-
totalQuantity: { $sum: '$items.quantity' },
|
|
380
|
-
orderCount: { $sum: 1 },
|
|
381
|
-
},
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
$project: {
|
|
385
|
-
_id: 0,
|
|
386
|
-
category: '$_id',
|
|
387
|
-
totalRevenue: { $round: ['$totalRevenue', 2] },
|
|
388
|
-
totalQuantity: 1,
|
|
389
|
-
orderCount: 1,
|
|
390
|
-
avgOrderValue: {
|
|
391
|
-
$round: [{ $divide: ['$totalRevenue', '$orderCount'] }, 2],
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
},
|
|
395
|
-
{ $sort: { totalRevenue: -1 } },
|
|
396
|
-
]);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Top customers with order summary
|
|
400
|
-
async getTopCustomers(limit: number = 10) {
|
|
401
|
-
return Order.aggregate([
|
|
402
|
-
{ $match: { status: 'completed' } },
|
|
403
|
-
{
|
|
404
|
-
$group: {
|
|
405
|
-
_id: '$userId',
|
|
406
|
-
totalOrders: { $sum: 1 },
|
|
407
|
-
totalSpent: { $sum: '$total' },
|
|
408
|
-
avgOrderValue: { $avg: '$total' },
|
|
409
|
-
lastOrderDate: { $max: '$createdAt' },
|
|
410
|
-
},
|
|
411
|
-
},
|
|
412
|
-
{
|
|
413
|
-
$lookup: {
|
|
414
|
-
from: 'users',
|
|
415
|
-
localField: '_id',
|
|
416
|
-
foreignField: '_id',
|
|
417
|
-
as: 'user',
|
|
418
|
-
},
|
|
419
|
-
},
|
|
420
|
-
{ $unwind: '$user' },
|
|
421
|
-
{
|
|
422
|
-
$project: {
|
|
423
|
-
_id: 0,
|
|
424
|
-
userId: '$_id',
|
|
425
|
-
email: '$user.email',
|
|
426
|
-
name: {
|
|
427
|
-
$concat: ['$user.profile.firstName', ' ', '$user.profile.lastName'],
|
|
428
|
-
},
|
|
429
|
-
totalOrders: 1,
|
|
430
|
-
totalSpent: { $round: ['$totalSpent', 2] },
|
|
431
|
-
avgOrderValue: { $round: ['$avgOrderValue', 2] },
|
|
432
|
-
lastOrderDate: 1,
|
|
433
|
-
},
|
|
434
|
-
},
|
|
435
|
-
{ $sort: { totalSpent: -1 } },
|
|
436
|
-
{ $limit: limit },
|
|
437
|
-
]);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Search with text score
|
|
441
|
-
async searchProducts(query: string, options: { category?: string; limit?: number }) {
|
|
442
|
-
const pipeline: any[] = [
|
|
443
|
-
{
|
|
444
|
-
$search: {
|
|
445
|
-
index: 'products_search',
|
|
446
|
-
text: {
|
|
447
|
-
query,
|
|
448
|
-
path: ['name', 'description', 'tags'],
|
|
449
|
-
fuzzy: { maxEdits: 1 },
|
|
450
|
-
},
|
|
451
|
-
},
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
$addFields: {
|
|
455
|
-
score: { $meta: 'searchScore' },
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
];
|
|
459
|
-
|
|
460
|
-
if (options.category) {
|
|
461
|
-
pipeline.push({ $match: { category: options.category } });
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
pipeline.push(
|
|
465
|
-
{ $sort: { score: -1 } },
|
|
466
|
-
{ $limit: options.limit || 20 }
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
return Product.aggregate(pipeline);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
29
|
+
export const User = mongoose.model<IUser>('User', userSchema);
|
|
472
30
|
```
|
|
473
31
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
```typescript
|
|
477
|
-
// src/services/order.service.ts
|
|
478
|
-
import mongoose from 'mongoose';
|
|
479
|
-
import { Order } from '../models/order.model';
|
|
480
|
-
import { Product } from '../models/product.model';
|
|
481
|
-
import { User } from '../models/user.model';
|
|
482
|
-
|
|
483
|
-
export class OrderService {
|
|
484
|
-
async createOrder(userId: string, items: Array<{ productId: string; quantity: number }>) {
|
|
485
|
-
const session = await mongoose.startSession();
|
|
486
|
-
|
|
487
|
-
try {
|
|
488
|
-
session.startTransaction();
|
|
489
|
-
|
|
490
|
-
// Validate user
|
|
491
|
-
const user = await User.findById(userId).session(session);
|
|
492
|
-
if (!user) {
|
|
493
|
-
throw new Error('User not found');
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Calculate order total and validate stock
|
|
497
|
-
let total = 0;
|
|
498
|
-
const orderItems = [];
|
|
499
|
-
|
|
500
|
-
for (const item of items) {
|
|
501
|
-
const product = await Product.findById(item.productId).session(session);
|
|
502
|
-
if (!product) {
|
|
503
|
-
throw new Error(`Product ${item.productId} not found`);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (product.stock < item.quantity) {
|
|
507
|
-
throw new Error(`Insufficient stock for ${product.name}`);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Decrement stock
|
|
511
|
-
await Product.updateOne(
|
|
512
|
-
{ _id: item.productId },
|
|
513
|
-
{ $inc: { stock: -item.quantity } },
|
|
514
|
-
{ session }
|
|
515
|
-
);
|
|
516
|
-
|
|
517
|
-
const itemTotal = product.price * item.quantity;
|
|
518
|
-
total += itemTotal;
|
|
519
|
-
|
|
520
|
-
orderItems.push({
|
|
521
|
-
productId: product._id,
|
|
522
|
-
name: product.name,
|
|
523
|
-
price: product.price,
|
|
524
|
-
quantity: item.quantity,
|
|
525
|
-
total: itemTotal,
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Create order
|
|
530
|
-
const [order] = await Order.create([{
|
|
531
|
-
userId,
|
|
532
|
-
items: orderItems,
|
|
533
|
-
total,
|
|
534
|
-
status: 'pending',
|
|
535
|
-
}], { session });
|
|
536
|
-
|
|
537
|
-
await session.commitTransaction();
|
|
538
|
-
return order;
|
|
539
|
-
} catch (error) {
|
|
540
|
-
await session.abortTransaction();
|
|
541
|
-
throw error;
|
|
542
|
-
} finally {
|
|
543
|
-
session.endSession();
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
async cancelOrder(orderId: string) {
|
|
548
|
-
const session = await mongoose.startSession();
|
|
549
|
-
|
|
550
|
-
try {
|
|
551
|
-
session.startTransaction();
|
|
552
|
-
|
|
553
|
-
const order = await Order.findById(orderId).session(session);
|
|
554
|
-
if (!order) {
|
|
555
|
-
throw new Error('Order not found');
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
if (order.status !== 'pending') {
|
|
559
|
-
throw new Error('Only pending orders can be cancelled');
|
|
560
|
-
}
|
|
32
|
+
## Features
|
|
561
33
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
34
|
+
| Feature | Description | Guide |
|
|
35
|
+
|---------|-------------|-------|
|
|
36
|
+
| Document Schema | Type-safe schema design with Mongoose | Embed related data, use references for large collections |
|
|
37
|
+
| CRUD Operations | Create, read, update, delete with type safety | Use `findById`, `findOne`, `updateOne`, `deleteOne` |
|
|
38
|
+
| Aggregation Pipelines | Complex data transformations and analytics | Chain `$match`, `$group`, `$lookup`, `$project` stages |
|
|
39
|
+
| Indexing | Query optimization with proper indexes | Create compound indexes matching query patterns |
|
|
40
|
+
| Transactions | Multi-document ACID operations | Use sessions for operations requiring atomicity |
|
|
41
|
+
| Change Streams | Real-time data change notifications | Watch collections for inserts, updates, deletes |
|
|
570
42
|
|
|
571
|
-
|
|
572
|
-
order.status = 'cancelled';
|
|
573
|
-
await order.save({ session });
|
|
43
|
+
## Common Patterns
|
|
574
44
|
|
|
575
|
-
|
|
576
|
-
return order;
|
|
577
|
-
} catch (error) {
|
|
578
|
-
await session.abortTransaction();
|
|
579
|
-
throw error;
|
|
580
|
-
} finally {
|
|
581
|
-
session.endSession();
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
### 5. Connection and Configuration
|
|
45
|
+
### Repository Pattern with Pagination
|
|
588
46
|
|
|
589
47
|
```typescript
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
export async function connectDatabase(config: DatabaseConfig): Promise<void> {
|
|
599
|
-
const defaultOptions: mongoose.ConnectOptions = {
|
|
600
|
-
maxPoolSize: 10,
|
|
601
|
-
serverSelectionTimeoutMS: 5000,
|
|
602
|
-
socketTimeoutMS: 45000,
|
|
603
|
-
family: 4,
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
mongoose.set('strictQuery', true);
|
|
607
|
-
|
|
608
|
-
mongoose.connection.on('connected', () => {
|
|
609
|
-
console.log('MongoDB connected successfully');
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
mongoose.connection.on('error', (err) => {
|
|
613
|
-
console.error('MongoDB connection error:', err);
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
mongoose.connection.on('disconnected', () => {
|
|
617
|
-
console.log('MongoDB disconnected');
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
process.on('SIGINT', async () => {
|
|
621
|
-
await mongoose.connection.close();
|
|
622
|
-
process.exit(0);
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
await mongoose.connect(config.uri, {
|
|
626
|
-
...defaultOptions,
|
|
627
|
-
...config.options,
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
export async function disconnectDatabase(): Promise<void> {
|
|
632
|
-
await mongoose.connection.close();
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Health check
|
|
636
|
-
export async function checkDatabaseHealth(): Promise<boolean> {
|
|
637
|
-
try {
|
|
638
|
-
await mongoose.connection.db.admin().ping();
|
|
639
|
-
return true;
|
|
640
|
-
} catch {
|
|
641
|
-
return false;
|
|
642
|
-
}
|
|
48
|
+
async findPaginated(filter: FilterQuery<IUser>, page = 1, limit = 20) {
|
|
49
|
+
const [data, total] = await Promise.all([
|
|
50
|
+
User.find(filter).sort({ createdAt: -1 }).skip((page - 1) * limit).limit(limit),
|
|
51
|
+
User.countDocuments(filter),
|
|
52
|
+
]);
|
|
53
|
+
return { data, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) } };
|
|
643
54
|
}
|
|
644
55
|
```
|
|
645
56
|
|
|
646
|
-
###
|
|
647
|
-
|
|
648
|
-
```javascript
|
|
649
|
-
// Creating indexes for optimal query performance
|
|
650
|
-
|
|
651
|
-
// Compound index for common query patterns
|
|
652
|
-
db.orders.createIndex(
|
|
653
|
-
{ userId: 1, status: 1, createdAt: -1 },
|
|
654
|
-
{ name: 'user_status_date' }
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
// Partial index for active records only
|
|
658
|
-
db.users.createIndex(
|
|
659
|
-
{ email: 1 },
|
|
660
|
-
{
|
|
661
|
-
unique: true,
|
|
662
|
-
partialFilterExpression: { isActive: true },
|
|
663
|
-
name: 'active_users_email'
|
|
664
|
-
}
|
|
665
|
-
);
|
|
666
|
-
|
|
667
|
-
// TTL index for expiring documents
|
|
668
|
-
db.sessions.createIndex(
|
|
669
|
-
{ expiresAt: 1 },
|
|
670
|
-
{ expireAfterSeconds: 0, name: 'session_ttl' }
|
|
671
|
-
);
|
|
672
|
-
|
|
673
|
-
// Text index for full-text search
|
|
674
|
-
db.products.createIndex(
|
|
675
|
-
{ name: 'text', description: 'text', tags: 'text' },
|
|
676
|
-
{
|
|
677
|
-
weights: { name: 10, tags: 5, description: 1 },
|
|
678
|
-
name: 'product_search'
|
|
679
|
-
}
|
|
680
|
-
);
|
|
681
|
-
|
|
682
|
-
// Geospatial index
|
|
683
|
-
db.stores.createIndex(
|
|
684
|
-
{ location: '2dsphere' },
|
|
685
|
-
{ name: 'store_location' }
|
|
686
|
-
);
|
|
687
|
-
|
|
688
|
-
// Wildcard index for dynamic fields
|
|
689
|
-
db.logs.createIndex(
|
|
690
|
-
{ 'metadata.$**': 1 },
|
|
691
|
-
{ name: 'log_metadata_wildcard' }
|
|
692
|
-
);
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
## Use Cases
|
|
696
|
-
|
|
697
|
-
### Real-time Analytics Dashboard
|
|
57
|
+
### Aggregation Pipeline
|
|
698
58
|
|
|
699
59
|
```typescript
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
createdAt: { $gte: timeRange.start, $lte: timeRange.end },
|
|
707
|
-
},
|
|
708
|
-
},
|
|
709
|
-
{
|
|
710
|
-
$group: {
|
|
711
|
-
_id: '$status',
|
|
712
|
-
count: { $sum: 1 },
|
|
713
|
-
total: { $sum: '$total' },
|
|
714
|
-
},
|
|
715
|
-
},
|
|
716
|
-
]),
|
|
717
|
-
|
|
718
|
-
User.aggregate([
|
|
719
|
-
{
|
|
720
|
-
$facet: {
|
|
721
|
-
total: [{ $count: 'count' }],
|
|
722
|
-
newUsers: [
|
|
723
|
-
{
|
|
724
|
-
$match: {
|
|
725
|
-
createdAt: { $gte: timeRange.start, $lte: timeRange.end },
|
|
726
|
-
},
|
|
727
|
-
},
|
|
728
|
-
{ $count: 'count' },
|
|
729
|
-
],
|
|
730
|
-
byRole: [{ $group: { _id: '$role', count: { $sum: 1 } } }],
|
|
731
|
-
},
|
|
732
|
-
},
|
|
733
|
-
]),
|
|
734
|
-
|
|
735
|
-
Order.aggregate([
|
|
736
|
-
{
|
|
737
|
-
$match: {
|
|
738
|
-
status: 'completed',
|
|
739
|
-
createdAt: { $gte: timeRange.start, $lte: timeRange.end },
|
|
740
|
-
},
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
$group: {
|
|
744
|
-
_id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } },
|
|
745
|
-
revenue: { $sum: '$total' },
|
|
746
|
-
orders: { $sum: 1 },
|
|
747
|
-
},
|
|
748
|
-
},
|
|
749
|
-
{ $sort: { _id: 1 } },
|
|
750
|
-
]),
|
|
751
|
-
]);
|
|
752
|
-
|
|
753
|
-
return { orderStats, userStats: userStats[0], revenueByDay };
|
|
754
|
-
}
|
|
60
|
+
const stats = await Order.aggregate([
|
|
61
|
+
{ $match: { status: 'completed', createdAt: { $gte: startDate } } },
|
|
62
|
+
{ $group: { _id: '$userId', total: { $sum: '$amount' }, count: { $sum: 1 } } },
|
|
63
|
+
{ $sort: { total: -1 } },
|
|
64
|
+
{ $limit: 10 },
|
|
65
|
+
]);
|
|
755
66
|
```
|
|
756
67
|
|
|
757
|
-
###
|
|
68
|
+
### Transaction for Order Creation
|
|
758
69
|
|
|
759
70
|
```typescript
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
], {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
return () => changeStream.close();
|
|
71
|
+
const session = await mongoose.startSession();
|
|
72
|
+
try {
|
|
73
|
+
session.startTransaction();
|
|
74
|
+
await Product.updateOne({ _id: productId }, { $inc: { stock: -quantity } }, { session });
|
|
75
|
+
const order = await Order.create([{ userId, items, total }], { session });
|
|
76
|
+
await session.commitTransaction();
|
|
77
|
+
return order[0];
|
|
78
|
+
} catch (error) {
|
|
79
|
+
await session.abortTransaction();
|
|
80
|
+
throw error;
|
|
81
|
+
} finally {
|
|
82
|
+
session.endSession();
|
|
775
83
|
}
|
|
776
84
|
```
|
|
777
85
|
|
|
778
86
|
## Best Practices
|
|
779
87
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
- Use aggregation for complex queries
|
|
790
|
-
- Monitor slow queries
|
|
791
|
-
- Set up replica sets for production
|
|
792
|
-
|
|
793
|
-
### Don'ts
|
|
794
|
-
|
|
795
|
-
- Don't embed large arrays in documents
|
|
796
|
-
- Don't create too many indexes
|
|
797
|
-
- Don't use $where or mapReduce
|
|
798
|
-
- Don't ignore index usage in queries
|
|
799
|
-
- Don't skip validation
|
|
800
|
-
- Don't store large files directly
|
|
801
|
-
- Don't use synchronous operations
|
|
802
|
-
- Don't ignore connection errors
|
|
803
|
-
- Don't hardcode connection strings
|
|
804
|
-
- Don't skip backups
|
|
805
|
-
|
|
806
|
-
## References
|
|
807
|
-
|
|
808
|
-
- [MongoDB Documentation](https://docs.mongodb.com/)
|
|
809
|
-
- [Mongoose Documentation](https://mongoosejs.com/docs/)
|
|
810
|
-
- [MongoDB University](https://university.mongodb.com/)
|
|
811
|
-
- [MongoDB Performance Best Practices](https://www.mongodb.com/docs/manual/administration/analyzing-mongodb-performance/)
|
|
812
|
-
- [Schema Design Patterns](https://www.mongodb.com/blog/post/building-with-patterns-a-summary)
|
|
88
|
+
| Do | Avoid |
|
|
89
|
+
|----|-------|
|
|
90
|
+
| Design schemas based on query patterns | Embedding large arrays in documents |
|
|
91
|
+
| Create indexes for frequently queried fields | Using `$where` or mapReduce in production |
|
|
92
|
+
| Use `lean()` for read-only queries | Skipping validation on writes |
|
|
93
|
+
| Implement pagination for large datasets | Storing large files directly (use GridFS) |
|
|
94
|
+
| Set connection pool size appropriately | Hardcoding connection strings |
|
|
95
|
+
| Use transactions for multi-document ops | Ignoring index usage in explain plans |
|
|
96
|
+
| Add TTL indexes for expiring data | Creating too many indexes (write overhead) |
|