omgkit 2.1.1 → 2.2.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/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,43 +1,812 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mongodb
|
|
3
|
-
description: MongoDB database
|
|
3
|
+
description: MongoDB NoSQL database with document modeling, aggregation pipelines, indexing, and Mongoose ODM
|
|
4
|
+
category: databases
|
|
5
|
+
triggers:
|
|
6
|
+
- mongodb
|
|
7
|
+
- mongo
|
|
8
|
+
- mongoose
|
|
9
|
+
- nosql
|
|
10
|
+
- document database
|
|
11
|
+
- bson
|
|
12
|
+
- aggregation
|
|
4
13
|
---
|
|
5
14
|
|
|
6
|
-
# MongoDB
|
|
15
|
+
# MongoDB
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
Enterprise-grade **MongoDB NoSQL database** development following industry best practices. This skill covers document modeling, CRUD operations, aggregation pipelines, indexing strategies, Mongoose ODM, transactions, and production-ready patterns used by top engineering teams.
|
|
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
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// src/models/user.model.ts
|
|
37
|
+
import mongoose, { Schema, Document, Model } from 'mongoose';
|
|
38
|
+
|
|
39
|
+
// TypeScript interfaces
|
|
40
|
+
export interface IAddress {
|
|
41
|
+
street: string;
|
|
42
|
+
city: string;
|
|
43
|
+
state: string;
|
|
44
|
+
zipCode: string;
|
|
45
|
+
country: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface IUser extends Document {
|
|
49
|
+
email: string;
|
|
50
|
+
password: string;
|
|
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;
|
|
61
|
+
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
|
+
}
|
|
74
|
+
|
|
75
|
+
// Schema definition
|
|
76
|
+
const addressSchema = new Schema<IAddress>({
|
|
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
|
+
},
|
|
13
102
|
profile: {
|
|
14
|
-
|
|
15
|
-
|
|
103
|
+
firstName: { type: String, required: true, trim: true },
|
|
104
|
+
lastName: { type: String, required: true, trim: true },
|
|
105
|
+
avatar: String,
|
|
106
|
+
bio: { type: String, maxlength: 500 },
|
|
107
|
+
},
|
|
108
|
+
addresses: [addressSchema],
|
|
109
|
+
role: {
|
|
110
|
+
type: String,
|
|
111
|
+
enum: ['admin', 'user', 'guest'],
|
|
112
|
+
default: 'user',
|
|
16
113
|
},
|
|
17
|
-
|
|
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
|
+
});
|
|
127
|
+
|
|
128
|
+
// Indexes
|
|
129
|
+
userSchema.index({ email: 1 });
|
|
130
|
+
userSchema.index({ 'profile.firstName': 1, 'profile.lastName': 1 });
|
|
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
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 4. Transactions
|
|
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
|
+
}
|
|
561
|
+
|
|
562
|
+
// Restore stock
|
|
563
|
+
for (const item of order.items) {
|
|
564
|
+
await Product.updateOne(
|
|
565
|
+
{ _id: item.productId },
|
|
566
|
+
{ $inc: { stock: item.quantity } },
|
|
567
|
+
{ session }
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Update order status
|
|
572
|
+
order.status = 'cancelled';
|
|
573
|
+
await order.save({ session });
|
|
574
|
+
|
|
575
|
+
await session.commitTransaction();
|
|
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
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
// src/config/database.ts
|
|
591
|
+
import mongoose from 'mongoose';
|
|
592
|
+
|
|
593
|
+
interface DatabaseConfig {
|
|
594
|
+
uri: string;
|
|
595
|
+
options?: mongoose.ConnectOptions;
|
|
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
|
+
}
|
|
18
643
|
}
|
|
19
644
|
```
|
|
20
645
|
|
|
21
|
-
|
|
646
|
+
### 6. Indexing Strategies
|
|
647
|
+
|
|
22
648
|
```javascript
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
{ $group: { _id: "$role", count: { $sum: 1 } } }
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
|
-
// Update
|
|
33
|
-
db.users.updateOne(
|
|
34
|
-
{ _id: id },
|
|
35
|
-
{ $set: { name: "New Name" } }
|
|
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' }
|
|
36
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
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
// Aggregation for dashboard metrics
|
|
701
|
+
async function getDashboardMetrics(timeRange: { start: Date; end: Date }) {
|
|
702
|
+
const [orderStats, userStats, revenueByDay] = await Promise.all([
|
|
703
|
+
Order.aggregate([
|
|
704
|
+
{
|
|
705
|
+
$match: {
|
|
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
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### Change Streams for Real-time Updates
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
// Watch for real-time changes
|
|
761
|
+
async function watchOrderChanges(callback: (change: any) => void) {
|
|
762
|
+
const changeStream = Order.watch([
|
|
763
|
+
{ $match: { operationType: { $in: ['insert', 'update'] } } },
|
|
764
|
+
], { fullDocument: 'updateLookup' });
|
|
765
|
+
|
|
766
|
+
changeStream.on('change', (change) => {
|
|
767
|
+
callback({
|
|
768
|
+
type: change.operationType,
|
|
769
|
+
document: change.fullDocument,
|
|
770
|
+
timestamp: change.clusterTime,
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
return () => changeStream.close();
|
|
775
|
+
}
|
|
37
776
|
```
|
|
38
777
|
|
|
39
778
|
## Best Practices
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
779
|
+
|
|
780
|
+
### Do's
|
|
781
|
+
|
|
782
|
+
- Design schemas based on query patterns
|
|
783
|
+
- Use appropriate indexes for queries
|
|
784
|
+
- Implement proper error handling
|
|
785
|
+
- Use transactions for multi-document operations
|
|
786
|
+
- Set up connection pooling
|
|
787
|
+
- Use lean() for read-only queries
|
|
788
|
+
- Implement pagination for large datasets
|
|
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)
|