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.
- package/README.md +77 -0
- package/agents/developer.md +56 -0
- package/agents/frontend-design.md +69 -0
- package/agents/mobile-reviewer.md +36 -0
- package/agents/review-code.md +49 -0
- package/agents/security-scanner.md +50 -0
- package/agents/tester.md +72 -0
- package/bin/cli.js +226 -0
- package/package.json +20 -0
- package/skills/ai-asset-generator/SKILL.md +255 -0
- package/skills/ai-asset-generator/docs/gen-image.md +274 -0
- package/skills/ai-asset-generator/docs/genvideo.md +341 -0
- package/skills/ai-asset-generator/docs/remove-background.md +19 -0
- package/skills/ai-asset-generator/generate-credit-assets.mjs +180 -0
- package/skills/ai-asset-generator/generate-ginbrowser-assets.mjs +242 -0
- package/skills/ai-asset-generator/generate-sty-icon.mjs +149 -0
- package/skills/ai-asset-generator/lib/bg-remove.mjs +34 -0
- package/skills/ai-asset-generator/lib/env.mjs +38 -0
- package/skills/ai-asset-generator/lib/kie-client.mjs +88 -0
- package/skills/ai-asset-generator/scripts/scaffold-generator.mjs +203 -0
- package/skills/ai-build-ai/SKILL.md +124 -0
- package/skills/ai-build-ai/docs/agent-teams.md +293 -0
- package/skills/ai-build-ai/docs/checkpointing.md +161 -0
- package/skills/ai-build-ai/docs/create-agent.md +399 -0
- package/skills/ai-build-ai/docs/create-mcp.md +395 -0
- package/skills/ai-build-ai/docs/create-skill.md +299 -0
- package/skills/ai-build-ai/docs/headless-mode.md +614 -0
- package/skills/ai-build-ai/docs/hooks.md +578 -0
- package/skills/ai-build-ai/docs/memory-claude-md.md +375 -0
- package/skills/ai-build-ai/docs/output-styles.md +208 -0
- package/skills/ai-build-ai/docs/overview.md +162 -0
- package/skills/ai-build-ai/docs/permissions.md +391 -0
- package/skills/ai-build-ai/docs/plugins.md +396 -0
- package/skills/ai-build-ai/docs/sandbox.md +262 -0
- package/skills/ai-build-ai/scripts/load-tutorial.sh +54 -0
- package/skills/icon-generator/SKILL.md +270 -0
- package/skills/mobile-app-review/SKILL.md +321 -0
- package/skills/mobile-app-review/references/apple-review.md +132 -0
- package/skills/mobile-app-review/references/google-play-review.md +203 -0
- package/skills/mongodb/SKILL.md +667 -0
- package/skills/mongodb/references/mongoose-patterns.md +368 -0
- package/skills/nestjs-architecture/SKILL.md +1086 -0
- package/skills/nestjs-architecture/references/advanced-patterns.md +590 -0
- package/skills/performance/SKILL.md +509 -0
- package/skills/react-fsd-architecture/SKILL.md +693 -0
- package/skills/react-fsd-architecture/references/fsd-patterns.md +747 -0
- package/skills/react-query/SKILL.md +685 -0
- package/skills/react-query/references/query-patterns.md +365 -0
- package/skills/review-code/SKILL.md +321 -0
- package/skills/review-code/references/clean-code-principles.md +395 -0
- package/skills/review-code/references/frontend-patterns.md +136 -0
- package/skills/review-code/references/nestjs-patterns.md +184 -0
- package/skills/review-code/scripts/check-module.sh +201 -0
- package/skills/review-code/scripts/deep-scan.sh +604 -0
- package/skills/review-code/scripts/dep-check.sh +522 -0
- package/skills/review-code/scripts/detect-duplicates.sh +466 -0
- package/skills/review-code/scripts/format-check.sh +577 -0
- package/skills/review-code/scripts/run-review.sh +167 -0
- package/skills/review-code/scripts/scan-codebase.sh +152 -0
- package/skills/security-scanner/SKILL.md +327 -0
- package/skills/security-scanner/references/nestjs-security.md +260 -0
- package/skills/security-scanner/references/nextjs-security.md +201 -0
- package/skills/security-scanner/references/react-native-security.md +199 -0
- package/skills/security-scanner/scripts/security-scan.sh +478 -0
- package/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- 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
|