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.
Files changed (104) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +2 -1
  3. package/lib/modules/caching-setup.js +76 -73
  4. package/lib/modules/kafka-setup.js +249 -191
  5. package/lib/modules/project-setup.js +1 -0
  6. package/package.json +13 -2
  7. package/templates/clean-architecture/js/src/errors/BadRequestError.js +11 -10
  8. package/templates/clean-architecture/js/src/errors/BadRequestError.spec.js.ejs +22 -21
  9. package/templates/clean-architecture/js/src/errors/NotFoundError.js +11 -10
  10. package/templates/clean-architecture/js/src/errors/NotFoundError.spec.js.ejs +22 -21
  11. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +69 -39
  12. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +142 -81
  13. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +156 -75
  14. package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +234 -138
  15. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +27 -21
  16. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +66 -49
  17. package/templates/clean-architecture/js/src/interfaces/graphql/typeDefs/user.types.js.ejs +19 -17
  18. package/templates/clean-architecture/js/src/interfaces/routes/api.js +12 -10
  19. package/templates/clean-architecture/js/src/usecases/DeleteUser.js +11 -0
  20. package/templates/clean-architecture/js/src/usecases/DeleteUser.spec.js.ejs +47 -0
  21. package/templates/clean-architecture/js/src/usecases/UpdateUser.js +11 -0
  22. package/templates/clean-architecture/js/src/usecases/UpdateUser.spec.js.ejs +48 -0
  23. package/templates/clean-architecture/js/src/utils/errorMessages.js +14 -0
  24. package/templates/clean-architecture/ts/src/errors/BadRequestError.spec.ts.ejs +22 -21
  25. package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +9 -8
  26. package/templates/clean-architecture/ts/src/errors/NotFoundError.spec.ts.ejs +22 -21
  27. package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +9 -8
  28. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +175 -85
  29. package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +74 -0
  30. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +331 -185
  31. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +173 -84
  32. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +68 -51
  33. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +29 -21
  34. package/templates/clean-architecture/ts/src/interfaces/graphql/typeDefs/user.types.ts.ejs +17 -15
  35. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +13 -11
  36. package/templates/clean-architecture/ts/src/usecases/deleteUser.spec.ts.ejs +47 -0
  37. package/templates/clean-architecture/ts/src/usecases/deleteUser.ts +9 -0
  38. package/templates/clean-architecture/ts/src/usecases/updateUser.spec.ts.ejs +48 -0
  39. package/templates/clean-architecture/ts/src/usecases/updateUser.ts +9 -0
  40. package/templates/clean-architecture/ts/src/utils/errorMessages.ts +12 -0
  41. package/templates/common/.gitattributes +46 -0
  42. package/templates/common/README.md.ejs +294 -270
  43. package/templates/common/caching/clean/js/DeleteUser.js.ejs +27 -0
  44. package/templates/common/caching/clean/js/UpdateUser.js.ejs +27 -0
  45. package/templates/common/caching/clean/ts/deleteUser.ts.ejs +24 -0
  46. package/templates/common/caching/clean/ts/updateUser.ts.ejs +25 -0
  47. package/templates/common/caching/ts/memoryCache.ts.ejs +73 -64
  48. package/templates/common/caching/ts/redisClient.ts.ejs +89 -80
  49. package/templates/common/database/js/models/User.js.ejs +79 -53
  50. package/templates/common/database/js/models/User.js.mongoose.ejs +23 -19
  51. package/templates/common/database/js/models/User.spec.js.ejs +94 -84
  52. package/templates/common/database/ts/models/User.spec.ts.ejs +100 -84
  53. package/templates/common/database/ts/models/User.ts.ejs +87 -61
  54. package/templates/common/database/ts/models/User.ts.mongoose.ejs +30 -25
  55. package/templates/common/health/js/healthRoute.js.ejs +50 -47
  56. package/templates/common/health/ts/healthRoute.ts.ejs +49 -46
  57. package/templates/common/jest.e2e.config.js.ejs +8 -8
  58. package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -30
  59. package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +12 -11
  60. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +44 -31
  61. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +86 -49
  62. package/templates/common/kafka/js/services/kafkaService.js.ejs +93 -93
  63. package/templates/common/kafka/js/utils/kafkaEvents.js.ejs +7 -0
  64. package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -51
  65. package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +12 -11
  66. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +86 -49
  67. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +38 -25
  68. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +95 -95
  69. package/templates/common/kafka/ts/utils/kafkaEvents.ts.ejs +5 -0
  70. package/templates/common/shutdown/js/gracefulShutdown.js.ejs +65 -61
  71. package/templates/common/shutdown/js/gracefulShutdown.spec.js.ejs +149 -160
  72. package/templates/common/shutdown/ts/gracefulShutdown.spec.ts.ejs +179 -158
  73. package/templates/common/shutdown/ts/gracefulShutdown.ts.ejs +59 -55
  74. package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +120 -49
  75. package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +120 -49
  76. package/templates/common/swagger.yml.ejs +118 -66
  77. package/templates/db/mysql/V1__Initial_Setup.sql.ejs +10 -9
  78. package/templates/db/postgres/V1__Initial_Setup.sql.ejs +10 -9
  79. package/templates/mvc/js/src/controllers/userController.js.ejs +246 -105
  80. package/templates/mvc/js/src/controllers/userController.spec.js.ejs +481 -209
  81. package/templates/mvc/js/src/errors/BadRequestError.js +11 -10
  82. package/templates/mvc/js/src/errors/BadRequestError.spec.js.ejs +22 -21
  83. package/templates/mvc/js/src/errors/NotFoundError.js +11 -10
  84. package/templates/mvc/js/src/errors/NotFoundError.spec.js.ejs +22 -21
  85. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +25 -19
  86. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +64 -47
  87. package/templates/mvc/js/src/graphql/typeDefs/user.types.js.ejs +19 -17
  88. package/templates/mvc/js/src/routes/api.js +10 -8
  89. package/templates/mvc/js/src/routes/api.spec.js.ejs +41 -36
  90. package/templates/mvc/js/src/utils/errorMessages.js +14 -0
  91. package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +481 -203
  92. package/templates/mvc/ts/src/controllers/userController.ts.ejs +248 -107
  93. package/templates/mvc/ts/src/errors/BadRequestError.spec.ts.ejs +22 -21
  94. package/templates/mvc/ts/src/errors/BadRequestError.ts +9 -8
  95. package/templates/mvc/ts/src/errors/NotFoundError.spec.ts.ejs +27 -21
  96. package/templates/mvc/ts/src/errors/NotFoundError.ts +9 -8
  97. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +68 -51
  98. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +29 -21
  99. package/templates/mvc/ts/src/graphql/typeDefs/user.types.ts.ejs +17 -15
  100. package/templates/mvc/ts/src/index.ts.ejs +156 -153
  101. package/templates/mvc/ts/src/routes/api.spec.ts.ejs +59 -40
  102. package/templates/mvc/ts/src/routes/api.ts +12 -10
  103. package/templates/mvc/ts/src/utils/errorMessages.ts +12 -0
  104. 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();
@@ -0,0 +1,5 @@
1
+ export enum KAFKA_ACTIONS {
2
+ USER_CREATED = 'USER_CREATED',
3
+ USER_UPDATED = 'USER_UPDATED',
4
+ USER_DELETED = 'USER_DELETED'
5
+ }
@@ -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
- logger.info('HTTP server closed.');
21
- try {
22
- <%_ if (database !== 'None') { -%>
23
- <%_ if (database === 'MongoDB') { -%>
24
- const mongoose = require('mongoose');
25
- await mongoose.connection.close(false);
26
- logger.info('MongoDB connection closed.');
27
- <%_ } else { -%>
28
- const sequelize = require('<%- dbPath %>');
29
- await sequelize.close();
30
- logger.info('Database connection closed.');
31
- <%_ } -%>
32
- <%_ } -%>
33
- <%_ if (caching === 'Redis') { -%>
34
- const redisService = require('<%- redisPath %>');
35
- await redisService.quit();
36
- logger.info('Redis connection closed.');
37
- <%_ } -%>
38
- <%_ if (communication === 'Kafka') { -%>
39
- const { disconnectKafka } = require('<%- kafkaPath %>');
40
- await disconnectKafka();
41
- logger.info('Kafka connection closed.');
42
- <%_ } -%>
43
- logger.info('Graceful shutdown fully completed.');
44
- process.exit(0);
45
- } catch (err) {
46
- logger.error('Error during shutdown:', err);
47
- process.exit(1);
48
- }
49
- });
50
-
51
- setTimeout(() => {
52
- logger.error('Could not close connections in time, forcefully shutting down');
53
- process.exit(1);
54
- }, 15000);
55
- };
56
-
57
- process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
58
- process.on('SIGINT', () => gracefulShutdown('SIGINT'));
59
- };
60
-
61
- module.exports = setupGracefulShutdown;
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
- <%_ if (communication === 'Kafka' || database !== 'None' || caching === 'Redis') { _%>
126
- it('should handle errors during shutdown and exit 1', async () => {
127
- <%_ if (communication === 'Kafka') { _%>
128
- const { disconnectKafka } = require('<%- kafkaPath %>');
129
- disconnectKafka.mockRejectedValueOnce(new Error('Shutdown Error'));
130
- <%_ } else if (database === 'MongoDB') { _%>
131
- const mongoose = require('mongoose');
132
- mongoose.connection.close = jest.fn().mockRejectedValueOnce(new Error('Shutdown Error'));
133
- <%_ } else if (database !== 'None') { _%>
134
- const sequelize = require('<%- dbPath %>');
135
- sequelize.close = jest.fn().mockRejectedValueOnce(new Error('Shutdown Error'));
136
- <%_ } else if (caching === 'Redis') { _%>
137
- const redisService = require('<%- redisPath %>');
138
- redisService.quit = jest.fn().mockRejectedValueOnce(new Error('Shutdown Error'));
139
- <%_ } _%>
140
-
141
- setupGracefulShutdown(mockServer);
142
- processListeners['SIGTERM']();
143
-
144
- await flushPromises();
145
- await flushPromises();
146
- await flushPromises();
147
-
148
- expect(mockExit).toHaveBeenCalledWith(1);
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
+ });