nodejs-quickstart-structure 1.12.0 → 1.14.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 +26 -3
- package/README.md +5 -3
- package/lib/generator.js +17 -3
- package/lib/modules/app-setup.js +167 -47
- package/lib/modules/caching-setup.js +13 -0
- package/lib/modules/config-files.js +25 -62
- package/lib/modules/database-setup.js +35 -30
- package/lib/modules/kafka-setup.js +79 -13
- package/package.json +1 -2
- package/templates/clean-architecture/js/src/errors/BadRequestError.js +1 -1
- package/templates/clean-architecture/js/src/errors/BadRequestError.spec.js.ejs +21 -0
- package/templates/clean-architecture/js/src/errors/NotFoundError.js +1 -1
- package/templates/clean-architecture/js/src/errors/NotFoundError.spec.js.ejs +21 -0
- package/templates/clean-architecture/js/src/infrastructure/log/logger.spec.js.ejs +63 -0
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +2 -3
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +81 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +20 -9
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +8 -4
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +102 -0
- package/templates/clean-architecture/js/src/interfaces/graphql/context.spec.js.ejs +31 -0
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +49 -0
- package/templates/clean-architecture/js/src/interfaces/routes/api.spec.js.ejs +38 -0
- package/templates/clean-architecture/js/src/usecases/CreateUser.spec.js.ejs +51 -0
- package/templates/clean-architecture/js/src/usecases/GetAllUsers.spec.js.ejs +61 -0
- package/templates/clean-architecture/ts/src/errors/BadRequestError.spec.ts.ejs +21 -0
- package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +1 -1
- package/templates/clean-architecture/ts/src/errors/NotFoundError.spec.ts.ejs +21 -0
- package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +1 -1
- package/templates/clean-architecture/ts/src/index.ts.ejs +15 -11
- package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +64 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +85 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.ts.ejs +2 -3
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +166 -0
- package/templates/clean-architecture/ts/src/interfaces/graphql/context.spec.ts.ejs +32 -0
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +51 -0
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.spec.ts.ejs +40 -0
- package/templates/clean-architecture/ts/src/usecases/createUser.spec.ts.ejs +51 -0
- package/templates/clean-architecture/ts/src/usecases/getAllUsers.spec.ts.ejs +63 -0
- package/templates/clean-architecture/ts/src/utils/{error.middleware.ts.ejs → errorMiddleware.ts.ejs} +1 -2
- package/templates/common/caching/js/memoryCache.spec.js.ejs +101 -0
- package/templates/common/caching/js/redisClient.js.ejs +4 -0
- package/templates/common/caching/js/redisClient.spec.js.ejs +149 -0
- package/templates/common/caching/ts/memoryCache.spec.ts.ejs +102 -0
- package/templates/common/caching/ts/redisClient.spec.ts.ejs +157 -0
- package/templates/common/caching/ts/redisClient.ts.ejs +4 -0
- package/templates/common/database/js/database.spec.js.ejs +56 -0
- package/templates/common/database/js/models/User.js.ejs +22 -0
- package/templates/common/database/js/models/User.spec.js.ejs +84 -0
- package/templates/common/database/js/mongoose.spec.js.ejs +43 -0
- package/templates/common/database/ts/database.spec.ts.ejs +56 -0
- package/templates/common/database/ts/models/User.spec.ts.ejs +84 -0
- package/templates/common/database/ts/models/User.ts.ejs +26 -0
- package/templates/common/database/ts/mongoose.spec.ts.ejs +42 -0
- package/templates/common/eslint.config.mjs.ejs +11 -2
- package/templates/common/health/js/healthRoute.js.ejs +44 -0
- package/templates/common/health/js/healthRoute.spec.js.ejs +70 -0
- package/templates/common/health/ts/healthRoute.spec.ts.ejs +76 -0
- package/templates/common/health/ts/healthRoute.ts.ejs +43 -0
- package/templates/common/jest.config.js.ejs +19 -5
- package/templates/common/kafka/js/config/kafka.spec.js.ejs +21 -0
- package/templates/common/kafka/js/services/kafkaService.js.ejs +13 -4
- package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +60 -0
- package/templates/common/kafka/ts/config/kafka.spec.ts.ejs +21 -0
- package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +61 -0
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +6 -1
- package/templates/common/package.json.ejs +0 -3
- package/templates/common/shutdown/js/gracefulShutdown.js.ejs +61 -0
- package/templates/common/shutdown/js/gracefulShutdown.spec.js.ejs +160 -0
- package/templates/common/shutdown/ts/gracefulShutdown.spec.ts.ejs +158 -0
- package/templates/common/shutdown/ts/gracefulShutdown.ts.ejs +58 -0
- package/templates/common/src/utils/errorMiddleware.spec.js.ejs +79 -0
- package/templates/common/src/utils/errorMiddleware.spec.ts.ejs +94 -0
- package/templates/common/tsconfig.json +1 -1
- package/templates/mvc/js/src/controllers/userController.js.ejs +4 -31
- package/templates/mvc/js/src/controllers/userController.spec.js.ejs +170 -0
- package/templates/mvc/js/src/errors/BadRequestError.js +1 -1
- package/templates/mvc/js/src/errors/BadRequestError.spec.js.ejs +21 -0
- package/templates/mvc/js/src/errors/NotFoundError.js +1 -1
- package/templates/mvc/js/src/errors/NotFoundError.spec.js.ejs +21 -0
- package/templates/mvc/js/src/graphql/context.spec.js.ejs +29 -0
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +47 -0
- package/templates/mvc/js/src/index.js.ejs +11 -9
- package/templates/mvc/js/src/routes/api.spec.js.ejs +36 -0
- package/templates/mvc/js/src/utils/logger.spec.js.ejs +63 -0
- package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +185 -0
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +4 -31
- package/templates/mvc/ts/src/errors/BadRequestError.spec.ts.ejs +21 -0
- package/templates/mvc/ts/src/errors/BadRequestError.ts +1 -1
- package/templates/mvc/ts/src/errors/NotFoundError.spec.ts.ejs +21 -0
- package/templates/mvc/ts/src/errors/NotFoundError.ts +1 -1
- package/templates/mvc/ts/src/graphql/context.spec.ts.ejs +30 -0
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +51 -0
- package/templates/mvc/ts/src/index.ts.ejs +13 -9
- package/templates/mvc/ts/src/routes/api.spec.ts.ejs +40 -0
- package/templates/mvc/ts/src/utils/{error.middleware.ts.ejs → errorMiddleware.ts.ejs} +1 -2
- package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +64 -0
- package/docs/demo.gif +0 -0
- package/docs/generateCase.md +0 -265
- package/docs/generatorFlow.md +0 -233
- package/docs/releaseNoteRule.md +0 -42
- package/docs/ruleDevelop.md +0 -30
- package/templates/common/tests/health.test.ts.ejs +0 -14
- /package/templates/clean-architecture/js/src/infrastructure/webserver/{middlewares/error.middleware.js → middleware/errorMiddleware.js} +0 -0
- /package/templates/mvc/js/src/utils/{error.middleware.js → errorMiddleware.js} +0 -0
|
@@ -0,0 +1,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
|
+
<%_ 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
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { setupGracefulShutdown } from '@/utils/gracefulShutdown';
|
|
2
|
+
import { Server } from 'http';
|
|
3
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
4
|
+
import mongoose from 'mongoose';
|
|
5
|
+
<%_ } else if (database !== 'None') { -%>
|
|
6
|
+
import sequelize from '<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>';
|
|
7
|
+
<%_ } -%>
|
|
8
|
+
<%_ if (caching === 'Redis') { -%>
|
|
9
|
+
import redisService from '<% if (architecture === "MVC") { %>@/config/redisClient<% } else { %>@/infrastructure/caching/redisClient<% } %>';
|
|
10
|
+
<%_ } -%>
|
|
11
|
+
|
|
12
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
13
|
+
jest.mock('mongoose', () => {
|
|
14
|
+
return {
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: {
|
|
17
|
+
connection: {
|
|
18
|
+
close: jest.fn().mockResolvedValue(true)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
<%_ } else if (database !== 'None') { -%>
|
|
24
|
+
jest.mock('<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>', () => {
|
|
25
|
+
return {
|
|
26
|
+
__esModule: true,
|
|
27
|
+
default: {
|
|
28
|
+
close: jest.fn().mockResolvedValue(true)
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
<%_ } -%>
|
|
33
|
+
|
|
34
|
+
<%_ if (caching === 'Redis') { -%>
|
|
35
|
+
jest.mock('<% if (architecture === "MVC") { %>@/config/redisClient<% } else { %>@/infrastructure/caching/redisClient<% } %>', () => {
|
|
36
|
+
return {
|
|
37
|
+
__esModule: true,
|
|
38
|
+
default: {
|
|
39
|
+
quit: jest.fn().mockResolvedValue(true)
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
<%_ } -%>
|
|
44
|
+
|
|
45
|
+
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
|
|
46
|
+
|
|
47
|
+
describe('Graceful Shutdown', () => {
|
|
48
|
+
let mockServer: Partial<Server>;
|
|
49
|
+
let mockExit: jest.SpyInstance;
|
|
50
|
+
let processListeners: Record<string, (...args: any[]) => void>;
|
|
51
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
52
|
+
let mockKafkaService: { disconnect: jest.Mock };
|
|
53
|
+
<%_ } -%>
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
jest.useFakeTimers({ legacyFakeTimers: true });
|
|
57
|
+
jest.clearAllMocks();
|
|
58
|
+
processListeners = {};
|
|
59
|
+
|
|
60
|
+
mockServer = {
|
|
61
|
+
close: jest.fn().mockImplementation((cb?: (err?: Error) => void) => {
|
|
62
|
+
if (cb) Promise.resolve().then(() => cb());
|
|
63
|
+
return mockServer;
|
|
64
|
+
})
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {}) as any);
|
|
68
|
+
jest.spyOn(process, 'on').mockImplementation(((event: string, handler: (...args: any[]) => void) => {
|
|
69
|
+
processListeners[event] = handler;
|
|
70
|
+
return process;
|
|
71
|
+
}) as any);
|
|
72
|
+
|
|
73
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
74
|
+
mockKafkaService = { disconnect: jest.fn().mockResolvedValue(true) };
|
|
75
|
+
<%_ } -%>
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
jest.restoreAllMocks();
|
|
80
|
+
jest.useRealTimers();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should register SIGTERM and SIGINT events', () => {
|
|
84
|
+
setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
|
|
85
|
+
expect(processListeners['SIGTERM']).toBeDefined();
|
|
86
|
+
expect(processListeners['SIGINT']).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should cleanly shutdown all connections and exit 0 on SIGTERM', async () => {
|
|
90
|
+
setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
|
|
91
|
+
|
|
92
|
+
processListeners['SIGTERM']();
|
|
93
|
+
|
|
94
|
+
// Flush microtask queue multiple times for nested async operations
|
|
95
|
+
await flushPromises();
|
|
96
|
+
await flushPromises();
|
|
97
|
+
await flushPromises();
|
|
98
|
+
|
|
99
|
+
expect(mockServer.close).toHaveBeenCalled();
|
|
100
|
+
|
|
101
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
102
|
+
expect(mongoose.connection.close).toHaveBeenCalledWith(false);
|
|
103
|
+
<%_ } else if (database !== 'None') { -%>
|
|
104
|
+
expect(sequelize.close).toHaveBeenCalled();
|
|
105
|
+
<%_ } -%>
|
|
106
|
+
|
|
107
|
+
<%_ if (caching === 'Redis') { -%>
|
|
108
|
+
expect(redisService.quit).toHaveBeenCalled();
|
|
109
|
+
<%_ } -%>
|
|
110
|
+
|
|
111
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
112
|
+
expect(mockKafkaService.disconnect).toHaveBeenCalled();
|
|
113
|
+
<%_ } -%>
|
|
114
|
+
|
|
115
|
+
expect(mockExit).toHaveBeenCalledWith(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should exit 0 on SIGINT', async () => {
|
|
119
|
+
setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
|
|
120
|
+
processListeners['SIGINT']();
|
|
121
|
+
await flushPromises();
|
|
122
|
+
await flushPromises();
|
|
123
|
+
expect(mockExit).toHaveBeenCalledWith(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
<%_ if (communication === 'Kafka' || database !== 'None' || caching === 'Redis') { _%>
|
|
127
|
+
it('should handle errors during shutdown and exit 1', async () => {
|
|
128
|
+
<%_ if (communication === 'Kafka') { _%>
|
|
129
|
+
mockKafkaService.disconnect.mockRejectedValueOnce(new Error('Shutdown Error'));
|
|
130
|
+
<%_ } else if (database === 'MongoDB') { _%>
|
|
131
|
+
(mongoose.connection.close as jest.Mock).mockRejectedValueOnce(new Error('Shutdown Error'));
|
|
132
|
+
<%_ } else if (database !== 'None') { _%>
|
|
133
|
+
(sequelize.close as jest.Mock).mockRejectedValueOnce(new Error('Shutdown Error'));
|
|
134
|
+
<%_ } else if (caching === 'Redis') { _%>
|
|
135
|
+
(redisService.quit as jest.Mock).mockRejectedValueOnce(new Error('Shutdown Error'));
|
|
136
|
+
<%_ } _%>
|
|
137
|
+
|
|
138
|
+
setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
|
|
139
|
+
processListeners['SIGTERM']();
|
|
140
|
+
|
|
141
|
+
await flushPromises();
|
|
142
|
+
await flushPromises();
|
|
143
|
+
await flushPromises();
|
|
144
|
+
|
|
145
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should forcefully shutdown if cleanup takes too long', async () => {
|
|
149
|
+
setupGracefulShutdown(mockServer as Server<% if (communication === 'Kafka') { %>, mockKafkaService<% } %>);
|
|
150
|
+
processListeners['SIGTERM']();
|
|
151
|
+
|
|
152
|
+
jest.advanceTimersByTime(15000);
|
|
153
|
+
|
|
154
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
155
|
+
});
|
|
156
|
+
<%_ } _%>
|
|
157
|
+
|
|
158
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Server } from 'http';
|
|
2
|
+
<%_ if (architecture === 'MVC') { -%>
|
|
3
|
+
import logger from '@/utils/logger';
|
|
4
|
+
<%_ } else { -%>
|
|
5
|
+
import logger from '@/infrastructure/log/logger';
|
|
6
|
+
<%_ } -%>
|
|
7
|
+
|
|
8
|
+
export const setupGracefulShutdown = (server: Server<% if (communication === 'Kafka') { %>, kafkaService: { disconnect: () => Promise<void> }<% } %>) => {
|
|
9
|
+
const gracefulShutdown = async (signal: string) => {
|
|
10
|
+
logger.info(`Received ${signal}. Shutting down gracefully...`);
|
|
11
|
+
server.close(async () => {
|
|
12
|
+
logger.info('HTTP server closed.');
|
|
13
|
+
try {
|
|
14
|
+
<%_ if (database !== 'None') { -%>
|
|
15
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
16
|
+
const mongoose = (await import('mongoose')).default;
|
|
17
|
+
await mongoose.connection.close(false);
|
|
18
|
+
logger.info('MongoDB connection closed.');
|
|
19
|
+
<%_ } else { -%>
|
|
20
|
+
<%_ if (architecture === 'MVC') { -%>
|
|
21
|
+
const sequelize = (await import('@/config/database')).default;
|
|
22
|
+
<%_ } else { -%>
|
|
23
|
+
const sequelize = (await import('@/infrastructure/database/database')).default;
|
|
24
|
+
<%_ } -%>
|
|
25
|
+
await sequelize.close();
|
|
26
|
+
logger.info('Database connection closed.');
|
|
27
|
+
<%_ } -%>
|
|
28
|
+
<%_ } -%>
|
|
29
|
+
<%_ if (caching === 'Redis') { -%>
|
|
30
|
+
<%_ if (architecture === 'MVC') { -%>
|
|
31
|
+
const redisService = (await import('@/config/redisClient')).default;
|
|
32
|
+
<%_ } else { -%>
|
|
33
|
+
const redisService = (await import('@/infrastructure/caching/redisClient')).default;
|
|
34
|
+
<%_ } -%>
|
|
35
|
+
await redisService.quit();
|
|
36
|
+
logger.info('Redis connection closed.');
|
|
37
|
+
<%_ } -%>
|
|
38
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
39
|
+
await kafkaService.disconnect();
|
|
40
|
+
logger.info('Kafka connection closed.');
|
|
41
|
+
<%_ } -%>
|
|
42
|
+
logger.info('Graceful shutdown fully completed.');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
logger.error('Error during shutdown:', err);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
logger.error('Could not close connections in time, forcefully shutting down');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}, 15000);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
57
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
58
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { errorMiddleware } = require('<% if (architecture === "MVC") { %>@/utils/errorMiddleware<% } else { %>@/infrastructure/webserver/middleware/errorMiddleware<% } %>');
|
|
2
|
+
const { ApiError } = require('@/errors/ApiError');
|
|
3
|
+
const HTTP_STATUS = require('@/utils/httpCodes');
|
|
4
|
+
const logger = require('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>');
|
|
5
|
+
|
|
6
|
+
jest.mock('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>');
|
|
7
|
+
|
|
8
|
+
describe('Error Middleware', () => {
|
|
9
|
+
let mockRequest;
|
|
10
|
+
let mockResponse;
|
|
11
|
+
let nextFunction;
|
|
12
|
+
const originalEnv = process.env.NODE_ENV;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockRequest = {
|
|
16
|
+
originalUrl: '/test',
|
|
17
|
+
method: 'GET',
|
|
18
|
+
ip: '127.0.0.1'
|
|
19
|
+
};
|
|
20
|
+
mockResponse = {
|
|
21
|
+
status: jest.fn().mockReturnThis(),
|
|
22
|
+
json: jest.fn()
|
|
23
|
+
};
|
|
24
|
+
nextFunction = jest.fn();
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
process.env.NODE_ENV = originalEnv;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should handle standard Error by wrapping it in a 500 ApiError', () => {
|
|
33
|
+
const error = new Error('Standard Error');
|
|
34
|
+
errorMiddleware(error, mockRequest, mockResponse, nextFunction);
|
|
35
|
+
|
|
36
|
+
expect(logger.error).toHaveBeenCalled();
|
|
37
|
+
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should handle custom ApiError directly', () => {
|
|
41
|
+
const customError = new ApiError(HTTP_STATUS.BAD_REQUEST, 'Bad Request Data', true);
|
|
42
|
+
errorMiddleware(customError, mockRequest, mockResponse, nextFunction);
|
|
43
|
+
|
|
44
|
+
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
|
|
45
|
+
expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
46
|
+
statusCode: HTTP_STATUS.BAD_REQUEST,
|
|
47
|
+
message: 'Bad Request Data'
|
|
48
|
+
}));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should include stack trace in development environment', () => {
|
|
52
|
+
process.env.NODE_ENV = 'development';
|
|
53
|
+
const error = new Error('Test Error');
|
|
54
|
+
errorMiddleware(error, mockRequest, mockResponse, nextFunction);
|
|
55
|
+
|
|
56
|
+
expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
57
|
+
stack: expect.any(String)
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should omit stack trace in production environment', () => {
|
|
62
|
+
process.env.NODE_ENV = 'production';
|
|
63
|
+
const error = new Error('Test Error');
|
|
64
|
+
errorMiddleware(error, mockRequest, mockResponse, nextFunction);
|
|
65
|
+
|
|
66
|
+
const jsonArg = mockResponse.json.mock.calls[0][0];
|
|
67
|
+
expect(jsonArg.stack).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle error without stack trace', () => {
|
|
71
|
+
const { ApiError } = require('@/errors/ApiError');
|
|
72
|
+
const customError = new ApiError(500, 'No Stack', false);
|
|
73
|
+
delete customError.stack;
|
|
74
|
+
errorMiddleware(customError, mockRequest, mockResponse, nextFunction);
|
|
75
|
+
|
|
76
|
+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('No Stack'));
|
|
77
|
+
expect(logger.error).toHaveBeenCalledWith('No stack trace');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { errorMiddleware } from '<% if (architecture === "MVC" || language === "TypeScript") { %>@/utils/errorMiddleware<% } else { %>@/infrastructure/webserver/middleware/errorMiddleware<% } %>';
|
|
2
|
+
import { Request, Response } from 'express';
|
|
3
|
+
import { ApiError } from '@/errors/ApiError';
|
|
4
|
+
import { HTTP_STATUS } from '@/utils/httpCodes';
|
|
5
|
+
import logger from '<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>';
|
|
6
|
+
|
|
7
|
+
jest.mock('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>');
|
|
8
|
+
|
|
9
|
+
describe('Error Middleware', () => {
|
|
10
|
+
let mockRequest: Partial<Request>;
|
|
11
|
+
let mockResponse: Partial<Response>;
|
|
12
|
+
let nextFunction: jest.Mock;
|
|
13
|
+
const originalEnv = process.env.NODE_ENV;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockRequest = {
|
|
17
|
+
originalUrl: '/test',
|
|
18
|
+
method: 'GET',
|
|
19
|
+
ip: '127.0.0.1'
|
|
20
|
+
};
|
|
21
|
+
mockResponse = {
|
|
22
|
+
status: jest.fn().mockReturnThis(),
|
|
23
|
+
json: jest.fn()
|
|
24
|
+
};
|
|
25
|
+
nextFunction = jest.fn();
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
process.env.NODE_ENV = originalEnv;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle standard Error by wrapping it in a 500 ApiError', () => {
|
|
34
|
+
const error = new Error('Standard Error');
|
|
35
|
+
errorMiddleware(error, mockRequest as Request, mockResponse as Response, nextFunction);
|
|
36
|
+
|
|
37
|
+
expect(logger.error).toHaveBeenCalled();
|
|
38
|
+
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
39
|
+
expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
40
|
+
statusCode: HTTP_STATUS.INTERNAL_SERVER_ERROR,
|
|
41
|
+
message: 'Standard Error'
|
|
42
|
+
}));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should default to Internal Server Error message if none provided on Error', () => {
|
|
46
|
+
const error = new Error();
|
|
47
|
+
errorMiddleware(error, mockRequest as Request, mockResponse as Response, nextFunction);
|
|
48
|
+
|
|
49
|
+
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
50
|
+
expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
51
|
+
message: 'Internal Server Error'
|
|
52
|
+
}));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should handle custom ApiError directly', () => {
|
|
56
|
+
const customError = new ApiError(HTTP_STATUS.BAD_REQUEST, 'Bad Request Data', true);
|
|
57
|
+
errorMiddleware(customError, mockRequest as Request, mockResponse as Response, nextFunction);
|
|
58
|
+
|
|
59
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
60
|
+
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.BAD_REQUEST);
|
|
61
|
+
expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
62
|
+
statusCode: HTTP_STATUS.BAD_REQUEST,
|
|
63
|
+
message: 'Bad Request Data'
|
|
64
|
+
}));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should include stack trace in development environment', () => {
|
|
68
|
+
process.env.NODE_ENV = 'development';
|
|
69
|
+
const error = new Error('Test Error');
|
|
70
|
+
errorMiddleware(error, mockRequest as Request, mockResponse as Response, nextFunction);
|
|
71
|
+
|
|
72
|
+
expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
73
|
+
stack: expect.any(String)
|
|
74
|
+
}));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should omit stack trace in production environment', () => {
|
|
78
|
+
process.env.NODE_ENV = 'production';
|
|
79
|
+
const error = new Error('Test Error');
|
|
80
|
+
errorMiddleware(error, mockRequest as Request, mockResponse as Response, nextFunction);
|
|
81
|
+
|
|
82
|
+
const jsonArg = (mockResponse.json as jest.Mock).mock.calls[0][0];
|
|
83
|
+
expect(jsonArg.stack).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle error without stack trace', () => {
|
|
87
|
+
const customError = new ApiError(HTTP_STATUS.INTERNAL_SERVER_ERROR, 'No Stack', false);
|
|
88
|
+
delete customError.stack;
|
|
89
|
+
errorMiddleware(customError, mockRequest as Request, mockResponse as Response, nextFunction);
|
|
90
|
+
|
|
91
|
+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('No Stack'));
|
|
92
|
+
expect(logger.error).toHaveBeenCalledWith('No stack trace');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -14,19 +14,15 @@ const getUsers = async () => {
|
|
|
14
14
|
try {
|
|
15
15
|
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
16
16
|
const users = await cacheService.getOrSet('users:all', async () => {
|
|
17
|
-
<%_ if (database === 'MongoDB') { -%>
|
|
17
|
+
<%_ if (database === 'MongoDB' || database === 'None') { -%>
|
|
18
18
|
return await User.find();
|
|
19
|
-
<%_ } else if (database === 'None') { -%>
|
|
20
|
-
return User.mockData;
|
|
21
19
|
<%_ } else { -%>
|
|
22
20
|
return await User.findAll();
|
|
23
21
|
<%_ } -%>
|
|
24
22
|
}, 60);
|
|
25
23
|
<%_ } else { -%>
|
|
26
|
-
<%_ if (database === 'MongoDB') { -%>
|
|
24
|
+
<%_ if (database === 'MongoDB' || database === 'None') { -%>
|
|
27
25
|
const users = await User.find();
|
|
28
|
-
<%_ } else if (database === 'None') { -%>
|
|
29
|
-
const users = User.mockData;
|
|
30
26
|
<%_ } else { -%>
|
|
31
27
|
const users = await User.findAll();
|
|
32
28
|
<%_ } -%>
|
|
@@ -41,20 +37,11 @@ const getUsers = async () => {
|
|
|
41
37
|
const createUser = async (data) => {
|
|
42
38
|
try {
|
|
43
39
|
const { name, email } = data;
|
|
44
|
-
<%_ if (database === 'None') { -%>
|
|
45
|
-
const newUser = { id: String(User.mockData.length + 1), name, email };
|
|
46
|
-
User.mockData.push(newUser);
|
|
47
|
-
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
48
|
-
await cacheService.del('users:all');
|
|
49
|
-
<%_ } -%>
|
|
50
|
-
return newUser;
|
|
51
|
-
<%_ } else { -%>
|
|
52
40
|
const user = await User.create({ name, email });
|
|
53
41
|
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
54
42
|
await cacheService.del('users:all');
|
|
55
43
|
<%_ } -%>
|
|
56
44
|
return user;
|
|
57
|
-
<%_ } -%>
|
|
58
45
|
} catch (error) {
|
|
59
46
|
logger.error('Error creating user:', error);
|
|
60
47
|
throw error;
|
|
@@ -65,19 +52,15 @@ const getUsers = async (req, res, next) => {
|
|
|
65
52
|
try {
|
|
66
53
|
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
67
54
|
const users = await cacheService.getOrSet('users:all', async () => {
|
|
68
|
-
<%_ if (database === 'MongoDB') { -%>
|
|
55
|
+
<%_ if (database === 'MongoDB' || database === 'None') { -%>
|
|
69
56
|
return await User.find();
|
|
70
|
-
<%_ } else if (database === 'None') { -%>
|
|
71
|
-
return User.mockData;
|
|
72
57
|
<%_ } else { -%>
|
|
73
58
|
return await User.findAll();
|
|
74
59
|
<%_ } -%>
|
|
75
60
|
}, 60);
|
|
76
61
|
<%_ } else { -%>
|
|
77
|
-
<%_ if (database === 'MongoDB') { -%>
|
|
62
|
+
<%_ if (database === 'MongoDB' || database === 'None') { -%>
|
|
78
63
|
const users = await User.find();
|
|
79
|
-
<%_ } else if (database === 'None') { -%>
|
|
80
|
-
const users = User.mockData;
|
|
81
64
|
<%_ } else { -%>
|
|
82
65
|
const users = await User.findAll();
|
|
83
66
|
<%_ } -%>
|
|
@@ -92,20 +75,11 @@ const getUsers = async (req, res, next) => {
|
|
|
92
75
|
const createUser = async (req, res, next) => {
|
|
93
76
|
try {
|
|
94
77
|
const { name, email } = req.body;
|
|
95
|
-
<%_ if (database === 'None') { -%>
|
|
96
|
-
const newUser = { id: String(User.mockData.length + 1), name, email };
|
|
97
|
-
User.mockData.push(newUser);
|
|
98
|
-
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
99
|
-
await cacheService.del('users:all');
|
|
100
|
-
<%_ } -%>
|
|
101
|
-
res.status(HTTP_STATUS.CREATED).json(newUser);
|
|
102
|
-
<%_ } else { -%>
|
|
103
78
|
const user = await User.create({ name, email });
|
|
104
79
|
<%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
|
|
105
80
|
await cacheService.del('users:all');
|
|
106
81
|
<%_ } -%>
|
|
107
82
|
res.status(HTTP_STATUS.CREATED).json(user);
|
|
108
|
-
<%_ } -%>
|
|
109
83
|
} catch (error) {
|
|
110
84
|
logger.error('Error creating user:', error);
|
|
111
85
|
next(error);
|
|
@@ -114,4 +88,3 @@ const createUser = async (req, res, next) => {
|
|
|
114
88
|
<% } -%>
|
|
115
89
|
|
|
116
90
|
module.exports = { getUsers, createUser };
|
|
117
|
-
|