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,42 @@
|
|
|
1
|
+
jest.mock('mongoose', () => ({
|
|
2
|
+
connect: jest.fn().mockResolvedValue(true),
|
|
3
|
+
}));
|
|
4
|
+
|
|
5
|
+
const logger = {
|
|
6
|
+
info: jest.fn(),
|
|
7
|
+
error: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
jest.mock('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>', () => logger);
|
|
11
|
+
|
|
12
|
+
describe('Mongoose Configuration', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
jest.resetModules();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should call mongoose.connect with correct parameters', async () => {
|
|
19
|
+
const connectDB = require('<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>').default;
|
|
20
|
+
const mongoose = require('mongoose');
|
|
21
|
+
await connectDB();
|
|
22
|
+
expect(mongoose.connect).toHaveBeenCalledWith(
|
|
23
|
+
expect.stringContaining('mongodb://')
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should handle connection failure and retry', async () => {
|
|
28
|
+
const connectDB = require('<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>').default;
|
|
29
|
+
const mongoose = require('mongoose');
|
|
30
|
+
(mongoose.connect as jest.Mock)
|
|
31
|
+
.mockRejectedValueOnce(new Error('Connection failed'))
|
|
32
|
+
.mockResolvedValueOnce(true);
|
|
33
|
+
|
|
34
|
+
const timeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(cb => { (cb as any)(); return {} as any; });
|
|
35
|
+
|
|
36
|
+
await connectDB();
|
|
37
|
+
|
|
38
|
+
expect(logger.error).toHaveBeenCalled();
|
|
39
|
+
expect(mongoose.connect).toHaveBeenCalledTimes(2);
|
|
40
|
+
timeoutSpy.mockRestore();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -17,8 +17,17 @@ export default tseslint.config(
|
|
|
17
17
|
rules: {
|
|
18
18
|
"no-console": "warn",
|
|
19
19
|
"no-unused-vars": "off",
|
|
20
|
-
"@typescript-eslint/no-unused-vars": "warn"
|
|
21
|
-
|
|
20
|
+
"@typescript-eslint/no-unused-vars": "warn",
|
|
21
|
+
"@typescript-eslint/no-require-imports": "error",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
files: ["**/*.test.ts", "**/*.spec.ts", "tests/**/*.ts"],
|
|
26
|
+
rules: {
|
|
27
|
+
"@typescript-eslint/no-require-imports": "off",
|
|
28
|
+
"@typescript-eslint/no-unused-vars": "warn",
|
|
29
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
30
|
+
},
|
|
22
31
|
}
|
|
23
32
|
);<% } else { %>
|
|
24
33
|
export default [
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const logger = require('<% if (architecture === "MVC") { %>../utils/logger<% } else { %>../../infrastructure/log/logger<% } %>');
|
|
4
|
+
const HTTP_STATUS = require('<% if (architecture === "MVC") { %>../utils/httpCodes<% } else { %>../../utils/httpCodes<% } %>');
|
|
5
|
+
|
|
6
|
+
router.get('/', async (req, res) => {
|
|
7
|
+
const healthData = {
|
|
8
|
+
status: 'UP',
|
|
9
|
+
uptime: process.uptime(),
|
|
10
|
+
memory: process.memoryUsage(),
|
|
11
|
+
database: 'disconnected',
|
|
12
|
+
timestamp: Date.now()
|
|
13
|
+
};
|
|
14
|
+
logger.info('Health Check');
|
|
15
|
+
|
|
16
|
+
<%_ if (database !== 'None') { -%>
|
|
17
|
+
try {
|
|
18
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
19
|
+
const mongoose = require('mongoose');
|
|
20
|
+
if (mongoose.connection.readyState === 1) {
|
|
21
|
+
if (mongoose.connection.db && mongoose.connection.db.admin) {
|
|
22
|
+
await mongoose.connection.db.admin().ping();
|
|
23
|
+
}
|
|
24
|
+
healthData.database = 'connected';
|
|
25
|
+
}
|
|
26
|
+
<%_ } else { -%>
|
|
27
|
+
const sequelize = require('<% if (architecture === "MVC") { %>../config/database<% } else { %>../../infrastructure/database/database<% } %>');
|
|
28
|
+
await sequelize.authenticate();
|
|
29
|
+
healthData.database = 'connected';
|
|
30
|
+
<%_ } -%>
|
|
31
|
+
} catch (err) {
|
|
32
|
+
healthData.database = 'error';
|
|
33
|
+
healthData.status = 'DOWN';
|
|
34
|
+
logger.error('Health Check Database Ping Failed:', err);
|
|
35
|
+
return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json(healthData);
|
|
36
|
+
}
|
|
37
|
+
<%_ } else { -%>
|
|
38
|
+
healthData.database = 'None';
|
|
39
|
+
<%_ } -%>
|
|
40
|
+
|
|
41
|
+
res.status(HTTP_STATUS.OK).json(healthData);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
module.exports = router;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const request = require('supertest');
|
|
2
|
+
const express = require('express');
|
|
3
|
+
<% if (architecture === 'MVC') { -%>
|
|
4
|
+
const healthRoute = require('@/routes/healthRoute');
|
|
5
|
+
<% } else { -%>
|
|
6
|
+
const healthRoute = require('@/interfaces/routes/healthRoute');
|
|
7
|
+
<% } -%>
|
|
8
|
+
const HTTP_STATUS = require('@/utils/httpCodes');
|
|
9
|
+
|
|
10
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
11
|
+
jest.mock('mongoose', () => {
|
|
12
|
+
return {
|
|
13
|
+
connection: {
|
|
14
|
+
readyState: 1,
|
|
15
|
+
db: {
|
|
16
|
+
admin: jest.fn().mockReturnValue({
|
|
17
|
+
ping: jest.fn().mockResolvedValue(true)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
<%_ } else if (database !== 'None') { -%>
|
|
24
|
+
jest.mock('<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>', () => {
|
|
25
|
+
return {
|
|
26
|
+
authenticate: jest.fn()
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
<%_ } -%>
|
|
30
|
+
|
|
31
|
+
describe('Health Route', () => {
|
|
32
|
+
let app;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
app = express();
|
|
36
|
+
app.use('/health', healthRoute);
|
|
37
|
+
jest.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return 200 OK with UP status', async () => {
|
|
41
|
+
const res = await request(app).get('/health');
|
|
42
|
+
expect(res.status).toBe(HTTP_STATUS.OK);
|
|
43
|
+
expect(res.body.status).toBe('UP');
|
|
44
|
+
expect(res.body.database).toBe('<% if (database === "None") { %>None<% } else { %>connected<% } %>');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
48
|
+
it('should handle database ping failure and return 500', async () => {
|
|
49
|
+
const mongoose = require('mongoose');
|
|
50
|
+
mongoose.connection.db.admin.mockReturnValueOnce({
|
|
51
|
+
ping: jest.fn().mockRejectedValueOnce(new Error('DB Error'))
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const res = await request(app).get('/health');
|
|
55
|
+
expect(res.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
56
|
+
expect(res.body.status).toBe('DOWN');
|
|
57
|
+
expect(res.body.database).toBe('error');
|
|
58
|
+
});
|
|
59
|
+
<%_ } else if (database !== 'None') { -%>
|
|
60
|
+
it('should handle database authentication failure and return 500', async () => {
|
|
61
|
+
const sequelize = require('<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>');
|
|
62
|
+
sequelize.authenticate.mockRejectedValueOnce(new Error('DB Error'));
|
|
63
|
+
|
|
64
|
+
const res = await request(app).get('/health');
|
|
65
|
+
expect(res.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
66
|
+
expect(res.body.status).toBe('DOWN');
|
|
67
|
+
expect(res.body.database).toBe('error');
|
|
68
|
+
});
|
|
69
|
+
<%_ } -%>
|
|
70
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
<% if (architecture === 'MVC') { -%>
|
|
4
|
+
import healthRoute from '@/routes/healthRoute';
|
|
5
|
+
<% } else { -%>
|
|
6
|
+
import healthRoute from '@/interfaces/routes/healthRoute';
|
|
7
|
+
<% } -%>
|
|
8
|
+
import { HTTP_STATUS } from '@/utils/httpCodes';
|
|
9
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
10
|
+
import mongoose from 'mongoose';
|
|
11
|
+
<%_ } else if (database !== 'None') { -%>
|
|
12
|
+
import sequelize from '<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>';
|
|
13
|
+
<%_ } -%>
|
|
14
|
+
|
|
15
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
16
|
+
jest.mock('mongoose', () => {
|
|
17
|
+
return {
|
|
18
|
+
connection: {
|
|
19
|
+
readyState: 1,
|
|
20
|
+
db: {
|
|
21
|
+
admin: jest.fn().mockReturnValue({
|
|
22
|
+
ping: jest.fn().mockResolvedValue(true)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
<%_ } else if (database !== 'None') { -%>
|
|
29
|
+
jest.mock('<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>', () => {
|
|
30
|
+
return {
|
|
31
|
+
__esModule: true,
|
|
32
|
+
default: {
|
|
33
|
+
authenticate: jest.fn()
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
<%_ } -%>
|
|
38
|
+
|
|
39
|
+
describe('Health Route', () => {
|
|
40
|
+
let app: express.Express;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
app = express();
|
|
44
|
+
app.use('/health', healthRoute);
|
|
45
|
+
jest.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return 200 OK with UP status', async () => {
|
|
49
|
+
const res = await request(app).get('/health');
|
|
50
|
+
expect(res.status).toBe(HTTP_STATUS.OK);
|
|
51
|
+
expect(res.body.status).toBe('UP');
|
|
52
|
+
expect(res.body.database).toBe('<% if (database === "None") { %>None<% } else { %>connected<% } %>');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
56
|
+
it('should handle database ping failure and return 500', async () => {
|
|
57
|
+
((mongoose.connection.db as any).admin as jest.Mock).mockReturnValueOnce({
|
|
58
|
+
ping: jest.fn().mockRejectedValueOnce(new Error('DB Error'))
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const res = await request(app).get('/health');
|
|
62
|
+
expect(res.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
63
|
+
expect(res.body.status).toBe('DOWN');
|
|
64
|
+
expect(res.body.database).toBe('error');
|
|
65
|
+
});
|
|
66
|
+
<%_ } else if (database !== 'None') { -%>
|
|
67
|
+
it('should handle database authentication failure and return 500', async () => {
|
|
68
|
+
(sequelize.authenticate as jest.Mock).mockRejectedValueOnce(new Error('DB Error'));
|
|
69
|
+
|
|
70
|
+
const res = await request(app).get('/health');
|
|
71
|
+
expect(res.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
72
|
+
expect(res.body.status).toBe('DOWN');
|
|
73
|
+
expect(res.body.database).toBe('error');
|
|
74
|
+
});
|
|
75
|
+
<%_ } -%>
|
|
76
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import logger from '<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>';
|
|
3
|
+
import { HTTP_STATUS } from '@/utils/httpCodes';
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
router.get('/', async (req: Request, res: Response) => {
|
|
8
|
+
const healthData: Record<string, unknown> = {
|
|
9
|
+
status: 'UP',
|
|
10
|
+
uptime: process.uptime(),
|
|
11
|
+
memory: process.memoryUsage(),
|
|
12
|
+
database: 'disconnected',
|
|
13
|
+
timestamp: Date.now()
|
|
14
|
+
};
|
|
15
|
+
logger.info('Health Check');
|
|
16
|
+
|
|
17
|
+
<%_ if (database !== 'None') { -%>
|
|
18
|
+
try {
|
|
19
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
20
|
+
const mongoose = (await import('mongoose')).default;
|
|
21
|
+
if (mongoose.connection.readyState === 1) {
|
|
22
|
+
await mongoose.connection.db?.admin().ping();
|
|
23
|
+
healthData.database = 'connected';
|
|
24
|
+
}
|
|
25
|
+
<%_ } else { -%>
|
|
26
|
+
const sequelize = (await import('<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>')).default;
|
|
27
|
+
await sequelize.authenticate();
|
|
28
|
+
healthData.database = 'connected';
|
|
29
|
+
<%_ } -%>
|
|
30
|
+
} catch (err) {
|
|
31
|
+
healthData.database = 'error';
|
|
32
|
+
healthData.status = 'DOWN';
|
|
33
|
+
logger.error('Health Check Database Ping Failed:', err);
|
|
34
|
+
return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json(healthData);
|
|
35
|
+
}
|
|
36
|
+
<%_ } else { -%>
|
|
37
|
+
healthData.database = 'None';
|
|
38
|
+
<%_ } -%>
|
|
39
|
+
|
|
40
|
+
res.status(HTTP_STATUS.OK).json(healthData);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export default router;
|
|
@@ -2,13 +2,27 @@ module.exports = {
|
|
|
2
2
|
testEnvironment: 'node',
|
|
3
3
|
coverageDirectory: 'coverage',
|
|
4
4
|
collectCoverageFrom: ['src/**/*.{js,ts}'],
|
|
5
|
-
testMatch: ['**/*.test.ts', '**/*.test.js'],
|
|
6
|
-
<% if (language === 'TypeScript') { %>preset: 'ts-jest'
|
|
5
|
+
testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.spec.ts', '**/*.spec.js'],
|
|
6
|
+
<% if (language === 'TypeScript') { %>preset: 'ts-jest',<% } %>
|
|
7
7
|
moduleNameMapper: {
|
|
8
8
|
'^@/(.*)$': '<rootDir>/src/$1',
|
|
9
|
-
}
|
|
9
|
+
},
|
|
10
10
|
coveragePathIgnorePatterns: [
|
|
11
11
|
"/node_modules/",
|
|
12
|
-
"/dist/"
|
|
13
|
-
|
|
12
|
+
"/dist/",
|
|
13
|
+
"src/index",
|
|
14
|
+
"src/app",
|
|
15
|
+
"src/config/env",
|
|
16
|
+
"src/config/swagger",
|
|
17
|
+
"src/infrastructure/webserver/swagger",
|
|
18
|
+
"src/infrastructure/webserver/server"
|
|
19
|
+
],
|
|
20
|
+
coverageThreshold: {
|
|
21
|
+
global: {
|
|
22
|
+
branches: 65,
|
|
23
|
+
functions: 70,
|
|
24
|
+
lines: 70,
|
|
25
|
+
statements: 70
|
|
26
|
+
}
|
|
27
|
+
}
|
|
14
28
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { Kafka } = require('kafkajs');
|
|
2
|
+
|
|
3
|
+
jest.mock('kafkajs', () => {
|
|
4
|
+
return {
|
|
5
|
+
Kafka: jest.fn().mockImplementation(() => ({
|
|
6
|
+
producer: jest.fn(),
|
|
7
|
+
consumer: jest.fn(),
|
|
8
|
+
})),
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('Kafka Configuration', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should initialize Kafka', () => {
|
|
18
|
+
require('<% if (architecture === "MVC") { %>@/config/kafka<% } else { %>@/infrastructure/config/kafka<% } %>');
|
|
19
|
+
expect(Kafka).toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
const { kafka } = require('../config/kafka');
|
|
2
2
|
const logger = require('<%= loggerPath %>');
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
let producer = null;
|
|
5
|
+
let consumer = null;
|
|
6
6
|
|
|
7
7
|
const connectKafka = async () => {
|
|
8
|
+
if (!producer) producer = kafka.producer();
|
|
9
|
+
if (!consumer) consumer = kafka.consumer({ groupId: 'test-group' });
|
|
10
|
+
|
|
8
11
|
await producer.connect();
|
|
9
12
|
await consumer.connect();
|
|
10
13
|
await consumer.subscribe({ topic: 'test-topic', fromBeginning: true });
|
|
11
14
|
|
|
12
15
|
await consumer.run({
|
|
13
|
-
eachMessage: async ({
|
|
16
|
+
eachMessage: async ({ _topic, _partition, message }) => {
|
|
14
17
|
logger.info({
|
|
15
18
|
value: message.value.toString(),
|
|
16
19
|
});
|
|
@@ -19,6 +22,7 @@ const connectKafka = async () => {
|
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
const sendMessage = async (topic, message) => {
|
|
25
|
+
if (!producer) producer = kafka.producer();
|
|
22
26
|
await producer.send({
|
|
23
27
|
topic,
|
|
24
28
|
messages: [
|
|
@@ -27,4 +31,9 @@ const sendMessage = async (topic, message) => {
|
|
|
27
31
|
});
|
|
28
32
|
};
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
const disconnectKafka = async () => {
|
|
35
|
+
if (producer) await producer.disconnect();
|
|
36
|
+
if (consumer) await consumer.disconnect();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
module.exports = { connectKafka, sendMessage, disconnectKafka };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
let kafka;
|
|
2
|
+
let connectKafka, sendMessage, disconnectKafka;
|
|
3
|
+
|
|
4
|
+
jest.mock('<%= configPath %>', () => ({
|
|
5
|
+
kafka: {
|
|
6
|
+
producer: jest.fn().mockReturnValue({
|
|
7
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
8
|
+
send: jest.fn().mockResolvedValue(undefined),
|
|
9
|
+
disconnect: jest.fn().mockResolvedValue(undefined),
|
|
10
|
+
}),
|
|
11
|
+
consumer: jest.fn().mockReturnValue({
|
|
12
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
13
|
+
subscribe: jest.fn().mockResolvedValue(undefined),
|
|
14
|
+
run: jest.fn().mockResolvedValue(undefined),
|
|
15
|
+
disconnect: jest.fn().mockResolvedValue(undefined),
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock('<%= loggerPath %>');
|
|
21
|
+
|
|
22
|
+
describe('Kafka Client', () => {
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
jest.resetModules();
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
kafka = require('<%= configPath %>').kafka;
|
|
27
|
+
({ connectKafka, sendMessage, disconnectKafka } = require('<%= servicePath %>'));
|
|
28
|
+
await connectKafka();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should connect producer and consumer', async () => {
|
|
32
|
+
await connectKafka();
|
|
33
|
+
const producer = kafka.producer.mock.results[0].value;
|
|
34
|
+
const consumer = kafka.consumer.mock.results[0].value;
|
|
35
|
+
|
|
36
|
+
expect(producer.connect).toHaveBeenCalled();
|
|
37
|
+
expect(consumer.connect).toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should send a message', async () => {
|
|
41
|
+
const topic = 'test-topic';
|
|
42
|
+
const message = 'test-message';
|
|
43
|
+
await sendMessage(topic, message);
|
|
44
|
+
const producer = kafka.producer.mock.results[0].value;
|
|
45
|
+
|
|
46
|
+
expect(producer.send).toHaveBeenCalledWith({
|
|
47
|
+
topic,
|
|
48
|
+
messages: [{ value: message }],
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should disconnect Kafka', async () => {
|
|
53
|
+
await disconnectKafka();
|
|
54
|
+
const producer = kafka.producer.mock.results[0].value;
|
|
55
|
+
const consumer = kafka.consumer.mock.results[0].value;
|
|
56
|
+
|
|
57
|
+
expect(producer.disconnect).toHaveBeenCalled();
|
|
58
|
+
expect(consumer.disconnect).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Kafka } from 'kafkajs';
|
|
2
|
+
|
|
3
|
+
jest.mock('kafkajs', () => {
|
|
4
|
+
return {
|
|
5
|
+
Kafka: jest.fn().mockImplementation(() => ({
|
|
6
|
+
producer: jest.fn(),
|
|
7
|
+
consumer: jest.fn(),
|
|
8
|
+
})),
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('Kafka Configuration', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should initialize Kafka', () => {
|
|
18
|
+
require('<% if (architecture === "MVC") { %>@/config/kafka<% } else { %>@/infrastructure/config/kafka<% } %>');
|
|
19
|
+
expect(Kafka).toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { KafkaService } from '<% if (architecture === "Clean Architecture") { %>@/infrastructure/messaging/kafkaClient<% } else { %>@/services/kafkaService<% } %>';
|
|
2
|
+
import { kafka } from '<%= configPath %>';
|
|
3
|
+
|
|
4
|
+
jest.mock('<%= configPath %>', () => ({
|
|
5
|
+
kafka: {
|
|
6
|
+
producer: jest.fn().mockReturnValue({
|
|
7
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
8
|
+
send: jest.fn().mockResolvedValue(undefined),
|
|
9
|
+
disconnect: jest.fn().mockResolvedValue(undefined),
|
|
10
|
+
}),
|
|
11
|
+
consumer: jest.fn().mockReturnValue({
|
|
12
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
13
|
+
subscribe: jest.fn().mockResolvedValue(undefined),
|
|
14
|
+
run: jest.fn().mockResolvedValue(undefined),
|
|
15
|
+
disconnect: jest.fn().mockResolvedValue(undefined),
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock('<%= loggerPath %>');
|
|
21
|
+
|
|
22
|
+
describe('KafkaService', () => {
|
|
23
|
+
let kafkaService: KafkaService;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
kafkaService = new KafkaService();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should connect producer and consumer', async () => {
|
|
31
|
+
await kafkaService.connect();
|
|
32
|
+
const producer = (kafka.producer as jest.Mock).mock.results[0].value;
|
|
33
|
+
const consumer = (kafka.consumer as jest.Mock).mock.results[0].value;
|
|
34
|
+
|
|
35
|
+
expect(producer.connect).toHaveBeenCalled();
|
|
36
|
+
expect(consumer.connect).toHaveBeenCalled();
|
|
37
|
+
expect(consumer.subscribe).toHaveBeenCalledWith({ topic: 'test-topic', fromBeginning: true });
|
|
38
|
+
expect(consumer.run).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should send a message', async () => {
|
|
42
|
+
const topic = 'test-topic';
|
|
43
|
+
const message = 'test-message';
|
|
44
|
+
await kafkaService.sendMessage(topic, message);
|
|
45
|
+
const producer = (kafka.producer as jest.Mock).mock.results[0].value;
|
|
46
|
+
|
|
47
|
+
expect(producer.send).toHaveBeenCalledWith({
|
|
48
|
+
topic,
|
|
49
|
+
messages: [{ value: message }],
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should disconnect producer and consumer', async () => {
|
|
54
|
+
await kafkaService.disconnect();
|
|
55
|
+
const producer = (kafka.producer as jest.Mock).mock.results[0].value;
|
|
56
|
+
const consumer = (kafka.consumer as jest.Mock).mock.results[0].value;
|
|
57
|
+
|
|
58
|
+
expect(producer.disconnect).toHaveBeenCalled();
|
|
59
|
+
expect(consumer.disconnect).toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -17,7 +17,7 @@ export class KafkaService {
|
|
|
17
17
|
await this.consumer.subscribe({ topic: 'test-topic', fromBeginning: true });
|
|
18
18
|
|
|
19
19
|
await this.consumer.run({
|
|
20
|
-
eachMessage: async ({
|
|
20
|
+
eachMessage: async ({ message }: EachMessagePayload) => {
|
|
21
21
|
logger.info({
|
|
22
22
|
value: message.value?.toString(),
|
|
23
23
|
});
|
|
@@ -33,5 +33,10 @@ export class KafkaService {
|
|
|
33
33
|
],
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
async disconnect() {
|
|
38
|
+
await this.producer.disconnect();
|
|
39
|
+
await this.consumer.disconnect();
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -0,0 +1,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 () => {
|
|
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;
|