express-genix 2.0.1 → 3.0.1
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 +61 -11
- package/index.js +22 -1
- package/lib/features.js +8 -0
- package/lib/generator.js +77 -2
- package/package.json +2 -2
- package/templates/config/queue.js.ejs +29 -0
- package/templates/config/schema.prisma.ejs +2 -0
- package/templates/controllers/adminController.js.ejs +109 -0
- package/templates/controllers/authController.js.ejs +9 -4
- package/templates/controllers/userController.js.ejs +1 -0
- package/templates/core/app.js.ejs +50 -1
- package/templates/core/env.ejs +17 -0
- package/templates/core/env.example.ejs +17 -0
- package/templates/core/package.json.ejs +8 -1
- package/templates/core/server.js.ejs +5 -2
- package/templates/graphql/resolvers.js.ejs +61 -0
- package/templates/graphql/typeDefs.js.ejs +53 -0
- package/templates/jobs/worker.js.ejs +60 -0
- package/templates/middleware/auditLog.js.ejs +62 -0
- package/templates/middleware/metrics.js.ejs +65 -0
- package/templates/middleware/rbac.js.ejs +86 -0
- package/templates/middleware/upload.js.ejs +50 -0
- package/templates/models/User.mongo.js.ejs +29 -0
- package/templates/models/User.postgres.js.ejs +7 -1
- package/templates/routes/adminRoutes.js.ejs +150 -0
- package/templates/routes/index.js.ejs +6 -0
- package/templates/routes/jobRoutes.js.ejs +85 -0
- package/templates/routes/uploadRoutes.js.ejs +100 -0
- package/templates/services/authService.js.ejs +1 -0
- package/templates/services/emailService.js.ejs +88 -0
- package/templates/services/userService.mongodb.js.ejs +32 -2
- package/templates/services/userService.postgres.js.ejs +33 -1
- package/templates/services/userService.prisma.js.ejs +50 -6
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
A production-grade CLI tool that generates Express.js applications with best-in-class defaults. Scaffold a complete REST API in seconds — with TypeScript, authentication, Prisma/Mongoose/Sequelize, Docker, CI/CD, and more.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/express-genix)
|
|
6
|
+
[](https://www.npmjs.com/package/express-genix)
|
|
6
7
|
[](https://opensource.org/licenses/MIT)
|
|
7
8
|
[](https://nodejs.org)
|
|
8
9
|
|
|
@@ -15,20 +16,32 @@ A production-grade CLI tool that generates Express.js applications with best-in-
|
|
|
15
16
|
|
|
16
17
|
**Security & Auth**
|
|
17
18
|
- JWT access + refresh tokens with token blacklist logout
|
|
19
|
+
- Role-Based Access Control (RBAC) — admin, moderator, user roles with permission system
|
|
20
|
+
- Admin panel routes for user management (list, view, update roles, delete)
|
|
18
21
|
- Password reset flow (forgot-password / reset-password with crypto tokens)
|
|
19
22
|
- Optional Redis-backed blacklist for production multi-instance deployments
|
|
20
23
|
- Zod request validation with pre-built schemas (register, login, reset, etc.)
|
|
21
24
|
- bcrypt password hashing, input sanitization (`validator`)
|
|
22
25
|
- Helmet, CORS, environment validation on startup
|
|
23
26
|
- Auto-generated cryptographically secure JWT secrets
|
|
27
|
+
- Soft deletes with restore capability
|
|
24
28
|
|
|
25
29
|
**API & Documentation**
|
|
26
30
|
- Swagger UI + swagger-jsdoc annotation-based docs with example request/response bodies
|
|
31
|
+
- GraphQL (Apollo Server) with type definitions and resolvers
|
|
27
32
|
- Consistent `{ success, data, meta }` response envelope
|
|
33
|
+
- API versioning (`/api/v1/` prefix)
|
|
28
34
|
- Paginated list endpoints
|
|
29
35
|
- Request ID / correlation tracking
|
|
30
36
|
- Response caching middleware (Redis-backed, configurable TTL)
|
|
31
37
|
|
|
38
|
+
**Services & Infrastructure**
|
|
39
|
+
- Email service (Nodemailer) with welcome and password reset emails
|
|
40
|
+
- File uploads (Multer) with type filtering and size limits
|
|
41
|
+
- Background jobs (BullMQ) with Redis-backed queues and workers
|
|
42
|
+
- Prometheus metrics (`/metrics` endpoint with prom-client)
|
|
43
|
+
- Audit logging middleware with request tracking and sensitive field redaction
|
|
44
|
+
|
|
32
45
|
**Developer Experience**
|
|
33
46
|
- Interactive prompts — pick language, database, features via checkbox
|
|
34
47
|
- Winston or Pino logger (you choose)
|
|
@@ -71,7 +84,7 @@ You'll be prompted for:
|
|
|
71
84
|
1. **Project name**
|
|
72
85
|
2. **Language** — JavaScript or TypeScript
|
|
73
86
|
3. **Database** — MongoDB, PostgreSQL (Sequelize), PostgreSQL (Prisma), or None
|
|
74
|
-
4. **Features** — Auth, Rate Limiting, Swagger, Redis
|
|
87
|
+
4. **Features** — Auth, Rate Limiting, Swagger, Redis, Docker, CI/CD, WebSocket, Request ID, Email, File Uploads, Soft Deletes, Audit Logging, Prometheus Metrics, API Versioning, Background Jobs, GraphQL
|
|
75
88
|
5. **Logger** — Winston or Pino
|
|
76
89
|
|
|
77
90
|
The CLI generates your project, installs dependencies, formats code, and creates an initial git commit.
|
|
@@ -103,19 +116,22 @@ express-genix add prisma # Adds Prisma schema, client config, migrations
|
|
|
103
116
|
```
|
|
104
117
|
my-express-app/
|
|
105
118
|
├── src/
|
|
106
|
-
│ ├── config/ # Database, Swagger, WebSocket configuration
|
|
107
|
-
│ ├── controllers/ # Route handlers
|
|
108
|
-
│ ├──
|
|
109
|
-
│ ├──
|
|
119
|
+
│ ├── config/ # Database, Swagger, WebSocket, queue configuration
|
|
120
|
+
│ ├── controllers/ # Route handlers (auth, user, admin, example)
|
|
121
|
+
│ ├── graphql/ # GraphQL type definitions & resolvers (if selected)
|
|
122
|
+
│ ├── jobs/ # BullMQ background workers (if selected)
|
|
123
|
+
│ ├── middleware/ # Auth, RBAC, validation, error handling, uploads, metrics, audit log
|
|
124
|
+
│ ├── models/ # Database models (Mongoose/Sequelize/Prisma)
|
|
110
125
|
│ ├── routes/ # API route definitions with Swagger annotations
|
|
111
|
-
│ ├── services/ # Business logic layer
|
|
126
|
+
│ ├── services/ # Business logic layer (auth, user, email)
|
|
112
127
|
│ ├── utils/ # Logger, errors, response helpers, validators
|
|
113
128
|
│ ├── app.js|ts # Express app setup
|
|
114
|
-
│ └── server.js|ts # Server + clustering + WebSocket
|
|
129
|
+
│ └── server.js|ts # Server + clustering + WebSocket + workers
|
|
115
130
|
├── tests/ # Jest + Supertest suites
|
|
116
131
|
├── prisma/ # Prisma schema (if selected)
|
|
117
132
|
├── migrations/ # Sequelize migrations (if PostgreSQL + Sequelize)
|
|
118
133
|
├── seeders/ # Sequelize seeders (if PostgreSQL + Sequelize)
|
|
134
|
+
├── uploads/ # File upload directory (if selected)
|
|
119
135
|
├── .github/workflows/ # CI/CD pipeline (if selected)
|
|
120
136
|
├── .env # Auto-generated environment config
|
|
121
137
|
├── .env.example # Template for team sharing
|
|
@@ -126,6 +142,8 @@ my-express-app/
|
|
|
126
142
|
|
|
127
143
|
## API Endpoints (with database + auth)
|
|
128
144
|
|
|
145
|
+
> When API versioning is enabled, all `/api/*` routes become `/api/v1/*`.
|
|
146
|
+
|
|
129
147
|
### Authentication
|
|
130
148
|
| Method | Endpoint | Description |
|
|
131
149
|
|--------|----------|-------------|
|
|
@@ -141,12 +159,35 @@ my-express-app/
|
|
|
141
159
|
|--------|----------|-------------|
|
|
142
160
|
| GET | `/api/users/profile` | Get current user |
|
|
143
161
|
| PUT | `/api/users/profile` | Update profile |
|
|
144
|
-
| DELETE | `/api/users/profile` | Delete account |
|
|
162
|
+
| DELETE | `/api/users/profile` | Delete account (soft delete if enabled) |
|
|
163
|
+
|
|
164
|
+
### Admin (RBAC — admin role required)
|
|
165
|
+
| Method | Endpoint | Description |
|
|
166
|
+
|--------|----------|-------------|
|
|
167
|
+
| GET | `/api/admin/users` | List all users (paginated) |
|
|
168
|
+
| GET | `/api/admin/users/:id` | Get user by ID |
|
|
169
|
+
| PUT | `/api/admin/users/:id/role` | Update user role |
|
|
170
|
+
| DELETE | `/api/admin/users/:id` | Delete user |
|
|
171
|
+
| POST | `/api/admin/users/:id/restore` | Restore soft-deleted user |
|
|
172
|
+
|
|
173
|
+
### File Uploads
|
|
174
|
+
| Method | Endpoint | Description |
|
|
175
|
+
|--------|----------|-------------|
|
|
176
|
+
| POST | `/api/uploads/single` | Upload a single file |
|
|
177
|
+
| POST | `/api/uploads/multiple` | Upload multiple files (max 10) |
|
|
178
|
+
|
|
179
|
+
### Background Jobs (BullMQ)
|
|
180
|
+
| Method | Endpoint | Description |
|
|
181
|
+
|--------|----------|-------------|
|
|
182
|
+
| POST | `/api/jobs` | Dispatch a background job |
|
|
183
|
+
| GET | `/api/jobs/:id` | Get job status |
|
|
145
184
|
|
|
146
|
-
### Health
|
|
185
|
+
### Observability & Health
|
|
147
186
|
| Method | Endpoint | Description |
|
|
148
187
|
|--------|----------|-------------|
|
|
149
|
-
| GET | `/health` | Health check
|
|
188
|
+
| GET | `/health` | Health check (uptime, DB, Redis, memory) |
|
|
189
|
+
| GET | `/metrics` | Prometheus metrics (prom-client) |
|
|
190
|
+
| GET | `/graphql` | GraphQL Playground (Apollo Server) |
|
|
150
191
|
|
|
151
192
|
## Available Scripts
|
|
152
193
|
|
|
@@ -185,10 +226,17 @@ Generated `.env` includes auto-generated JWT secrets:
|
|
|
185
226
|
| `JWT_SECRET` | Access token secret (auto-generated) | — |
|
|
186
227
|
| `JWT_REFRESH_SECRET` | Refresh token secret (auto-generated) | — |
|
|
187
228
|
| `MONGO_URI` / `DATABASE_URL` | Database connection string | — |
|
|
188
|
-
| `REDIS_URL` | Redis URL (
|
|
229
|
+
| `REDIS_URL` | Redis URL (token blacklist, caching, BullMQ) | `redis://localhost:6379` |
|
|
189
230
|
| `RATE_LIMIT_WINDOW_MS` | Rate limit window (ms) | `900000` |
|
|
190
231
|
| `RATE_LIMIT_MAX` | Max requests per window | `100` |
|
|
191
232
|
| `LOG_LEVEL` | Logging level | `info` |
|
|
233
|
+
| `SMTP_HOST` | SMTP server host (email service) | — |
|
|
234
|
+
| `SMTP_PORT` | SMTP server port | `587` |
|
|
235
|
+
| `SMTP_USER` | SMTP username | — |
|
|
236
|
+
| `SMTP_PASS` | SMTP password | — |
|
|
237
|
+
| `EMAIL_FROM` | Default sender address | — |
|
|
238
|
+
| `UPLOAD_MAX_SIZE` | Max file upload size in bytes | `5242880` (5 MB) |
|
|
239
|
+
| `UPLOAD_DIR` | Upload destination directory | `uploads` |
|
|
192
240
|
|
|
193
241
|
## Docker
|
|
194
242
|
|
|
@@ -196,6 +244,8 @@ Generated `.env` includes auto-generated JWT secrets:
|
|
|
196
244
|
docker-compose up --build
|
|
197
245
|
```
|
|
198
246
|
|
|
247
|
+
The generated `docker-compose.yml` includes services for your app and its dependencies (MongoDB/PostgreSQL, Redis) with health checks, volumes, and a shared network. The `Dockerfile` uses multi-stage builds, runs as a non-root user, and only copies production dependencies.
|
|
248
|
+
|
|
199
249
|
## Contributing
|
|
200
250
|
|
|
201
251
|
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
|
package/index.js
CHANGED
|
@@ -75,6 +75,14 @@ async function main() {
|
|
|
75
75
|
{ name: 'CI/CD (GitHub Actions)', value: 'cicd' },
|
|
76
76
|
{ name: 'WebSocket (Socket.io)', value: 'websocket' },
|
|
77
77
|
{ name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
|
|
78
|
+
{ name: 'Email Service (Nodemailer)', value: 'email' },
|
|
79
|
+
{ name: 'File Uploads (Multer)', value: 'fileUpload' },
|
|
80
|
+
{ name: 'Soft Deletes', value: 'softDelete' },
|
|
81
|
+
{ name: 'Audit Logging', value: 'auditLog' },
|
|
82
|
+
{ name: 'Prometheus Metrics', value: 'metrics' },
|
|
83
|
+
{ name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
|
|
84
|
+
{ name: 'Background Jobs (BullMQ)', value: 'backgroundJobs' },
|
|
85
|
+
{ name: 'GraphQL (Apollo Server)', value: 'graphql' },
|
|
78
86
|
],
|
|
79
87
|
when: (ans) => ans.db !== 'none',
|
|
80
88
|
},
|
|
@@ -89,6 +97,11 @@ async function main() {
|
|
|
89
97
|
{ name: 'CI/CD (GitHub Actions)', value: 'cicd' },
|
|
90
98
|
{ name: 'WebSocket (Socket.io)', value: 'websocket' },
|
|
91
99
|
{ name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
|
|
100
|
+
{ name: 'File Uploads (Multer)', value: 'fileUpload' },
|
|
101
|
+
{ name: 'Audit Logging', value: 'auditLog' },
|
|
102
|
+
{ name: 'Prometheus Metrics', value: 'metrics' },
|
|
103
|
+
{ name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
|
|
104
|
+
{ name: 'GraphQL (Apollo Server)', value: 'graphql' },
|
|
92
105
|
],
|
|
93
106
|
when: (ans) => ans.db === 'none',
|
|
94
107
|
},
|
|
@@ -121,6 +134,14 @@ async function main() {
|
|
|
121
134
|
hasWebsocket: features.includes('websocket'),
|
|
122
135
|
hasRequestId: features.includes('requestId'),
|
|
123
136
|
hasRedis: features.includes('redis') && features.includes('auth') && answers.db !== 'none',
|
|
137
|
+
hasEmail: features.includes('email') && answers.db !== 'none',
|
|
138
|
+
hasFileUpload: features.includes('fileUpload'),
|
|
139
|
+
hasSoftDelete: features.includes('softDelete') && answers.db !== 'none',
|
|
140
|
+
hasAuditLog: features.includes('auditLog'),
|
|
141
|
+
hasMetrics: features.includes('metrics'),
|
|
142
|
+
hasApiVersioning: features.includes('apiVersioning'),
|
|
143
|
+
hasBackgroundJobs: features.includes('backgroundJobs'),
|
|
144
|
+
hasGraphQL: features.includes('graphql'),
|
|
124
145
|
jwtSecret: generateSecret(),
|
|
125
146
|
jwtRefreshSecret: generateSecret(),
|
|
126
147
|
};
|
|
@@ -163,7 +184,7 @@ async function main() {
|
|
|
163
184
|
To get started:
|
|
164
185
|
cd ${config.projectName}
|
|
165
186
|
npm run dev
|
|
166
|
-
${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}
|
|
187
|
+
${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}${config.hasGraphQL ? `\nGraphQL Playground: http://localhost:3000/graphql` : ''}${config.hasMetrics ? `\nPrometheus Metrics: http://localhost:3000/metrics` : ''}
|
|
167
188
|
Health Check: http://localhost:3000/health
|
|
168
189
|
|
|
169
190
|
Available scripts:
|
package/lib/features.js
CHANGED
|
@@ -232,6 +232,14 @@ const inferConfig = (projectDir, pkg) => {
|
|
|
232
232
|
hasWebsocket: !!(pkg.dependencies && pkg.dependencies['socket.io']),
|
|
233
233
|
hasRedis: !!(pkg.dependencies && pkg.dependencies.ioredis),
|
|
234
234
|
hasRequestId: !!(pkg.dependencies && pkg.dependencies.uuid),
|
|
235
|
+
hasEmail: !!(pkg.dependencies && pkg.dependencies.nodemailer),
|
|
236
|
+
hasFileUpload: !!(pkg.dependencies && pkg.dependencies.multer),
|
|
237
|
+
hasSoftDelete: false,
|
|
238
|
+
hasAuditLog: false,
|
|
239
|
+
hasMetrics: !!(pkg.dependencies && pkg.dependencies['prom-client']),
|
|
240
|
+
hasApiVersioning: false,
|
|
241
|
+
hasBackgroundJobs: !!(pkg.dependencies && pkg.dependencies.bullmq),
|
|
242
|
+
hasGraphQL: !!(pkg.dependencies && pkg.dependencies['@apollo/server']),
|
|
235
243
|
logger: pkg.dependencies && pkg.dependencies.pino ? 'pino' : 'winston',
|
|
236
244
|
};
|
|
237
245
|
};
|
package/lib/generator.js
CHANGED
|
@@ -49,6 +49,18 @@ const createDirectoryStructure = (projectDir, config) => {
|
|
|
49
49
|
dirs.push('.github', '.github/workflows');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
if (config.hasFileUpload) {
|
|
53
|
+
dirs.push('uploads');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (config.hasBackgroundJobs) {
|
|
57
|
+
dirs.push('src/jobs');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.hasGraphQL) {
|
|
61
|
+
dirs.push('src/graphql');
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
fs.mkdirSync(projectDir);
|
|
53
65
|
dirs.forEach((dir) => {
|
|
54
66
|
fs.mkdirSync(path.join(projectDir, dir), { recursive: true });
|
|
@@ -128,6 +140,10 @@ const generateFiles = async (config, projectDir) => {
|
|
|
128
140
|
configFiles.push({ template: 'config/redis.js.ejs', output: `src/config/redis.${ext}` });
|
|
129
141
|
}
|
|
130
142
|
|
|
143
|
+
if (config.hasBackgroundJobs) {
|
|
144
|
+
configFiles.push({ template: 'config/queue.js.ejs', output: `src/config/queue.${ext}` });
|
|
145
|
+
}
|
|
146
|
+
|
|
131
147
|
// Route files
|
|
132
148
|
const routeFiles = [
|
|
133
149
|
{ template: 'routes/index.js.ejs', output: `src/routes/index.${ext}` },
|
|
@@ -141,11 +157,13 @@ const generateFiles = async (config, projectDir) => {
|
|
|
141
157
|
if (config.hasAuth) {
|
|
142
158
|
controllerFiles.push(
|
|
143
159
|
{ template: 'controllers/authController.js.ejs', output: `src/controllers/authController.${ext}` },
|
|
144
|
-
{ template: 'controllers/userController.js.ejs', output: `src/controllers/userController.${ext}` }
|
|
160
|
+
{ template: 'controllers/userController.js.ejs', output: `src/controllers/userController.${ext}` },
|
|
161
|
+
{ template: 'controllers/adminController.js.ejs', output: `src/controllers/adminController.${ext}` }
|
|
145
162
|
);
|
|
146
163
|
routeFiles.push(
|
|
147
164
|
{ template: 'routes/authRoutes.js.ejs', output: `src/routes/authRoutes.${ext}` },
|
|
148
|
-
{ template: 'routes/userRoutes.js.ejs', output: `src/routes/userRoutes.${ext}` }
|
|
165
|
+
{ template: 'routes/userRoutes.js.ejs', output: `src/routes/userRoutes.${ext}` },
|
|
166
|
+
{ template: 'routes/adminRoutes.js.ejs', output: `src/routes/adminRoutes.${ext}` }
|
|
149
167
|
);
|
|
150
168
|
serviceFiles.push(
|
|
151
169
|
{ template: 'services/authService.js.ejs', output: `src/services/authService.${ext}` }
|
|
@@ -178,6 +196,24 @@ const generateFiles = async (config, projectDir) => {
|
|
|
178
196
|
);
|
|
179
197
|
}
|
|
180
198
|
|
|
199
|
+
if (config.hasEmail) {
|
|
200
|
+
serviceFiles.push(
|
|
201
|
+
{ template: 'services/emailService.js.ejs', output: `src/services/emailService.${ext}` }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (config.hasFileUpload) {
|
|
206
|
+
routeFiles.push(
|
|
207
|
+
{ template: 'routes/uploadRoutes.js.ejs', output: `src/routes/uploadRoutes.${ext}` }
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (config.hasBackgroundJobs) {
|
|
212
|
+
routeFiles.push(
|
|
213
|
+
{ template: 'routes/jobRoutes.js.ejs', output: `src/routes/jobRoutes.${ext}` }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
181
217
|
// Middleware files
|
|
182
218
|
const middlewareFiles = [
|
|
183
219
|
{ template: 'middleware/errorHandler.js.ejs', output: `src/middleware/errorHandler.${ext}` },
|
|
@@ -196,6 +232,30 @@ const generateFiles = async (config, projectDir) => {
|
|
|
196
232
|
);
|
|
197
233
|
}
|
|
198
234
|
|
|
235
|
+
if (config.hasAuth) {
|
|
236
|
+
middlewareFiles.push(
|
|
237
|
+
{ template: 'middleware/rbac.js.ejs', output: `src/middleware/rbac.${ext}` }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (config.hasFileUpload) {
|
|
242
|
+
middlewareFiles.push(
|
|
243
|
+
{ template: 'middleware/upload.js.ejs', output: `src/middleware/upload.${ext}` }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (config.hasAuditLog) {
|
|
248
|
+
middlewareFiles.push(
|
|
249
|
+
{ template: 'middleware/auditLog.js.ejs', output: `src/middleware/auditLog.${ext}` }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (config.hasMetrics) {
|
|
254
|
+
middlewareFiles.push(
|
|
255
|
+
{ template: 'middleware/metrics.js.ejs', output: `src/middleware/metrics.${ext}` }
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
199
259
|
if (config.hasRedis) {
|
|
200
260
|
middlewareFiles.push(
|
|
201
261
|
{ template: 'middleware/cache.js.ejs', output: `src/middleware/cache.${ext}` }
|
|
@@ -245,6 +305,21 @@ const generateFiles = async (config, projectDir) => {
|
|
|
245
305
|
...testFiles,
|
|
246
306
|
];
|
|
247
307
|
|
|
308
|
+
// GraphQL files
|
|
309
|
+
if (config.hasGraphQL) {
|
|
310
|
+
allFiles.push(
|
|
311
|
+
{ template: 'graphql/typeDefs.js.ejs', output: `src/graphql/typeDefs.${ext}` },
|
|
312
|
+
{ template: 'graphql/resolvers.js.ejs', output: `src/graphql/resolvers.${ext}` }
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Background job worker
|
|
317
|
+
if (config.hasBackgroundJobs) {
|
|
318
|
+
allFiles.push(
|
|
319
|
+
{ template: 'jobs/worker.js.ejs', output: `src/jobs/worker.${ext}` }
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
248
323
|
const errors = [];
|
|
249
324
|
|
|
250
325
|
for (const file of allFiles) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-genix",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Production-grade CLI to generate Express apps with JWT, TypeScript, Prisma, MongoDB, PostgreSQL,
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "Production-grade CLI to generate Express apps with JWT, RBAC, GraphQL, TypeScript, Prisma, MongoDB, PostgreSQL, file uploads, email, background jobs, and more",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"express-genix": "index.js"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { Queue } = require('bullmq');
|
|
2
|
+
const logger = require('../utils/logger');
|
|
3
|
+
|
|
4
|
+
const redisConnection = {
|
|
5
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
6
|
+
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Define your queues here
|
|
10
|
+
const emailQueue = new Queue('email', { connection: redisConnection });
|
|
11
|
+
const defaultQueue = new Queue('default', { connection: redisConnection });
|
|
12
|
+
|
|
13
|
+
const addJob = async (queueName, jobName, data, options = {}) => {
|
|
14
|
+
const queues = { email: emailQueue, default: defaultQueue };
|
|
15
|
+
const queue = queues[queueName] || defaultQueue;
|
|
16
|
+
|
|
17
|
+
const job = await queue.add(jobName, data, {
|
|
18
|
+
attempts: 3,
|
|
19
|
+
backoff: { type: 'exponential', delay: 1000 },
|
|
20
|
+
removeOnComplete: { count: 100 },
|
|
21
|
+
removeOnFail: { count: 500 },
|
|
22
|
+
...options,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
logger.info(`Job added: ${jobName} (${job.id}) to queue ${queueName}`);
|
|
26
|
+
return job;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
module.exports = { emailQueue, defaultQueue, addJob, redisConnection };
|
|
@@ -12,8 +12,10 @@ model User {
|
|
|
12
12
|
username String @unique @db.VarChar(50)
|
|
13
13
|
email String @unique
|
|
14
14
|
password String
|
|
15
|
+
role String @default("user")
|
|
15
16
|
createdAt DateTime @default(now()) @map("created_at")
|
|
16
17
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
18
|
+
<% if (hasSoftDelete) { %> deletedAt DateTime? @map("deleted_at")<% } %>
|
|
17
19
|
|
|
18
20
|
@@map("users")
|
|
19
21
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const userService = require('../services/userService');
|
|
2
|
+
const { AppError } = require('../utils/errors');
|
|
3
|
+
const { success } = require('../utils/response');
|
|
4
|
+
|
|
5
|
+
const listUsers = async (req, res, next) => {
|
|
6
|
+
try {
|
|
7
|
+
const page = parseInt(req.query.page, 10) || 1;
|
|
8
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
9
|
+
const users = await userService.findAll({ page, limit });
|
|
10
|
+
|
|
11
|
+
return success(res, users);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
next(error);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const getUserById = async (req, res, next) => {
|
|
18
|
+
try {
|
|
19
|
+
const user = await userService.findById(req.params.id);
|
|
20
|
+
if (!user) {
|
|
21
|
+
throw new AppError('User not found', 404);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return success(res, {
|
|
25
|
+
user: {
|
|
26
|
+
id: user.id,
|
|
27
|
+
username: user.username,
|
|
28
|
+
email: user.email,
|
|
29
|
+
role: user.role,
|
|
30
|
+
createdAt: user.createdAt,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
next(error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const updateUserRole = async (req, res, next) => {
|
|
39
|
+
try {
|
|
40
|
+
const { role } = req.body;
|
|
41
|
+
const validRoles = ['user', 'moderator', 'admin'];
|
|
42
|
+
|
|
43
|
+
if (!validRoles.includes(role)) {
|
|
44
|
+
throw new AppError(`Invalid role. Must be one of: ${validRoles.join(', ')}`, 400);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const user = await userService.findById(req.params.id);
|
|
48
|
+
if (!user) {
|
|
49
|
+
throw new AppError('User not found', 404);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const updatedUser = await userService.updateById(req.params.id, { role });
|
|
53
|
+
|
|
54
|
+
return success(res, {
|
|
55
|
+
message: 'User role updated successfully',
|
|
56
|
+
user: {
|
|
57
|
+
id: updatedUser.id,
|
|
58
|
+
username: updatedUser.username,
|
|
59
|
+
email: updatedUser.email,
|
|
60
|
+
role: updatedUser.role,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
next(error);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const deleteUser = async (req, res, next) => {
|
|
69
|
+
try {
|
|
70
|
+
if (req.params.id === req.user.userId) {
|
|
71
|
+
throw new AppError('Cannot delete your own admin account', 400);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const user = await userService.findById(req.params.id);
|
|
75
|
+
if (!user) {
|
|
76
|
+
throw new AppError('User not found', 404);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await userService.deleteById(req.params.id);
|
|
80
|
+
return success(res, { message: 'User deleted successfully' });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
next(error);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
<% if (hasSoftDelete) { %>
|
|
86
|
+
const restoreUser = async (req, res, next) => {
|
|
87
|
+
try {
|
|
88
|
+
const user = await userService.restoreById(req.params.id);
|
|
89
|
+
if (!user) {
|
|
90
|
+
throw new AppError('User not found or not deleted', 404);
|
|
91
|
+
}
|
|
92
|
+
return success(res, { message: 'User restored successfully' });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
next(error);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const listDeletedUsers = async (req, res, next) => {
|
|
99
|
+
try {
|
|
100
|
+
const page = parseInt(req.query.page, 10) || 1;
|
|
101
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
102
|
+
const users = await userService.findDeleted({ page, limit });
|
|
103
|
+
return success(res, users);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
next(error);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
<% } %>
|
|
109
|
+
module.exports = { listUsers, getUserById, updateUserRole, deleteUser<% if (hasSoftDelete) { %>, restoreUser, listDeletedUsers<% } %> };
|
|
@@ -3,6 +3,7 @@ const authService = require('../services/authService');
|
|
|
3
3
|
const userService = require('../services/userService');
|
|
4
4
|
const { AppError } = require('../utils/errors');
|
|
5
5
|
const { success, created } = require('../utils/response');
|
|
6
|
+
<% if (hasEmail) { %>const emailService = require('../services/emailService');<% } %>
|
|
6
7
|
|
|
7
8
|
const register = async (req, res, next) => {
|
|
8
9
|
try {
|
|
@@ -16,7 +17,10 @@ const register = async (req, res, next) => {
|
|
|
16
17
|
const hashedPassword = await bcrypt.hash(password, 12);
|
|
17
18
|
const user = await userService.create({ username, email, password: hashedPassword });
|
|
18
19
|
const tokens = authService.generateTokens(user);
|
|
19
|
-
|
|
20
|
+
<% if (hasEmail) { %>
|
|
21
|
+
// Send welcome email (non-blocking)
|
|
22
|
+
emailService.sendWelcomeEmail(email, username).catch(() => {});
|
|
23
|
+
<% } %>
|
|
20
24
|
return created(res, {
|
|
21
25
|
message: 'User registered successfully',
|
|
22
26
|
user: { id: user.id, username: user.username, email: user.email },
|
|
@@ -113,12 +117,13 @@ const forgotPassword = async (req, res, next) => {
|
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
const resetToken = await authService.generateResetToken(email);
|
|
116
|
-
|
|
120
|
+
<% if (hasEmail) { %>
|
|
121
|
+
await emailService.sendPasswordResetEmail(email, resetToken);
|
|
122
|
+
<% } else { %>
|
|
117
123
|
// TODO: Send email with reset link
|
|
118
|
-
// await emailService.sendResetEmail(email, resetToken);
|
|
119
|
-
//
|
|
120
124
|
// For development, log the token:
|
|
121
125
|
console.log(`Password reset token for ${email}: ${resetToken}`);
|
|
126
|
+
<% } %>
|
|
122
127
|
|
|
123
128
|
return success(res, { message: 'If an account with that email exists, a reset link has been sent.' });
|
|
124
129
|
} catch (error) {
|
|
@@ -6,6 +6,13 @@ const morgan = require('morgan');
|
|
|
6
6
|
<% if (hasSwagger) { %>const swaggerUi = require('swagger-ui-express');
|
|
7
7
|
const swaggerSpec = require('./config/swagger');<% } %>
|
|
8
8
|
<% if (hasRequestId) { %>const { requestId } = require('./middleware/requestId');<% } %>
|
|
9
|
+
<% if (hasMetrics) { %>const { metricsMiddleware, metricsEndpoint } = require('./middleware/metrics');<% } %>
|
|
10
|
+
<% if (hasAuditLog) { %>const { auditLog } = require('./middleware/auditLog');<% } %>
|
|
11
|
+
<% if (hasGraphQL) { %>const { ApolloServer } = require('@apollo/server');
|
|
12
|
+
const { expressMiddleware } = require('@apollo/server/express4');
|
|
13
|
+
const typeDefs = require('./graphql/typeDefs');
|
|
14
|
+
const resolvers = require('./graphql/resolvers');<% if (hasAuth) { %>
|
|
15
|
+
const authService = require('./services/authService');<% } %><% } %>
|
|
9
16
|
const routes = require('./routes');
|
|
10
17
|
const errorHandler = require('./middleware/errorHandler');
|
|
11
18
|
<% if (hasDatabase || hasAuth) { %>const { validateEnv } = require('./utils/envValidator');
|
|
@@ -31,6 +38,18 @@ app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
|
|
31
38
|
// Body parsing
|
|
32
39
|
app.use(express.json({ limit: '10mb' }));
|
|
33
40
|
app.use(express.urlencoded({ extended: true }));
|
|
41
|
+
<% if (hasFileUpload) { %>
|
|
42
|
+
// Serve uploaded files
|
|
43
|
+
const path = require('path');
|
|
44
|
+
app.use('/uploads', express.static(path.join(process.cwd(), process.env.UPLOAD_DIR || 'uploads')));
|
|
45
|
+
<% } %>
|
|
46
|
+
<% if (hasMetrics) { %>
|
|
47
|
+
// Prometheus metrics
|
|
48
|
+
app.use(metricsMiddleware);
|
|
49
|
+
app.get('/metrics', metricsEndpoint);<% } %>
|
|
50
|
+
<% if (hasAuditLog) { %>
|
|
51
|
+
// Audit logging
|
|
52
|
+
app.use(auditLog());<% } %>
|
|
34
53
|
<% if (hasRateLimit) { %>
|
|
35
54
|
// Rate limiting
|
|
36
55
|
const limiter = rateLimit({
|
|
@@ -96,9 +115,39 @@ app.get('/health', async (req, res) => {
|
|
|
96
115
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
|
|
97
116
|
customCss: '.swagger-ui .topbar { display: none }',
|
|
98
117
|
}));<% } %>
|
|
118
|
+
<% if (hasGraphQL) { %>
|
|
119
|
+
// GraphQL setup (async — call initGraphQL() before starting server)
|
|
120
|
+
let apolloServer;
|
|
121
|
+
|
|
122
|
+
const initGraphQL = async () => {
|
|
123
|
+
apolloServer = new ApolloServer({ typeDefs, resolvers });
|
|
124
|
+
await apolloServer.start();
|
|
125
|
+
|
|
126
|
+
app.use('/graphql', expressMiddleware(apolloServer, {
|
|
127
|
+
context: async ({ req }) => {
|
|
128
|
+
<% if (hasAuth) { %> // Extract user from JWT if present
|
|
129
|
+
const authHeader = req.headers.authorization;
|
|
130
|
+
if (authHeader) {
|
|
131
|
+
const token = authHeader.split(' ')[1];
|
|
132
|
+
try {
|
|
133
|
+
const user = authService.verifyToken(token);
|
|
134
|
+
return { user };
|
|
135
|
+
} catch {
|
|
136
|
+
return { user: null };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
<% } %> return { user: null };
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
};
|
|
99
143
|
|
|
144
|
+
app.initGraphQL = initGraphQL;
|
|
145
|
+
<% } %>
|
|
100
146
|
// API routes
|
|
101
|
-
app.use('/api', routes);
|
|
147
|
+
<% if (hasApiVersioning) { %>app.use('/api/v1', routes);
|
|
148
|
+
// Future versions: app.use('/api/v2', v2Routes);
|
|
149
|
+
<% } else { %>app.use('/api', routes);
|
|
150
|
+
<% } %>
|
|
102
151
|
|
|
103
152
|
// 404 handler
|
|
104
153
|
app.use('*', (req, res) => {
|
package/templates/core/env.ejs
CHANGED
|
@@ -11,6 +11,23 @@ JWT_REFRESH_EXPIRE=7d
|
|
|
11
11
|
<% } %><% if (hasRedis) { %>
|
|
12
12
|
# Redis (token blacklist store)
|
|
13
13
|
REDIS_URL=redis://localhost:6379
|
|
14
|
+
<% } %><% if (hasBackgroundJobs && !hasRedis) { %>
|
|
15
|
+
# Redis (required for BullMQ)
|
|
16
|
+
REDIS_HOST=localhost
|
|
17
|
+
REDIS_PORT=6379
|
|
18
|
+
<% } %><% if (hasEmail) { %>
|
|
19
|
+
# Email (SMTP)
|
|
20
|
+
SMTP_HOST=smtp.ethereal.email
|
|
21
|
+
SMTP_PORT=587
|
|
22
|
+
SMTP_SECURE=false
|
|
23
|
+
SMTP_USER=
|
|
24
|
+
SMTP_PASS=
|
|
25
|
+
SMTP_FROM="App" <noreply@example.com>
|
|
26
|
+
FRONTEND_URL=http://localhost:3000
|
|
27
|
+
<% } %><% if (hasFileUpload) { %>
|
|
28
|
+
# File Uploads
|
|
29
|
+
UPLOAD_DIR=uploads
|
|
30
|
+
UPLOAD_MAX_SIZE=5242880
|
|
14
31
|
<% } %><% if (db === 'mongodb') { %>
|
|
15
32
|
# Database
|
|
16
33
|
MONGO_URI=mongodb://localhost:27017/<%= projectName %>
|
|
@@ -11,6 +11,23 @@ JWT_REFRESH_EXPIRE=7d
|
|
|
11
11
|
<% } %><% if (hasRedis) { %>
|
|
12
12
|
# Redis (token blacklist store)
|
|
13
13
|
REDIS_URL=redis://localhost:6379
|
|
14
|
+
<% } %><% if (hasBackgroundJobs && !hasRedis) { %>
|
|
15
|
+
# Redis (required for BullMQ)
|
|
16
|
+
REDIS_HOST=localhost
|
|
17
|
+
REDIS_PORT=6379
|
|
18
|
+
<% } %><% if (hasEmail) { %>
|
|
19
|
+
# Email (SMTP)
|
|
20
|
+
SMTP_HOST=smtp.ethereal.email
|
|
21
|
+
SMTP_PORT=587
|
|
22
|
+
SMTP_SECURE=false
|
|
23
|
+
SMTP_USER=
|
|
24
|
+
SMTP_PASS=
|
|
25
|
+
SMTP_FROM="App" <noreply@example.com>
|
|
26
|
+
FRONTEND_URL=http://localhost:3000
|
|
27
|
+
<% } %><% if (hasFileUpload) { %>
|
|
28
|
+
# File Uploads
|
|
29
|
+
UPLOAD_DIR=uploads
|
|
30
|
+
UPLOAD_MAX_SIZE=5242880
|
|
14
31
|
<% } %><% if (db === 'mongodb') { %>
|
|
15
32
|
# Database
|
|
16
33
|
MONGO_URI=mongodb://localhost:27017/<%= projectName %>
|