@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,253 @@
|
|
|
1
|
+
# Module Scaffold Reference
|
|
2
|
+
|
|
3
|
+
Complete 4-file template. Replace `user`/`User`/`users` with your resource name.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## {module}.validation.js
|
|
8
|
+
|
|
9
|
+
Validates `body`, `params`, and `query` using Zod v4. One schema per endpoint.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
|
|
14
|
+
// Body schema for POST /users
|
|
15
|
+
export const createUserSchema = z.object({
|
|
16
|
+
body: z.object({
|
|
17
|
+
name: z.string().min(2, 'Name min 2 chars').max(100),
|
|
18
|
+
email: z.string().email('Invalid email'),
|
|
19
|
+
password: z.string().min(8, 'Password min 8 chars'),
|
|
20
|
+
role: z.enum(['USER', 'ADMIN']).default('USER'),
|
|
21
|
+
}).strict(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Body + params for PATCH /users/:id
|
|
25
|
+
export const updateUserSchema = z.object({
|
|
26
|
+
params: z.object({ id: z.string().uuid('Invalid ID') }),
|
|
27
|
+
body: z.object({
|
|
28
|
+
name: z.string().min(2).max(100).optional(),
|
|
29
|
+
email: z.string().email().optional(),
|
|
30
|
+
role: z.enum(['USER', 'ADMIN']).optional(),
|
|
31
|
+
}).strict(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Params only for GET /users/:id, DELETE /users/:id
|
|
35
|
+
export const userIdSchema = z.object({
|
|
36
|
+
params: z.object({ id: z.string().uuid('Invalid ID') }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Query for GET /users?page=1&limit=10&search=
|
|
40
|
+
export const listUsersSchema = z.object({
|
|
41
|
+
query: z.object({
|
|
42
|
+
page: z.coerce.number().int().positive().default(1),
|
|
43
|
+
limit: z.coerce.number().int().positive().max(100).default(10),
|
|
44
|
+
search: z.string().optional(),
|
|
45
|
+
role: z.enum(['USER', 'ADMIN']).optional(),
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** @typedef {import('zod').infer<typeof createUserSchema>['body']} CreateUserDto */
|
|
50
|
+
/** @typedef {import('zod').infer<typeof updateUserSchema>['body']} UpdateUserDto */
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## {module}.service.js
|
|
56
|
+
|
|
57
|
+
Business logic + all Prisma calls. Throw `AppError` for expected failures.
|
|
58
|
+
Never import `res` or `req` here.
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { Prisma } from '@prisma/client';
|
|
62
|
+
import prisma from '../../config/db.js';
|
|
63
|
+
import { AppError } from '../../utils/AppError.js';
|
|
64
|
+
import bcrypt from 'bcrypt';
|
|
65
|
+
|
|
66
|
+
export const userService = {
|
|
67
|
+
|
|
68
|
+
async getAll({ page = 1, limit = 10, search, role } = {}) {
|
|
69
|
+
const skip = (page - 1) * limit;
|
|
70
|
+
const where = {};
|
|
71
|
+
if (role) where.role = role;
|
|
72
|
+
if (search) where.OR = [
|
|
73
|
+
{ name: { contains: search, mode: 'insensitive' } },
|
|
74
|
+
{ email: { contains: search, mode: 'insensitive' } },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const [data, total] = await prisma.$transaction([
|
|
78
|
+
prisma.user.findMany({
|
|
79
|
+
where,
|
|
80
|
+
skip,
|
|
81
|
+
take: limit,
|
|
82
|
+
orderBy: { createdAt: 'desc' },
|
|
83
|
+
select: { id: true, name: true, email: true, role: true, createdAt: true },
|
|
84
|
+
}),
|
|
85
|
+
prisma.user.count({ where }),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
return { data, total, page, limit };
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async getById(id) {
|
|
92
|
+
const user = await prisma.user.findUnique({
|
|
93
|
+
where: { id },
|
|
94
|
+
select: { id: true, name: true, email: true, role: true, createdAt: true },
|
|
95
|
+
});
|
|
96
|
+
if (!user) throw new AppError('User not found', 404);
|
|
97
|
+
return user;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
async create(data) {
|
|
101
|
+
const exists = await prisma.user.findUnique({ where: { email: data.email } });
|
|
102
|
+
if (exists) throw new AppError('Email already registered', 409);
|
|
103
|
+
|
|
104
|
+
const passwordHash = await bcrypt.hash(data.password, 12);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const user = await prisma.user.create({
|
|
108
|
+
data: { ...data, password: passwordHash },
|
|
109
|
+
select: { id: true, name: true, email: true, role: true, createdAt: true },
|
|
110
|
+
});
|
|
111
|
+
return user;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
|
114
|
+
if (err.code === 'P2002') throw new AppError('Email already registered', 409);
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async update(id, data) {
|
|
121
|
+
try {
|
|
122
|
+
return await prisma.user.update({
|
|
123
|
+
where: { id },
|
|
124
|
+
data,
|
|
125
|
+
select: { id: true, name: true, email: true, role: true, updatedAt: true },
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
|
129
|
+
if (err.code === 'P2025') throw new AppError('User not found', 404);
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async remove(id) {
|
|
136
|
+
try {
|
|
137
|
+
await prisma.user.delete({ where: { id } });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
|
140
|
+
if (err.code === 'P2025') throw new AppError('User not found', 404);
|
|
141
|
+
}
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## {module}.controller.js
|
|
151
|
+
|
|
152
|
+
HTTP layer only. No business logic, no Prisma, no try/catch.
|
|
153
|
+
Express v5 auto-forwards async errors to the global error handler.
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
import { userService } from './user.service.js';
|
|
157
|
+
import { sendSuccess } from '../../utils/response.js';
|
|
158
|
+
|
|
159
|
+
export const userController = {
|
|
160
|
+
|
|
161
|
+
getAll: async (req, res) => {
|
|
162
|
+
const result = await userService.getAll(req.query);
|
|
163
|
+
sendSuccess(res, 'Users fetched', result);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
getById: async (req, res) => {
|
|
167
|
+
const user = await userService.getById(req.params.id);
|
|
168
|
+
sendSuccess(res, 'User fetched', user);
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
create: async (req, res) => {
|
|
172
|
+
const user = await userService.create(req.body);
|
|
173
|
+
sendSuccess(res, 'User created', user, 201);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
update: async (req, res) => {
|
|
177
|
+
const user = await userService.update(req.params.id, req.body);
|
|
178
|
+
sendSuccess(res, 'User updated', user);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
remove: async (req, res) => {
|
|
182
|
+
await userService.remove(req.params.id);
|
|
183
|
+
sendSuccess(res, 'User deleted');
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## {module}.routes.js
|
|
191
|
+
|
|
192
|
+
Wires middleware → controller. Route order matters: specific before dynamic.
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import { Router } from 'express';
|
|
196
|
+
import { validate } from '../../middlewares/validate.middleware.js';
|
|
197
|
+
import { authenticate, requireRole } from '../../middlewares/auth.middleware.js';
|
|
198
|
+
import {
|
|
199
|
+
createUserSchema, updateUserSchema,
|
|
200
|
+
userIdSchema, listUsersSchema,
|
|
201
|
+
} from './user.validation.js';
|
|
202
|
+
import { userController } from './user.controller.js';
|
|
203
|
+
|
|
204
|
+
const router = Router();
|
|
205
|
+
|
|
206
|
+
router.get( '/', authenticate, validate(listUsersSchema), userController.getAll);
|
|
207
|
+
router.get( '/:id', authenticate, validate(userIdSchema), userController.getById);
|
|
208
|
+
router.post( '/', authenticate, validate(createUserSchema), userController.create);
|
|
209
|
+
router.patch('/:id', authenticate, validate(updateUserSchema), userController.update);
|
|
210
|
+
router.delete('/:id',authenticate, validate(userIdSchema), userController.remove);
|
|
211
|
+
|
|
212
|
+
export default router;
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Register in src/routes/index.js
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
import userRouter from '../modules/user/user.routes.js';
|
|
221
|
+
|
|
222
|
+
router.use('/users', userRouter);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Pagination Response Shape
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"success": true,
|
|
232
|
+
"message": "Users fetched",
|
|
233
|
+
"data": [...],
|
|
234
|
+
"meta": {
|
|
235
|
+
"total": 85,
|
|
236
|
+
"page": 2,
|
|
237
|
+
"limit": 10,
|
|
238
|
+
"totalPages": 9,
|
|
239
|
+
"hasNext": true,
|
|
240
|
+
"hasPrev": true
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Single item:
|
|
246
|
+
```json
|
|
247
|
+
{ "success": true, "message": "User fetched", "data": { "id": "...", "name": "..." } }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Deleted / no data:
|
|
251
|
+
```json
|
|
252
|
+
{ "success": true, "message": "User deleted" }
|
|
253
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Prisma Setup Reference
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @prisma/client
|
|
7
|
+
npm install -D prisma
|
|
8
|
+
npx prisma init --datasource-provider postgresql
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## prisma/schema.prisma — Base Template
|
|
14
|
+
|
|
15
|
+
```prisma
|
|
16
|
+
generator client {
|
|
17
|
+
provider = "prisma-client-js"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
datasource db {
|
|
21
|
+
provider = "postgresql"
|
|
22
|
+
url = env("DATABASE_URL")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
model User {
|
|
26
|
+
id String @id @default(uuid())
|
|
27
|
+
email String @unique
|
|
28
|
+
name String
|
|
29
|
+
password String
|
|
30
|
+
role Role @default(USER)
|
|
31
|
+
createdAt DateTime @default(now())
|
|
32
|
+
updatedAt DateTime @updatedAt
|
|
33
|
+
|
|
34
|
+
@@map("users")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
enum Role {
|
|
38
|
+
USER
|
|
39
|
+
ADMIN
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Conventions:**
|
|
44
|
+
- Always use `uuid()` for IDs (not auto-increment)
|
|
45
|
+
- Always add `createdAt` + `updatedAt` to every model
|
|
46
|
+
- Use `@@map("snake_case_table_name")` to keep DB column names clean
|
|
47
|
+
- Use enums for fixed value sets (role, status, type)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Migration Commands
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx prisma migrate dev --name init # Create + apply migration (dev)
|
|
55
|
+
npx prisma migrate deploy # Apply migrations in production
|
|
56
|
+
npx prisma migrate reset # Reset DB (dev only — destroys data)
|
|
57
|
+
npx prisma generate # Regenerate client after schema change
|
|
58
|
+
npx prisma studio # Open visual DB browser
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Handling Prisma Errors in Repository
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
import { Prisma } from '@prisma/client';
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// ...prisma call
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
72
|
+
switch (error.code) {
|
|
73
|
+
case 'P2002': throw new AppError('Duplicate entry: ' + error.meta?.target, 409);
|
|
74
|
+
case 'P2025': throw new AppError('Record not found', 404);
|
|
75
|
+
case 'P2003': throw new AppError('Related record not found', 400);
|
|
76
|
+
case 'P2014': throw new AppError('Relation violation', 400);
|
|
77
|
+
default: throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Transaction Example
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
90
|
+
const order = await tx.order.create({ data: orderData });
|
|
91
|
+
await tx.product.update({
|
|
92
|
+
where: { id: orderData.productId },
|
|
93
|
+
data: { stock: { decrement: orderData.quantity } },
|
|
94
|
+
});
|
|
95
|
+
return order;
|
|
96
|
+
});
|
|
97
|
+
```
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Response Helpers Reference
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## src/utils/response.js
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
/**
|
|
9
|
+
* Send a successful JSON response.
|
|
10
|
+
* If data has { data, total, page, limit } shape → auto-adds meta pagination block.
|
|
11
|
+
*/
|
|
12
|
+
export const sendSuccess = (res, message, data = null, statusCode = 200) => {
|
|
13
|
+
const body = { success: true, message };
|
|
14
|
+
|
|
15
|
+
if (data !== null) {
|
|
16
|
+
if (isPaginated(data)) {
|
|
17
|
+
body.data = data.data;
|
|
18
|
+
body.meta = buildMeta(data);
|
|
19
|
+
} else {
|
|
20
|
+
body.data = data;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return res.status(statusCode).json(body);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isPaginated = (d) =>
|
|
28
|
+
d !== null &&
|
|
29
|
+
typeof d === 'object' &&
|
|
30
|
+
'data' in d &&
|
|
31
|
+
'total' in d &&
|
|
32
|
+
'page' in d &&
|
|
33
|
+
'limit' in d;
|
|
34
|
+
|
|
35
|
+
const buildMeta = ({ total, page, limit }) => ({
|
|
36
|
+
total,
|
|
37
|
+
page,
|
|
38
|
+
limit,
|
|
39
|
+
totalPages: Math.ceil(total / limit),
|
|
40
|
+
hasNext: page * limit < total,
|
|
41
|
+
hasPrev: page > 1,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Usage in controllers
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
// List (auto-paginates)
|
|
51
|
+
sendSuccess(res, 'Users fetched', { data: users, total, page, limit });
|
|
52
|
+
|
|
53
|
+
// Single item
|
|
54
|
+
sendSuccess(res, 'User fetched', user);
|
|
55
|
+
|
|
56
|
+
// Created
|
|
57
|
+
sendSuccess(res, 'User created', user, 201);
|
|
58
|
+
|
|
59
|
+
// Deleted / no data
|
|
60
|
+
sendSuccess(res, 'User deleted');
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Response Shapes
|
|
66
|
+
|
|
67
|
+
### Single item
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"success": true,
|
|
71
|
+
"message": "User fetched",
|
|
72
|
+
"data": { "id": "uuid", "name": "Alice", "email": "alice@example.com" }
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Paginated list
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"success": true,
|
|
80
|
+
"message": "Users fetched",
|
|
81
|
+
"data": [...],
|
|
82
|
+
"meta": {
|
|
83
|
+
"total": 85,
|
|
84
|
+
"page": 2,
|
|
85
|
+
"limit": 10,
|
|
86
|
+
"totalPages": 9,
|
|
87
|
+
"hasNext": true,
|
|
88
|
+
"hasPrev": true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Created (201)
|
|
94
|
+
```json
|
|
95
|
+
{ "success": true, "message": "User created", "data": { "id": "...", "name": "..." } }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### No data (delete, void operations)
|
|
99
|
+
```json
|
|
100
|
+
{ "success": true, "message": "User deleted" }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Error (from errorHandler)
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"success": false,
|
|
107
|
+
"message": "User not found"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Validation error (from validate middleware)
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"success": false,
|
|
115
|
+
"message": "Invalid email",
|
|
116
|
+
"errors": [
|
|
117
|
+
{ "field": "body.email", "message": "Invalid email" }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Pagination in service layer
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
async getAll({ page = 1, limit = 10, search } = {}) {
|
|
128
|
+
const skip = (page - 1) * limit;
|
|
129
|
+
const where = search
|
|
130
|
+
? { name: { contains: search, mode: 'insensitive' } }
|
|
131
|
+
: {};
|
|
132
|
+
|
|
133
|
+
const [data, total] = await prisma.$transaction([
|
|
134
|
+
prisma.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' } }),
|
|
135
|
+
prisma.user.count({ where }),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
return { data, total, page, limit }; // sendSuccess auto-detects this shape
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## HTTP Status Codes
|
|
145
|
+
|
|
146
|
+
| Code | Use case |
|
|
147
|
+
|------|---------|
|
|
148
|
+
| 200 | OK — GET, PATCH, DELETE success |
|
|
149
|
+
| 201 | Created — POST success |
|
|
150
|
+
| 400 | Bad Request — business logic error |
|
|
151
|
+
| 401 | Unauthorized — missing or invalid token |
|
|
152
|
+
| 403 | Forbidden — authenticated, insufficient role |
|
|
153
|
+
| 404 | Not Found |
|
|
154
|
+
| 409 | Conflict — duplicate entry |
|
|
155
|
+
| 422 | Unprocessable — validation error |
|
|
156
|
+
| 429 | Too Many Requests — rate limit |
|
|
157
|
+
| 500 | Internal Server Error |
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Zod v4 Validation Reference
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## validate.middleware.js
|
|
6
|
+
|
|
7
|
+
Validates `body`, `params`, and `query` in one call. Uses Zod v4 `.issues` API.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { AppError } from '../utils/AppError.js';
|
|
11
|
+
|
|
12
|
+
export const validate = (schema) => (req, res, next) => {
|
|
13
|
+
const result = schema.safeParse({
|
|
14
|
+
body: req.body,
|
|
15
|
+
params: req.params,
|
|
16
|
+
query: req.query,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
// Zod v4: use .issues (not .flatten())
|
|
21
|
+
const errors = result.error.issues.map((i) => ({
|
|
22
|
+
field: i.path.join('.'),
|
|
23
|
+
message: i.message,
|
|
24
|
+
}));
|
|
25
|
+
return next(new AppError(errors[0]?.message || 'Validation failed', 422, errors));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Replace with parsed/coerced/defaulted values
|
|
29
|
+
if (result.data.body) req.body = result.data.body;
|
|
30
|
+
if (result.data.params) req.params = result.data.params;
|
|
31
|
+
if (result.data.query) req.query = result.data.query;
|
|
32
|
+
|
|
33
|
+
next();
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Schema Structure
|
|
40
|
+
|
|
41
|
+
Always use `z.object({ body?, params?, query? })` for the validate middleware:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { z } from 'zod';
|
|
45
|
+
|
|
46
|
+
// POST body only
|
|
47
|
+
export const createSchema = z.object({
|
|
48
|
+
body: z.object({ name: z.string().min(1), email: z.string().email() }).strict(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Params + body
|
|
52
|
+
export const updateSchema = z.object({
|
|
53
|
+
params: z.object({ id: z.string().uuid() }),
|
|
54
|
+
body: z.object({
|
|
55
|
+
name: z.string().min(1).optional(),
|
|
56
|
+
email: z.string().email().optional(),
|
|
57
|
+
}).strict(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Params only
|
|
61
|
+
export const idSchema = z.object({
|
|
62
|
+
params: z.object({ id: z.string().uuid() }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Query only (list / paginate)
|
|
66
|
+
export const listSchema = z.object({
|
|
67
|
+
query: z.object({
|
|
68
|
+
page: z.coerce.number().int().positive().default(1),
|
|
69
|
+
limit: z.coerce.number().int().positive().max(100).default(10),
|
|
70
|
+
search: z.string().optional(),
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Zod v4 Validator Reference
|
|
78
|
+
|
|
79
|
+
### Strings
|
|
80
|
+
```js
|
|
81
|
+
z.string().min(1, 'Required').max(255)
|
|
82
|
+
z.string().email()
|
|
83
|
+
z.string().uuid()
|
|
84
|
+
z.string().url()
|
|
85
|
+
z.string().regex(/^\d{6}$/, 'Must be 6 digits')
|
|
86
|
+
z.string().trim().toLowerCase() // transform on parse
|
|
87
|
+
z.string().nonempty() // min(1) shorthand
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Numbers
|
|
91
|
+
```js
|
|
92
|
+
z.number().int().positive()
|
|
93
|
+
z.number().min(0).max(100)
|
|
94
|
+
z.coerce.number().int().positive() // always use coerce for query params
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Optional / Nullable
|
|
98
|
+
```js
|
|
99
|
+
z.string().optional() // string | undefined
|
|
100
|
+
z.string().nullable() // string | null
|
|
101
|
+
z.string().nullish() // string | null | undefined
|
|
102
|
+
z.string().default('x') // fallback when undefined
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Enums
|
|
106
|
+
```js
|
|
107
|
+
z.enum(['USER', 'ADMIN', 'MANAGER'])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Objects
|
|
111
|
+
```js
|
|
112
|
+
z.object({ name: z.string() }).strict() // fail on unknown keys
|
|
113
|
+
z.object({ name: z.string() }).partial() // all optional
|
|
114
|
+
z.object({ name: z.string() }).passthrough() // allow extra keys
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Arrays
|
|
118
|
+
```js
|
|
119
|
+
z.array(z.string()).min(1).max(50)
|
|
120
|
+
z.array(z.string().uuid())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Dates
|
|
124
|
+
```js
|
|
125
|
+
z.coerce.date() // "2024-01-01" → Date
|
|
126
|
+
z.date().min(new Date()) // must be in the future
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Transformations
|
|
130
|
+
```js
|
|
131
|
+
z.string().transform((v) => v.trim().toLowerCase())
|
|
132
|
+
z.preprocess((v) => Number(v), z.number())
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Cross-field validation
|
|
136
|
+
```js
|
|
137
|
+
z.object({
|
|
138
|
+
password: z.string().min(8),
|
|
139
|
+
confirmPassword: z.string(),
|
|
140
|
+
}).refine((d) => d.password === d.confirmPassword, {
|
|
141
|
+
message: 'Passwords do not match',
|
|
142
|
+
path: ['confirmPassword'],
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Zod v4 error format in validate middleware response
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"success": false,
|
|
151
|
+
"message": "Invalid email",
|
|
152
|
+
"errors": [
|
|
153
|
+
{ "field": "body.email", "message": "Invalid email" },
|
|
154
|
+
{ "field": "body.name", "message": "Name min 2 chars" }
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
```
|