nodejs-quickstart-structure 1.14.0 → 1.16.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/CHANGELOG.md +32 -0
- package/bin/index.js +84 -80
- package/lib/generator.js +11 -1
- package/lib/modules/app-setup.js +3 -3
- package/lib/modules/config-files.js +27 -2
- package/lib/modules/kafka-setup.js +70 -24
- package/package.json +8 -3
- package/templates/clean-architecture/js/src/index.js.ejs +1 -3
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +11 -10
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +16 -1
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +36 -0
- package/templates/clean-architecture/ts/src/config/swagger.ts.ejs +1 -1
- package/templates/clean-architecture/ts/src/index.ts.ejs +12 -14
- package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +0 -1
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +19 -0
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +16 -0
- package/templates/common/.cursorrules.ejs +60 -0
- package/templates/common/.dockerignore +2 -0
- package/templates/common/.gitlab-ci.yml.ejs +5 -5
- package/templates/common/Dockerfile +2 -0
- package/templates/common/Jenkinsfile.ejs +1 -1
- package/templates/common/README.md.ejs +34 -1
- package/templates/common/_github/workflows/ci.yml +7 -4
- package/templates/common/database/js/models/User.js.ejs +2 -1
- package/templates/common/database/ts/models/User.ts.ejs +4 -3
- package/templates/common/eslint.config.mjs.ejs +30 -3
- package/templates/common/jest.config.js.ejs +4 -1
- package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -0
- package/templates/common/kafka/js/messaging/baseConsumer.spec.js.ejs +58 -0
- package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +11 -0
- package/templates/common/kafka/js/messaging/userEventSchema.spec.js.ejs +27 -0
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +31 -0
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +49 -0
- package/templates/common/kafka/js/services/kafkaService.js.ejs +75 -23
- package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +53 -7
- package/templates/common/kafka/ts/messaging/baseConsumer.spec.ts.ejs +50 -0
- package/templates/common/kafka/ts/messaging/baseConsumer.ts.ejs +27 -0
- package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -0
- package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +11 -0
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +49 -0
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +25 -0
- package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +22 -2
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +72 -12
- package/templates/common/package.json.ejs +6 -4
- package/templates/common/prompts/add-feature.md.ejs +26 -0
- package/templates/common/prompts/project-context.md.ejs +43 -0
- package/templates/common/prompts/troubleshoot.md.ejs +28 -0
- package/templates/mvc/js/src/controllers/userController.js.ejs +14 -0
- package/templates/mvc/js/src/controllers/userController.spec.js.ejs +39 -0
- package/templates/mvc/js/src/index.js.ejs +12 -11
- package/templates/mvc/ts/src/config/swagger.ts.ejs +1 -1
- package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +18 -0
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +16 -0
- package/templates/mvc/ts/src/index.ts.ejs +13 -16
- package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +0 -1
package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs
CHANGED
|
@@ -4,6 +4,12 @@ const GetAllUsers = require('@/usecases/GetAllUsers');
|
|
|
4
4
|
|
|
5
5
|
jest.mock('@/usecases/CreateUser');
|
|
6
6
|
jest.mock('@/usecases/GetAllUsers');
|
|
7
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
8
|
+
jest.mock('@/infrastructure/messaging/kafkaClient', () => ({
|
|
9
|
+
sendMessage: jest.fn().mockResolvedValue(undefined)
|
|
10
|
+
}));
|
|
11
|
+
<%_ } -%>
|
|
12
|
+
|
|
7
13
|
|
|
8
14
|
describe('UserController (Clean Architecture)', () => {
|
|
9
15
|
let userController;
|
|
@@ -81,7 +87,12 @@ describe('UserController (Clean Architecture)', () => {
|
|
|
81
87
|
expect(result).toEqual(expectedUser);
|
|
82
88
|
<%_ } else { -%>
|
|
83
89
|
await userController.createUser(mockRequest, mockResponse, mockNext);
|
|
90
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
91
|
+
const { sendMessage } = require('@/infrastructure/messaging/kafkaClient');
|
|
92
|
+
expect(sendMessage).toHaveBeenCalled();
|
|
93
|
+
<%_ } -%>
|
|
84
94
|
expect(mockResponse.status).toHaveBeenCalledWith(201);
|
|
95
|
+
|
|
85
96
|
expect(mockResponse.json).toHaveBeenCalledWith(expectedUser);
|
|
86
97
|
<%_ } -%>
|
|
87
98
|
expect(mockCreateUserUseCase.execute).toHaveBeenCalledWith(payload.name, payload.email);
|
|
@@ -98,5 +109,30 @@ describe('UserController (Clean Architecture)', () => {
|
|
|
98
109
|
expect(mockNext).toHaveBeenCalledWith(error);
|
|
99
110
|
<%_ } -%>
|
|
100
111
|
});
|
|
112
|
+
|
|
113
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
114
|
+
it('should successfully create a new user with _id for Kafka (Happy Path)', async () => {
|
|
115
|
+
const payload = { name: 'Bob', email: 'bob@example.com' };
|
|
116
|
+
<% if (communication === 'GraphQL') { -%>
|
|
117
|
+
const dataArg = payload;
|
|
118
|
+
<% } else { -%>
|
|
119
|
+
mockRequest.body = payload;
|
|
120
|
+
<% } -%>
|
|
121
|
+
const expectedUser = { _id: '2', ...payload };
|
|
122
|
+
|
|
123
|
+
mockCreateUserUseCase.execute.mockResolvedValue(expectedUser);
|
|
124
|
+
|
|
125
|
+
<% if (communication === 'GraphQL') { -%>
|
|
126
|
+
await userController.createUser(dataArg);
|
|
127
|
+
<% } else { -%>
|
|
128
|
+
await userController.createUser(mockRequest, mockResponse, mockNext);
|
|
129
|
+
<% } -%>
|
|
130
|
+
const { sendMessage } = require('@/infrastructure/messaging/kafkaClient');
|
|
131
|
+
expect(sendMessage).toHaveBeenCalledWith(
|
|
132
|
+
'user-topic',
|
|
133
|
+
expect.stringContaining('"id":"2"')
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
<%_ } -%>
|
|
101
137
|
});
|
|
102
138
|
});
|
|
@@ -8,11 +8,11 @@ import morgan from 'morgan';
|
|
|
8
8
|
import { errorMiddleware } from '@/utils/errorMiddleware';
|
|
9
9
|
import { setupGracefulShutdown } from '@/utils/gracefulShutdown';
|
|
10
10
|
import healthRoutes from '@/interfaces/routes/healthRoute';
|
|
11
|
-
<% if (communication === 'REST APIs') { -%>
|
|
11
|
+
<% if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
12
12
|
import userRoutes from '@/interfaces/routes/userRoutes';
|
|
13
13
|
import swaggerUi from 'swagger-ui-express';
|
|
14
|
-
import swaggerSpecs from '@/config/swagger';<% }
|
|
15
|
-
<%_ if (communication === 'Kafka') { -%>import {
|
|
14
|
+
import swaggerSpecs from '@/config/swagger';<% } %>
|
|
15
|
+
<%_ if (communication === 'Kafka') { -%>import { kafkaService } from '@/infrastructure/messaging/kafkaClient';<%_ } -%>
|
|
16
16
|
<%_ if (communication === 'GraphQL') { -%>
|
|
17
17
|
import { ApolloServer } from '@apollo/server';
|
|
18
18
|
import { expressMiddleware } from '@apollo/server/express4';
|
|
@@ -52,10 +52,10 @@ app.use(limiter);
|
|
|
52
52
|
app.use(express.json());
|
|
53
53
|
app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } }));
|
|
54
54
|
|
|
55
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
55
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
56
56
|
app.use('/api/users', userRoutes);
|
|
57
57
|
<%_ } -%>
|
|
58
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
58
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
59
59
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
60
60
|
<%_ } -%>
|
|
61
61
|
app.use('/health', healthRoutes);
|
|
@@ -92,18 +92,16 @@ const startServer = async () => {
|
|
|
92
92
|
app.use('/graphql', expressMiddleware(apolloServer, { context: gqlContext }));
|
|
93
93
|
<%_ } -%>
|
|
94
94
|
app.use(errorMiddleware);
|
|
95
|
-
<%_ if (communication === 'Kafka') { -%>
|
|
96
|
-
const kafkaService = new KafkaService();
|
|
97
|
-
<%_ } -%>
|
|
98
95
|
const server = app.listen(port, () => {
|
|
99
96
|
logger.info(`Server running on port ${port}`);
|
|
100
97
|
<%_ if (communication === 'Kafka') { -%>
|
|
101
|
-
kafkaService.connect()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
kafkaService.connect()
|
|
99
|
+
.then(async () => {
|
|
100
|
+
logger.info('Kafka connected');
|
|
101
|
+
})
|
|
102
|
+
.catch(err => {
|
|
103
|
+
logger.error('Failed to connect to Kafka after retries:', (err as Error).message);
|
|
104
|
+
});
|
|
107
105
|
<%_ } -%>
|
|
108
106
|
});
|
|
109
107
|
|
|
@@ -56,7 +56,6 @@ describe('Logger', () => {
|
|
|
56
56
|
const winston = require('winston');
|
|
57
57
|
jest.resetModules();
|
|
58
58
|
process.env.NODE_ENV = 'production';
|
|
59
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
60
59
|
require('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>');
|
|
61
60
|
expect(winston.format.json).toHaveBeenCalled();
|
|
62
61
|
process.env.NODE_ENV = 'test';
|
package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs
CHANGED
|
@@ -11,6 +11,20 @@ jest.mock('@/infrastructure/repositories/UserRepository');
|
|
|
11
11
|
jest.mock('@/usecases/createUser');
|
|
12
12
|
jest.mock('@/usecases/getAllUsers');
|
|
13
13
|
jest.mock('@/infrastructure/log/logger');
|
|
14
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
15
|
+
jest.mock('@/infrastructure/messaging/kafkaClient', () => {
|
|
16
|
+
const mockSendMessage = jest.fn().mockResolvedValue(undefined);
|
|
17
|
+
return {
|
|
18
|
+
kafkaService: {
|
|
19
|
+
sendMessage: mockSendMessage
|
|
20
|
+
},
|
|
21
|
+
KafkaService: jest.fn().mockImplementation(() => ({
|
|
22
|
+
sendMessage: mockSendMessage
|
|
23
|
+
}))
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
<%_ } -%>
|
|
27
|
+
|
|
14
28
|
|
|
15
29
|
describe('UserController (Clean Architecture)', () => {
|
|
16
30
|
let userController: UserController;
|
|
@@ -115,7 +129,12 @@ describe('UserController (Clean Architecture)', () => {
|
|
|
115
129
|
await userController.createUser(mockRequest as Request, mockResponse as Response, mockNext);
|
|
116
130
|
|
|
117
131
|
// Assert
|
|
132
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
133
|
+
const { kafkaService } = require('@/infrastructure/messaging/kafkaClient');
|
|
134
|
+
expect(kafkaService.sendMessage).toHaveBeenCalled();
|
|
135
|
+
<%_ } -%>
|
|
118
136
|
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.CREATED);
|
|
137
|
+
|
|
119
138
|
expect(mockResponse.json).toHaveBeenCalledWith(expectedUser);
|
|
120
139
|
<% } -%>
|
|
121
140
|
expect(mockCreateUserUseCase.execute).toHaveBeenCalledWith(payload.name, payload.email);
|
|
@@ -22,6 +22,14 @@ export class UserController {
|
|
|
22
22
|
try {
|
|
23
23
|
const { name, email } = data;
|
|
24
24
|
const user = await this.createUserUseCase.execute(name, email);
|
|
25
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
26
|
+
const { kafkaService } = await import('@/infrastructure/messaging/kafkaClient');
|
|
27
|
+
await kafkaService.sendMessage('user-topic', JSON.stringify({
|
|
28
|
+
action: 'USER_CREATED',
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
payload: { id: (user as any).id || (user as any)._id, email: user.email }
|
|
31
|
+
}));
|
|
32
|
+
<%_ } -%>
|
|
25
33
|
return user;
|
|
26
34
|
} catch (error: unknown) {
|
|
27
35
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -45,6 +53,14 @@ export class UserController {
|
|
|
45
53
|
try {
|
|
46
54
|
const { name, email } = req.body;
|
|
47
55
|
const user = await this.createUserUseCase.execute(name, email);
|
|
56
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
57
|
+
const { kafkaService } = await import('@/infrastructure/messaging/kafkaClient');
|
|
58
|
+
await kafkaService.sendMessage('user-topic', JSON.stringify({
|
|
59
|
+
action: 'USER_CREATED',
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
payload: { id: (user as any).id || (user as any)._id, email: user.email }
|
|
62
|
+
}));
|
|
63
|
+
<%_ } -%>
|
|
48
64
|
res.status(HTTP_STATUS.CREATED).json(user);
|
|
49
65
|
} catch (error: unknown) {
|
|
50
66
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Cursor AI Coding Rules for <%= projectName %>
|
|
2
|
+
|
|
3
|
+
## Project Context
|
|
4
|
+
You are an expert working on **<%= projectName %>**.
|
|
5
|
+
- **Project Goal**: [Replace this with your business logic, e.g., E-commerce API]
|
|
6
|
+
- **Language**: <%= language %>
|
|
7
|
+
- **Architecture**: <%= architecture %>
|
|
8
|
+
- **Database**: <%= database %>
|
|
9
|
+
- **Communication**: <%= communication %>
|
|
10
|
+
|
|
11
|
+
## Excluded Files/Folders
|
|
12
|
+
When indexing or searching the workspace, ignore the following paths to prevent context pollution:
|
|
13
|
+
- `node_modules/`
|
|
14
|
+
- `dist/`
|
|
15
|
+
- `build/`
|
|
16
|
+
- `coverage/`
|
|
17
|
+
- `.git/`
|
|
18
|
+
|
|
19
|
+
## Strict Rules
|
|
20
|
+
|
|
21
|
+
### 1. Testing First
|
|
22
|
+
- Every new service or controller method MUST have a test file in `tests/`.
|
|
23
|
+
- **Coverage Gate**: Aim for > 70% coverage (Statement/Line/Function/Branch).
|
|
24
|
+
- **Format**: Use Jest with the AAA (Arrange, Act, Assert) pattern.
|
|
25
|
+
- **Isolation**: Mock external dependencies (DB, Redis, etc.) using `jest.mock()`.
|
|
26
|
+
|
|
27
|
+
### 2. Error Handling
|
|
28
|
+
- Do NOT use generic `Error`.
|
|
29
|
+
- Use custom classes from `src/errors/` (e.g., `ApiError`, `NotFoundError`, `BadRequestError`).
|
|
30
|
+
<% if (language === 'TypeScript') { -%>
|
|
31
|
+
- Use `HTTP_STATUS` constants from `@/utils/httpCodes` for status codes.
|
|
32
|
+
<% } else { -%>
|
|
33
|
+
- Use `HTTP_STATUS` constants from `../utils/httpCodes.js` for status codes.
|
|
34
|
+
<% } -%>
|
|
35
|
+
|
|
36
|
+
### 3. File Naming & Style
|
|
37
|
+
- **Controllers**: camelCase (e.g., `userController.<% if (language === 'TypeScript') { %>ts<% } else { %>js<% } %>`).
|
|
38
|
+
- **Services**: camelCase (e.g., `userService.<% if (language === 'TypeScript') { %>ts<% } else { %>js<% } %>`).
|
|
39
|
+
- **Routes**: camelCase (e.g., `userRoutes.<% if (language === 'TypeScript') { %>ts<% } else { %>js<% } %>`).
|
|
40
|
+
<% if (language === 'TypeScript') { -%>
|
|
41
|
+
- **Imports**: Use path aliases (e.g., `@/services/...`) instead of relative paths.
|
|
42
|
+
- **Typing**: Ensure strong typing for interfaces and DTOs. Do not use `any` unless absolutely necessary.
|
|
43
|
+
<% } else { -%>
|
|
44
|
+
- **Imports**: Use relative paths as dictated by the directory structure.
|
|
45
|
+
<% } -%>
|
|
46
|
+
|
|
47
|
+
### 4. Architecture Standards
|
|
48
|
+
<% if (architecture === 'Clean Architecture') { -%>
|
|
49
|
+
- Enforce strict separation of concerns:
|
|
50
|
+
- `domain`: Entities and enterprise business rules.
|
|
51
|
+
- `usecases`: Application business rules.
|
|
52
|
+
- `interfaces`: Controllers and Routes.
|
|
53
|
+
- `infrastructure`: Frameworks, Database, Caching, and Web Server.
|
|
54
|
+
- Dependencies point inward toward the `domain`.
|
|
55
|
+
<% } else { -%>
|
|
56
|
+
- Enforce MVC standards:
|
|
57
|
+
- `models`: Data layer.
|
|
58
|
+
- `controllers`: Request handlers and business logic.
|
|
59
|
+
- `routes`: Define endpoints routing to controllers.
|
|
60
|
+
<% } -%>
|
|
@@ -12,24 +12,24 @@ cache:
|
|
|
12
12
|
|
|
13
13
|
install_dependencies:
|
|
14
14
|
stage: .pre
|
|
15
|
-
image: node:
|
|
15
|
+
image: node:22-alpine
|
|
16
16
|
script:
|
|
17
17
|
- npm ci
|
|
18
18
|
|
|
19
19
|
lint_code:
|
|
20
20
|
stage: lint
|
|
21
|
-
image: node:
|
|
21
|
+
image: node:22-alpine
|
|
22
22
|
script:
|
|
23
23
|
- npm run lint
|
|
24
24
|
|
|
25
25
|
run_tests:
|
|
26
26
|
stage: test
|
|
27
|
-
image: node:
|
|
27
|
+
image: node:22-alpine
|
|
28
28
|
script:
|
|
29
|
-
- npm test
|
|
29
|
+
- npm run test:coverage
|
|
30
30
|
|
|
31
31
|
build_app:
|
|
32
32
|
stage: build
|
|
33
|
-
image: node:
|
|
33
|
+
image: node:22-alpine
|
|
34
34
|
script:
|
|
35
35
|
- npm run build --if-present
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
FROM node:22-alpine AS builder
|
|
5
5
|
|
|
6
6
|
WORKDIR /app
|
|
7
|
+
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
|
|
7
8
|
|
|
8
9
|
COPY package*.json ./
|
|
9
10
|
COPY tsconfig*.json ./
|
|
@@ -24,6 +25,7 @@ FROM node:22-alpine AS production
|
|
|
24
25
|
WORKDIR /app
|
|
25
26
|
|
|
26
27
|
ENV NODE_ENV=production
|
|
28
|
+
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
|
|
27
29
|
|
|
28
30
|
COPY package*.json ./
|
|
29
31
|
|
|
@@ -115,6 +115,29 @@ mutation CreateUser {
|
|
|
115
115
|
API is exposed via **REST**.
|
|
116
116
|
A Swagger UI for API documentation is available at:
|
|
117
117
|
- **URL**: `http://localhost:3000/api-docs` (Dynamic based on PORT)
|
|
118
|
+
<%_ } -%>
|
|
119
|
+
|
|
120
|
+
<% if (communication === 'Kafka') { -%>
|
|
121
|
+
## 📡 Testing Kafka Asynchronous Flow
|
|
122
|
+
This project demonstrates a production-ready Kafka flow:
|
|
123
|
+
1. **Producer**: When a user is created via the API, a `USER_CREATED` event is sent to `user-topic`.
|
|
124
|
+
2. **Consumer**: `WelcomeEmailConsumer` listens to `user-topic` and simulates sending an email.
|
|
125
|
+
|
|
126
|
+
### How to verify:
|
|
127
|
+
1. Ensure infrastructure is running: `docker-compose up -d<% if (database !== 'None') { %> db<% } %><% if (caching === 'Redis') { %> redis<% } %><% if (communication === 'Kafka') { %> zookeeper kafka<% } %>`
|
|
128
|
+
2. Start the app: `npm run dev`
|
|
129
|
+
3. Trigger an event by creating a user (via Postman or curl):
|
|
130
|
+
```bash
|
|
131
|
+
curl -X POST http://localhost:3000/api/users \
|
|
132
|
+
-H "Content-Type: application/json" \
|
|
133
|
+
-d '{"name": "Kafka Tester", "email": "kafka@example.com"}'
|
|
134
|
+
```
|
|
135
|
+
4. Observe the logs:
|
|
136
|
+
```text
|
|
137
|
+
[Kafka] Producer: Sent USER_CREATED event for 'kafka@example.com'
|
|
138
|
+
[Kafka] Consumer: Received USER_CREATED.
|
|
139
|
+
[Kafka] Consumer: 📧 Sending welcome email to 'kafka@example.com'... Done!
|
|
140
|
+
```
|
|
118
141
|
<% } -%>
|
|
119
142
|
|
|
120
143
|
<% if (caching === 'Redis') { -%>
|
|
@@ -217,4 +240,14 @@ docker-compose down
|
|
|
217
240
|
- **Helmet**: Sets secure HTTP headers.
|
|
218
241
|
- **CORS**: Configured for cross-origin requests.
|
|
219
242
|
- **Rate Limiting**: Protects against DDoS / Brute-force.
|
|
220
|
-
- **HPP**: Prevents HTTP Parameter Pollution attacks.
|
|
243
|
+
- **HPP**: Prevents HTTP Parameter Pollution attacks.
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
## 🤖 AI-Native Development
|
|
247
|
+
|
|
248
|
+
This project is "AI-Ready" out of the box. We have pre-configured industry-leading AI context files to bridge the gap between "Generated Code" and "AI-Assisted Development."
|
|
249
|
+
|
|
250
|
+
- **Magic Defaults**: We've automatically tailored your AI context to focus on **<%= projectName %>** and its specific architectural stack (<%= architecture %>, <%= database %>, etc.).
|
|
251
|
+
- **Use Cursor?** We've configured **`.cursorrules`** at the root. It enforces project standards (70% coverage, MVC/Clean) directly within the editor.
|
|
252
|
+
- *Pro-tip*: You can customize the `Project Goal` placeholder in `.cursorrules` to help the AI understand your specific business logic!
|
|
253
|
+
- **Use ChatGPT/Gemini/Claude?** Check the **`prompts/`** directory. It contains highly-specialized Agent Skill templates. You can copy-paste these into any LLM to give it a "Senior Developer" understanding of your codebase immediately.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
name: Node.js CI
|
|
2
2
|
|
|
3
|
+
env:
|
|
4
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
|
5
|
+
|
|
3
6
|
on:
|
|
4
7
|
push:
|
|
5
8
|
branches: [ "main" ]
|
|
@@ -13,13 +16,13 @@ jobs:
|
|
|
13
16
|
|
|
14
17
|
strategy:
|
|
15
18
|
matrix:
|
|
16
|
-
node-version: [
|
|
19
|
+
node-version: [20.x, 22.x]
|
|
17
20
|
|
|
18
21
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@v4
|
|
20
23
|
|
|
21
24
|
- name: Use Node.js ${{ matrix.node-version }}
|
|
22
|
-
uses: actions/setup-node@
|
|
25
|
+
uses: actions/setup-node@v4
|
|
23
26
|
with:
|
|
24
27
|
node-version: ${{ matrix.node-version }}
|
|
25
28
|
cache: 'npm'
|
|
@@ -31,7 +34,7 @@ jobs:
|
|
|
31
34
|
run: npm run lint
|
|
32
35
|
|
|
33
36
|
- name: Run Tests
|
|
34
|
-
run: npm test
|
|
37
|
+
run: npm run test:coverage
|
|
35
38
|
|
|
36
39
|
- name: Build
|
|
37
40
|
run: npm run build --if-present
|
|
@@ -11,7 +11,8 @@ class UserModel {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
static async create(data) {
|
|
14
|
-
const
|
|
14
|
+
const { id, ...rest } = data;
|
|
15
|
+
const newUser = { id: String(this.mockData.length + 1), ...rest };
|
|
15
16
|
this.mockData.push(newUser);
|
|
16
17
|
return newUser;
|
|
17
18
|
}
|
|
@@ -16,10 +16,11 @@ export default class UserModel {
|
|
|
16
16
|
return this.mockData;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
static async create(data: Omit<User, 'id'>) {
|
|
20
|
-
const
|
|
19
|
+
static async create(data: Omit<User, 'id'> & { id?: string | number | null }) {
|
|
20
|
+
const { id, ...rest } = data;
|
|
21
|
+
const newUser: User = { id: String(this.mockData.length + 1), ...rest };
|
|
21
22
|
this.mockData.push(newUser);
|
|
22
|
-
return newUser
|
|
23
|
+
return newUser;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
<% } else { -%>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import eslint from '@eslint/js';
|
|
2
2
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
|
3
3
|
import globals from 'globals';
|
|
4
|
+
import importPlugin from 'eslint-plugin-import-x';
|
|
4
5
|
<% if (language === 'TypeScript') { %>import tseslint from 'typescript-eslint';
|
|
5
6
|
|
|
6
7
|
export default tseslint.config(
|
|
@@ -8,6 +9,15 @@ export default tseslint.config(
|
|
|
8
9
|
...tseslint.configs.recommended,
|
|
9
10
|
eslintConfigPrettier,
|
|
10
11
|
{
|
|
12
|
+
plugins: {
|
|
13
|
+
import: importPlugin,
|
|
14
|
+
},
|
|
15
|
+
settings: {
|
|
16
|
+
'import-x/resolver': {
|
|
17
|
+
typescript: true,
|
|
18
|
+
node: true,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
11
21
|
languageOptions: {
|
|
12
22
|
globals: {
|
|
13
23
|
...globals.node,
|
|
@@ -17,16 +27,24 @@ export default tseslint.config(
|
|
|
17
27
|
rules: {
|
|
18
28
|
"no-console": "warn",
|
|
19
29
|
"no-unused-vars": "off",
|
|
20
|
-
"@typescript-eslint/no-unused-vars": "warn",
|
|
30
|
+
"@typescript-eslint/no-unused-vars": ["warn", {
|
|
31
|
+
"argsIgnorePattern": "^_",
|
|
32
|
+
"varsIgnorePattern": "^_"
|
|
33
|
+
}],
|
|
21
34
|
"@typescript-eslint/no-require-imports": "error",
|
|
35
|
+
"import/no-unresolved": [2, { "caseSensitive": false }]
|
|
22
36
|
},
|
|
23
37
|
},
|
|
24
38
|
{
|
|
25
39
|
files: ["**/*.test.ts", "**/*.spec.ts", "tests/**/*.ts"],
|
|
26
40
|
rules: {
|
|
27
41
|
"@typescript-eslint/no-require-imports": "off",
|
|
28
|
-
"@typescript-eslint/no-unused-vars": "warn",
|
|
42
|
+
"@typescript-eslint/no-unused-vars": ["warn", {
|
|
43
|
+
"argsIgnorePattern": "^_",
|
|
44
|
+
"varsIgnorePattern": "^_"
|
|
45
|
+
}],
|
|
29
46
|
"@typescript-eslint/no-explicit-any": "off",
|
|
47
|
+
"import/no-unresolved": [2, { "caseSensitive": false }]
|
|
30
48
|
},
|
|
31
49
|
}
|
|
32
50
|
);<% } else { %>
|
|
@@ -34,6 +52,14 @@ export default [
|
|
|
34
52
|
eslint.configs.recommended,
|
|
35
53
|
eslintConfigPrettier,
|
|
36
54
|
{
|
|
55
|
+
plugins: {
|
|
56
|
+
import: importPlugin,
|
|
57
|
+
},
|
|
58
|
+
settings: {
|
|
59
|
+
'import-x/resolver': {
|
|
60
|
+
node: true,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
37
63
|
languageOptions: {
|
|
38
64
|
ecmaVersion: 'latest',
|
|
39
65
|
sourceType: 'module',
|
|
@@ -44,7 +70,8 @@ export default [
|
|
|
44
70
|
},
|
|
45
71
|
rules: {
|
|
46
72
|
"no-console": "warn",
|
|
47
|
-
"no-unused-vars": "warn"
|
|
73
|
+
"no-unused-vars": "warn",
|
|
74
|
+
"import/no-unresolved": [2, { "caseSensitive": false }]
|
|
48
75
|
}
|
|
49
76
|
}
|
|
50
77
|
];<% } %>
|
|
@@ -13,9 +13,12 @@ module.exports = {
|
|
|
13
13
|
"src/index",
|
|
14
14
|
"src/app",
|
|
15
15
|
"src/config/env",
|
|
16
|
+
"src/infrastructure/config/env",
|
|
16
17
|
"src/config/swagger",
|
|
17
18
|
"src/infrastructure/webserver/swagger",
|
|
18
|
-
"src/infrastructure/webserver/server"
|
|
19
|
+
"src/infrastructure/webserver/server",
|
|
20
|
+
"src/utils/logger",
|
|
21
|
+
"src/infrastructure/log/logger"
|
|
19
22
|
],
|
|
20
23
|
coverageThreshold: {
|
|
21
24
|
global: {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const logger = require('<% if (architecture === "Clean Architecture") { %>../../infrastructure/log/logger<% } else { %>../utils/logger<% } %>');
|
|
2
|
+
|
|
3
|
+
class BaseConsumer {
|
|
4
|
+
constructor() {
|
|
5
|
+
if (this.constructor === BaseConsumer) {
|
|
6
|
+
throw new Error("Abstract class 'BaseConsumer' cannot be instantiated.");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get topic() { throw new Error("Property 'topic' must be implemented"); }
|
|
11
|
+
get groupId() { throw new Error("Property 'groupId' must be implemented"); }
|
|
12
|
+
|
|
13
|
+
async onMessage({ message }) {
|
|
14
|
+
try {
|
|
15
|
+
const rawValue = message.value?.toString();
|
|
16
|
+
if (!rawValue) return;
|
|
17
|
+
|
|
18
|
+
const data = JSON.parse(rawValue);
|
|
19
|
+
await this.handle(data);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
logger.error(`[Kafka] Error processing message on topic ${this.topic}:`, error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async handle(data) {
|
|
26
|
+
throw new Error("Method 'handle()' must be implemented");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = BaseConsumer;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const BaseConsumer = require('<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/baseConsumer<% } else { %>@/messaging/baseConsumer<% } %>');
|
|
2
|
+
const logger = require('<% if (architecture === "Clean Architecture") { %>@/infrastructure/log/logger<% } else { %>@/utils/logger<% } %>');
|
|
3
|
+
|
|
4
|
+
jest.mock('<% if (architecture === "Clean Architecture") { %>@/infrastructure/log/logger<% } else { %>@/utils/logger<% } %>');
|
|
5
|
+
|
|
6
|
+
class TestConsumer extends BaseConsumer {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this._topic = 'test-topic';
|
|
10
|
+
}
|
|
11
|
+
get topic() { return this._topic; }
|
|
12
|
+
get groupId() { return 'test-group'; }
|
|
13
|
+
async handle(data) {
|
|
14
|
+
logger.info('Handled', data);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('BaseConsumer', () => {
|
|
19
|
+
let consumer;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
consumer = new TestConsumer();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should throw error if instantiated directly', () => {
|
|
27
|
+
expect(() => new BaseConsumer()).toThrow("Abstract class 'BaseConsumer' cannot be instantiated.");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should process a valid message', async () => {
|
|
31
|
+
const message = { value: Buffer.from(JSON.stringify({ test: 'data' })) };
|
|
32
|
+
await consumer.onMessage({ message });
|
|
33
|
+
expect(logger.info).toHaveBeenCalledWith('Handled', { test: 'data' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should handle invalid JSON', async () => {
|
|
37
|
+
const message = { value: Buffer.from('invalid-json') };
|
|
38
|
+
await consumer.onMessage({ message });
|
|
39
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
40
|
+
expect.stringContaining('[Kafka] Error processing message on topic test-topic:'),
|
|
41
|
+
expect.anything()
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should skip empty messages', async () => {
|
|
46
|
+
const message = { value: null };
|
|
47
|
+
await consumer.onMessage({ message });
|
|
48
|
+
expect(logger.info).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should throw if topic, groupId or handle are not implemented', async () => {
|
|
52
|
+
class IncompleteConsumer extends BaseConsumer {}
|
|
53
|
+
const incomplete = new IncompleteConsumer();
|
|
54
|
+
expect(() => incomplete.topic).toThrow("Property 'topic' must be implemented");
|
|
55
|
+
expect(() => incomplete.groupId).toThrow("Property 'groupId' must be implemented");
|
|
56
|
+
await expect(incomplete.handle({})).rejects.toThrow("Method 'handle()' must be implemented");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
|
|
3
|
+
const UserEventSchema = z.object({
|
|
4
|
+
action: z.enum(['USER_CREATED', 'UPDATE_USER', 'DELETE_USER']),
|
|
5
|
+
payload: z.object({
|
|
6
|
+
id: z.union([z.string(), z.number()]),
|
|
7
|
+
email: z.string().email(),
|
|
8
|
+
}),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
module.exports = { UserEventSchema };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { UserEventSchema } = require('<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/schemas/userEventSchema<% } else { %>@/messaging/schemas/userEventSchema<% } %>');
|
|
2
|
+
|
|
3
|
+
describe('UserEventSchema', () => {
|
|
4
|
+
it('should validate a correct USER_CREATED event', () => {
|
|
5
|
+
const validEvent = {
|
|
6
|
+
action: 'USER_CREATED',
|
|
7
|
+
payload: {
|
|
8
|
+
id: 1,
|
|
9
|
+
email: 'test@example.com'
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const result = UserEventSchema.safeParse(validEvent);
|
|
13
|
+
expect(result.success).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should fail validation for invalid email', () => {
|
|
17
|
+
const invalidEvent = {
|
|
18
|
+
action: 'USER_CREATED',
|
|
19
|
+
payload: {
|
|
20
|
+
id: 1,
|
|
21
|
+
email: 'invalid-email'
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const result = UserEventSchema.safeParse(invalidEvent);
|
|
25
|
+
expect(result.success).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|