nodejs-quickstart-structure 1.18.1 → 1.19.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 +7 -0
- package/README.md +2 -1
- package/lib/modules/caching-setup.js +76 -73
- package/lib/modules/kafka-setup.js +249 -191
- package/lib/modules/project-setup.js +1 -0
- package/package.json +13 -2
- package/templates/clean-architecture/js/src/errors/BadRequestError.js +11 -10
- package/templates/clean-architecture/js/src/errors/BadRequestError.spec.js.ejs +22 -21
- package/templates/clean-architecture/js/src/errors/NotFoundError.js +11 -10
- package/templates/clean-architecture/js/src/errors/NotFoundError.spec.js.ejs +22 -21
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +69 -39
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +142 -81
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +156 -75
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +234 -138
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +27 -21
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +66 -49
- package/templates/clean-architecture/js/src/interfaces/graphql/typeDefs/user.types.js.ejs +19 -17
- package/templates/clean-architecture/js/src/interfaces/routes/api.js +12 -10
- package/templates/clean-architecture/js/src/usecases/DeleteUser.js +11 -0
- package/templates/clean-architecture/js/src/usecases/DeleteUser.spec.js.ejs +47 -0
- package/templates/clean-architecture/js/src/usecases/UpdateUser.js +11 -0
- package/templates/clean-architecture/js/src/usecases/UpdateUser.spec.js.ejs +48 -0
- package/templates/clean-architecture/js/src/utils/errorMessages.js +14 -0
- package/templates/clean-architecture/ts/src/errors/BadRequestError.spec.ts.ejs +22 -21
- package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +9 -8
- package/templates/clean-architecture/ts/src/errors/NotFoundError.spec.ts.ejs +22 -21
- package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +9 -8
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +175 -85
- package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +74 -0
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +331 -185
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +173 -84
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +68 -51
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +29 -21
- package/templates/clean-architecture/ts/src/interfaces/graphql/typeDefs/user.types.ts.ejs +17 -15
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +13 -11
- package/templates/clean-architecture/ts/src/usecases/deleteUser.spec.ts.ejs +47 -0
- package/templates/clean-architecture/ts/src/usecases/deleteUser.ts +9 -0
- package/templates/clean-architecture/ts/src/usecases/updateUser.spec.ts.ejs +48 -0
- package/templates/clean-architecture/ts/src/usecases/updateUser.ts +9 -0
- package/templates/clean-architecture/ts/src/utils/errorMessages.ts +12 -0
- package/templates/common/.gitattributes +46 -0
- package/templates/common/README.md.ejs +294 -270
- package/templates/common/caching/clean/js/DeleteUser.js.ejs +27 -0
- package/templates/common/caching/clean/js/UpdateUser.js.ejs +27 -0
- package/templates/common/caching/clean/ts/deleteUser.ts.ejs +24 -0
- package/templates/common/caching/clean/ts/updateUser.ts.ejs +25 -0
- package/templates/common/caching/ts/memoryCache.ts.ejs +73 -64
- package/templates/common/caching/ts/redisClient.ts.ejs +89 -80
- package/templates/common/database/js/models/User.js.ejs +79 -53
- package/templates/common/database/js/models/User.js.mongoose.ejs +23 -19
- package/templates/common/database/js/models/User.spec.js.ejs +94 -84
- package/templates/common/database/ts/models/User.spec.ts.ejs +100 -84
- package/templates/common/database/ts/models/User.ts.ejs +87 -61
- package/templates/common/database/ts/models/User.ts.mongoose.ejs +30 -25
- package/templates/common/health/js/healthRoute.js.ejs +50 -47
- package/templates/common/health/ts/healthRoute.ts.ejs +49 -46
- package/templates/common/jest.e2e.config.js.ejs +8 -8
- package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -30
- package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +12 -11
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +44 -31
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +86 -49
- package/templates/common/kafka/js/services/kafkaService.js.ejs +93 -93
- package/templates/common/kafka/js/utils/kafkaEvents.js.ejs +7 -0
- package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -51
- package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +12 -11
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +86 -49
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +38 -25
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +95 -95
- package/templates/common/kafka/ts/utils/kafkaEvents.ts.ejs +5 -0
- package/templates/common/shutdown/js/gracefulShutdown.js.ejs +65 -61
- package/templates/common/shutdown/js/gracefulShutdown.spec.js.ejs +149 -160
- package/templates/common/shutdown/ts/gracefulShutdown.spec.ts.ejs +179 -158
- package/templates/common/shutdown/ts/gracefulShutdown.ts.ejs +59 -55
- package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +120 -49
- package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +120 -49
- package/templates/common/swagger.yml.ejs +118 -66
- package/templates/db/mysql/V1__Initial_Setup.sql.ejs +10 -9
- package/templates/db/postgres/V1__Initial_Setup.sql.ejs +10 -9
- package/templates/mvc/js/src/controllers/userController.js.ejs +246 -105
- package/templates/mvc/js/src/controllers/userController.spec.js.ejs +481 -209
- package/templates/mvc/js/src/errors/BadRequestError.js +11 -10
- package/templates/mvc/js/src/errors/BadRequestError.spec.js.ejs +22 -21
- package/templates/mvc/js/src/errors/NotFoundError.js +11 -10
- package/templates/mvc/js/src/errors/NotFoundError.spec.js.ejs +22 -21
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +25 -19
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +64 -47
- package/templates/mvc/js/src/graphql/typeDefs/user.types.js.ejs +19 -17
- package/templates/mvc/js/src/routes/api.js +10 -8
- package/templates/mvc/js/src/routes/api.spec.js.ejs +41 -36
- package/templates/mvc/js/src/utils/errorMessages.js +14 -0
- package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +481 -203
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +248 -107
- package/templates/mvc/ts/src/errors/BadRequestError.spec.ts.ejs +22 -21
- package/templates/mvc/ts/src/errors/BadRequestError.ts +9 -8
- package/templates/mvc/ts/src/errors/NotFoundError.spec.ts.ejs +27 -21
- package/templates/mvc/ts/src/errors/NotFoundError.ts +9 -8
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +68 -51
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +29 -21
- package/templates/mvc/ts/src/graphql/typeDefs/user.types.ts.ejs +17 -15
- package/templates/mvc/ts/src/index.ts.ejs +156 -153
- package/templates/mvc/ts/src/routes/api.spec.ts.ejs +59 -40
- package/templates/mvc/ts/src/routes/api.ts +12 -10
- package/templates/mvc/ts/src/utils/errorMessages.ts +12 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.ts.ejs +0 -37
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
import { kafka } from '<%= configPath %>';
|
|
2
|
-
import { EachMessagePayload, Producer, Consumer } from 'kafkajs';
|
|
3
|
-
import logger from '<%= loggerPath %>';
|
|
4
|
-
|
|
5
|
-
export class KafkaService {
|
|
6
|
-
private producer: Producer;
|
|
7
|
-
private consumer: Consumer;
|
|
8
|
-
private isConnected = false;
|
|
9
|
-
private connectionPromise: Promise<void> | null = null;
|
|
10
|
-
|
|
11
|
-
constructor() {
|
|
12
|
-
this.producer = kafka.producer();
|
|
13
|
-
this.consumer = kafka.consumer({ groupId: 'test-group' });
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async connect(retries = 10) {
|
|
17
|
-
if (this.connectionPromise) return this.connectionPromise;
|
|
18
|
-
|
|
19
|
-
this.connectionPromise = (async () => {
|
|
20
|
-
let attempt = 0;
|
|
21
|
-
// Auto-register WelcomeEmailConsumer if it exists
|
|
22
|
-
// Note: Dynamic import used here for simplicity and to avoid startup crashes.
|
|
23
|
-
// In enterprise production, consider using Dependency Injection.
|
|
24
|
-
const { WelcomeEmailConsumer } = await import('<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/consumers/instances/welcomeEmailConsumer<% } else { %>@/messaging/consumers/instances/welcomeEmailConsumer<% } %>');
|
|
25
|
-
while (attempt < retries) {
|
|
26
|
-
try {
|
|
27
|
-
await this.producer.connect();
|
|
28
|
-
await this.consumer.connect();
|
|
29
|
-
logger.info('[Kafka] Producer connected successfully');
|
|
30
|
-
logger.info('[Kafka] Consumer connected successfully');
|
|
31
|
-
this.isConnected = true;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const welcomeConsumer = new WelcomeEmailConsumer();
|
|
35
|
-
await this.consumer.subscribe({ topic: welcomeConsumer.topic, fromBeginning: true });
|
|
36
|
-
logger.info(`[Kafka] Registered consumer for topic: ${welcomeConsumer.topic}`);
|
|
37
|
-
|
|
38
|
-
await this.consumer.run({
|
|
39
|
-
eachMessage: async (payload) => welcomeConsumer.onMessage(payload),
|
|
40
|
-
});
|
|
41
|
-
} catch (e) {
|
|
42
|
-
// Fallback or no consumers found
|
|
43
|
-
logger.warn(`[Kafka] Could not load WelcomeEmailConsumer, using fallback: ${(e as Error).message}`);
|
|
44
|
-
await this.consumer.subscribe({ topic: 'user-topic', fromBeginning: true });
|
|
45
|
-
await this.consumer.run({
|
|
46
|
-
eachMessage: async ({ message }: EachMessagePayload) => {
|
|
47
|
-
logger.info(`[Kafka] Consumer: Received message on user-topic: ${message.value?.toString()}`);
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return; // Success
|
|
52
|
-
} catch (error) {
|
|
53
|
-
attempt++;
|
|
54
|
-
logger.error(`[Kafka] Connection attempt ${attempt} failed:`, (error as Error).message);
|
|
55
|
-
if (attempt >= retries) {
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
await new Promise(res => setTimeout(res, 3000));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
})();
|
|
62
|
-
|
|
63
|
-
return this.connectionPromise;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async sendMessage(topic: string, message: string) {
|
|
67
|
-
if (this.connectionPromise) {
|
|
68
|
-
await this.connectionPromise;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!this.isConnected) {
|
|
72
|
-
throw new Error('[Kafka] Producer not connected. Check logs for connection errors.');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
await this.producer.send({
|
|
76
|
-
topic,
|
|
77
|
-
messages: [
|
|
78
|
-
{ value: message },
|
|
79
|
-
],
|
|
80
|
-
});
|
|
81
|
-
try {
|
|
82
|
-
const parsed = JSON.parse(message);
|
|
83
|
-
logger.info(`[Kafka] Producer: Sent ${parsed.action} event for '${parsed.payload?.email || 'unknown'}'`);
|
|
84
|
-
} catch {
|
|
85
|
-
logger.info(`[Kafka] Producer: Sent message to ${topic}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async disconnect() {
|
|
90
|
-
await this.producer.disconnect();
|
|
91
|
-
await this.consumer.disconnect();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export const kafkaService = new KafkaService();
|
|
1
|
+
import { kafka } from '<%= configPath %>';
|
|
2
|
+
import { EachMessagePayload, Producer, Consumer } from 'kafkajs';
|
|
3
|
+
import logger from '<%= loggerPath %>';
|
|
4
|
+
|
|
5
|
+
export class KafkaService {
|
|
6
|
+
private producer: Producer;
|
|
7
|
+
private consumer: Consumer;
|
|
8
|
+
private isConnected = false;
|
|
9
|
+
private connectionPromise: Promise<void> | null = null;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.producer = kafka.producer();
|
|
13
|
+
this.consumer = kafka.consumer({ groupId: 'test-group' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async connect(retries = 10) {
|
|
17
|
+
if (this.connectionPromise) return this.connectionPromise;
|
|
18
|
+
|
|
19
|
+
this.connectionPromise = (async () => {
|
|
20
|
+
let attempt = 0;
|
|
21
|
+
// Auto-register WelcomeEmailConsumer if it exists
|
|
22
|
+
// Note: Dynamic import used here for simplicity and to avoid startup crashes.
|
|
23
|
+
// In enterprise production, consider using Dependency Injection.
|
|
24
|
+
const { WelcomeEmailConsumer } = await import('<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/consumers/instances/welcomeEmailConsumer<% } else { %>@/messaging/consumers/instances/welcomeEmailConsumer<% } %>');
|
|
25
|
+
while (attempt < retries) {
|
|
26
|
+
try {
|
|
27
|
+
await this.producer.connect();
|
|
28
|
+
await this.consumer.connect();
|
|
29
|
+
logger.info('[Kafka] Producer connected successfully');
|
|
30
|
+
logger.info('[Kafka] Consumer connected successfully');
|
|
31
|
+
this.isConnected = true;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const welcomeConsumer = new WelcomeEmailConsumer();
|
|
35
|
+
await this.consumer.subscribe({ topic: welcomeConsumer.topic, fromBeginning: true });
|
|
36
|
+
logger.info(`[Kafka] Registered consumer for topic: ${welcomeConsumer.topic}`);
|
|
37
|
+
|
|
38
|
+
await this.consumer.run({
|
|
39
|
+
eachMessage: async (payload) => welcomeConsumer.onMessage(payload),
|
|
40
|
+
});
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Fallback or no consumers found
|
|
43
|
+
logger.warn(`[Kafka] Could not load WelcomeEmailConsumer, using fallback: ${(e as Error).message}`);
|
|
44
|
+
await this.consumer.subscribe({ topic: 'user-topic', fromBeginning: true });
|
|
45
|
+
await this.consumer.run({
|
|
46
|
+
eachMessage: async ({ message }: EachMessagePayload) => {
|
|
47
|
+
logger.info(`[Kafka] Consumer: Received message on user-topic: ${message.value?.toString()}`);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return; // Success
|
|
52
|
+
} catch (error) {
|
|
53
|
+
attempt++;
|
|
54
|
+
logger.error(`[Kafka] Connection attempt ${attempt} failed:`, (error as Error).message);
|
|
55
|
+
if (attempt >= retries) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
await new Promise(res => setTimeout(res, 3000));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
62
|
+
|
|
63
|
+
return this.connectionPromise;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async sendMessage(topic: string, message: string, key?: string) {
|
|
67
|
+
if (this.connectionPromise) {
|
|
68
|
+
await this.connectionPromise;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!this.isConnected) {
|
|
72
|
+
throw new Error('[Kafka] Producer not connected. Check logs for connection errors.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await this.producer.send({
|
|
76
|
+
topic,
|
|
77
|
+
messages: [
|
|
78
|
+
{ key, value: message },
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(message);
|
|
83
|
+
logger.info(`[Kafka] Producer: Sent ${parsed.action} event for '${parsed.payload?.email || 'unknown'}'`);
|
|
84
|
+
} catch {
|
|
85
|
+
logger.info(`[Kafka] Producer: Sent message to ${topic}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async disconnect() {
|
|
90
|
+
await this.producer.disconnect();
|
|
91
|
+
await this.consumer.disconnect();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const kafkaService = new KafkaService();
|
|
@@ -1,61 +1,65 @@
|
|
|
1
|
-
<%_
|
|
2
|
-
let loggerPath = './logger';
|
|
3
|
-
let dbPath = '../config/database';
|
|
4
|
-
let redisPath = '../config/redisClient';
|
|
5
|
-
let kafkaPath = '../services/kafkaService';
|
|
6
|
-
|
|
7
|
-
if (architecture === 'Clean Architecture') {
|
|
8
|
-
loggerPath = '../infrastructure/log/logger';
|
|
9
|
-
dbPath = '../infrastructure/database/database';
|
|
10
|
-
redisPath = '../infrastructure/caching/redisClient';
|
|
11
|
-
kafkaPath = '../infrastructure/messaging/kafkaClient';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const logger = require('<%- loggerPath %>');
|
|
15
|
-
|
|
16
|
-
const setupGracefulShutdown = (server) => {
|
|
17
|
-
const gracefulShutdown = async (signal) => {
|
|
18
|
-
logger.info(`Received ${signal}. Shutting down gracefully...`);
|
|
19
|
-
server.close(async () => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<%_
|
|
28
|
-
const
|
|
29
|
-
await
|
|
30
|
-
logger.info('
|
|
31
|
-
<%_ } -%>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<%_
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<%_
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
|
|
1
|
+
<%_
|
|
2
|
+
let loggerPath = './logger';
|
|
3
|
+
let dbPath = '../config/database';
|
|
4
|
+
let redisPath = '../config/redisClient';
|
|
5
|
+
let kafkaPath = '../services/kafkaService';
|
|
6
|
+
|
|
7
|
+
if (architecture === 'Clean Architecture') {
|
|
8
|
+
loggerPath = '../infrastructure/log/logger';
|
|
9
|
+
dbPath = '../infrastructure/database/database';
|
|
10
|
+
redisPath = '../infrastructure/caching/redisClient';
|
|
11
|
+
kafkaPath = '../infrastructure/messaging/kafkaClient';
|
|
12
|
+
}
|
|
13
|
+
-%>
|
|
14
|
+
const logger = require('<%- loggerPath %>');
|
|
15
|
+
|
|
16
|
+
const setupGracefulShutdown = (server) => {
|
|
17
|
+
const gracefulShutdown = async (signal) => {
|
|
18
|
+
logger.info(`Received ${signal}. Shutting down gracefully...`);
|
|
19
|
+
server.close(async (err) => {
|
|
20
|
+
if (err) {
|
|
21
|
+
logger.error('Error closing HTTP server:', err);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
logger.info('HTTP server closed.');
|
|
25
|
+
try {
|
|
26
|
+
<%_ if (database !== 'None') { -%>
|
|
27
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
28
|
+
const mongoose = require('mongoose');
|
|
29
|
+
await mongoose.connection.close(false);
|
|
30
|
+
logger.info('MongoDB connection closed.');
|
|
31
|
+
<%_ } else { -%>
|
|
32
|
+
const sequelize = require('<%- dbPath %>');
|
|
33
|
+
await sequelize.close();
|
|
34
|
+
logger.info('Database connection closed.');
|
|
35
|
+
<%_ } -%>
|
|
36
|
+
<%_ } -%>
|
|
37
|
+
<%_ if (caching === 'Redis') { -%>
|
|
38
|
+
const redisService = require('<%- redisPath %>');
|
|
39
|
+
await redisService.quit();
|
|
40
|
+
logger.info('Redis connection closed.');
|
|
41
|
+
<%_ } -%>
|
|
42
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
43
|
+
const { disconnectKafka } = require('<%- kafkaPath %>');
|
|
44
|
+
await disconnectKafka();
|
|
45
|
+
logger.info('Kafka connection closed.');
|
|
46
|
+
<%_ } -%>
|
|
47
|
+
logger.info('Graceful shutdown fully completed.');
|
|
48
|
+
process.exit(0);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
logger.error('Error during shutdown:', err);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
logger.error('Could not close connections in time, forcefully shutting down');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}, 15000);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
62
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
module.exports = setupGracefulShutdown;
|
|
@@ -1,160 +1,149 @@
|
|
|
1
|
-
<%_
|
|
2
|
-
let loggerPath = '@/utils/logger';
|
|
3
|
-
let dbPath = '@/config/database';
|
|
4
|
-
let redisPath = '@/config/redisClient';
|
|
5
|
-
let kafkaPath = '@/services/kafkaService';
|
|
6
|
-
|
|
7
|
-
if (architecture === 'Clean Architecture') {
|
|
8
|
-
loggerPath = '@/infrastructure/log/logger';
|
|
9
|
-
dbPath = '@/infrastructure/database/database';
|
|
10
|
-
redisPath = '@/infrastructure/caching/redisClient';
|
|
11
|
-
kafkaPath = '@/infrastructure/messaging/kafkaClient';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const setupGracefulShutdown = require('@/utils/gracefulShutdown');
|
|
15
|
-
|
|
16
|
-
<%_ if (database === 'MongoDB') { -%>
|
|
17
|
-
jest.mock('mongoose', () => {
|
|
18
|
-
return {
|
|
19
|
-
connection: {
|
|
20
|
-
close: jest.fn().mockResolvedValue(true)
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
<%_ } else if (database !== 'None') { -%>
|
|
25
|
-
jest.mock('<%- dbPath %>', () => {
|
|
26
|
-
return {
|
|
27
|
-
close: jest.fn().mockResolvedValue(true)
|
|
28
|
-
};
|
|
29
|
-
});
|
|
30
|
-
<%_ } -%>
|
|
31
|
-
|
|
32
|
-
<%_ if (caching === 'Redis') { -%>
|
|
33
|
-
jest.mock('<%- redisPath %>', () => {
|
|
34
|
-
return {
|
|
35
|
-
quit: jest.fn().mockResolvedValue(true)
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
<%_ } -%>
|
|
39
|
-
|
|
40
|
-
<%_ if (communication === 'Kafka') { -%>
|
|
41
|
-
jest.mock('<%- kafkaPath %>', () => {
|
|
42
|
-
return {
|
|
43
|
-
disconnectKafka: jest.fn().mockResolvedValue(true)
|
|
44
|
-
};
|
|
45
|
-
});
|
|
46
|
-
<%_ } -%>
|
|
47
|
-
|
|
48
|
-
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
|
|
49
|
-
|
|
50
|
-
describe('Graceful Shutdown', () => {
|
|
51
|
-
let mockServer;
|
|
52
|
-
let mockExit;
|
|
53
|
-
let processListeners;
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
jest.useFakeTimers({ legacyFakeTimers: true });
|
|
57
|
-
jest.clearAllMocks();
|
|
58
|
-
processListeners = {};
|
|
59
|
-
|
|
60
|
-
mockServer = {
|
|
61
|
-
close: jest.fn().mockImplementation((cb) => {
|
|
62
|
-
if (cb) Promise.resolve().then(() => cb());
|
|
63
|
-
return mockServer;
|
|
64
|
-
})
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
68
|
-
jest.spyOn(process, 'on').mockImplementation((event, handler) => {
|
|
69
|
-
processListeners[event] = handler;
|
|
70
|
-
return process;
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
afterEach(() => {
|
|
75
|
-
jest.restoreAllMocks();
|
|
76
|
-
jest.useRealTimers();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should register SIGTERM and SIGINT events', () => {
|
|
80
|
-
setupGracefulShutdown(mockServer);
|
|
81
|
-
expect(processListeners['SIGTERM']).toBeDefined();
|
|
82
|
-
expect(processListeners['SIGINT']).toBeDefined();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should cleanly shutdown all connections and exit 0', async () => {
|
|
86
|
-
setupGracefulShutdown(mockServer);
|
|
87
|
-
|
|
88
|
-
processListeners['SIGTERM']();
|
|
89
|
-
|
|
90
|
-
await flushPromises();
|
|
91
|
-
await flushPromises();
|
|
92
|
-
await flushPromises();
|
|
93
|
-
|
|
94
|
-
expect(mockServer.close).toHaveBeenCalled();
|
|
95
|
-
|
|
96
|
-
<%_ if (database === 'MongoDB') { -%>
|
|
97
|
-
const mongoose = require('mongoose');
|
|
98
|
-
expect(mongoose.connection.close).toHaveBeenCalledWith(false);
|
|
99
|
-
<%_ } else if (database !== 'None') { -%>
|
|
100
|
-
const sequelize = require('<%- dbPath %>');
|
|
101
|
-
expect(sequelize.close).toHaveBeenCalled();
|
|
102
|
-
<%_ } -%>
|
|
103
|
-
|
|
104
|
-
<%_ if (caching === 'Redis') { -%>
|
|
105
|
-
const redisService = require('<%- redisPath %>');
|
|
106
|
-
expect(redisService.quit).toHaveBeenCalled();
|
|
107
|
-
<%_ } -%>
|
|
108
|
-
|
|
109
|
-
<%_ if (communication === 'Kafka') { -%>
|
|
110
|
-
const { disconnectKafka } = require('<%- kafkaPath %>');
|
|
111
|
-
expect(disconnectKafka).toHaveBeenCalled();
|
|
112
|
-
<%_ } -%>
|
|
113
|
-
|
|
114
|
-
expect(mockExit).toHaveBeenCalledWith(0);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should exit 0 on SIGINT', async () => {
|
|
118
|
-
setupGracefulShutdown(mockServer);
|
|
119
|
-
processListeners['SIGINT']();
|
|
120
|
-
await flushPromises();
|
|
121
|
-
await flushPromises();
|
|
122
|
-
expect(mockExit).toHaveBeenCalledWith(0);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
it('should forcefully shutdown if cleanup takes too long', async () => {
|
|
152
|
-
setupGracefulShutdown(mockServer);
|
|
153
|
-
processListeners['SIGTERM']();
|
|
154
|
-
|
|
155
|
-
jest.advanceTimersByTime(15000);
|
|
156
|
-
|
|
157
|
-
expect(mockExit).toHaveBeenCalledWith(1);
|
|
158
|
-
});
|
|
159
|
-
<%_ } _%>
|
|
160
|
-
});
|
|
1
|
+
<%_
|
|
2
|
+
let loggerPath = '@/utils/logger';
|
|
3
|
+
let dbPath = '@/config/database';
|
|
4
|
+
let redisPath = '@/config/redisClient';
|
|
5
|
+
let kafkaPath = '@/services/kafkaService';
|
|
6
|
+
|
|
7
|
+
if (architecture === 'Clean Architecture') {
|
|
8
|
+
loggerPath = '@/infrastructure/log/logger';
|
|
9
|
+
dbPath = '@/infrastructure/database/database';
|
|
10
|
+
redisPath = '@/infrastructure/caching/redisClient';
|
|
11
|
+
kafkaPath = '@/infrastructure/messaging/kafkaClient';
|
|
12
|
+
}
|
|
13
|
+
-%>
|
|
14
|
+
const setupGracefulShutdown = require('@/utils/gracefulShutdown');
|
|
15
|
+
|
|
16
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
17
|
+
jest.mock('mongoose', () => {
|
|
18
|
+
return {
|
|
19
|
+
connection: {
|
|
20
|
+
close: jest.fn().mockResolvedValue(true)
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
<%_ } else if (database !== 'None') { -%>
|
|
25
|
+
jest.mock('<%- dbPath %>', () => {
|
|
26
|
+
return {
|
|
27
|
+
close: jest.fn().mockResolvedValue(true)
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
<%_ } -%>
|
|
31
|
+
|
|
32
|
+
<%_ if (caching === 'Redis') { -%>
|
|
33
|
+
jest.mock('<%- redisPath %>', () => {
|
|
34
|
+
return {
|
|
35
|
+
quit: jest.fn().mockResolvedValue(true)
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
<%_ } -%>
|
|
39
|
+
|
|
40
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
41
|
+
jest.mock('<%- kafkaPath %>', () => {
|
|
42
|
+
return {
|
|
43
|
+
disconnectKafka: jest.fn().mockResolvedValue(true)
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
<%_ } -%>
|
|
47
|
+
|
|
48
|
+
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
|
|
49
|
+
|
|
50
|
+
describe('Graceful Shutdown', () => {
|
|
51
|
+
let mockServer;
|
|
52
|
+
let mockExit;
|
|
53
|
+
let processListeners;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
jest.useFakeTimers({ legacyFakeTimers: true });
|
|
57
|
+
jest.clearAllMocks();
|
|
58
|
+
processListeners = {};
|
|
59
|
+
|
|
60
|
+
mockServer = {
|
|
61
|
+
close: jest.fn().mockImplementation((cb) => {
|
|
62
|
+
if (cb) Promise.resolve().then(() => cb());
|
|
63
|
+
return mockServer;
|
|
64
|
+
})
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
68
|
+
jest.spyOn(process, 'on').mockImplementation((event, handler) => {
|
|
69
|
+
processListeners[event] = handler;
|
|
70
|
+
return process;
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
jest.restoreAllMocks();
|
|
76
|
+
jest.useRealTimers();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should register SIGTERM and SIGINT events', () => {
|
|
80
|
+
setupGracefulShutdown(mockServer);
|
|
81
|
+
expect(processListeners['SIGTERM']).toBeDefined();
|
|
82
|
+
expect(processListeners['SIGINT']).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should cleanly shutdown all connections and exit 0', async () => {
|
|
86
|
+
setupGracefulShutdown(mockServer);
|
|
87
|
+
|
|
88
|
+
processListeners['SIGTERM']();
|
|
89
|
+
|
|
90
|
+
await flushPromises();
|
|
91
|
+
await flushPromises();
|
|
92
|
+
await flushPromises();
|
|
93
|
+
|
|
94
|
+
expect(mockServer.close).toHaveBeenCalled();
|
|
95
|
+
|
|
96
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
97
|
+
const mongoose = require('mongoose');
|
|
98
|
+
expect(mongoose.connection.close).toHaveBeenCalledWith(false);
|
|
99
|
+
<%_ } else if (database !== 'None') { -%>
|
|
100
|
+
const sequelize = require('<%- dbPath %>');
|
|
101
|
+
expect(sequelize.close).toHaveBeenCalled();
|
|
102
|
+
<%_ } -%>
|
|
103
|
+
|
|
104
|
+
<%_ if (caching === 'Redis') { -%>
|
|
105
|
+
const redisService = require('<%- redisPath %>');
|
|
106
|
+
expect(redisService.quit).toHaveBeenCalled();
|
|
107
|
+
<%_ } -%>
|
|
108
|
+
|
|
109
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
110
|
+
const { disconnectKafka } = require('<%- kafkaPath %>');
|
|
111
|
+
expect(disconnectKafka).toHaveBeenCalled();
|
|
112
|
+
<%_ } -%>
|
|
113
|
+
|
|
114
|
+
expect(mockExit).toHaveBeenCalledWith(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should exit 0 on SIGINT', async () => {
|
|
118
|
+
setupGracefulShutdown(mockServer);
|
|
119
|
+
processListeners['SIGINT']();
|
|
120
|
+
await flushPromises();
|
|
121
|
+
await flushPromises();
|
|
122
|
+
expect(mockExit).toHaveBeenCalledWith(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle errors during shutdown and exit 1', async () => {
|
|
126
|
+
// Force server.close to fail to test the error path
|
|
127
|
+
mockServer.close.mockImplementationOnce((cb) => {
|
|
128
|
+
if (cb) Promise.resolve().then(() => cb(new Error('Server Close Error')));
|
|
129
|
+
return mockServer;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
setupGracefulShutdown(mockServer);
|
|
133
|
+
processListeners['SIGTERM']();
|
|
134
|
+
|
|
135
|
+
await flushPromises();
|
|
136
|
+
await flushPromises();
|
|
137
|
+
|
|
138
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should forcefully shutdown if cleanup takes too long', async () => {
|
|
142
|
+
setupGracefulShutdown(mockServer);
|
|
143
|
+
processListeners['SIGTERM']();
|
|
144
|
+
|
|
145
|
+
jest.advanceTimersByTime(15000);
|
|
146
|
+
|
|
147
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
148
|
+
});
|
|
149
|
+
});
|