express-genix 4.5.2 → 4.6.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 +18 -0
- package/index.js +29 -112
- package/lib/cleanup.js +7 -5
- package/lib/features.js +97 -9
- package/lib/generator.js +3 -2
- package/lib/scaffold.js +165 -0
- package/package.json +1 -1
- package/templates/controllers/authController.js.ejs +16 -7
- package/templates/core/docker-compose.yml.ejs +16 -3
- package/templates/core/env.ejs +1 -3
- package/templates/core/env.example.ejs +1 -3
- package/templates/core/server.js.ejs +6 -21
- package/templates/infra/Makefile.ejs +30 -0
- package/templates/infra/nginx.conf.ejs +25 -0
- package/templates/middleware/errorHandler.js.ejs +7 -1
- package/templates/middleware/validation.js.ejs +2 -2
- package/templates/migrations/create-users.js.ejs +5 -1
- package/templates/scaffold/controller.js.ejs +94 -0
- package/templates/scaffold/route.js.ejs +88 -0
- package/templates/scaffold/service.js.ejs +77 -0
- package/templates/services/authService.js.ejs +20 -2
- package/templates/services/userService.prisma.js.ejs +3 -2
- package/lib/ai-cli.js +0 -133
- package/templates/agents/graph.js.ejs +0 -84
- package/templates/config/ai.js.ejs +0 -47
- package/templates/controllers/aiController.js.ejs +0 -100
- package/templates/routes/aiRoutes.js.ejs +0 -117
- package/templates/services/aiService.js.ejs +0 -80
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
# templates/core/docker-compose.yml.ejs
|
|
2
2
|
version: '3.8'
|
|
3
3
|
|
|
4
|
-
services
|
|
4
|
+
services:<% if (typeof hasNginx !== 'undefined' && hasNginx) { %>
|
|
5
|
+
nginx:
|
|
6
|
+
image: nginx:alpine
|
|
7
|
+
ports:
|
|
8
|
+
- "80:80"
|
|
9
|
+
volumes:
|
|
10
|
+
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
11
|
+
depends_on:
|
|
12
|
+
- app
|
|
13
|
+
restart: unless-stopped
|
|
14
|
+
networks:
|
|
15
|
+
- app-network
|
|
16
|
+
<% } %>
|
|
5
17
|
app:
|
|
6
18
|
build: .
|
|
7
|
-
ports
|
|
8
|
-
- "3000
|
|
19
|
+
ports:<% if (typeof hasNginx !== 'undefined' && hasNginx) { %>
|
|
20
|
+
- "3000" # Exposed to Nginx, not host directly<% } else { %>
|
|
21
|
+
- "3000:3000"<% } %>
|
|
9
22
|
environment:
|
|
10
23
|
- NODE_ENV=production
|
|
11
24
|
env_file:
|
package/templates/core/env.ejs
CHANGED
|
@@ -9,10 +9,8 @@ JWT_REFRESH_SECRET=<%= jwtRefreshSecret %>
|
|
|
9
9
|
JWT_EXPIRE=15m
|
|
10
10
|
JWT_REFRESH_EXPIRE=7d
|
|
11
11
|
<% } %><% if (hasRedis) { %>
|
|
12
|
-
# Redis
|
|
12
|
+
# Redis
|
|
13
13
|
REDIS_URL=redis://localhost:6379
|
|
14
|
-
<% } %><% if (hasBackgroundJobs && !hasRedis) { %>
|
|
15
|
-
# Redis (required for BullMQ)
|
|
16
14
|
REDIS_HOST=localhost
|
|
17
15
|
REDIS_PORT=6379
|
|
18
16
|
<% } %><% if (hasEmail) { %>
|
|
@@ -9,10 +9,8 @@ JWT_REFRESH_SECRET=CHANGE_ME_generate_a_64_char_hex_secret
|
|
|
9
9
|
JWT_EXPIRE=15m
|
|
10
10
|
JWT_REFRESH_EXPIRE=7d
|
|
11
11
|
<% } %><% if (hasRedis) { %>
|
|
12
|
-
# Redis
|
|
12
|
+
# Redis
|
|
13
13
|
REDIS_URL=redis://localhost:6379
|
|
14
|
-
<% } %><% if (hasBackgroundJobs && !hasRedis) { %>
|
|
15
|
-
# Redis (required for BullMQ)
|
|
16
14
|
REDIS_HOST=localhost
|
|
17
15
|
REDIS_PORT=6379
|
|
18
16
|
<% } %><% if (hasEmail) { %>
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
require('dotenv').config();
|
|
2
2
|
|
|
3
|
-
const cluster = require('cluster');
|
|
4
|
-
const os = require('os');
|
|
5
3
|
<% if (hasWebsocket) { %>const http = require('http');<% } %>
|
|
6
4
|
const app = require('./app');<% if (hasDatabase) { %>
|
|
7
5
|
const db = require('./config/database');<% } %><% if (hasRedis) { %>
|
|
@@ -12,22 +10,9 @@ const { createLogger } = require('./utils/logger');
|
|
|
12
10
|
|
|
13
11
|
const logger = createLogger('Server');
|
|
14
12
|
const port = process.env.PORT || 3000;
|
|
15
|
-
const isPrimary = cluster.isPrimary ?? cluster.isMaster;
|
|
16
13
|
|
|
17
14
|
const startServer = async () => {
|
|
18
|
-
if (
|
|
19
|
-
const numCPUs = os.cpus().length;
|
|
20
|
-
logger.info(`Master ${process.pid} is running, forking ${numCPUs} workers`);
|
|
21
|
-
|
|
22
|
-
for (let i = 0; i < numCPUs; i++) {
|
|
23
|
-
cluster.fork();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
cluster.on('exit', (worker, code, signal) => {
|
|
27
|
-
logger.warn(`Worker ${worker.process.pid} died (code: ${code}, signal: ${signal}). Restarting...`);
|
|
28
|
-
cluster.fork();
|
|
29
|
-
});
|
|
30
|
-
} else {<% if (hasDatabase) { %>
|
|
15
|
+
try {<% if (hasDatabase) { %>
|
|
31
16
|
await db.connect();<% } %><% if (hasRedis) { %>
|
|
32
17
|
await connectRedis();<% } %><% if (hasGraphQL) { %>
|
|
33
18
|
await app.initGraphQL();<% } %><% if (hasBackgroundJobs) { %>
|
|
@@ -38,7 +23,7 @@ const startServer = async () => {
|
|
|
38
23
|
app.set('io', io);
|
|
39
24
|
|
|
40
25
|
const server = httpServer.listen(port, () => {<% } else { %> const server = app.listen(port, () => {<% } %>
|
|
41
|
-
logger.info(`
|
|
26
|
+
logger.info(`Server running on http://localhost:${port}`);
|
|
42
27
|
logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
43
28
|
});
|
|
44
29
|
|
|
@@ -59,10 +44,10 @@ const startServer = async () => {
|
|
|
59
44
|
|
|
60
45
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
61
46
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
logger.error('Failed to start server', { error: err.message });
|
|
49
|
+
process.exit(1);
|
|
62
50
|
}
|
|
63
51
|
};
|
|
64
52
|
|
|
65
|
-
startServer()
|
|
66
|
-
logger.error('Failed to start server', { error: err.message });
|
|
67
|
-
process.exit(1);
|
|
68
|
-
});
|
|
53
|
+
startServer();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.PHONY: build up down logs test lint clean
|
|
2
|
+
|
|
3
|
+
# Build the Docker image
|
|
4
|
+
build:
|
|
5
|
+
docker-compose build
|
|
6
|
+
|
|
7
|
+
# Start the application in detached mode
|
|
8
|
+
up:
|
|
9
|
+
docker-compose up -d
|
|
10
|
+
|
|
11
|
+
# Stop the application
|
|
12
|
+
down:
|
|
13
|
+
docker-compose down
|
|
14
|
+
|
|
15
|
+
# View logs
|
|
16
|
+
logs:
|
|
17
|
+
docker-compose logs -f app
|
|
18
|
+
|
|
19
|
+
# Run tests
|
|
20
|
+
test:
|
|
21
|
+
npm test
|
|
22
|
+
|
|
23
|
+
# Run linter
|
|
24
|
+
lint:
|
|
25
|
+
npm run lint
|
|
26
|
+
|
|
27
|
+
# Clean up Docker resources
|
|
28
|
+
clean:
|
|
29
|
+
docker-compose down -v --remove-orphans
|
|
30
|
+
docker system prune -f
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
events {
|
|
2
|
+
worker_connections 1024;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
http {
|
|
6
|
+
upstream app_servers {
|
|
7
|
+
server app:3000;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
server {
|
|
11
|
+
listen 80;
|
|
12
|
+
server_name localhost;
|
|
13
|
+
|
|
14
|
+
location / {
|
|
15
|
+
proxy_pass http://app_servers;
|
|
16
|
+
proxy_http_version 1.1;
|
|
17
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
18
|
+
proxy_set_header Connection 'upgrade';
|
|
19
|
+
proxy_set_header Host $host;
|
|
20
|
+
proxy_cache_bypass $http_upgrade;
|
|
21
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
22
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -31,6 +31,10 @@ const errorHandler = (err, req, res, next) => {
|
|
|
31
31
|
const message = err.errors.map((e) => e.message).join(', ');
|
|
32
32
|
error = new AppError(message, 400);
|
|
33
33
|
}
|
|
34
|
+
<% } %><% if (db === 'prisma') { %>
|
|
35
|
+
if (err.code === 'P2002') {
|
|
36
|
+
error = new AppError('Duplicate field value entered', 400);
|
|
37
|
+
}
|
|
34
38
|
<% } %><% if (hasAuth) { %>
|
|
35
39
|
if (err.name === 'JsonWebTokenError') {
|
|
36
40
|
error = new AppError('Invalid token', 401);
|
|
@@ -41,9 +45,11 @@ const errorHandler = (err, req, res, next) => {
|
|
|
41
45
|
}
|
|
42
46
|
<% } %>
|
|
43
47
|
|
|
48
|
+
const message = error.isOperational ? error.message : 'Internal Server Error';
|
|
49
|
+
|
|
44
50
|
res.status(error.statusCode || 500).json({
|
|
45
51
|
success: false,
|
|
46
|
-
error: error.message || 'Internal Server Error',
|
|
52
|
+
error: process.env.NODE_ENV === 'development' ? (error.message || 'Internal Server Error') : message,
|
|
47
53
|
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
|
|
48
54
|
});
|
|
49
55
|
};
|
|
@@ -9,11 +9,11 @@ const { AppError } = require('../utils/errors');
|
|
|
9
9
|
* Usage:
|
|
10
10
|
* router.post('/users', validate({ body: createUserSchema }), controller.create);
|
|
11
11
|
*/
|
|
12
|
-
const validate = (schemas) => (req, res, next) => {
|
|
12
|
+
const validate = (schemas) => async (req, res, next) => {
|
|
13
13
|
const errors = [];
|
|
14
14
|
|
|
15
15
|
for (const [source, schema] of Object.entries(schemas)) {
|
|
16
|
-
const result = schema.
|
|
16
|
+
const result = await schema.safeParseAsync(req[source]);
|
|
17
17
|
if (!result.success) {
|
|
18
18
|
result.error.issues.forEach((issue) => {
|
|
19
19
|
errors.push(`${source}.${issue.path.join('.')}: ${issue.message}`);
|
|
@@ -37,7 +37,11 @@ module.exports = {
|
|
|
37
37
|
type: Sequelize.DATE,
|
|
38
38
|
allowNull: false,
|
|
39
39
|
defaultValue: Sequelize.literal('NOW()'),
|
|
40
|
-
}
|
|
40
|
+
},<% if (hasSoftDelete) { %>
|
|
41
|
+
deletedAt: {
|
|
42
|
+
type: Sequelize.DATE,
|
|
43
|
+
allowNull: true,
|
|
44
|
+
},<% } %>
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
await queryInterface.addIndex('users', ['email'], { unique: true });
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const <%= resourceNameCamel %>Service = require('../services/<%= resourceNameCamel %>Service');
|
|
2
|
+
const { AppError } = require('../utils/errors');
|
|
3
|
+
const { success, created, paginated } = require('../utils/response');
|
|
4
|
+
const { createLogger } = require('../utils/logger');
|
|
5
|
+
|
|
6
|
+
const logger = createLogger('<%= resourceNamePascal %>Controller');
|
|
7
|
+
|
|
8
|
+
const getAll<%= resourceNamePascal %>s = async (req, res, next) => {
|
|
9
|
+
try {
|
|
10
|
+
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
|
|
11
|
+
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit, 10) || 10));
|
|
12
|
+
|
|
13
|
+
logger.info('Fetching <%= resourceNamePlural %>', { page, limit });
|
|
14
|
+
const result = await <%= resourceNameCamel %>Service.getAll<%= resourceNamePascal %>s(page, limit);
|
|
15
|
+
|
|
16
|
+
return paginated(res, result.<%= resourceNamePlural %>, {
|
|
17
|
+
page,
|
|
18
|
+
limit,
|
|
19
|
+
total: result.total,
|
|
20
|
+
totalPages: result.totalPages,
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logger.error('Error fetching <%= resourceNamePlural %>', { error: error.message });
|
|
24
|
+
next(error);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const get<%= resourceNamePascal %>ById = async (req, res, next) => {
|
|
29
|
+
try {
|
|
30
|
+
const { id } = req.params;
|
|
31
|
+
const <%= resourceNameCamel %> = await <%= resourceNameCamel %>Service.get<%= resourceNamePascal %>ById(id);
|
|
32
|
+
|
|
33
|
+
if (!<%= resourceNameCamel %>) {
|
|
34
|
+
throw new AppError('<%= resourceNamePascal %> not found', 404);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return success(res, <%= resourceNameCamel %>);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
next(error);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const create<%= resourceNamePascal %> = async (req, res, next) => {
|
|
44
|
+
try {
|
|
45
|
+
const data = req.body;
|
|
46
|
+
|
|
47
|
+
logger.info('Creating <%= resourceNameCamel %>', { data });
|
|
48
|
+
const <%= resourceNameCamel %> = await <%= resourceNameCamel %>Service.create<%= resourceNamePascal %>(data);
|
|
49
|
+
|
|
50
|
+
return created(res, <%= resourceNameCamel %>);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
next(error);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const update<%= resourceNamePascal %> = async (req, res, next) => {
|
|
57
|
+
try {
|
|
58
|
+
const { id } = req.params;
|
|
59
|
+
const data = req.body;
|
|
60
|
+
|
|
61
|
+
const <%= resourceNameCamel %> = await <%= resourceNameCamel %>Service.update<%= resourceNamePascal %>(id, data);
|
|
62
|
+
|
|
63
|
+
if (!<%= resourceNameCamel %>) {
|
|
64
|
+
throw new AppError('<%= resourceNamePascal %> not found', 404);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return success(res, <%= resourceNameCamel %>);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
next(error);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const delete<%= resourceNamePascal %> = async (req, res, next) => {
|
|
74
|
+
try {
|
|
75
|
+
const { id } = req.params;
|
|
76
|
+
const deleted = await <%= resourceNameCamel %>Service.delete<%= resourceNamePascal %>(id);
|
|
77
|
+
|
|
78
|
+
if (!deleted) {
|
|
79
|
+
throw new AppError('<%= resourceNamePascal %> not found', 404);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return success(res, { message: '<%= resourceNamePascal %> deleted successfully' });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
next(error);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
getAll<%= resourceNamePascal %>s,
|
|
90
|
+
get<%= resourceNamePascal %>ById,
|
|
91
|
+
create<%= resourceNamePascal %>,
|
|
92
|
+
update<%= resourceNamePascal %>,
|
|
93
|
+
delete<%= resourceNamePascal %>
|
|
94
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const <%= resourceNameCamel %>Controller = require('../controllers/<%= resourceNameCamel %>Controller');
|
|
3
|
+
<% if (hasAuth) { %>const { authenticate } = require('../middleware/auth');<% } %>
|
|
4
|
+
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
|
|
7
|
+
<% if (hasAuth) { %>// Apply authentication middleware to all routes
|
|
8
|
+
router.use(authenticate);
|
|
9
|
+
<% } %>
|
|
10
|
+
/**
|
|
11
|
+
* @swagger
|
|
12
|
+
* /<%= resourceNamePlural %>:
|
|
13
|
+
* get:
|
|
14
|
+
* summary: Get all <%= resourceNamePlural %>
|
|
15
|
+
* tags: [<%= resourceNamePascal %>]
|
|
16
|
+
* responses:
|
|
17
|
+
* 200:
|
|
18
|
+
* description: List of <%= resourceNamePlural %>
|
|
19
|
+
*/
|
|
20
|
+
router.get('/', <%= resourceNameCamel %>Controller.getAll<%= resourceNamePascal %>s);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @swagger
|
|
24
|
+
* /<%= resourceNamePlural %>/{id}:
|
|
25
|
+
* get:
|
|
26
|
+
* summary: Get a <%= resourceNameCamel %> by ID
|
|
27
|
+
* tags: [<%= resourceNamePascal %>]
|
|
28
|
+
* parameters:
|
|
29
|
+
* - in: path
|
|
30
|
+
* name: id
|
|
31
|
+
* required: true
|
|
32
|
+
* schema:
|
|
33
|
+
* type: string
|
|
34
|
+
* responses:
|
|
35
|
+
* 200:
|
|
36
|
+
* description: <%= resourceNamePascal %> details
|
|
37
|
+
*/
|
|
38
|
+
router.get('/:id', <%= resourceNameCamel %>Controller.get<%= resourceNamePascal %>ById);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @swagger
|
|
42
|
+
* /<%= resourceNamePlural %>:
|
|
43
|
+
* post:
|
|
44
|
+
* summary: Create a new <%= resourceNameCamel %>
|
|
45
|
+
* tags: [<%= resourceNamePascal %>]
|
|
46
|
+
* responses:
|
|
47
|
+
* 201:
|
|
48
|
+
* description: Created <%= resourceNameCamel %>
|
|
49
|
+
*/
|
|
50
|
+
router.post('/', <%= resourceNameCamel %>Controller.create<%= resourceNamePascal %>);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @swagger
|
|
54
|
+
* /<%= resourceNamePlural %>/{id}:
|
|
55
|
+
* put:
|
|
56
|
+
* summary: Update a <%= resourceNameCamel %>
|
|
57
|
+
* tags: [<%= resourceNamePascal %>]
|
|
58
|
+
* parameters:
|
|
59
|
+
* - in: path
|
|
60
|
+
* name: id
|
|
61
|
+
* required: true
|
|
62
|
+
* schema:
|
|
63
|
+
* type: string
|
|
64
|
+
* responses:
|
|
65
|
+
* 200:
|
|
66
|
+
* description: Updated <%= resourceNameCamel %>
|
|
67
|
+
*/
|
|
68
|
+
router.put('/:id', <%= resourceNameCamel %>Controller.update<%= resourceNamePascal %>);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @swagger
|
|
72
|
+
* /<%= resourceNamePlural %>/{id}:
|
|
73
|
+
* delete:
|
|
74
|
+
* summary: Delete a <%= resourceNameCamel %>
|
|
75
|
+
* tags: [<%= resourceNamePascal %>]
|
|
76
|
+
* parameters:
|
|
77
|
+
* - in: path
|
|
78
|
+
* name: id
|
|
79
|
+
* required: true
|
|
80
|
+
* schema:
|
|
81
|
+
* type: string
|
|
82
|
+
* responses:
|
|
83
|
+
* 200:
|
|
84
|
+
* description: <%= resourceNamePascal %> deleted successfully
|
|
85
|
+
*/
|
|
86
|
+
router.delete('/:id', <%= resourceNameCamel %>Controller.delete<%= resourceNamePascal %>);
|
|
87
|
+
|
|
88
|
+
module.exports = router;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const { createLogger } = require('../utils/logger');
|
|
2
|
+
|
|
3
|
+
const logger = createLogger('<%= resourceNamePascal %>Service');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get all <%= resourceNamePlural %> with pagination
|
|
7
|
+
*/
|
|
8
|
+
const getAll<%= resourceNamePascal %>s = async (page = 1, limit = 10) => {
|
|
9
|
+
logger.info('Fetching all <%= resourceNamePlural %>', { page, limit });
|
|
10
|
+
|
|
11
|
+
// TODO: Implement database logic here
|
|
12
|
+
// Example (Prisma): return prisma.<%= resourceNameCamel %>.findMany({ skip: (page - 1) * limit, take: limit });
|
|
13
|
+
// Example (Mongoose): return <%= resourceNamePascal %>.find().skip((page - 1) * limit).limit(limit);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
<%= resourceNamePlural %>: [],
|
|
17
|
+
total: 0,
|
|
18
|
+
totalPages: 0,
|
|
19
|
+
currentPage: page,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get <%= resourceNameCamel %> by ID
|
|
25
|
+
*/
|
|
26
|
+
const get<%= resourceNamePascal %>ById = async (id) => {
|
|
27
|
+
logger.info('Fetching <%= resourceNameCamel %> by ID', { id });
|
|
28
|
+
|
|
29
|
+
// TODO: Implement database logic here
|
|
30
|
+
// Example (Prisma): return prisma.<%= resourceNameCamel %>.findUnique({ where: { id } });
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create new <%= resourceNameCamel %>
|
|
37
|
+
*/
|
|
38
|
+
const create<%= resourceNamePascal %> = async (data) => {
|
|
39
|
+
logger.info('Creating new <%= resourceNameCamel %>');
|
|
40
|
+
|
|
41
|
+
// TODO: Implement database logic here
|
|
42
|
+
// Example (Prisma): return prisma.<%= resourceNameCamel %>.create({ data });
|
|
43
|
+
|
|
44
|
+
return { id: 'new-id', ...data };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Update <%= resourceNameCamel %> by ID
|
|
49
|
+
*/
|
|
50
|
+
const update<%= resourceNamePascal %> = async (id, data) => {
|
|
51
|
+
logger.info('Updating <%= resourceNameCamel %>', { id });
|
|
52
|
+
|
|
53
|
+
// TODO: Implement database logic here
|
|
54
|
+
// Example (Prisma): return prisma.<%= resourceNameCamel %>.update({ where: { id }, data });
|
|
55
|
+
|
|
56
|
+
return { id, ...data };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Delete <%= resourceNameCamel %> by ID
|
|
61
|
+
*/
|
|
62
|
+
const delete<%= resourceNamePascal %> = async (id) => {
|
|
63
|
+
logger.info('Deleting <%= resourceNameCamel %>', { id });
|
|
64
|
+
|
|
65
|
+
// TODO: Implement database logic here
|
|
66
|
+
// Example (Prisma): await prisma.<%= resourceNameCamel %>.delete({ where: { id } });
|
|
67
|
+
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
getAll<%= resourceNamePascal %>s,
|
|
73
|
+
get<%= resourceNamePascal %>ById,
|
|
74
|
+
create<%= resourceNamePascal %>,
|
|
75
|
+
update<%= resourceNamePascal %>,
|
|
76
|
+
delete<%= resourceNamePascal %>,
|
|
77
|
+
};
|
|
@@ -4,8 +4,19 @@ const crypto = require('crypto');
|
|
|
4
4
|
<% } %>
|
|
5
5
|
<% if (!hasRedis) { %>
|
|
6
6
|
// In-memory token blacklist — replace with Redis for production.
|
|
7
|
-
const tokenBlacklist = new
|
|
7
|
+
const tokenBlacklist = new Map(); // token → expiresAt
|
|
8
8
|
const resetTokens = new Map(); // token → { email, expiresAt }
|
|
9
|
+
|
|
10
|
+
// Periodic cleanup to prevent memory leaks
|
|
11
|
+
setInterval(() => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [token, expiresAt] of tokenBlacklist.entries()) {
|
|
14
|
+
if (expiresAt < now) tokenBlacklist.delete(token);
|
|
15
|
+
}
|
|
16
|
+
for (const [hash, entry] of resetTokens.entries()) {
|
|
17
|
+
if (entry.expiresAt < now) resetTokens.delete(hash);
|
|
18
|
+
}
|
|
19
|
+
}, 60 * 60 * 1000).unref(); // Run every hour
|
|
9
20
|
<% } %>
|
|
10
21
|
const generateTokens = (user) => {
|
|
11
22
|
const payload = {
|
|
@@ -66,10 +77,17 @@ const consumeResetToken = async (token) => {
|
|
|
66
77
|
};
|
|
67
78
|
<% } else { %>
|
|
68
79
|
const blacklistToken = (token) => {
|
|
69
|
-
|
|
80
|
+
const decoded = jwt.decode(token);
|
|
81
|
+
const expiresAt = decoded && decoded.exp ? decoded.exp * 1000 : Date.now() + 86400000;
|
|
82
|
+
tokenBlacklist.set(token, expiresAt);
|
|
70
83
|
};
|
|
71
84
|
|
|
72
85
|
const isTokenBlacklisted = (token) => {
|
|
86
|
+
const expiresAt = tokenBlacklist.get(token);
|
|
87
|
+
if (expiresAt && expiresAt < Date.now()) {
|
|
88
|
+
tokenBlacklist.delete(token);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
73
91
|
return tokenBlacklist.has(token);
|
|
74
92
|
};
|
|
75
93
|
|
|
@@ -58,12 +58,13 @@ const findAll = async ({ page = 1, limit = 20 } = {}) => {
|
|
|
58
58
|
const skip = (page - 1) * limit;
|
|
59
59
|
const [users, total] = await Promise.all([
|
|
60
60
|
prisma.user.findMany({
|
|
61
|
-
|
|
61
|
+
<% if (hasSoftDelete) { %> where: { deletedAt: null },
|
|
62
|
+
<% } %> select: { id: true, username: true, email: true, role: true, createdAt: true, updatedAt: true },
|
|
62
63
|
orderBy: { createdAt: 'desc' },
|
|
63
64
|
skip,
|
|
64
65
|
take: limit,
|
|
65
66
|
}),
|
|
66
|
-
prisma.user.count(),
|
|
67
|
+
prisma.user.count(<% if (hasSoftDelete) { %>{ where: { deletedAt: null } }<% } %>),
|
|
67
68
|
]);
|
|
68
69
|
return { users, total, page, totalPages: Math.ceil(total / limit) };
|
|
69
70
|
};
|
package/lib/ai-cli.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AI CLI handler — interprets natural language project descriptions
|
|
3
|
-
* and maps them to express-genix configuration flags.
|
|
4
|
-
*
|
|
5
|
-
* Requires @langchain/openai or @langchain/anthropic to be installed.
|
|
6
|
-
* These are NOT bundled with express-genix to keep the CLI lightweight.
|
|
7
|
-
*
|
|
8
|
-
* Install with: npm install -g @langchain/core @langchain/openai
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const SYSTEM_PROMPT = `You are an expert Express.js project configurator for the express-genix CLI tool.
|
|
12
|
-
Given a natural language description of a web application, output a JSON configuration object.
|
|
13
|
-
|
|
14
|
-
Available options:
|
|
15
|
-
- projectName: string (lowercase, hyphens only, no spaces)
|
|
16
|
-
- language: "javascript" | "typescript"
|
|
17
|
-
- db: "mongodb" | "postgresql" | "prisma" | "none"
|
|
18
|
-
- logger: "winston" | "pino"
|
|
19
|
-
- features: array of feature strings from this list:
|
|
20
|
-
- "auth" (JWT authentication — requires a database)
|
|
21
|
-
- "rateLimit" (rate limiting)
|
|
22
|
-
- "swagger" (Swagger/OpenAPI docs)
|
|
23
|
-
- "redis" (Redis token blacklist — requires auth)
|
|
24
|
-
- "docker" (Docker & Docker Compose)
|
|
25
|
-
- "cicd" (GitHub Actions CI/CD)
|
|
26
|
-
- "websocket" (WebSocket via Socket.io)
|
|
27
|
-
- "requestId" (request ID / correlation tracking)
|
|
28
|
-
- "email" (Nodemailer email service — requires database)
|
|
29
|
-
- "fileUpload" (Multer file uploads)
|
|
30
|
-
- "softDelete" (soft deletes — requires database + auth)
|
|
31
|
-
- "auditLog" (audit logging middleware)
|
|
32
|
-
- "metrics" (Prometheus metrics)
|
|
33
|
-
- "apiVersioning" (API versioning /api/v1)
|
|
34
|
-
- "backgroundJobs" (BullMQ background jobs)
|
|
35
|
-
- "graphql" (GraphQL via Apollo Server)
|
|
36
|
-
- "mcp" (MCP Server — exposes API as AI-agent tools)
|
|
37
|
-
- "ai" (LangChain/LangGraph AI service)
|
|
38
|
-
|
|
39
|
-
Rules:
|
|
40
|
-
- Always include "auth", "rateLimit", "swagger", "requestId" unless explicitly unwanted
|
|
41
|
-
- Include "docker" for production-ready apps
|
|
42
|
-
- Include "ai" if the description mentions AI, chatbot, LLM, or intelligence
|
|
43
|
-
- Include "mcp" if the description mentions AI agents, tools, or MCP
|
|
44
|
-
- Choose "prisma" for modern stacks, "mongodb" for documents, "postgresql" for relational
|
|
45
|
-
- Infer a short kebab-case project name from the description
|
|
46
|
-
- Output ONLY valid JSON. No explanation, no markdown fences.`;
|
|
47
|
-
|
|
48
|
-
const runAiCommand = async (description) => {
|
|
49
|
-
// Check for API key
|
|
50
|
-
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
|
51
|
-
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
|
52
|
-
const hasGemini = !!process.env.GOOGLE_API_KEY;
|
|
53
|
-
|
|
54
|
-
if (!hasOpenAI && !hasAnthropic && !hasGemini) {
|
|
55
|
-
console.error('❌ The AI command requires an API key.');
|
|
56
|
-
console.error(' Set one of these environment variables:');
|
|
57
|
-
console.error(' export OPENAI_API_KEY=sk-...');
|
|
58
|
-
console.error(' export ANTHROPIC_API_KEY=sk-ant-...');
|
|
59
|
-
console.error(' export GOOGLE_API_KEY=AI...');
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Try to load LangChain (not bundled — must be installed separately)
|
|
64
|
-
let ChatModel;
|
|
65
|
-
try {
|
|
66
|
-
if (hasGemini) {
|
|
67
|
-
const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
|
|
68
|
-
ChatModel = new ChatGoogleGenerativeAI({
|
|
69
|
-
model: 'gemini-2.0-flash',
|
|
70
|
-
maxOutputTokens: 1024,
|
|
71
|
-
apiKey: process.env.GOOGLE_API_KEY,
|
|
72
|
-
});
|
|
73
|
-
} else if (hasAnthropic) {
|
|
74
|
-
const { ChatAnthropic } = require('@langchain/anthropic');
|
|
75
|
-
ChatModel = new ChatAnthropic({
|
|
76
|
-
modelName: 'claude-sonnet-4-20250514',
|
|
77
|
-
maxTokens: 1024,
|
|
78
|
-
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
79
|
-
});
|
|
80
|
-
} else {
|
|
81
|
-
const { ChatOpenAI } = require('@langchain/openai');
|
|
82
|
-
ChatModel = new ChatOpenAI({
|
|
83
|
-
modelName: 'gpt-4o-mini',
|
|
84
|
-
maxTokens: 1024,
|
|
85
|
-
openAIApiKey: process.env.OPENAI_API_KEY,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
} catch {
|
|
89
|
-
console.error('❌ LangChain packages not found.');
|
|
90
|
-
console.error(' Install them globally to use the AI command:');
|
|
91
|
-
console.error(' npm install -g @langchain/core @langchain/openai @langchain/anthropic @langchain/google-genai');
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const { HumanMessage, SystemMessage } = require('@langchain/core/messages');
|
|
96
|
-
|
|
97
|
-
console.log('🤖 Analyzing your description...\n');
|
|
98
|
-
|
|
99
|
-
const response = await ChatModel.invoke([
|
|
100
|
-
new SystemMessage(SYSTEM_PROMPT),
|
|
101
|
-
new HumanMessage(description),
|
|
102
|
-
]);
|
|
103
|
-
|
|
104
|
-
let config;
|
|
105
|
-
try {
|
|
106
|
-
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
|
|
107
|
-
if (!jsonMatch) throw new Error('No JSON found in response');
|
|
108
|
-
config = JSON.parse(jsonMatch[0]);
|
|
109
|
-
} catch {
|
|
110
|
-
console.error('❌ Could not parse AI response. Try a clearer description.');
|
|
111
|
-
console.error(' Raw output:', response.content);
|
|
112
|
-
process.exit(1);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Validate required fields
|
|
116
|
-
if (!config.projectName || !config.language || !config.db) {
|
|
117
|
-
console.error('❌ AI response missing required fields. Try again.');
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Display interpreted config
|
|
122
|
-
console.log('📋 Interpreted configuration:\n');
|
|
123
|
-
console.log(` Project: ${config.projectName}`);
|
|
124
|
-
console.log(` Language: ${config.language}`);
|
|
125
|
-
console.log(` Database: ${config.db}`);
|
|
126
|
-
console.log(` Logger: ${config.logger || 'winston'}`);
|
|
127
|
-
console.log(` Features: ${(config.features || []).join(', ') || 'base'}`);
|
|
128
|
-
console.log('');
|
|
129
|
-
|
|
130
|
-
return config;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
module.exports = { runAiCommand };
|