@devmunna/agent-skillkit 0.1.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/LICENSE +21 -0
- package/README.md +147 -0
- package/bin/ai-skills.js +5 -0
- package/dist/cli/commands/add.d.ts +2 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/add.js +66 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +33 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +145 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +55 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +49 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +2 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +22 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/prompts/agent-selector.d.ts +3 -0
- package/dist/cli/prompts/agent-selector.d.ts.map +1 -0
- package/dist/cli/prompts/agent-selector.js +23 -0
- package/dist/cli/prompts/agent-selector.js.map +1 -0
- package/dist/cli/prompts/stack-selector.d.ts +3 -0
- package/dist/cli/prompts/stack-selector.d.ts.map +1 -0
- package/dist/cli/prompts/stack-selector.js +60 -0
- package/dist/cli/prompts/stack-selector.js.map +1 -0
- package/dist/core/config-manager.d.ts +20 -0
- package/dist/core/config-manager.d.ts.map +1 -0
- package/dist/core/config-manager.js +107 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/detector.d.ts +3 -0
- package/dist/core/detector.d.ts.map +1 -0
- package/dist/core/detector.js +50 -0
- package/dist/core/detector.js.map +1 -0
- package/dist/core/doctor.d.ts +12 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +102 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/skill-registry.d.ts +11 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +174 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/skill-resolver.d.ts +3 -0
- package/dist/core/skill-resolver.d.ts.map +1 -0
- package/dist/core/skill-resolver.js +36 -0
- package/dist/core/skill-resolver.js.map +1 -0
- package/dist/core/validator.d.ts +13 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +99 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/generators/agent-installer.d.ts +5 -0
- package/dist/generators/agent-installer.d.ts.map +1 -0
- package/dist/generators/agent-installer.js +20 -0
- package/dist/generators/agent-installer.js.map +1 -0
- package/dist/generators/agents-md.d.ts +3 -0
- package/dist/generators/agents-md.d.ts.map +1 -0
- package/dist/generators/agents-md.js +70 -0
- package/dist/generators/agents-md.js.map +1 -0
- package/dist/generators/claude-md.d.ts +3 -0
- package/dist/generators/claude-md.d.ts.map +1 -0
- package/dist/generators/claude-md.js +47 -0
- package/dist/generators/claude-md.js.map +1 -0
- package/dist/generators/skill-generator.d.ts +5 -0
- package/dist/generators/skill-generator.d.ts.map +1 -0
- package/dist/generators/skill-generator.js +34 -0
- package/dist/generators/skill-generator.js.map +1 -0
- package/dist/generators/workflows.d.ts +3 -0
- package/dist/generators/workflows.d.ts.map +1 -0
- package/dist/generators/workflows.js +57 -0
- package/dist/generators/workflows.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/file-utils.d.ts +12 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +39 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +73 -0
- package/skills/clean-architecture/SKILL.md +324 -0
- package/skills/express-mvc-prisma/SKILL.md +168 -0
- package/skills/express-mvc-prisma/references/auth.md +190 -0
- package/skills/express-mvc-prisma/references/boilerplate.md +196 -0
- package/skills/express-mvc-prisma/references/error-handling.md +121 -0
- package/skills/express-mvc-prisma/references/module-scaffold.md +253 -0
- package/skills/express-mvc-prisma/references/prisma-setup.md +97 -0
- package/skills/express-mvc-prisma/references/response-helpers.md +157 -0
- package/skills/express-mvc-prisma/references/zod-validation.md +157 -0
- package/skills/fastify-rest/SKILL.md +287 -0
- package/skills/mongoose-odm/SKILL.md +281 -0
- package/skills/nextjs-fullstack/SKILL.md +328 -0
- package/skills/nextjs-fullstack/references/auth.md +270 -0
- package/skills/nextjs-fullstack/references/caching.md +157 -0
- package/skills/nextjs-fullstack/references/route-handlers.md +194 -0
- package/skills/nextjs-fullstack/references/server-actions.md +214 -0
- package/skills/nextjs-fullstack/references/server-components.md +190 -0
- package/skills/node-base/SKILL.md +139 -0
- package/skills/prisma-orm/SKILL.md +334 -0
- package/skills/react-feature-arch/SKILL.md +208 -0
- package/skills/react-feature-arch/references/api-layer.md +110 -0
- package/skills/react-feature-arch/references/components.md +192 -0
- package/skills/react-feature-arch/references/data-fetching.md +198 -0
- package/skills/react-feature-arch/references/forms.md +194 -0
- package/skills/react-feature-arch/references/routing.md +148 -0
- package/skills/react-feature-arch/references/state-management.md +107 -0
- package/skills/tailwind-css/SKILL.md +236 -0
- package/skills/tailwind-css/references/components.md +340 -0
- package/skills/tailwind-css/references/design-tokens.md +230 -0
- package/skills/tailwind-css/references/patterns.md +375 -0
- package/skills/tailwind-css/references/setup.md +165 -0
- package/skills/zod-validation/SKILL.md +267 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fastify-rest
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: >
|
|
5
|
+
Apply this skill for any Fastify project task: server setup, route declaration, schema validation, plugins, hooks, authentication, error handling, or performance optimization. Triggers: "Fastify server", "Fastify route", "Fastify plugin", "Fastify schema", "Fastify hooks", "Fastify JWT", "register plugin".
|
|
6
|
+
stack: [fastify]
|
|
7
|
+
depends: [node-base]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Fastify REST API Skill
|
|
11
|
+
|
|
12
|
+
Production-ready Fastify patterns: plugin architecture, JSON schema validation, hooks, and performance-first design.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Project Structure
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
src/
|
|
20
|
+
├── app.js # Fastify instance builder (no listen)
|
|
21
|
+
├── server.js # Entry point (listen here)
|
|
22
|
+
├── config/
|
|
23
|
+
│ └── env.js # Validated env (Zod)
|
|
24
|
+
├── plugins/
|
|
25
|
+
│ ├── db.js # Database plugin (Prisma / Mongoose)
|
|
26
|
+
│ ├── auth.js # JWT plugin
|
|
27
|
+
│ └── sensible.js # @fastify/sensible
|
|
28
|
+
├── modules/
|
|
29
|
+
│ └── {module}/
|
|
30
|
+
│ ├── {module}.routes.js # Route declarations
|
|
31
|
+
│ ├── {module}.schema.js # JSON schemas
|
|
32
|
+
│ ├── {module}.service.js # Business logic
|
|
33
|
+
│ └── {module}.repository.js
|
|
34
|
+
└── utils/
|
|
35
|
+
└── AppError.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Application Bootstrap
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// src/app.js
|
|
44
|
+
import Fastify from 'fastify';
|
|
45
|
+
import { env } from './config/env.js';
|
|
46
|
+
|
|
47
|
+
export async function buildApp(opts = {}) {
|
|
48
|
+
const app = Fastify({
|
|
49
|
+
logger: env.NODE_ENV === 'development'
|
|
50
|
+
? { level: 'info', transport: { target: 'pino-pretty' } }
|
|
51
|
+
: { level: 'warn' },
|
|
52
|
+
...opts,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Register plugins
|
|
56
|
+
await app.register(import('./plugins/db.js'));
|
|
57
|
+
await app.register(import('./plugins/auth.js'));
|
|
58
|
+
|
|
59
|
+
// Register routes with prefix
|
|
60
|
+
await app.register(import('./modules/user/user.routes.js'), { prefix: '/api/v1/users' });
|
|
61
|
+
await app.register(import('./modules/auth/auth.routes.js'), { prefix: '/api/v1/auth' });
|
|
62
|
+
|
|
63
|
+
// Health check
|
|
64
|
+
app.get('/health', async () => ({ status: 'ok' }));
|
|
65
|
+
|
|
66
|
+
return app;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/server.js
|
|
70
|
+
import { buildApp } from './src/app.js';
|
|
71
|
+
import { env } from './src/config/env.js';
|
|
72
|
+
|
|
73
|
+
const app = await buildApp();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await app.listen({ port: env.PORT, host: '0.0.0.0' });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
app.log.error(err);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Plugin Registration
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
// src/plugins/db.js
|
|
89
|
+
import fp from 'fastify-plugin';
|
|
90
|
+
import { PrismaClient } from '@prisma/client';
|
|
91
|
+
|
|
92
|
+
async function dbPlugin(app) {
|
|
93
|
+
const prisma = new PrismaClient({
|
|
94
|
+
log: app.config.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error'],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await prisma.$connect();
|
|
98
|
+
|
|
99
|
+
// Decorate app so all routes can use app.prisma
|
|
100
|
+
app.decorate('prisma', prisma);
|
|
101
|
+
|
|
102
|
+
app.addHook('onClose', async () => {
|
|
103
|
+
await prisma.$disconnect();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// fp() makes the plugin available app-wide (not scoped)
|
|
108
|
+
export default fp(dbPlugin, { name: 'prisma' });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Route Declaration with JSON Schema
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
// src/modules/user/user.routes.js
|
|
117
|
+
import { userService } from './user.service.js';
|
|
118
|
+
import { createUserSchema, getUserSchema, listUsersSchema } from './user.schema.js';
|
|
119
|
+
|
|
120
|
+
export default async function userRoutes(app) {
|
|
121
|
+
// List users
|
|
122
|
+
app.get('/', { schema: listUsersSchema }, async (req, reply) => {
|
|
123
|
+
const result = await userService.getAll(req.query);
|
|
124
|
+
return result;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Get user by ID
|
|
128
|
+
app.get('/:id', { schema: getUserSchema }, async (req, reply) => {
|
|
129
|
+
const user = await userService.getById(req.params.id);
|
|
130
|
+
return user;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Create user (protected)
|
|
134
|
+
app.post('/', {
|
|
135
|
+
schema: createUserSchema,
|
|
136
|
+
preHandler: [app.authenticate], // JWT guard
|
|
137
|
+
}, async (req, reply) => {
|
|
138
|
+
const user = await userService.create(req.body);
|
|
139
|
+
reply.status(201);
|
|
140
|
+
return user;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Delete user
|
|
144
|
+
app.delete('/:id', {
|
|
145
|
+
schema: { params: { type: 'object', properties: { id: { type: 'string', format: 'uuid' } } } },
|
|
146
|
+
preHandler: [app.authenticate, app.authorize('ADMIN')],
|
|
147
|
+
}, async (req, reply) => {
|
|
148
|
+
await userService.delete(req.params.id);
|
|
149
|
+
reply.status(204).send();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## JSON Schema Validation
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
// src/modules/user/user.schema.js
|
|
160
|
+
|
|
161
|
+
const userResponse = {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
id: { type: 'string', format: 'uuid' },
|
|
165
|
+
name: { type: 'string' },
|
|
166
|
+
email: { type: 'string', format: 'email' },
|
|
167
|
+
role: { type: 'string', enum: ['USER', 'ADMIN'] },
|
|
168
|
+
createdAt: { type: 'string', format: 'date-time' },
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const createUserSchema = {
|
|
173
|
+
body: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
required: ['name', 'email', 'password'],
|
|
176
|
+
properties: {
|
|
177
|
+
name: { type: 'string', minLength: 2, maxLength: 100 },
|
|
178
|
+
email: { type: 'string', format: 'email' },
|
|
179
|
+
password: { type: 'string', minLength: 8 },
|
|
180
|
+
},
|
|
181
|
+
additionalProperties: false,
|
|
182
|
+
},
|
|
183
|
+
response: {
|
|
184
|
+
201: userResponse,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const listUsersSchema = {
|
|
189
|
+
querystring: {
|
|
190
|
+
type: 'object',
|
|
191
|
+
properties: {
|
|
192
|
+
page: { type: 'integer', minimum: 1, default: 1 },
|
|
193
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
|
194
|
+
search: { type: 'string' },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const getUserSchema = {
|
|
200
|
+
params: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
required: ['id'],
|
|
203
|
+
properties: {
|
|
204
|
+
id: { type: 'string', format: 'uuid' },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
response: {
|
|
208
|
+
200: userResponse,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## JWT Authentication Plugin
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
// src/plugins/auth.js
|
|
219
|
+
import fp from 'fastify-plugin';
|
|
220
|
+
import jwt from '@fastify/jwt';
|
|
221
|
+
import { env } from '../config/env.js';
|
|
222
|
+
|
|
223
|
+
async function authPlugin(app) {
|
|
224
|
+
await app.register(jwt, { secret: env.JWT_SECRET });
|
|
225
|
+
|
|
226
|
+
// Decorate with authenticate hook
|
|
227
|
+
app.decorate('authenticate', async function (req, reply) {
|
|
228
|
+
try {
|
|
229
|
+
await req.jwtVerify();
|
|
230
|
+
} catch {
|
|
231
|
+
reply.status(401).send({ error: 'Unauthorized' });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Role-based authorization
|
|
236
|
+
app.decorate('authorize', (role) => async function (req, reply) {
|
|
237
|
+
if (req.user.role !== role) {
|
|
238
|
+
reply.status(403).send({ error: 'Forbidden' });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export default fp(authPlugin, { name: 'auth' });
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Error Handling
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
// In app.js — global error handler
|
|
252
|
+
app.setErrorHandler(async (error, req, reply) => {
|
|
253
|
+
const statusCode = error.statusCode ?? 500;
|
|
254
|
+
|
|
255
|
+
if (statusCode >= 500) {
|
|
256
|
+
app.log.error(error);
|
|
257
|
+
return reply.status(500).send({ success: false, message: 'Internal server error' });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return reply.status(statusCode).send({
|
|
261
|
+
success: false,
|
|
262
|
+
message: error.message,
|
|
263
|
+
...(error.validation && { validation: error.validation }),
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// In routes — throw to trigger error handler
|
|
268
|
+
app.get('/example', async (req, reply) => {
|
|
269
|
+
const item = await service.getById(req.params.id);
|
|
270
|
+
if (!item) {
|
|
271
|
+
const err = new Error('Not found');
|
|
272
|
+
err.statusCode = 404;
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
return item;
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Performance Rules
|
|
282
|
+
|
|
283
|
+
- Define `response` JSON schemas to activate fast serialization (ajv-compiler bypasses JSON.stringify)
|
|
284
|
+
- Use `app.log` (pino) — never `console.log` in handlers
|
|
285
|
+
- Use `async/await` in handlers and plugins — Fastify handles the promise lifecycle
|
|
286
|
+
- Register all plugins with `fastify-plugin` (`fp`) if they should be available in all scopes
|
|
287
|
+
- Use `onRequest` hook for auth guards over route-level `preHandler` when applying globally
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mongoose-odm
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: >
|
|
5
|
+
Apply this skill for any MongoDB/Mongoose task: schema design, model creation, virtuals, indexes, population, aggregation, transactions, or repository abstraction. Triggers: "Mongoose schema", "MongoDB model", "populate relations", "aggregation pipeline", "MongoDB index", "Mongoose middleware", "MongoDB transaction".
|
|
6
|
+
stack: [mongoose, mongodb]
|
|
7
|
+
depends: []
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Mongoose ODM Skill
|
|
11
|
+
|
|
12
|
+
Production patterns for MongoDB/Mongoose: schema design, repository abstraction, performance, and transactions.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install mongoose
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Connection
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// src/config/db.js
|
|
28
|
+
import mongoose from 'mongoose';
|
|
29
|
+
import { env } from './env.js';
|
|
30
|
+
|
|
31
|
+
let isConnected = false;
|
|
32
|
+
|
|
33
|
+
export async function connectDB() {
|
|
34
|
+
if (isConnected) return;
|
|
35
|
+
|
|
36
|
+
mongoose.set('strictQuery', true);
|
|
37
|
+
|
|
38
|
+
await mongoose.connect(env.MONGODB_URI, {
|
|
39
|
+
dbName: env.DB_NAME,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
isConnected = true;
|
|
43
|
+
console.log('✅ MongoDB connected');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle connection events
|
|
47
|
+
mongoose.connection.on('error', (err) => console.error('MongoDB error:', err));
|
|
48
|
+
mongoose.connection.on('disconnected', () => { isConnected = false; });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Schema Design Conventions
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
// src/modules/user/user.model.js
|
|
57
|
+
import mongoose from 'mongoose';
|
|
58
|
+
import bcrypt from 'bcryptjs';
|
|
59
|
+
|
|
60
|
+
const userSchema = new mongoose.Schema(
|
|
61
|
+
{
|
|
62
|
+
name: {
|
|
63
|
+
type: String,
|
|
64
|
+
required: [true, 'Name is required'],
|
|
65
|
+
trim: true,
|
|
66
|
+
maxlength: [100, 'Name max 100 characters'],
|
|
67
|
+
},
|
|
68
|
+
email: {
|
|
69
|
+
type: String,
|
|
70
|
+
required: [true, 'Email is required'],
|
|
71
|
+
unique: true,
|
|
72
|
+
lowercase: true,
|
|
73
|
+
trim: true,
|
|
74
|
+
match: [/^\S+@\S+\.\S+$/, 'Invalid email format'],
|
|
75
|
+
},
|
|
76
|
+
password: {
|
|
77
|
+
type: String,
|
|
78
|
+
required: true,
|
|
79
|
+
minlength: 8,
|
|
80
|
+
select: false, // never returned by default
|
|
81
|
+
},
|
|
82
|
+
role: {
|
|
83
|
+
type: String,
|
|
84
|
+
enum: ['user', 'admin', 'moderator'],
|
|
85
|
+
default: 'user',
|
|
86
|
+
},
|
|
87
|
+
isActive: {
|
|
88
|
+
type: Boolean,
|
|
89
|
+
default: true,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
timestamps: true, // adds createdAt + updatedAt automatically
|
|
94
|
+
versionKey: false, // removes __v field
|
|
95
|
+
toJSON: { virtuals: true }, // include virtuals in JSON output
|
|
96
|
+
toObject: { virtuals: true },
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Index on frequently queried fields
|
|
101
|
+
userSchema.index({ email: 1 });
|
|
102
|
+
userSchema.index({ role: 1, isActive: 1 });
|
|
103
|
+
|
|
104
|
+
// Virtual: computed property (not stored in DB)
|
|
105
|
+
userSchema.virtual('fullName').get(function () {
|
|
106
|
+
return `${this.firstName} ${this.lastName}`;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Pre-save middleware: hash password
|
|
110
|
+
userSchema.pre('save', async function (next) {
|
|
111
|
+
if (!this.isModified('password')) return next();
|
|
112
|
+
this.password = await bcrypt.hash(this.password, 12);
|
|
113
|
+
next();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Instance method
|
|
117
|
+
userSchema.methods.comparePassword = async function (candidate) {
|
|
118
|
+
return bcrypt.compare(candidate, this.password);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Static method
|
|
122
|
+
userSchema.statics.findByEmail = function (email) {
|
|
123
|
+
return this.findOne({ email: email.toLowerCase() });
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const User = mongoose.model('User', userSchema);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Repository Pattern
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
// src/modules/user/user.repository.js
|
|
135
|
+
import { User } from './user.model.js';
|
|
136
|
+
import { AppError } from '../../utils/AppError.js';
|
|
137
|
+
|
|
138
|
+
export const userRepository = {
|
|
139
|
+
async findAll({ page = 1, limit = 10, search, role }) {
|
|
140
|
+
const query = {};
|
|
141
|
+
if (search) query.$text = { $search: search };
|
|
142
|
+
if (role) query.role = role;
|
|
143
|
+
|
|
144
|
+
const skip = (page - 1) * limit;
|
|
145
|
+
|
|
146
|
+
const [data, total] = await Promise.all([
|
|
147
|
+
User.find(query)
|
|
148
|
+
.select('-password')
|
|
149
|
+
.skip(skip)
|
|
150
|
+
.limit(limit)
|
|
151
|
+
.lean(), // lean() returns plain objects — faster
|
|
152
|
+
User.countDocuments(query),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
return { data, total, page, limit };
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async findById(id) {
|
|
159
|
+
const user = await User.findById(id).select('-password').lean();
|
|
160
|
+
if (!user) throw new AppError('User not found', 404);
|
|
161
|
+
return user;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async create(data) {
|
|
165
|
+
const user = await User.create(data);
|
|
166
|
+
const { password, ...safe } = user.toObject();
|
|
167
|
+
return safe;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
async update(id, data) {
|
|
171
|
+
const user = await User.findByIdAndUpdate(id, data, {
|
|
172
|
+
new: true, // return updated document
|
|
173
|
+
runValidators: true, // run schema validators on update
|
|
174
|
+
}).select('-password');
|
|
175
|
+
if (!user) throw new AppError('User not found', 404);
|
|
176
|
+
return user;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
async delete(id) {
|
|
180
|
+
const user = await User.findByIdAndDelete(id);
|
|
181
|
+
if (!user) throw new AppError('User not found', 404);
|
|
182
|
+
return user;
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Population (Relations)
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
// Reference (like FK)
|
|
193
|
+
const postSchema = new Schema({
|
|
194
|
+
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
|
195
|
+
tags: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Populate at query time
|
|
199
|
+
const posts = await Post.find()
|
|
200
|
+
.populate('author', 'name email') // select specific fields
|
|
201
|
+
.populate('tags', 'name slug')
|
|
202
|
+
.lean();
|
|
203
|
+
|
|
204
|
+
// Nested population
|
|
205
|
+
await Post.find().populate({
|
|
206
|
+
path: 'author',
|
|
207
|
+
select: 'name',
|
|
208
|
+
populate: { path: 'profile', select: 'avatar' },
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Rule**: Use `.lean()` with population when you don't need Mongoose document methods. It's 3-10x faster.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Aggregation Pipeline
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
// Complex reporting query
|
|
220
|
+
const stats = await Order.aggregate([
|
|
221
|
+
{ $match: { status: 'completed', createdAt: { $gte: startDate } } },
|
|
222
|
+
{
|
|
223
|
+
$group: {
|
|
224
|
+
_id: '$userId',
|
|
225
|
+
totalOrders: { $sum: 1 },
|
|
226
|
+
totalRevenue: { $sum: '$total' },
|
|
227
|
+
avgOrderValue: { $avg: '$total' },
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{ $sort: { totalRevenue: -1 } },
|
|
231
|
+
{ $limit: 10 },
|
|
232
|
+
{
|
|
233
|
+
$lookup: {
|
|
234
|
+
from: 'users',
|
|
235
|
+
localField: '_id',
|
|
236
|
+
foreignField: '_id',
|
|
237
|
+
as: 'user',
|
|
238
|
+
pipeline: [{ $project: { name: 1, email: 1 } }],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{ $unwind: '$user' },
|
|
242
|
+
]);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Transactions
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
const session = await mongoose.startSession();
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
session.startTransaction();
|
|
254
|
+
|
|
255
|
+
const order = await Order.create([orderData], { session });
|
|
256
|
+
await Product.findByIdAndUpdate(
|
|
257
|
+
productId,
|
|
258
|
+
{ $inc: { stock: -quantity } },
|
|
259
|
+
{ session, new: true, runValidators: true }
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
await session.commitTransaction();
|
|
263
|
+
return order[0];
|
|
264
|
+
} catch (err) {
|
|
265
|
+
await session.abortTransaction();
|
|
266
|
+
throw err;
|
|
267
|
+
} finally {
|
|
268
|
+
session.endSession();
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Rules
|
|
275
|
+
|
|
276
|
+
- Always use `timestamps: true` and `versionKey: false` in schema options
|
|
277
|
+
- Use `select: false` on sensitive fields (password, tokens)
|
|
278
|
+
- Add `lean()` to read-heavy queries not needing Mongoose methods
|
|
279
|
+
- Index all fields used in `find()`, `sort()`, and `$lookup` foreign keys
|
|
280
|
+
- Use `runValidators: true` on `findByIdAndUpdate` — it's off by default
|
|
281
|
+
- Prefer aggregation over application-level data processing for large datasets
|