nodejs-quickstart-structure 1.15.1 → 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 +13 -0
- package/lib/modules/app-setup.js +3 -3
- package/lib/modules/config-files.js +2 -2
- package/lib/modules/kafka-setup.js +70 -24
- package/package.json +1 -1
- 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/Dockerfile +2 -0
- package/templates/common/README.md.ejs +24 -1
- 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/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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { WelcomeEmailConsumer } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/consumers/instances/welcomeEmailConsumer<% } else { %>@/messaging/consumers/instances/welcomeEmailConsumer<% } %>';
|
|
2
|
+
import logger from '<%= loggerPath %>';
|
|
3
|
+
|
|
4
|
+
jest.mock('<%= loggerPath %>');
|
|
5
|
+
|
|
6
|
+
describe('WelcomeEmailConsumer', () => {
|
|
7
|
+
let consumer: WelcomeEmailConsumer;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
consumer = new WelcomeEmailConsumer();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should log welcome email simulation for USER_CREATED action', async () => {
|
|
15
|
+
const data = {
|
|
16
|
+
action: 'USER_CREATED',
|
|
17
|
+
payload: {
|
|
18
|
+
id: 1,
|
|
19
|
+
email: 'test@example.com'
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
await consumer.handle(data);
|
|
24
|
+
|
|
25
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
26
|
+
expect.stringContaining('[Kafka] Consumer: Received USER_CREATED.')
|
|
27
|
+
);
|
|
28
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
29
|
+
expect.stringContaining('📧 Sending welcome email to \'test@example.com\'... Done!')
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should log error for invalid data', async () => {
|
|
34
|
+
const data = {
|
|
35
|
+
action: 'USER_CREATED',
|
|
36
|
+
payload: {
|
|
37
|
+
id: 1,
|
|
38
|
+
email: 'invalid-email'
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
await consumer.handle(data);
|
|
43
|
+
|
|
44
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
45
|
+
expect.stringContaining('[Kafka] Invalid user event data:'),
|
|
46
|
+
expect.anything()
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BaseConsumer } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/baseConsumer<% } else { %>@/messaging/baseConsumer<% } %>';
|
|
2
|
+
import logger from '<%= loggerPath %>';
|
|
3
|
+
import { UserEventSchema } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/schemas/userEventSchema<% } else { %>@/messaging/schemas/userEventSchema<% } %>';
|
|
4
|
+
|
|
5
|
+
export class WelcomeEmailConsumer extends BaseConsumer {
|
|
6
|
+
topic = 'user-topic';
|
|
7
|
+
groupId = 'welcome-email-group';
|
|
8
|
+
|
|
9
|
+
async handle(data: unknown) {
|
|
10
|
+
const result = UserEventSchema.safeParse(data);
|
|
11
|
+
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
logger.error('[Kafka] Invalid user event data:', result.error.format());
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { action, payload } = result.data;
|
|
18
|
+
|
|
19
|
+
if (action === 'USER_CREATED') {
|
|
20
|
+
logger.info(`[Kafka] Consumer: Received USER_CREATED.`);
|
|
21
|
+
logger.info(`[Kafka] Consumer: 📧 Sending welcome email to '${payload.email}'... Done!`);
|
|
22
|
+
// In a real app, you would call an EmailService here
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -34,13 +34,14 @@ describe('KafkaService', () => {
|
|
|
34
34
|
|
|
35
35
|
expect(producer.connect).toHaveBeenCalled();
|
|
36
36
|
expect(consumer.connect).toHaveBeenCalled();
|
|
37
|
-
expect(consumer.subscribe).toHaveBeenCalledWith({ topic: '
|
|
37
|
+
expect(consumer.subscribe).toHaveBeenCalledWith(expect.objectContaining({ topic: 'user-topic', fromBeginning: true }));
|
|
38
38
|
expect(consumer.run).toHaveBeenCalled();
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it('should send a message', async () => {
|
|
42
|
+
await kafkaService.connect();
|
|
42
43
|
const topic = 'test-topic';
|
|
43
|
-
const message = 'test
|
|
44
|
+
const message = JSON.stringify({ action: 'TEST', payload: { email: 'test@example.com' } });
|
|
44
45
|
await kafkaService.sendMessage(topic, message);
|
|
45
46
|
const producer = (kafka.producer as jest.Mock).mock.results[0].value;
|
|
46
47
|
|
|
@@ -50,6 +51,25 @@ describe('KafkaService', () => {
|
|
|
50
51
|
});
|
|
51
52
|
});
|
|
52
53
|
|
|
54
|
+
it('should retry connection on failure', async () => {
|
|
55
|
+
const producer = (kafka.producer as jest.Mock).mock.results[0].value;
|
|
56
|
+
producer.connect
|
|
57
|
+
.mockRejectedValueOnce(new Error('Connection failed'))
|
|
58
|
+
.mockResolvedValueOnce(undefined);
|
|
59
|
+
|
|
60
|
+
jest.useFakeTimers();
|
|
61
|
+
const connectPromise = kafkaService.connect(2);
|
|
62
|
+
|
|
63
|
+
await jest.advanceTimersByTimeAsync(10000);
|
|
64
|
+
await connectPromise;
|
|
65
|
+
|
|
66
|
+
expect(producer.connect).toHaveBeenCalledTimes(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should throw error if producer not connected', async () => {
|
|
70
|
+
await expect(kafkaService.sendMessage('topic', 'msg')).rejects.toThrow('[Kafka] Producer not connected');
|
|
71
|
+
});
|
|
72
|
+
|
|
53
73
|
it('should disconnect producer and consumer', async () => {
|
|
54
74
|
await kafkaService.disconnect();
|
|
55
75
|
const producer = (kafka.producer as jest.Mock).mock.results[0].value;
|
|
@@ -5,33 +5,92 @@ import logger from '<%= loggerPath %>';
|
|
|
5
5
|
export class KafkaService {
|
|
6
6
|
private producer: Producer;
|
|
7
7
|
private consumer: Consumer;
|
|
8
|
+
private isConnected = false;
|
|
9
|
+
private connectionPromise: Promise<void> | null = null;
|
|
8
10
|
|
|
9
11
|
constructor() {
|
|
10
12
|
this.producer = kafka.producer();
|
|
11
13
|
this.consumer = kafka.consumer({ groupId: 'test-group' });
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
async connect() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
async connect(retries = 10) {
|
|
17
|
+
if (this.connectionPromise) return this.connectionPromise;
|
|
18
|
+
|
|
19
|
+
this.connectionPromise = (async () => {
|
|
20
|
+
let attempt = 0;
|
|
21
|
+
while (attempt < retries) {
|
|
22
|
+
try {
|
|
23
|
+
await this.producer.connect();
|
|
24
|
+
await this.consumer.connect();
|
|
25
|
+
logger.info('[Kafka] Producer connected successfully');
|
|
26
|
+
logger.info('[Kafka] Consumer connected successfully');
|
|
27
|
+
this.isConnected = true;
|
|
28
|
+
|
|
29
|
+
<%_ if (language === 'TypeScript') { -%>
|
|
30
|
+
// Auto-register WelcomeEmailConsumer if it exists
|
|
31
|
+
try {
|
|
32
|
+
const { WelcomeEmailConsumer } = await import('<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/consumers/instances/welcomeEmailConsumer<% } else { %>@/messaging/consumers/instances/welcomeEmailConsumer<% } %>');
|
|
33
|
+
const welcomeConsumer = new WelcomeEmailConsumer();
|
|
34
|
+
await this.consumer.subscribe({ topic: welcomeConsumer.topic, fromBeginning: true });
|
|
35
|
+
logger.info(`[Kafka] Registered consumer for topic: ${welcomeConsumer.topic}`);
|
|
36
|
+
|
|
37
|
+
await this.consumer.run({
|
|
38
|
+
eachMessage: async (payload) => welcomeConsumer.onMessage(payload),
|
|
39
|
+
});
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// Fallback or no consumers found
|
|
42
|
+
logger.warn(`[Kafka] Could not load WelcomeEmailConsumer, using fallback: ${(e as Error).message}`);
|
|
43
|
+
await this.consumer.subscribe({ topic: 'user-topic', fromBeginning: true });
|
|
44
|
+
await this.consumer.run({
|
|
45
|
+
eachMessage: async ({ message }: EachMessagePayload) => {
|
|
46
|
+
logger.info(`[Kafka] Consumer: Received message on user-topic: ${message.value?.toString()}`);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
<%_ } else { _%>
|
|
51
|
+
await this.consumer.subscribe({ topic: 'test-topic', fromBeginning: true });
|
|
52
|
+
await this.consumer.run({
|
|
53
|
+
eachMessage: async ({ message }: EachMessagePayload) => {
|
|
54
|
+
logger.info({ value: message.value?.toString() });
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
<%_ } _%>
|
|
58
|
+
return;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
attempt++;
|
|
61
|
+
logger.error(`[Kafka] Connection attempt ${attempt} failed:`, (error as Error).message);
|
|
62
|
+
if (attempt >= retries) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
await new Promise(res => setTimeout(res, 3000));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
|
|
70
|
+
return this.connectionPromise;
|
|
26
71
|
}
|
|
27
72
|
|
|
28
73
|
async sendMessage(topic: string, message: string) {
|
|
74
|
+
if (this.connectionPromise) {
|
|
75
|
+
await this.connectionPromise;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!this.isConnected) {
|
|
79
|
+
throw new Error('[Kafka] Producer not connected. Check logs for connection errors.');
|
|
80
|
+
}
|
|
81
|
+
|
|
29
82
|
await this.producer.send({
|
|
30
83
|
topic,
|
|
31
84
|
messages: [
|
|
32
85
|
{ value: message },
|
|
33
86
|
],
|
|
34
87
|
});
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(message);
|
|
90
|
+
logger.info(`[Kafka] Producer: Sent ${parsed.action} event for '${parsed.payload?.email || 'unknown'}'`);
|
|
91
|
+
} catch {
|
|
92
|
+
logger.info(`[Kafka] Producer: Sent message to ${topic}`);
|
|
93
|
+
}
|
|
35
94
|
}
|
|
36
95
|
|
|
37
96
|
async disconnect() {
|
|
@@ -40,3 +99,4 @@ export class KafkaService {
|
|
|
40
99
|
}
|
|
41
100
|
}
|
|
42
101
|
|
|
102
|
+
export const kafkaService = new KafkaService();
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "<% if (language === 'TypeScript') { %>node dist/index.js<% } else { %>node src/index.js<% } %>",
|
|
8
8
|
"dev": "<% if (language === 'TypeScript') { %>nodemon --exec ts-node -r tsconfig-paths/register src/index.ts<% } else { %>nodemon src/index.js<% } %>"<% if (language === 'TypeScript') { %>,
|
|
9
|
-
"build": "rimraf dist && tsc && tsc-alias<% if (viewEngine && viewEngine !== 'None') { %> && cpx \"src/views/**/*\" dist/views<% } %><% if (communication === 'REST APIs') { %> && cpx \"src/**/*.yml\" dist/<% } %>"<% } %>,
|
|
9
|
+
"build": "rimraf dist && tsc && tsc-alias<% if (viewEngine && viewEngine !== 'None') { %> && cpx \"src/views/**/*\" dist/views<% } %><% if (communication === 'REST APIs' || communication === 'Kafka') { %> && cpx \"src/**/*.yml\" dist/<% } %>"<% } %>,
|
|
10
10
|
"deploy": "npx pm2 start ecosystem.config.js --env production",
|
|
11
11
|
"lint": "eslint .",
|
|
12
12
|
"lint:fix": "eslint . --fix",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"express-rate-limit": "^7.1.5",
|
|
48
48
|
"winston": "^3.11.0",
|
|
49
49
|
"winston-daily-rotate-file": "^5.0.0",
|
|
50
|
-
"morgan": "^1.10.0"<% if (communication === 'REST APIs') { %>,
|
|
50
|
+
"morgan": "^1.10.0"<% if (communication === 'REST APIs' || communication === 'Kafka') { %>,
|
|
51
51
|
"swagger-ui-express": "^5.0.0",
|
|
52
52
|
"yamljs": "^0.3.0"<% } %><% if (communication === 'GraphQL') { %>,
|
|
53
53
|
"@apollo/server": "^4.10.0",
|
|
@@ -72,16 +72,18 @@
|
|
|
72
72
|
"@types/sequelize": "^4.28.19",
|
|
73
73
|
<%_ } -%>
|
|
74
74
|
"@types/morgan": "^1.9.9",
|
|
75
|
-
"rimraf": "^6.0.1"<% if ((viewEngine && viewEngine !== 'None') || communication === 'REST APIs') { %>,
|
|
75
|
+
"rimraf": "^6.0.1"<% if ((viewEngine && viewEngine !== 'None') || communication === 'REST APIs' || communication === 'Kafka') { %>,
|
|
76
76
|
"cpx2": "^8.0.0"<% } %><% } %>,
|
|
77
77
|
"eslint": "^9.20.1",
|
|
78
78
|
"@eslint/js": "^9.20.0",
|
|
79
79
|
"globals": "^15.14.0",
|
|
80
80
|
"prettier": "^3.5.1",
|
|
81
81
|
"eslint-config-prettier": "^10.0.1",
|
|
82
|
+
"eslint-plugin-import-x": "^4.6.1",
|
|
83
|
+
"eslint-import-resolver-typescript": "^3.7.0",
|
|
82
84
|
"husky": "^8.0.3",
|
|
83
85
|
"lint-staged": "^15.4.3"<% if (language === 'TypeScript') { %>,
|
|
84
|
-
"typescript-eslint": "^8.24.1",<%_ if (communication === 'REST APIs') { %>
|
|
86
|
+
"typescript-eslint": "^8.24.1",<%_ if (communication === 'REST APIs' || communication === 'Kafka') { %>
|
|
85
87
|
"@types/swagger-ui-express": "^4.1.6",
|
|
86
88
|
"@types/yamljs": "^0.2.34",<%_ } %>
|
|
87
89
|
"jest": "^29.7.0",
|
|
@@ -40,6 +40,13 @@ const createUser = async (data) => {
|
|
|
40
40
|
const user = await User.create({ name, email });
|
|
41
41
|
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
42
42
|
await cacheService.del('users:all');
|
|
43
|
+
<%_ } -%>
|
|
44
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
45
|
+
const { sendMessage } = require('../services/kafkaService');
|
|
46
|
+
await sendMessage('user-topic', JSON.stringify({
|
|
47
|
+
action: 'USER_CREATED',
|
|
48
|
+
payload: { id: user.id || user._id, email: user.email }
|
|
49
|
+
}));
|
|
43
50
|
<%_ } -%>
|
|
44
51
|
return user;
|
|
45
52
|
} catch (error) {
|
|
@@ -78,6 +85,13 @@ const createUser = async (req, res, next) => {
|
|
|
78
85
|
const user = await User.create({ name, email });
|
|
79
86
|
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
80
87
|
await cacheService.del('users:all');
|
|
88
|
+
<%_ } -%>
|
|
89
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
90
|
+
const { sendMessage } = require('../services/kafkaService');
|
|
91
|
+
await sendMessage('user-topic', JSON.stringify({
|
|
92
|
+
action: 'USER_CREATED',
|
|
93
|
+
payload: { id: user.id || user._id, email: user.email }
|
|
94
|
+
}));
|
|
81
95
|
<%_ } -%>
|
|
82
96
|
res.status(HTTP_STATUS.CREATED).json(user);
|
|
83
97
|
} catch (error) {
|
|
@@ -23,6 +23,14 @@ jest.mock('@/config/memoryCache', () => ({
|
|
|
23
23
|
}));
|
|
24
24
|
<%_ } -%>
|
|
25
25
|
jest.mock('@/utils/logger');
|
|
26
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
27
|
+
const { sendMessage } = require('@/services/kafkaService');
|
|
28
|
+
jest.mock('@/services/kafkaService', () => ({
|
|
29
|
+
sendMessage: jest.fn().mockResolvedValue(undefined),
|
|
30
|
+
connectKafka: jest.fn().mockResolvedValue(undefined)
|
|
31
|
+
}));
|
|
32
|
+
<%_ } -%>
|
|
33
|
+
|
|
26
34
|
|
|
27
35
|
describe('UserController', () => {
|
|
28
36
|
<% if (communication !== 'GraphQL') { -%>
|
|
@@ -143,6 +151,9 @@ describe('UserController', () => {
|
|
|
143
151
|
<% } -%>
|
|
144
152
|
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
145
153
|
expect(cacheService.del).toHaveBeenCalledWith('users:all');
|
|
154
|
+
<%_ } -%>
|
|
155
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
156
|
+
expect(sendMessage).toHaveBeenCalled();
|
|
146
157
|
<%_ } -%>
|
|
147
158
|
});
|
|
148
159
|
|
|
@@ -166,5 +177,33 @@ describe('UserController', () => {
|
|
|
166
177
|
expect(mockNext).toHaveBeenCalledWith(error);
|
|
167
178
|
<% } -%>
|
|
168
179
|
});
|
|
180
|
+
|
|
181
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
182
|
+
it('should successfully create a new user with _id for Kafka (Happy Path)', async () => {
|
|
183
|
+
// Arrange
|
|
184
|
+
const payload = { name: 'Bob', email: 'bob@example.com' };
|
|
185
|
+
<% if (communication === 'GraphQL') { -%>
|
|
186
|
+
const dataArg = payload;
|
|
187
|
+
<% } else { -%>
|
|
188
|
+
mockRequest.body = payload;
|
|
189
|
+
<% } -%>
|
|
190
|
+
|
|
191
|
+
const expectedUser = { _id: '2', ...payload };
|
|
192
|
+
User.create.mockResolvedValue(expectedUser);
|
|
193
|
+
|
|
194
|
+
// Act
|
|
195
|
+
<% if (communication === 'GraphQL') { -%>
|
|
196
|
+
await createUser(dataArg);
|
|
197
|
+
<% } else { -%>
|
|
198
|
+
await createUser(mockRequest, mockResponse, mockNext);
|
|
199
|
+
<% } -%>
|
|
200
|
+
|
|
201
|
+
// Assert
|
|
202
|
+
expect(sendMessage).toHaveBeenCalledWith(
|
|
203
|
+
'user-topic',
|
|
204
|
+
expect.stringContaining('"id":"2"')
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
<%_ } -%>
|
|
169
208
|
});
|
|
170
209
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const cors = require('cors');
|
|
3
|
-
<%_ if (communication === 'REST APIs') { -%>const apiRoutes = require('./routes/api');<%_ } %>
|
|
3
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>const apiRoutes = require('./routes/api');<%_ } %>
|
|
4
4
|
const healthRoutes = require('./routes/healthRoute');
|
|
5
5
|
<%_ if (communication === 'Kafka') { -%>const { connectKafka, sendMessage } = require('./services/kafkaService');<%_ } -%>
|
|
6
6
|
<%_ if (communication === 'GraphQL') { -%>
|
|
@@ -11,8 +11,8 @@ const { unwrapResolverError } = require('@apollo/server/errors');
|
|
|
11
11
|
const { ApiError } = require('./errors/ApiError');
|
|
12
12
|
const { typeDefs, resolvers } = require('./graphql');
|
|
13
13
|
const { gqlContext } = require('./graphql/context');
|
|
14
|
-
<% }
|
|
15
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
14
|
+
<% } %>
|
|
15
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
16
16
|
const swaggerUi = require('swagger-ui-express');
|
|
17
17
|
const swaggerSpecs = require('./config/swagger');
|
|
18
18
|
<%_ } -%>
|
|
@@ -28,7 +28,7 @@ app.use(cors());
|
|
|
28
28
|
app.use(express.json());
|
|
29
29
|
app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } }));
|
|
30
30
|
|
|
31
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
31
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
32
32
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
33
33
|
<%_ } -%>
|
|
34
34
|
<%_ if (viewEngine === 'EJS' || viewEngine === 'Pug') { -%>
|
|
@@ -37,7 +37,7 @@ const path = require('path');
|
|
|
37
37
|
app.set('views', path.join(__dirname, 'views'));
|
|
38
38
|
app.set('view engine', '<%= viewEngine.toLowerCase() %>');
|
|
39
39
|
app.use(express.static(path.join(__dirname, '../public')));<%_ } %>
|
|
40
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
40
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
41
41
|
app.use('/api', apiRoutes);
|
|
42
42
|
<%_ } -%><% if (viewEngine && viewEngine !== 'None') { -%>
|
|
43
43
|
app.get('/', (req, res) => {
|
|
@@ -86,12 +86,13 @@ const startServer = async () => {
|
|
|
86
86
|
const server = app.listen(PORT, () => {
|
|
87
87
|
logger.info(`Server running on port ${PORT}`);
|
|
88
88
|
<%_ if (communication === 'Kafka') { -%>
|
|
89
|
-
connectKafka()
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
connectKafka()
|
|
90
|
+
.then(async () => {
|
|
91
|
+
logger.info('Kafka connected');
|
|
92
|
+
})
|
|
93
|
+
.catch(err => {
|
|
94
|
+
logger.error('Failed to connect to Kafka after retries:', err.message);
|
|
95
|
+
});
|
|
95
96
|
<%_ } -%>
|
|
96
97
|
});
|
|
97
98
|
|
|
@@ -24,6 +24,20 @@ jest.mock('@/config/memoryCache', () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
<%_ } -%>
|
|
26
26
|
jest.mock('@/utils/logger');
|
|
27
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
28
|
+
jest.mock('@/services/kafkaService', () => {
|
|
29
|
+
const mockSendMessage = jest.fn().mockResolvedValue(undefined);
|
|
30
|
+
return {
|
|
31
|
+
kafkaService: {
|
|
32
|
+
sendMessage: mockSendMessage
|
|
33
|
+
},
|
|
34
|
+
KafkaService: jest.fn().mockImplementation(() => ({
|
|
35
|
+
sendMessage: mockSendMessage
|
|
36
|
+
}))
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
<%_ } -%>
|
|
40
|
+
|
|
27
41
|
|
|
28
42
|
describe('UserController', () => {
|
|
29
43
|
let userController: UserController;
|
|
@@ -158,6 +172,10 @@ describe('UserController', () => {
|
|
|
158
172
|
<% } -%>
|
|
159
173
|
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
160
174
|
expect(cacheService.del).toHaveBeenCalledWith('users:all');
|
|
175
|
+
<%_ } -%>
|
|
176
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
177
|
+
const { kafkaService } = require('@/services/kafkaService');
|
|
178
|
+
expect(kafkaService.sendMessage).toHaveBeenCalled();
|
|
161
179
|
<%_ } -%>
|
|
162
180
|
});
|
|
163
181
|
|
|
@@ -42,6 +42,14 @@ export class UserController {
|
|
|
42
42
|
const user = await User.create({ name, email });
|
|
43
43
|
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
44
44
|
await cacheService.del('users:all');
|
|
45
|
+
<%_ } -%>
|
|
46
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
47
|
+
const { kafkaService } = await import('@/services/kafkaService');
|
|
48
|
+
await kafkaService.sendMessage('user-topic', JSON.stringify({
|
|
49
|
+
action: 'USER_CREATED',
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
payload: { id: (user as any).id || (user as any)._id, email: user.email }
|
|
52
|
+
}));
|
|
45
53
|
<%_ } -%>
|
|
46
54
|
return user;
|
|
47
55
|
} catch (error) {
|
|
@@ -80,6 +88,14 @@ export class UserController {
|
|
|
80
88
|
const user = await User.create({ name, email });
|
|
81
89
|
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
82
90
|
await cacheService.del('users:all');
|
|
91
|
+
<%_ } -%>
|
|
92
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
93
|
+
const { kafkaService } = await import('@/services/kafkaService');
|
|
94
|
+
await kafkaService.sendMessage('user-topic', JSON.stringify({
|
|
95
|
+
action: 'USER_CREATED',
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
+
payload: { id: (user as any).id || (user as any)._id, email: user.email }
|
|
98
|
+
}));
|
|
83
99
|
<%_ } -%>
|
|
84
100
|
res.status(HTTP_STATUS.CREATED).json(user);
|
|
85
101
|
} catch (error) {
|
|
@@ -9,12 +9,11 @@ import morgan from 'morgan';
|
|
|
9
9
|
import { errorMiddleware } from '@/utils/errorMiddleware';
|
|
10
10
|
import { setupGracefulShutdown } from '@/utils/gracefulShutdown';
|
|
11
11
|
import healthRoutes from '@/routes/healthRoute';
|
|
12
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
13
|
-
import apiRoutes from '@/routes/api'
|
|
14
|
-
<% if (communication === 'REST APIs') { %>
|
|
12
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
13
|
+
import apiRoutes from '@/routes/api';
|
|
15
14
|
import swaggerUi from 'swagger-ui-express';
|
|
16
|
-
import swaggerSpecs from '@/config/swagger';<% }
|
|
17
|
-
<%_ if (communication === 'Kafka') { -%>import {
|
|
15
|
+
import swaggerSpecs from '@/config/swagger';<%_ } %>
|
|
16
|
+
<%_ if (communication === 'Kafka') { -%>import { kafkaService } from '@/services/kafkaService';<%_ } -%>
|
|
18
17
|
<%_ if (communication === 'GraphQL') { -%>
|
|
19
18
|
import { ApolloServer } from '@apollo/server';
|
|
20
19
|
import { expressMiddleware } from '@apollo/server/express4';
|
|
@@ -51,7 +50,7 @@ app.use(limiter);
|
|
|
51
50
|
|
|
52
51
|
app.use(express.json());
|
|
53
52
|
app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } }));
|
|
54
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
53
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
55
54
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
56
55
|
<%_ } -%>
|
|
57
56
|
<%_ if (viewEngine === 'EJS' || viewEngine === 'Pug') { -%>
|
|
@@ -60,7 +59,7 @@ import path from 'path';
|
|
|
60
59
|
app.set('views', path.join(__dirname, 'views'));
|
|
61
60
|
app.set('view engine', '<%= viewEngine.toLowerCase() %>');
|
|
62
61
|
app.use(express.static(path.join(__dirname, '../public')));<%_ } %>
|
|
63
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
62
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
64
63
|
app.use('/api', apiRoutes);
|
|
65
64
|
<%_ } -%><% if (viewEngine && viewEngine !== 'None') { -%>
|
|
66
65
|
app.get('/', (req: Request, res: Response) => {
|
|
@@ -106,18 +105,16 @@ const startServer = async () => {
|
|
|
106
105
|
app.use('/graphql', expressMiddleware(apolloServer, { context: gqlContext }));
|
|
107
106
|
<%_ } -%>
|
|
108
107
|
app.use(errorMiddleware);
|
|
109
|
-
<%_ if (communication === 'Kafka') { -%>
|
|
110
|
-
const kafkaService = new KafkaService();
|
|
111
|
-
<%_ } -%>
|
|
112
108
|
const server = app.listen(port, () => {
|
|
113
109
|
logger.info(`Server running on port ${port}`);
|
|
114
110
|
<%_ if (communication === 'Kafka') { -%>
|
|
115
|
-
kafkaService.connect()
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
kafkaService.connect()
|
|
112
|
+
.then(async () => {
|
|
113
|
+
logger.info('Kafka connected');
|
|
114
|
+
})
|
|
115
|
+
.catch(err => {
|
|
116
|
+
logger.error('Failed to connect to Kafka after retries:', (err as Error).message);
|
|
117
|
+
});
|
|
121
118
|
<%_ } -%>
|
|
122
119
|
});
|
|
123
120
|
|
|
@@ -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';
|