autoworkflow 3.1.4 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +174 -11
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# MongoDB Skill
|
|
2
|
+
|
|
3
|
+
## Schema Design Patterns
|
|
4
|
+
\`\`\`javascript
|
|
5
|
+
// Embedding (denormalization) - for data accessed together
|
|
6
|
+
{
|
|
7
|
+
_id: ObjectId("..."),
|
|
8
|
+
title: "Blog Post",
|
|
9
|
+
content: "...",
|
|
10
|
+
author: {
|
|
11
|
+
_id: ObjectId("..."),
|
|
12
|
+
name: "John Doe",
|
|
13
|
+
email: "john@example.com"
|
|
14
|
+
},
|
|
15
|
+
comments: [
|
|
16
|
+
{ user: "Jane", text: "Great post!", date: ISODate("...") },
|
|
17
|
+
{ user: "Bob", text: "Thanks!", date: ISODate("...") }
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Referencing (normalization) - for unbounded or frequently updated data
|
|
22
|
+
{
|
|
23
|
+
_id: ObjectId("..."),
|
|
24
|
+
title: "Blog Post",
|
|
25
|
+
content: "...",
|
|
26
|
+
authorId: ObjectId("..."), // Reference to users collection
|
|
27
|
+
commentIds: [ObjectId("..."), ObjectId("...")] // Bounded array
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Bucket pattern - for time series data
|
|
31
|
+
{
|
|
32
|
+
sensorId: "sensor_001",
|
|
33
|
+
date: ISODate("2024-01-15"),
|
|
34
|
+
readings: [
|
|
35
|
+
{ time: ISODate("2024-01-15T00:00:00Z"), value: 23.5 },
|
|
36
|
+
{ time: ISODate("2024-01-15T00:01:00Z"), value: 23.6 },
|
|
37
|
+
// ... up to 1440 readings per day
|
|
38
|
+
],
|
|
39
|
+
count: 1440
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Polymorphic pattern - for different item types
|
|
43
|
+
{
|
|
44
|
+
_id: ObjectId("..."),
|
|
45
|
+
itemType: "book",
|
|
46
|
+
title: "MongoDB Guide",
|
|
47
|
+
author: "Jane Doe",
|
|
48
|
+
isbn: "978-..."
|
|
49
|
+
}
|
|
50
|
+
{
|
|
51
|
+
_id: ObjectId("..."),
|
|
52
|
+
itemType: "movie",
|
|
53
|
+
title: "The Matrix",
|
|
54
|
+
director: "Wachowskis",
|
|
55
|
+
runtime: 136
|
|
56
|
+
}
|
|
57
|
+
\`\`\`
|
|
58
|
+
|
|
59
|
+
## Indexes
|
|
60
|
+
\`\`\`javascript
|
|
61
|
+
// Single field index
|
|
62
|
+
db.users.createIndex({ email: 1 }) // 1 = ascending, -1 = descending
|
|
63
|
+
|
|
64
|
+
// Unique index
|
|
65
|
+
db.users.createIndex({ email: 1 }, { unique: true })
|
|
66
|
+
|
|
67
|
+
// Compound index (order matters for queries!)
|
|
68
|
+
db.posts.createIndex({ authorId: 1, createdAt: -1 })
|
|
69
|
+
|
|
70
|
+
// Partial index (smaller, more efficient)
|
|
71
|
+
db.users.createIndex(
|
|
72
|
+
{ email: 1 },
|
|
73
|
+
{ partialFilterExpression: { status: "active" } }
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// TTL index (auto-expire documents)
|
|
77
|
+
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })
|
|
78
|
+
|
|
79
|
+
// Text index for full-text search
|
|
80
|
+
db.posts.createIndex({ title: "text", content: "text" })
|
|
81
|
+
|
|
82
|
+
// Wildcard index (flexible schema)
|
|
83
|
+
db.products.createIndex({ "attributes.$**": 1 })
|
|
84
|
+
|
|
85
|
+
// Geospatial index
|
|
86
|
+
db.locations.createIndex({ coordinates: "2dsphere" })
|
|
87
|
+
|
|
88
|
+
// Check index usage
|
|
89
|
+
db.users.aggregate([{ $indexStats: {} }])
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
## CRUD Operations
|
|
93
|
+
\`\`\`javascript
|
|
94
|
+
// Insert
|
|
95
|
+
db.users.insertOne({ email: "user@example.com", name: "John" })
|
|
96
|
+
db.users.insertMany([
|
|
97
|
+
{ email: "user1@example.com", name: "User 1" },
|
|
98
|
+
{ email: "user2@example.com", name: "User 2" }
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
// Find
|
|
102
|
+
db.users.findOne({ email: "user@example.com" })
|
|
103
|
+
db.users.find({ status: "active" }).limit(20).skip(0).sort({ createdAt: -1 })
|
|
104
|
+
|
|
105
|
+
// Projection (select fields)
|
|
106
|
+
db.users.find({ status: "active" }, { email: 1, name: 1, _id: 0 })
|
|
107
|
+
|
|
108
|
+
// Update
|
|
109
|
+
db.users.updateOne(
|
|
110
|
+
{ _id: ObjectId("...") },
|
|
111
|
+
{ $set: { name: "Jane", updatedAt: new Date() } }
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// Upsert
|
|
115
|
+
db.users.updateOne(
|
|
116
|
+
{ email: "user@example.com" },
|
|
117
|
+
{ $set: { name: "John" }, $setOnInsert: { createdAt: new Date() } },
|
|
118
|
+
{ upsert: true }
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
// Update operators
|
|
122
|
+
db.posts.updateOne({ _id }, {
|
|
123
|
+
$set: { title: "New Title" },
|
|
124
|
+
$inc: { views: 1 },
|
|
125
|
+
$push: { tags: "mongodb" },
|
|
126
|
+
$addToSet: { likes: userId }, // Only add if not exists
|
|
127
|
+
$pull: { dislikes: userId }, // Remove from array
|
|
128
|
+
$currentDate: { updatedAt: true }
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Delete
|
|
132
|
+
db.users.deleteOne({ _id: ObjectId("...") })
|
|
133
|
+
db.users.deleteMany({ status: "inactive", lastLogin: { $lt: oneYearAgo } })
|
|
134
|
+
\`\`\`
|
|
135
|
+
|
|
136
|
+
## Query Operators
|
|
137
|
+
\`\`\`javascript
|
|
138
|
+
// Comparison
|
|
139
|
+
db.users.find({ age: { $gt: 18, $lte: 65 } })
|
|
140
|
+
db.posts.find({ status: { $in: ["published", "featured"] } })
|
|
141
|
+
db.users.find({ deletedAt: { $exists: false } })
|
|
142
|
+
|
|
143
|
+
// Logical
|
|
144
|
+
db.users.find({
|
|
145
|
+
$and: [
|
|
146
|
+
{ status: "active" },
|
|
147
|
+
{ $or: [
|
|
148
|
+
{ role: "admin" },
|
|
149
|
+
{ email: { $regex: /@company\\.com$/ } }
|
|
150
|
+
]}
|
|
151
|
+
]
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Array queries
|
|
155
|
+
db.posts.find({ tags: "mongodb" }) // Array contains
|
|
156
|
+
db.posts.find({ tags: { $all: ["mongodb", "database"] } }) // Contains all
|
|
157
|
+
db.posts.find({ "tags.0": "featured" }) // First element
|
|
158
|
+
db.posts.find({ tags: { $size: 3 } }) // Exact array length
|
|
159
|
+
db.posts.find({ comments: { $elemMatch: { user: "John", score: { $gte: 5 } } } })
|
|
160
|
+
|
|
161
|
+
// Text search
|
|
162
|
+
db.posts.find({ $text: { $search: "mongodb tutorial" } })
|
|
163
|
+
.sort({ score: { $meta: "textScore" } })
|
|
164
|
+
\`\`\`
|
|
165
|
+
|
|
166
|
+
## Aggregation Pipeline
|
|
167
|
+
\`\`\`javascript
|
|
168
|
+
// Basic aggregation
|
|
169
|
+
db.orders.aggregate([
|
|
170
|
+
{ $match: { status: "completed" } },
|
|
171
|
+
{ $group: {
|
|
172
|
+
_id: "$customerId",
|
|
173
|
+
totalSpent: { $sum: "$total" },
|
|
174
|
+
orderCount: { $sum: 1 },
|
|
175
|
+
avgOrder: { $avg: "$total" }
|
|
176
|
+
}},
|
|
177
|
+
{ $sort: { totalSpent: -1 } },
|
|
178
|
+
{ $limit: 10 }
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
// Lookup (join)
|
|
182
|
+
db.posts.aggregate([
|
|
183
|
+
{ $match: { published: true } },
|
|
184
|
+
{ $lookup: {
|
|
185
|
+
from: "users",
|
|
186
|
+
localField: "authorId",
|
|
187
|
+
foreignField: "_id",
|
|
188
|
+
as: "author"
|
|
189
|
+
}},
|
|
190
|
+
{ $unwind: "$author" }, // Convert array to object
|
|
191
|
+
{ $project: {
|
|
192
|
+
title: 1,
|
|
193
|
+
"author.name": 1,
|
|
194
|
+
"author.email": 1
|
|
195
|
+
}}
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
// Faceted search (multiple aggregations)
|
|
199
|
+
db.products.aggregate([
|
|
200
|
+
{ $match: { status: "active" } },
|
|
201
|
+
{ $facet: {
|
|
202
|
+
results: [
|
|
203
|
+
{ $sort: { createdAt: -1 } },
|
|
204
|
+
{ $skip: 0 },
|
|
205
|
+
{ $limit: 20 }
|
|
206
|
+
],
|
|
207
|
+
totalCount: [
|
|
208
|
+
{ $count: "count" }
|
|
209
|
+
],
|
|
210
|
+
categories: [
|
|
211
|
+
{ $group: { _id: "$category", count: { $sum: 1 } } }
|
|
212
|
+
],
|
|
213
|
+
priceRange: [
|
|
214
|
+
{ $group: {
|
|
215
|
+
_id: null,
|
|
216
|
+
min: { $min: "$price" },
|
|
217
|
+
max: { $max: "$price" }
|
|
218
|
+
}}
|
|
219
|
+
]
|
|
220
|
+
}}
|
|
221
|
+
])
|
|
222
|
+
|
|
223
|
+
// Unwind arrays
|
|
224
|
+
db.posts.aggregate([
|
|
225
|
+
{ $unwind: "$tags" },
|
|
226
|
+
{ $group: { _id: "$tags", count: { $sum: 1 } } },
|
|
227
|
+
{ $sort: { count: -1 } }
|
|
228
|
+
])
|
|
229
|
+
|
|
230
|
+
// Date operations
|
|
231
|
+
db.orders.aggregate([
|
|
232
|
+
{ $group: {
|
|
233
|
+
_id: {
|
|
234
|
+
year: { $year: "$createdAt" },
|
|
235
|
+
month: { $month: "$createdAt" }
|
|
236
|
+
},
|
|
237
|
+
revenue: { $sum: "$total" }
|
|
238
|
+
}},
|
|
239
|
+
{ $sort: { "_id.year": 1, "_id.month": 1 } }
|
|
240
|
+
])
|
|
241
|
+
\`\`\`
|
|
242
|
+
|
|
243
|
+
## Transactions (Replica Set required)
|
|
244
|
+
\`\`\`javascript
|
|
245
|
+
const session = client.startSession();
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
session.startTransaction();
|
|
249
|
+
|
|
250
|
+
await users.insertOne({ email, name }, { session });
|
|
251
|
+
await posts.insertOne({ title, authorId: userId }, { session });
|
|
252
|
+
|
|
253
|
+
await session.commitTransaction();
|
|
254
|
+
} catch (error) {
|
|
255
|
+
await session.abortTransaction();
|
|
256
|
+
throw error;
|
|
257
|
+
} finally {
|
|
258
|
+
session.endSession();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// With callback (auto-retry)
|
|
262
|
+
await session.withTransaction(async () => {
|
|
263
|
+
await users.updateOne({ _id: senderId }, { $inc: { balance: -100 } }, { session });
|
|
264
|
+
await users.updateOne({ _id: receiverId }, { $inc: { balance: 100 } }, { session });
|
|
265
|
+
});
|
|
266
|
+
\`\`\`
|
|
267
|
+
|
|
268
|
+
## Change Streams
|
|
269
|
+
\`\`\`javascript
|
|
270
|
+
// Watch for changes (real-time)
|
|
271
|
+
const changeStream = db.collection('users').watch([
|
|
272
|
+
{ $match: { operationType: { $in: ['insert', 'update'] } } }
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
changeStream.on('change', (change) => {
|
|
276
|
+
console.log('Change detected:', change.operationType);
|
|
277
|
+
console.log('Document:', change.fullDocument);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Resume after failure
|
|
281
|
+
const resumeToken = change._id;
|
|
282
|
+
const resumedStream = collection.watch([], { resumeAfter: resumeToken });
|
|
283
|
+
\`\`\`
|
|
284
|
+
|
|
285
|
+
## Performance & Monitoring
|
|
286
|
+
\`\`\`javascript
|
|
287
|
+
// Explain query plan
|
|
288
|
+
db.users.find({ email: "test@example.com" }).explain("executionStats")
|
|
289
|
+
|
|
290
|
+
// Index recommendations
|
|
291
|
+
db.users.aggregate([{ $indexStats: {} }])
|
|
292
|
+
|
|
293
|
+
// Current operations
|
|
294
|
+
db.currentOp({ "active": true })
|
|
295
|
+
|
|
296
|
+
// Server status
|
|
297
|
+
db.serverStatus()
|
|
298
|
+
|
|
299
|
+
// Collection stats
|
|
300
|
+
db.users.stats()
|
|
301
|
+
\`\`\`
|
|
302
|
+
|
|
303
|
+
## ❌ DON'T
|
|
304
|
+
- Store unbounded arrays (use bucketing or references)
|
|
305
|
+
- Create too many indexes (write overhead)
|
|
306
|
+
- Use \`$where\` or JavaScript execution in queries
|
|
307
|
+
- Skip index for frequently queried fields
|
|
308
|
+
- Embed frequently changing data
|
|
309
|
+
- Use find() without limits on large collections
|
|
310
|
+
|
|
311
|
+
## ✅ DO
|
|
312
|
+
- Design schema for query patterns
|
|
313
|
+
- Use compound indexes matching query shapes
|
|
314
|
+
- Use aggregation pipeline for complex queries
|
|
315
|
+
- Use change streams for real-time updates
|
|
316
|
+
- Set appropriate read/write concerns
|
|
317
|
+
- Use TTL indexes for expiring data
|
|
318
|
+
- Monitor index usage and slow queries
|
|
319
|
+
- Use projection to limit returned fields
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Mongoose Skill
|
|
2
|
+
|
|
3
|
+
## Connection Setup
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
import mongoose from 'mongoose';
|
|
6
|
+
|
|
7
|
+
// Connection with options
|
|
8
|
+
await mongoose.connect(process.env.MONGODB_URI!, {
|
|
9
|
+
maxPoolSize: 10,
|
|
10
|
+
serverSelectionTimeoutMS: 5000,
|
|
11
|
+
socketTimeoutMS: 45000,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Connection events
|
|
15
|
+
mongoose.connection.on('connected', () => console.log('MongoDB connected'));
|
|
16
|
+
mongoose.connection.on('error', (err) => console.error('MongoDB error:', err));
|
|
17
|
+
mongoose.connection.on('disconnected', () => console.log('MongoDB disconnected'));
|
|
18
|
+
|
|
19
|
+
// Graceful shutdown
|
|
20
|
+
process.on('SIGINT', async () => {
|
|
21
|
+
await mongoose.connection.close();
|
|
22
|
+
process.exit(0);
|
|
23
|
+
});
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
## Schema Definition with TypeScript
|
|
27
|
+
\`\`\`typescript
|
|
28
|
+
import { Schema, model, Document, Types, Model } from 'mongoose';
|
|
29
|
+
|
|
30
|
+
// TypeScript interfaces
|
|
31
|
+
interface IUser {
|
|
32
|
+
email: string;
|
|
33
|
+
name: string;
|
|
34
|
+
password: string;
|
|
35
|
+
role: 'user' | 'admin';
|
|
36
|
+
settings: Record<string, unknown>;
|
|
37
|
+
posts: Types.ObjectId[];
|
|
38
|
+
createdAt: Date;
|
|
39
|
+
updatedAt: Date;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Methods interface
|
|
43
|
+
interface IUserMethods {
|
|
44
|
+
comparePassword(candidate: string): Promise<boolean>;
|
|
45
|
+
generateAuthToken(): string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Static methods interface
|
|
49
|
+
interface UserModel extends Model<IUser, {}, IUserMethods> {
|
|
50
|
+
findByEmail(email: string): Promise<IUser | null>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Schema definition
|
|
54
|
+
const userSchema = new Schema<IUser, UserModel, IUserMethods>({
|
|
55
|
+
email: {
|
|
56
|
+
type: String,
|
|
57
|
+
required: [true, 'Email is required'],
|
|
58
|
+
unique: true,
|
|
59
|
+
lowercase: true,
|
|
60
|
+
trim: true,
|
|
61
|
+
validate: {
|
|
62
|
+
validator: (v: string) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v),
|
|
63
|
+
message: 'Invalid email format',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
name: {
|
|
67
|
+
type: String,
|
|
68
|
+
required: true,
|
|
69
|
+
trim: true,
|
|
70
|
+
minlength: [2, 'Name must be at least 2 characters'],
|
|
71
|
+
maxlength: [100, 'Name cannot exceed 100 characters'],
|
|
72
|
+
},
|
|
73
|
+
password: {
|
|
74
|
+
type: String,
|
|
75
|
+
required: true,
|
|
76
|
+
select: false, // Excluded from queries by default
|
|
77
|
+
},
|
|
78
|
+
role: {
|
|
79
|
+
type: String,
|
|
80
|
+
enum: ['user', 'admin'],
|
|
81
|
+
default: 'user',
|
|
82
|
+
},
|
|
83
|
+
settings: {
|
|
84
|
+
type: Schema.Types.Mixed,
|
|
85
|
+
default: {},
|
|
86
|
+
},
|
|
87
|
+
posts: [{
|
|
88
|
+
type: Schema.Types.ObjectId,
|
|
89
|
+
ref: 'Post',
|
|
90
|
+
}],
|
|
91
|
+
}, {
|
|
92
|
+
timestamps: true, // Adds createdAt, updatedAt
|
|
93
|
+
toJSON: { virtuals: true },
|
|
94
|
+
toObject: { virtuals: true },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Indexes
|
|
98
|
+
userSchema.index({ email: 1 });
|
|
99
|
+
userSchema.index({ role: 1, createdAt: -1 });
|
|
100
|
+
|
|
101
|
+
export const User = model<IUser, UserModel>('User', userSchema);
|
|
102
|
+
\`\`\`
|
|
103
|
+
|
|
104
|
+
## Virtuals
|
|
105
|
+
\`\`\`typescript
|
|
106
|
+
// Virtual property (not stored in DB)
|
|
107
|
+
userSchema.virtual('fullName').get(function() {
|
|
108
|
+
return \`\${this.firstName} \${this.lastName}\`;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Virtual populate (referenced documents)
|
|
112
|
+
userSchema.virtual('postCount', {
|
|
113
|
+
ref: 'Post',
|
|
114
|
+
localField: '_id',
|
|
115
|
+
foreignField: 'author',
|
|
116
|
+
count: true,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Usage
|
|
120
|
+
const user = await User.findById(id).populate('postCount');
|
|
121
|
+
console.log(user.postCount); // Number of posts
|
|
122
|
+
\`\`\`
|
|
123
|
+
|
|
124
|
+
## Instance & Static Methods
|
|
125
|
+
\`\`\`typescript
|
|
126
|
+
// Instance method
|
|
127
|
+
userSchema.methods.comparePassword = async function(candidate: string): Promise<boolean> {
|
|
128
|
+
return bcrypt.compare(candidate, this.password);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
userSchema.methods.generateAuthToken = function(): string {
|
|
132
|
+
return jwt.sign({ id: this._id, role: this.role }, process.env.JWT_SECRET!);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Static method
|
|
136
|
+
userSchema.statics.findByEmail = function(email: string) {
|
|
137
|
+
return this.findOne({ email: email.toLowerCase() });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Usage
|
|
141
|
+
const user = await User.findByEmail('test@example.com'); // Static
|
|
142
|
+
const isValid = await user.comparePassword('password123'); // Instance
|
|
143
|
+
const token = user.generateAuthToken(); // Instance
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
146
|
+
## Middleware (Hooks)
|
|
147
|
+
\`\`\`typescript
|
|
148
|
+
// Pre-save hook (hash password)
|
|
149
|
+
userSchema.pre('save', async function(next) {
|
|
150
|
+
if (!this.isModified('password')) return next();
|
|
151
|
+
this.password = await bcrypt.hash(this.password, 12);
|
|
152
|
+
next();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Pre-find hook (exclude deleted)
|
|
156
|
+
userSchema.pre(/^find/, function(next) {
|
|
157
|
+
// 'this' is the query
|
|
158
|
+
this.where({ deletedAt: { $exists: false } });
|
|
159
|
+
next();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Post-save hook
|
|
163
|
+
userSchema.post('save', function(doc) {
|
|
164
|
+
console.log('User saved:', doc._id);
|
|
165
|
+
// Send welcome email, log audit, etc.
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Pre-remove hook (cascade delete)
|
|
169
|
+
userSchema.pre('deleteOne', { document: true }, async function() {
|
|
170
|
+
await Post.deleteMany({ author: this._id });
|
|
171
|
+
});
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
## Query Patterns
|
|
175
|
+
\`\`\`typescript
|
|
176
|
+
// Find operations
|
|
177
|
+
const user = await User.findById(id);
|
|
178
|
+
const user = await User.findOne({ email });
|
|
179
|
+
const users = await User.find({ role: 'user' });
|
|
180
|
+
|
|
181
|
+
// With population (joins)
|
|
182
|
+
const user = await User.findById(id)
|
|
183
|
+
.populate('posts')
|
|
184
|
+
.populate({
|
|
185
|
+
path: 'posts',
|
|
186
|
+
match: { published: true },
|
|
187
|
+
select: 'title createdAt',
|
|
188
|
+
options: { sort: { createdAt: -1 }, limit: 10 },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Select specific fields
|
|
192
|
+
const user = await User.findById(id).select('email name role');
|
|
193
|
+
const user = await User.findById(id).select('-password -settings'); // Exclude
|
|
194
|
+
|
|
195
|
+
// Include normally excluded field
|
|
196
|
+
const user = await User.findById(id).select('+password');
|
|
197
|
+
|
|
198
|
+
// Lean queries (plain JS objects, faster)
|
|
199
|
+
const user = await User.findById(id).lean();
|
|
200
|
+
|
|
201
|
+
// Pagination
|
|
202
|
+
const page = 1;
|
|
203
|
+
const limit = 20;
|
|
204
|
+
const users = await User.find({ role: 'user' })
|
|
205
|
+
.sort({ createdAt: -1 })
|
|
206
|
+
.skip((page - 1) * limit)
|
|
207
|
+
.limit(limit)
|
|
208
|
+
.lean();
|
|
209
|
+
|
|
210
|
+
const total = await User.countDocuments({ role: 'user' });
|
|
211
|
+
|
|
212
|
+
// Complex queries
|
|
213
|
+
const users = await User.find({
|
|
214
|
+
$and: [
|
|
215
|
+
{ role: 'user' },
|
|
216
|
+
{ createdAt: { $gte: new Date('2024-01-01') } },
|
|
217
|
+
{ $or: [
|
|
218
|
+
{ email: { $regex: '@company.com$' } },
|
|
219
|
+
{ name: { $regex: /^admin/i } },
|
|
220
|
+
]},
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
\`\`\`
|
|
224
|
+
|
|
225
|
+
## Aggregation Pipeline
|
|
226
|
+
\`\`\`typescript
|
|
227
|
+
// Basic aggregation
|
|
228
|
+
const stats = await User.aggregate([
|
|
229
|
+
{ $match: { role: 'user' } },
|
|
230
|
+
{ $group: {
|
|
231
|
+
_id: '$status',
|
|
232
|
+
count: { $sum: 1 },
|
|
233
|
+
avgAge: { $avg: '$age' },
|
|
234
|
+
}},
|
|
235
|
+
{ $sort: { count: -1 } },
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
// With lookup (join)
|
|
239
|
+
const usersWithPosts = await User.aggregate([
|
|
240
|
+
{ $match: { role: 'user' } },
|
|
241
|
+
{ $lookup: {
|
|
242
|
+
from: 'posts',
|
|
243
|
+
localField: '_id',
|
|
244
|
+
foreignField: 'author',
|
|
245
|
+
as: 'posts',
|
|
246
|
+
pipeline: [
|
|
247
|
+
{ $match: { published: true } },
|
|
248
|
+
{ $sort: { createdAt: -1 } },
|
|
249
|
+
{ $limit: 5 },
|
|
250
|
+
],
|
|
251
|
+
}},
|
|
252
|
+
{ $addFields: { postCount: { $size: '$posts' } } },
|
|
253
|
+
{ $project: { password: 0 } },
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
// Pagination with facet
|
|
257
|
+
const result = await User.aggregate([
|
|
258
|
+
{ $match: { role: 'user' } },
|
|
259
|
+
{ $facet: {
|
|
260
|
+
data: [
|
|
261
|
+
{ $sort: { createdAt: -1 } },
|
|
262
|
+
{ $skip: (page - 1) * limit },
|
|
263
|
+
{ $limit: limit },
|
|
264
|
+
],
|
|
265
|
+
meta: [
|
|
266
|
+
{ $count: 'total' },
|
|
267
|
+
],
|
|
268
|
+
}},
|
|
269
|
+
]);
|
|
270
|
+
\`\`\`
|
|
271
|
+
|
|
272
|
+
## Transactions
|
|
273
|
+
\`\`\`typescript
|
|
274
|
+
const session = await mongoose.startSession();
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
session.startTransaction();
|
|
278
|
+
|
|
279
|
+
const user = await User.create([{ email, name }], { session });
|
|
280
|
+
|
|
281
|
+
await Post.create([{
|
|
282
|
+
title: 'Welcome Post',
|
|
283
|
+
author: user[0]._id,
|
|
284
|
+
}], { session });
|
|
285
|
+
|
|
286
|
+
await session.commitTransaction();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
await session.abortTransaction();
|
|
289
|
+
throw error;
|
|
290
|
+
} finally {
|
|
291
|
+
session.endSession();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// With withTransaction helper
|
|
295
|
+
await session.withTransaction(async () => {
|
|
296
|
+
await User.create([userData], { session });
|
|
297
|
+
await Post.create([postData], { session });
|
|
298
|
+
});
|
|
299
|
+
\`\`\`
|
|
300
|
+
|
|
301
|
+
## Validation
|
|
302
|
+
\`\`\`typescript
|
|
303
|
+
const productSchema = new Schema({
|
|
304
|
+
price: {
|
|
305
|
+
type: Number,
|
|
306
|
+
required: true,
|
|
307
|
+
min: [0, 'Price cannot be negative'],
|
|
308
|
+
validate: {
|
|
309
|
+
validator: Number.isFinite,
|
|
310
|
+
message: 'Price must be a finite number',
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
category: {
|
|
314
|
+
type: String,
|
|
315
|
+
enum: {
|
|
316
|
+
values: ['electronics', 'clothing', 'food'],
|
|
317
|
+
message: '{VALUE} is not a valid category',
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
tags: {
|
|
321
|
+
type: [String],
|
|
322
|
+
validate: {
|
|
323
|
+
validator: (v: string[]) => v.length <= 10,
|
|
324
|
+
message: 'Cannot have more than 10 tags',
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Custom async validator
|
|
330
|
+
userSchema.path('email').validate({
|
|
331
|
+
validator: async function(email: string) {
|
|
332
|
+
const count = await User.countDocuments({ email, _id: { $ne: this._id } });
|
|
333
|
+
return count === 0;
|
|
334
|
+
},
|
|
335
|
+
message: 'Email already exists',
|
|
336
|
+
});
|
|
337
|
+
\`\`\`
|
|
338
|
+
|
|
339
|
+
## ❌ DON'T
|
|
340
|
+
- Store unbounded arrays in documents (use references)
|
|
341
|
+
- Forget to create indexes for query patterns
|
|
342
|
+
- Use \`find()\` without limits on large collections
|
|
343
|
+
- Nest populations deeply (performance killer)
|
|
344
|
+
- Use \`new Model()\` + \`save()\` when \`create()\` works
|
|
345
|
+
- Skip \`.lean()\` when you don't need Mongoose features
|
|
346
|
+
|
|
347
|
+
## ✅ DO
|
|
348
|
+
- Design schemas around query patterns
|
|
349
|
+
- Use \`lean()\` for read-only operations
|
|
350
|
+
- Create compound indexes for common queries
|
|
351
|
+
- Use aggregation for complex data transformations
|
|
352
|
+
- Use transactions for multi-document operations
|
|
353
|
+
- Validate data at schema level
|
|
354
|
+
- Use virtuals for computed properties
|
|
355
|
+
- Set \`select: false\` for sensitive fields
|