ginskill-init 1.0.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.
Files changed (92) hide show
  1. package/README.md +77 -0
  2. package/agents/developer.md +56 -0
  3. package/agents/frontend-design.md +69 -0
  4. package/agents/mobile-reviewer.md +36 -0
  5. package/agents/review-code.md +49 -0
  6. package/agents/security-scanner.md +50 -0
  7. package/agents/tester.md +72 -0
  8. package/bin/cli.js +226 -0
  9. package/package.json +20 -0
  10. package/skills/ai-asset-generator/SKILL.md +255 -0
  11. package/skills/ai-asset-generator/docs/gen-image.md +274 -0
  12. package/skills/ai-asset-generator/docs/genvideo.md +341 -0
  13. package/skills/ai-asset-generator/docs/remove-background.md +19 -0
  14. package/skills/ai-asset-generator/generate-credit-assets.mjs +180 -0
  15. package/skills/ai-asset-generator/generate-ginbrowser-assets.mjs +242 -0
  16. package/skills/ai-asset-generator/generate-sty-icon.mjs +149 -0
  17. package/skills/ai-asset-generator/lib/bg-remove.mjs +34 -0
  18. package/skills/ai-asset-generator/lib/env.mjs +38 -0
  19. package/skills/ai-asset-generator/lib/kie-client.mjs +88 -0
  20. package/skills/ai-asset-generator/scripts/scaffold-generator.mjs +203 -0
  21. package/skills/ai-build-ai/SKILL.md +124 -0
  22. package/skills/ai-build-ai/docs/agent-teams.md +293 -0
  23. package/skills/ai-build-ai/docs/checkpointing.md +161 -0
  24. package/skills/ai-build-ai/docs/create-agent.md +399 -0
  25. package/skills/ai-build-ai/docs/create-mcp.md +395 -0
  26. package/skills/ai-build-ai/docs/create-skill.md +299 -0
  27. package/skills/ai-build-ai/docs/headless-mode.md +614 -0
  28. package/skills/ai-build-ai/docs/hooks.md +578 -0
  29. package/skills/ai-build-ai/docs/memory-claude-md.md +375 -0
  30. package/skills/ai-build-ai/docs/output-styles.md +208 -0
  31. package/skills/ai-build-ai/docs/overview.md +162 -0
  32. package/skills/ai-build-ai/docs/permissions.md +391 -0
  33. package/skills/ai-build-ai/docs/plugins.md +396 -0
  34. package/skills/ai-build-ai/docs/sandbox.md +262 -0
  35. package/skills/ai-build-ai/scripts/load-tutorial.sh +54 -0
  36. package/skills/icon-generator/SKILL.md +270 -0
  37. package/skills/mobile-app-review/SKILL.md +321 -0
  38. package/skills/mobile-app-review/references/apple-review.md +132 -0
  39. package/skills/mobile-app-review/references/google-play-review.md +203 -0
  40. package/skills/mongodb/SKILL.md +667 -0
  41. package/skills/mongodb/references/mongoose-patterns.md +368 -0
  42. package/skills/nestjs-architecture/SKILL.md +1086 -0
  43. package/skills/nestjs-architecture/references/advanced-patterns.md +590 -0
  44. package/skills/performance/SKILL.md +509 -0
  45. package/skills/react-fsd-architecture/SKILL.md +693 -0
  46. package/skills/react-fsd-architecture/references/fsd-patterns.md +747 -0
  47. package/skills/react-query/SKILL.md +685 -0
  48. package/skills/react-query/references/query-patterns.md +365 -0
  49. package/skills/review-code/SKILL.md +321 -0
  50. package/skills/review-code/references/clean-code-principles.md +395 -0
  51. package/skills/review-code/references/frontend-patterns.md +136 -0
  52. package/skills/review-code/references/nestjs-patterns.md +184 -0
  53. package/skills/review-code/scripts/check-module.sh +201 -0
  54. package/skills/review-code/scripts/deep-scan.sh +604 -0
  55. package/skills/review-code/scripts/dep-check.sh +522 -0
  56. package/skills/review-code/scripts/detect-duplicates.sh +466 -0
  57. package/skills/review-code/scripts/format-check.sh +577 -0
  58. package/skills/review-code/scripts/run-review.sh +167 -0
  59. package/skills/review-code/scripts/scan-codebase.sh +152 -0
  60. package/skills/security-scanner/SKILL.md +327 -0
  61. package/skills/security-scanner/references/nestjs-security.md +260 -0
  62. package/skills/security-scanner/references/nextjs-security.md +201 -0
  63. package/skills/security-scanner/references/react-native-security.md +199 -0
  64. package/skills/security-scanner/scripts/security-scan.sh +478 -0
  65. package/skills/ui-ux-pro-max/SKILL.md +377 -0
  66. package/skills/ui-ux-pro-max/data/charts.csv +26 -0
  67. package/skills/ui-ux-pro-max/data/colors.csv +97 -0
  68. package/skills/ui-ux-pro-max/data/icons.csv +101 -0
  69. package/skills/ui-ux-pro-max/data/landing.csv +31 -0
  70. package/skills/ui-ux-pro-max/data/products.csv +97 -0
  71. package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  72. package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  73. package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  74. package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  75. package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  76. package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  77. package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  78. package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  79. package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  80. package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  81. package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  82. package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  83. package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  84. package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  85. package/skills/ui-ux-pro-max/data/styles.csv +68 -0
  86. package/skills/ui-ux-pro-max/data/typography.csv +58 -0
  87. package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  88. package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  89. package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  90. package/skills/ui-ux-pro-max/scripts/core.py +253 -0
  91. package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  92. package/skills/ui-ux-pro-max/scripts/search.py +114 -0
@@ -0,0 +1,667 @@
1
+ ---
2
+ name: mongodb
3
+ description: |
4
+ **MongoDB & Mongoose Best Practices**: Production patterns for schema design, indexing, aggregation pipelines, transactions, connection management, and common pitfalls — with NestJS/Mongoose focus.
5
+ - MANDATORY TRIGGERS: mongodb, mongoose, mongo, schema design, embedding vs referencing, compound index, aggregation pipeline, $lookup, $match, $group, populate, lean, mongoose query, nosql, mongodb performance, mongodb index, mongodb transaction, mongoose schema, mongoose plugin, mongoose virtual, mongoose discriminator, bucket pattern, outlier pattern, computed pattern, subset pattern, mongodb connection pool, mongodb replica set, insertMany, bulkWrite, mongodb atlas
6
+ - Use this skill whenever the user is designing MongoDB schemas, writing Mongoose queries, building aggregation pipelines, debugging MongoDB performance, or reviewing MongoDB/Mongoose code. Also trigger when discussing data modeling patterns, index optimization, transaction safety, or connection tuning.
7
+ ---
8
+
9
+ # MongoDB & Mongoose — Best Practices & Patterns
10
+
11
+ Production-ready patterns for MongoDB 7+ and Mongoose 8+ with NestJS. Covers schema design, indexing strategy, aggregation pipelines, transactions, connection management, and common anti-patterns.
12
+
13
+ ## Core Mental Model
14
+
15
+ **MongoDB is not a relational database with JSON syntax.** Model your data to match how your application reads and writes it — not how you would normalize tables. The biggest performance wins (and losses) happen at the schema design stage, not at query time.
16
+
17
+ Key implications:
18
+ - Embedding is the default; reference only when you have a reason
19
+ - Reads are fast when the working set fits in RAM — keep documents lean
20
+ - Every index speeds reads but slows writes — be deliberate
21
+ - Aggregation pipelines are your SQL replacement — learn the stage ordering
22
+
23
+ ## Schema Design: Embedding vs Referencing
24
+
25
+ ### Decision Framework
26
+
27
+ | Situation | Strategy | Why |
28
+ |-----------|----------|-----|
29
+ | Data always accessed together, bounded size | **Embed** | Single read, atomic writes |
30
+ | Data accessed independently or shared | **Reference** | Avoids duplication, separate lifecycle |
31
+ | Read-heavy, rarely-updated child field | **Denormalize** (copy field) | Eliminates join on hot path |
32
+ | > ~100 items on "many" side | **Reference array** | Array growth impacts performance |
33
+ | > ~thousands on "many" side | **Parent reference** (child stores parent ID) | Unbounded arrays hit 16MB limit |
34
+
35
+ ### One-to-Few: Embed
36
+
37
+ ```typescript
38
+ // Addresses on a user — bounded, always fetched together
39
+ @Schema({ timestamps: true })
40
+ export class User {
41
+ @Prop({ required: true })
42
+ name: string
43
+
44
+ @Prop({
45
+ type: [{
46
+ street: String,
47
+ city: String,
48
+ zipCode: String,
49
+ isDefault: { type: Boolean, default: false },
50
+ }],
51
+ default: [],
52
+ })
53
+ addresses: Array<{ street: string; city: string; zipCode: string; isDefault: boolean }>
54
+ }
55
+ ```
56
+
57
+ ### One-to-Many: Reference Array
58
+
59
+ ```typescript
60
+ // Product has parts — parts are accessed independently, shared across products
61
+ @Schema({ timestamps: true })
62
+ export class Product {
63
+ @Prop({ required: true })
64
+ name: string
65
+
66
+ @Prop({ type: [{ type: Types.ObjectId, ref: 'Part' }], default: [] })
67
+ parts: Types.ObjectId[] // bounded — a product has ~10-50 parts
68
+ }
69
+ ```
70
+
71
+ ### One-to-Squillions: Parent Reference
72
+
73
+ ```typescript
74
+ // Log messages for a host — unbounded, query from child side
75
+ @Schema({ timestamps: true })
76
+ export class LogMessage {
77
+ @Prop({ required: true })
78
+ message: string
79
+
80
+ @Prop({ type: Types.ObjectId, ref: 'Host', required: true, index: true })
81
+ host: Types.ObjectId // child stores parent reference
82
+ }
83
+
84
+ // Query: find recent logs for a host
85
+ await this.logModel.find({ host: hostId }).sort({ createdAt: -1 }).limit(100).lean()
86
+ ```
87
+
88
+ ### Extended Reference (Denormalize for Display)
89
+
90
+ ```typescript
91
+ // Order embeds frequently-read customer fields to avoid $lookup
92
+ @Schema({ timestamps: true })
93
+ export class Order {
94
+ @Prop({ type: Types.ObjectId, ref: 'User', required: true, index: true })
95
+ userId: Types.ObjectId
96
+
97
+ // Denormalized for display — avoids populate on order list
98
+ @Prop({ type: { name: String, email: String } })
99
+ customer: { name: string; email: string }
100
+
101
+ @Prop({ required: true })
102
+ total: number
103
+ }
104
+ // Trade-off: update denormalized copy when user name/email changes
105
+ ```
106
+
107
+ ## Indexing Strategy
108
+
109
+ ### The ESR Rule: Equality → Sort → Range
110
+
111
+ Order compound index fields as **E**quality, **S**ort, **R**ange — this is the single most important indexing principle.
112
+
113
+ ```typescript
114
+ // Query: active orders, sorted by date, amount in range
115
+ // db.orders.find({ status: 'active', amount: { $gte: 100 } }).sort({ createdAt: -1 })
116
+
117
+ // CORRECT — ESR order:
118
+ OrderSchema.index({ status: 1, createdAt: -1, amount: 1 })
119
+ // ^Equality ^Sort ^Range
120
+
121
+ // WRONG — range before sort causes in-memory sort:
122
+ OrderSchema.index({ status: 1, amount: 1, createdAt: -1 })
123
+ ```
124
+
125
+ **Why:** Equality narrows the dataset. Sort fields served from index (no blocking sort). Range breaks sort ordering, so it must trail.
126
+
127
+ ### Covered Queries
128
+
129
+ A query answered entirely from the index — `totalDocsExamined: 0` in explain.
130
+
131
+ ```typescript
132
+ // Index covers all query + sort + projection fields
133
+ UserSchema.index({ email: 1, status: 1, firstName: 1, lastName: 1 })
134
+
135
+ // Covered query — _id must be explicitly excluded
136
+ await this.userModel
137
+ .find({ email: 'test@example.com', status: 'active' })
138
+ .select({ _id: 0, firstName: 1, lastName: 1 })
139
+ .lean()
140
+ ```
141
+
142
+ ### Partial Indexes
143
+
144
+ Index only documents matching a filter — smaller index, faster writes.
145
+
146
+ ```typescript
147
+ // Index only active orders — much smaller than full index
148
+ OrderSchema.index(
149
+ { customerId: 1, createdAt: -1 },
150
+ { partialFilterExpression: { status: 'active' } }
151
+ )
152
+
153
+ // Index only where field exists
154
+ UserSchema.index(
155
+ { phoneNumber: 1 },
156
+ { partialFilterExpression: { phoneNumber: { $exists: true } } }
157
+ )
158
+ ```
159
+
160
+ ### TTL Indexes (Auto-Delete)
161
+
162
+ ```typescript
163
+ // Sessions expire 24 hours after creation
164
+ @Prop({ type: Date, default: Date.now, expires: '24h' })
165
+ createdAt: Date
166
+ // Mongoose handles creating the TTL index from `expires`
167
+ ```
168
+
169
+ ### Text Indexes
170
+
171
+ ```typescript
172
+ // Full-text search with field weights
173
+ ArticleSchema.index(
174
+ { title: 'text', body: 'text', tags: 'text' },
175
+ { weights: { title: 10, tags: 5, body: 1 } }
176
+ )
177
+
178
+ // Search
179
+ await this.articleModel
180
+ .find({ $text: { $search: 'mongodb performance' } })
181
+ .select({ score: { $meta: 'textScore' } })
182
+ .sort({ score: { $meta: 'textScore' } })
183
+ .lean()
184
+ ```
185
+
186
+ > For production full-text search, prefer MongoDB Atlas Search (Lucene-backed) — supports fuzzy matching, synonyms, autocomplete, and facets.
187
+
188
+ ### Case-Insensitive Queries
189
+
190
+ ```typescript
191
+ // BAD: regex disables index (or causes IXSCAN with slow regex)
192
+ await this.userModel.find({ email: /^john@example\.com$/i })
193
+
194
+ // GOOD: collation-based case-insensitive index
195
+ UserSchema.index({ email: 1 }, { collation: { locale: 'en', strength: 2 } })
196
+
197
+ await this.userModel
198
+ .find({ email: 'John@Example.Com' })
199
+ .collation({ locale: 'en', strength: 2 })
200
+ .lean()
201
+ ```
202
+
203
+ ### Index Hygiene Rules
204
+
205
+ 1. **Never index low-cardinality booleans alone** — `isActive: 1` is useless alone (50% selectivity). Use as part of a compound index.
206
+ 2. **Audit unused indexes** — `db.collection.aggregate([{ $indexStats: {} }])`. Each index slows every write.
207
+ 3. **Every write updates every covering index** — more indexes = slower writes.
208
+ 4. **Working set must fit in RAM** — if indexes exceed available RAM, performance collapses.
209
+
210
+ ## Aggregation Pipelines
211
+
212
+ ### Golden Rule: Filter Early, Reshape Early, Join Late
213
+
214
+ ```typescript
215
+ // ANTI-PATTERN: $lookup before $match
216
+ const result = await this.orderModel.aggregate([
217
+ { $lookup: { from: 'users', localField: 'userId', foreignField: '_id', as: 'user' } },
218
+ { $match: { status: 'shipped' } }, // too late — joined everything first
219
+ ])
220
+
221
+ // CORRECT: $match first, $project to trim, $lookup last
222
+ const result = await this.orderModel.aggregate([
223
+ // 1. Filter first — uses index
224
+ { $match: { status: 'shipped', createdAt: { $gte: startDate } } },
225
+
226
+ // 2. Project only needed fields
227
+ { $project: { userId: 1, total: 1, items: 1 } },
228
+
229
+ // 3. Group before lookup if applicable
230
+ { $group: { _id: '$userId', orderCount: { $sum: 1 }, revenue: { $sum: '$total' } } },
231
+
232
+ // 4. Join AFTER reducing the dataset
233
+ { $lookup: { from: 'users', localField: '_id', foreignField: '_id', as: 'user' } },
234
+ { $unwind: '$user' },
235
+
236
+ // 5. Final projection
237
+ { $project: { 'user.password': 0 } },
238
+ ])
239
+ ```
240
+
241
+ ### Stage Order Cheat Sheet
242
+
243
+ | Priority | Stage | Reason |
244
+ |----------|-------|--------|
245
+ | 1st | `$match` | Hit the index, reduce document count |
246
+ | 2nd | `$sort` + `$limit` | Use index for sort; limit early |
247
+ | 3rd | `$project` / `$addFields` | Reduce document size flowing through pipeline |
248
+ | 4th | `$group` | Aggregate the reduced set |
249
+ | Last | `$lookup` | Join only surviving documents |
250
+
251
+ ### Index Usage in Aggregation
252
+
253
+ MongoDB uses indexes **only** in `$match` and `$sort` at the **beginning** of the pipeline. Once `$group`, `$lookup`, or reshaping stages appear, subsequent `$match` stages cannot use indexes.
254
+
255
+ ### $lookup: Simple vs Pipeline Form
256
+
257
+ ```typescript
258
+ // PREFER simple form — uses index on foreignField directly
259
+ { $lookup: { from: 'users', localField: 'userId', foreignField: '_id', as: 'user' } }
260
+
261
+ // Use pipeline form only when filtering/reshaping joined docs
262
+ {
263
+ $lookup: {
264
+ from: 'users',
265
+ let: { uid: '$userId' },
266
+ pipeline: [
267
+ { $match: { $expr: { $eq: ['$_id', '$$uid'] } } },
268
+ { $project: { name: 1, email: 1 } }, // trim joined doc
269
+ ],
270
+ as: 'user',
271
+ },
272
+ }
273
+ ```
274
+
275
+ ### Pagination with Aggregation
276
+
277
+ ```typescript
278
+ // Cursor-based pagination (preferred over skip/limit for large datasets)
279
+ async findPaginated(lastId?: string, limit = 20) {
280
+ const match: any = { status: 'active' }
281
+ if (lastId) match._id = { $gt: new Types.ObjectId(lastId) }
282
+
283
+ return this.orderModel.aggregate([
284
+ { $match: match },
285
+ { $sort: { _id: 1 } },
286
+ { $limit: limit + 1 }, // fetch one extra to detect hasMore
287
+ ])
288
+ }
289
+ ```
290
+
291
+ ## Transactions
292
+
293
+ ### Prerequisites
294
+
295
+ Multi-document transactions require a **replica set** (MongoDB 4.0+) or **sharded cluster** (4.2+). Single-document writes are already atomic — don't wrap them in transactions.
296
+
297
+ ### Recommended: withTransaction()
298
+
299
+ Handles commit, abort, and transient error retry automatically.
300
+
301
+ ```typescript
302
+ async transferFunds(fromId: string, toId: string, amount: number) {
303
+ const session = await this.connection.startSession()
304
+ try {
305
+ await session.withTransaction(async () => {
306
+ const from = await this.accountModel.findById(fromId).session(session)
307
+ if (from.balance < amount) throw new Error('Insufficient funds')
308
+
309
+ await this.accountModel.findByIdAndUpdate(fromId, { $inc: { balance: -amount } }, { session })
310
+ await this.accountModel.findByIdAndUpdate(toId, { $inc: { balance: amount } }, { session })
311
+ await this.transactionModel.create([{ from: fromId, to: toId, amount }], { session })
312
+ })
313
+ } finally {
314
+ await session.endSession()
315
+ }
316
+ }
317
+ ```
318
+
319
+ ### Transaction Rules
320
+
321
+ - **Keep transactions short** — long transactions hold locks, abort after 60s default
322
+ - **Do not parallelize** inside a transaction — operations must be sequential
323
+ - **Use `w: 'majority'`** write concern for durability
324
+ - **Read preference must be `primary`** inside transactions
325
+ - **Don't wrap single-document operations** — they're already atomic
326
+
327
+ ## Mongoose Patterns for NestJS
328
+
329
+ ### Always Use lean() on Read Endpoints
330
+
331
+ `lean()` returns plain JS objects — ~3x less memory, no change tracking overhead.
332
+
333
+ ```typescript
334
+ // GET endpoint — always lean
335
+ async findAll(): Promise<User[]> {
336
+ return this.userModel.find().lean().exec()
337
+ }
338
+
339
+ // PUT/POST endpoint — needs full doc for .save(), hooks, validation
340
+ async update(id: string, dto: UpdateUserDto): Promise<User> {
341
+ const user = await this.userModel.findById(id)
342
+ Object.assign(user, dto)
343
+ return user.save()
344
+ }
345
+ ```
346
+
347
+ | Operation | Use lean() | Reason |
348
+ |-----------|-----------|--------|
349
+ | GET / read endpoints | Yes | 3x less memory, faster |
350
+ | PUT / PATCH / POST | No | Needs `.save()`, hooks, change tracking |
351
+ | Aggregation | N/A | Returns plain objects already |
352
+ | Streaming cursors | Yes | Critical for large result sets |
353
+
354
+ ### Always Use select() / Projection
355
+
356
+ ```typescript
357
+ // BAD — returns all fields including hashed password
358
+ const user = await this.userModel.findById(id)
359
+
360
+ // GOOD — explicit projection
361
+ const user = await this.userModel.findById(id).select('firstName lastName email avatar').lean()
362
+ ```
363
+
364
+ ### Avoid populate() Chains — Use Aggregation
365
+
366
+ ```typescript
367
+ // BAD — N+1 queries, no field limiting
368
+ const orders = await this.orderModel.find()
369
+ .populate('user')
370
+ .populate('items.product')
371
+
372
+ // BETTER — limit populate fields + lean
373
+ const orders = await this.orderModel.find()
374
+ .populate('user', 'name email')
375
+ .populate('items.product', 'name price')
376
+ .lean()
377
+
378
+ // BEST for complex joins — single aggregation pipeline
379
+ const orders = await this.orderModel.aggregate([
380
+ { $match: { status: 'pending' } },
381
+ { $lookup: { from: 'users', localField: 'userId', foreignField: '_id', as: 'user' } },
382
+ { $unwind: '$user' },
383
+ { $project: { 'user.password': 0 } },
384
+ ])
385
+ ```
386
+
387
+ ### Bulk Operations Instead of Loops
388
+
389
+ ```typescript
390
+ // BAD — N round trips
391
+ for (const item of items) {
392
+ await new this.productModel(item).save()
393
+ }
394
+
395
+ // GOOD — single round trip
396
+ await this.productModel.insertMany(items, { ordered: false })
397
+
398
+ // GOOD — mixed operations in one call
399
+ await this.productModel.bulkWrite([
400
+ { insertOne: { document: newProduct } },
401
+ { updateOne: { filter: { sku: 'ABC' }, update: { $inc: { qty: -1 } } } },
402
+ { deleteOne: { filter: { sku: 'DEPRECATED' } } },
403
+ ])
404
+ ```
405
+
406
+ ### Virtuals with lean()
407
+
408
+ By default, `lean()` strips virtuals. Use `mongoose-lean-virtuals`:
409
+
410
+ ```typescript
411
+ import mongooseLeanVirtuals from 'mongoose-lean-virtuals'
412
+
413
+ UserSchema.plugin(mongooseLeanVirtuals)
414
+
415
+ UserSchema.virtual('fullName').get(function () {
416
+ return `${this.firstName} ${this.lastName}`
417
+ })
418
+
419
+ // In service:
420
+ await this.userModel.find().lean({ virtuals: true })
421
+ ```
422
+
423
+ ### Discriminators (Schema Inheritance)
424
+
425
+ ```typescript
426
+ // Base event schema
427
+ @Schema({ discriminatorKey: 'kind', timestamps: true })
428
+ export class Event {
429
+ @Prop({ required: true })
430
+ kind: string
431
+
432
+ @Prop()
433
+ occurredAt: Date
434
+ }
435
+
436
+ // Click event extends base
437
+ @Schema()
438
+ export class ClickEvent {
439
+ @Prop() url: string
440
+ @Prop() elementId: string
441
+ }
442
+
443
+ // Module registration
444
+ MongooseModule.forFeature([{
445
+ name: Event.name,
446
+ schema: EventSchema,
447
+ discriminators: [{ name: ClickEvent.name, schema: ClickEventSchema }],
448
+ }])
449
+ ```
450
+
451
+ ### Connection Events — Always Handle
452
+
453
+ ```typescript
454
+ // database.module.ts
455
+ mongoose.connection.on('error', (err) => logger.error('MongoDB error:', err))
456
+ mongoose.connection.on('disconnected', () => logger.warn('MongoDB disconnected'))
457
+ mongoose.connection.on('reconnected', () => logger.info('MongoDB reconnected'))
458
+
459
+ process.on('SIGINT', async () => {
460
+ await mongoose.connection.close()
461
+ process.exit(0)
462
+ })
463
+ ```
464
+
465
+ ## Connection Management
466
+
467
+ ### Production Configuration
468
+
469
+ ```typescript
470
+ MongooseModule.forRoot(process.env.MONGODB_URI, {
471
+ maxPoolSize: 10, // tune to load — default 100 is often too high
472
+ minPoolSize: 2, // keep connections warm
473
+ serverSelectionTimeoutMS: 5000, // fail fast if server unreachable
474
+ socketTimeoutMS: 45000, // close idle sockets
475
+ connectTimeoutMS: 10000, // TCP connection timeout
476
+ heartbeatFrequencyMS: 10000, // health check frequency
477
+ maxIdleTimeMS: 30000, // close long-idle connections
478
+ family: 4, // IPv4, skip IPv6 probe
479
+ })
480
+ ```
481
+
482
+ ### Pool Size Rule of Thumb
483
+
484
+ ```
485
+ maxPoolSize = (numCPUs * 2) + effectiveSpindleCount
486
+ ```
487
+
488
+ Start with 10-20 for typical API servers. With 10 pods at `maxPoolSize: 100`, you get 1,000 connections to MongoDB — check your Atlas tier limits.
489
+
490
+ ## Data Modeling Patterns
491
+
492
+ ### Bucket Pattern (Time Series)
493
+
494
+ Group related time-series data into buckets instead of one doc per reading.
495
+
496
+ ```typescript
497
+ @Schema()
498
+ export class SensorBucket {
499
+ @Prop({ required: true, index: true })
500
+ sensorId: string
501
+
502
+ @Prop({ required: true })
503
+ startDate: Date
504
+
505
+ @Prop({ required: true })
506
+ endDate: Date
507
+
508
+ @Prop({ type: [{ timestamp: Date, value: Number }] })
509
+ measurements: Array<{ timestamp: Date; value: number }>
510
+
511
+ @Prop({ default: 0 })
512
+ count: number
513
+
514
+ @Prop({ default: 0 })
515
+ sum: number // pre-computed for fast avg = sum/count
516
+ }
517
+ ```
518
+
519
+ > MongoDB 5.0+ has native time series collections — prefer those for new workloads.
520
+
521
+ ### Subset Pattern
522
+
523
+ Embed only the most-recent/relevant subset; full data in a separate collection.
524
+
525
+ ```typescript
526
+ @Schema({ timestamps: true })
527
+ export class Post {
528
+ @Prop() title: string
529
+ @Prop() body: string
530
+
531
+ // Only last 10 comments — enough for initial render
532
+ @Prop({ type: [{ author: String, text: String, createdAt: Date }], default: [] })
533
+ recentComments: Array<{ author: string; text: string; createdAt: Date }>
534
+
535
+ @Prop({ default: 0 })
536
+ commentCount: number
537
+ }
538
+ // Full comments in separate `comments` collection — fetched on "Load more"
539
+ ```
540
+
541
+ ### Computed Pattern
542
+
543
+ Pre-compute expensive aggregations; update on writes.
544
+
545
+ ```typescript
546
+ @Schema({ timestamps: true })
547
+ export class ProductStats {
548
+ @Prop({ type: Types.ObjectId, ref: 'Product', unique: true })
549
+ productId: Types.ObjectId
550
+
551
+ @Prop({ default: 0 })
552
+ totalRevenue: number
553
+
554
+ @Prop({ default: 0 })
555
+ orderCount: number
556
+ }
557
+
558
+ // On sale — atomic update, no aggregation needed on read
559
+ await this.productStatsModel.findOneAndUpdate(
560
+ { productId },
561
+ { $inc: { totalRevenue: saleAmount, orderCount: 1 } },
562
+ { upsert: true }
563
+ )
564
+ ```
565
+
566
+ ### Outlier Pattern
567
+
568
+ Handle documents with abnormally large arrays by flagging overflow.
569
+
570
+ ```typescript
571
+ // Main document — bounded array
572
+ { username: 'celebrity', followers: [...first1000], hasOverflow: true }
573
+
574
+ // Overflow collection
575
+ { userId: ObjectId, followers: [...next1000], page: 2 }
576
+ ```
577
+
578
+ ## Common Anti-Patterns
579
+
580
+ ### 1. Unbounded Arrays
581
+
582
+ ```typescript
583
+ // BAD — array grows without limit, hits 16MB doc limit
584
+ @Prop({ type: [Types.ObjectId] })
585
+ followers: Types.ObjectId[] // could be 1M entries
586
+
587
+ // GOOD — separate collection with parent reference
588
+ @Schema({ timestamps: true })
589
+ export class Follow {
590
+ @Prop({ type: Types.ObjectId, ref: 'User', required: true, index: true })
591
+ followedId: Types.ObjectId
592
+
593
+ @Prop({ type: Types.ObjectId, ref: 'User', required: true, index: true })
594
+ followerId: Types.ObjectId
595
+ }
596
+ ```
597
+
598
+ ### 2. No lean() on Read Endpoints
599
+
600
+ Full Mongoose documents use ~3x the memory of plain objects. On an API serving thousands of requests, this adds up fast.
601
+
602
+ ### 3. Missing timestamps
603
+
604
+ ```typescript
605
+ // BAD — no audit trail
606
+ @Schema()
607
+
608
+ // GOOD — always
609
+ @Schema({ timestamps: true })
610
+ ```
611
+
612
+ ### 4. save() in a Loop
613
+
614
+ ```typescript
615
+ // BAD — 100 round trips
616
+ for (const item of items) await new this.model(item).save()
617
+
618
+ // GOOD — 1 round trip
619
+ await this.model.insertMany(items, { ordered: false })
620
+ ```
621
+
622
+ ### 5. Querying Without Indexes
623
+
624
+ Run `explain('executionStats')` on slow queries. If `totalDocsExamined` >> `totalKeysExamined`, you're missing an index.
625
+
626
+ ### 6. $lookup Before $match
627
+
628
+ Always filter first, join last. A `$lookup` on 100K docs followed by `$match` is orders of magnitude slower than filtering to 100 docs first.
629
+
630
+ ### 7. populate() N+1
631
+
632
+ Deep `.populate()` chains fire multiple queries. For complex joins, use aggregation `$lookup`.
633
+
634
+ ### 8. Not Using Projection
635
+
636
+ Every field transmitted costs network and memory. Use `.select()` to fetch only what you need.
637
+
638
+ ### 9. Wrapping Single-Doc Ops in Transactions
639
+
640
+ Single-document writes are already atomic in MongoDB. Transactions add overhead and require replica sets — don't use them unnecessarily.
641
+
642
+ ### 10. Ignoring Connection Pool Sizing
643
+
644
+ Default `maxPoolSize: 100` * 10 pods = 1,000 connections. Atlas tiers have limits. Start with 10-20 per process and tune based on monitoring.
645
+
646
+ ## Quick Reference
647
+
648
+ | Task | Pattern |
649
+ |------|---------|
650
+ | Read endpoint | `.find().lean().exec()` |
651
+ | Read with few fields | `.select('a b c').lean()` |
652
+ | Simple join | `.populate('ref', 'field1 field2').lean()` |
653
+ | Complex join | Aggregation `$lookup` |
654
+ | Compound index order | Equality → Sort → Range |
655
+ | Bulk insert | `Model.insertMany(docs, { ordered: false })` |
656
+ | Mixed bulk ops | `Model.bulkWrite([...])` |
657
+ | Transaction | `session.withTransaction(async () => { ... })` |
658
+ | Auto-delete expired docs | TTL index (`expires: '24h'`) |
659
+ | Count without fetching | `Model.countDocuments(filter)` |
660
+ | Check if exists | `Model.exists(filter)` |
661
+ | Atomic increment | `Model.findByIdAndUpdate(id, { $inc: { count: 1 } })` |
662
+ | Upsert | `findOneAndUpdate(filter, update, { upsert: true, new: true })` |
663
+
664
+ ## Further Reading
665
+
666
+ For detailed reference on specific topics, see:
667
+ - `references/mongoose-patterns.md` — Advanced patterns: pre/post hooks, custom methods, statics, plugins, change streams, cursor-based streaming