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,31 @@
|
|
|
1
|
+
const { gqlContext } = require('@/interfaces/graphql/context');
|
|
2
|
+
const { resolvers } = require('@/interfaces/graphql/resolvers');
|
|
3
|
+
const { typeDefs } = require('@/interfaces/graphql/typeDefs');
|
|
4
|
+
|
|
5
|
+
jest.mock('@/infrastructure/repositories/UserRepository');
|
|
6
|
+
|
|
7
|
+
describe('GraphQL Context', () => {
|
|
8
|
+
it('should exercise GraphQL index entry points', () => {
|
|
9
|
+
expect(resolvers).toBeDefined();
|
|
10
|
+
expect(typeDefs).toBeDefined();
|
|
11
|
+
});
|
|
12
|
+
it('should return context with token when authorization header is present', async () => {
|
|
13
|
+
const mockRequest = {
|
|
14
|
+
headers: {
|
|
15
|
+
authorization: 'Bearer token123',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const context = await gqlContext({ req: mockRequest });
|
|
20
|
+
expect(context.token).toBe('Bearer token123');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return context with empty token when authorization header is missing', async () => {
|
|
24
|
+
const mockRequest = {
|
|
25
|
+
headers: {},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const context = await gqlContext({ req: mockRequest });
|
|
29
|
+
expect(context.token).toBe('');
|
|
30
|
+
});
|
|
31
|
+
});
|
package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { userResolvers } = require('@/interfaces/graphql/resolvers/user.resolvers');
|
|
2
|
+
|
|
3
|
+
const mockGetUsers = jest.fn().mockResolvedValue([{ id: '1', name: 'John Doe', email: 'john@example.com' }]);
|
|
4
|
+
const mockCreateUser = jest.fn().mockResolvedValue({ id: '1', name: 'Jane', email: 'jane@example.com' });
|
|
5
|
+
|
|
6
|
+
jest.mock('@/interfaces/controllers/userController', () => {
|
|
7
|
+
return jest.fn().mockImplementation(() => ({
|
|
8
|
+
getUsers: (...args) => mockGetUsers(...args),
|
|
9
|
+
createUser: (...args) => mockCreateUser(...args)
|
|
10
|
+
}));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('User Resolvers', () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('Query.getAllUsers', () => {
|
|
19
|
+
it('should return all users', async () => {
|
|
20
|
+
const result = await userResolvers.Query.getAllUsers();
|
|
21
|
+
expect(result).toEqual([{ id: '1', name: 'John Doe', email: 'john@example.com' }]);
|
|
22
|
+
expect(mockGetUsers).toHaveBeenCalledTimes(1);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('Mutation.createUser', () => {
|
|
27
|
+
it('should create and return a new user', async () => {
|
|
28
|
+
const result = await userResolvers.Mutation.createUser(null, { name: 'Jane', email: 'jane@example.com' });
|
|
29
|
+
expect(result).toEqual({ id: '1', name: 'Jane', email: 'jane@example.com' });
|
|
30
|
+
expect(mockCreateUser).toHaveBeenCalledWith({ name: 'Jane', email: 'jane@example.com' });
|
|
31
|
+
expect(mockCreateUser).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
35
|
+
describe('User.id', () => {
|
|
36
|
+
it('should return parent.id if available', () => {
|
|
37
|
+
const parent = { id: '123' };
|
|
38
|
+
const result = userResolvers.User.id(parent);
|
|
39
|
+
expect(result).toBe('123');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should fallback to parent._id if id is not available', () => {
|
|
43
|
+
const parent = { _id: '456' };
|
|
44
|
+
const result = userResolvers.User.id(parent);
|
|
45
|
+
expect(result).toBe('456');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
<%_ } -%>
|
|
49
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const request = require('supertest');
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const router = require('@/interfaces/routes/api');
|
|
4
|
+
|
|
5
|
+
const mockGetUsers = jest.fn().mockImplementation((req, res) => res.status(200).json([{ id: '1', name: 'John Doe' }]));
|
|
6
|
+
const mockCreateUser = jest.fn().mockImplementation((req, res) => res.status(201).json({ id: '1', name: 'Test' }));
|
|
7
|
+
|
|
8
|
+
jest.mock('@/interfaces/controllers/userController', () => {
|
|
9
|
+
return jest.fn().mockImplementation(() => ({
|
|
10
|
+
getUsers: (...args) => mockGetUsers(...args),
|
|
11
|
+
createUser: (...args) => mockCreateUser(...args)
|
|
12
|
+
}));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('ApiRoutes', () => {
|
|
16
|
+
let app;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
app = express();
|
|
20
|
+
app.use(express.json());
|
|
21
|
+
app.use('/api', router);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('POST /api/users should call controller.createUser', async () => {
|
|
25
|
+
await request(app)
|
|
26
|
+
.post('/api/users')
|
|
27
|
+
.send({ name: 'Test', email: 'test@example.com' });
|
|
28
|
+
|
|
29
|
+
expect(mockCreateUser).toHaveBeenCalledTimes(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('GET /api/users should call controller.getUsers', async () => {
|
|
33
|
+
await request(app)
|
|
34
|
+
.get('/api/users');
|
|
35
|
+
|
|
36
|
+
expect(mockGetUsers).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const CreateUser = require('@/usecases/CreateUser');
|
|
2
|
+
const UserRepository = require('@/infrastructure/repositories/UserRepository');
|
|
3
|
+
<%_ if (caching !== 'None') { -%>
|
|
4
|
+
const cacheService = require('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>');
|
|
5
|
+
<%_ } -%>
|
|
6
|
+
|
|
7
|
+
jest.mock('@/infrastructure/repositories/UserRepository');
|
|
8
|
+
<%_ if (caching !== 'None') { -%>
|
|
9
|
+
jest.mock('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>', () => ({
|
|
10
|
+
get: jest.fn(),
|
|
11
|
+
set: jest.fn(),
|
|
12
|
+
del: jest.fn()
|
|
13
|
+
}));
|
|
14
|
+
<%_ } -%>
|
|
15
|
+
|
|
16
|
+
describe('CreateUser UseCase', () => {
|
|
17
|
+
let createUser;
|
|
18
|
+
let mockUserRepository;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockUserRepository = new UserRepository();
|
|
22
|
+
createUser = new CreateUser(mockUserRepository);
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should create and save a new user', async () => {
|
|
27
|
+
const name = 'Test User';
|
|
28
|
+
const email = 'test@example.com';
|
|
29
|
+
const expectedResult = { id: 1, name, email };
|
|
30
|
+
|
|
31
|
+
mockUserRepository.save.mockResolvedValue(expectedResult);
|
|
32
|
+
|
|
33
|
+
const result = await createUser.execute(name, email);
|
|
34
|
+
|
|
35
|
+
expect(mockUserRepository.save).toHaveBeenCalledTimes(1);
|
|
36
|
+
const savedUser = mockUserRepository.save.mock.calls[0][0];
|
|
37
|
+
expect(savedUser.name).toBe(name);
|
|
38
|
+
expect(savedUser.email).toBe(email);
|
|
39
|
+
expect(result).toEqual(expectedResult);
|
|
40
|
+
<%_ if (caching !== 'None') { -%>
|
|
41
|
+
expect(cacheService.del).toHaveBeenCalledWith('users:all');
|
|
42
|
+
<%_ } -%>
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should throw an error if repository fails', async () => {
|
|
46
|
+
const error = new Error('Database error');
|
|
47
|
+
mockUserRepository.save.mockRejectedValue(error);
|
|
48
|
+
|
|
49
|
+
await expect(createUser.execute('Test', 'test@test.com')).rejects.toThrow(error);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const GetAllUsers = require('@/usecases/GetAllUsers');
|
|
2
|
+
const UserRepository = require('@/infrastructure/repositories/UserRepository');
|
|
3
|
+
<%_ if (caching !== 'None') { -%>
|
|
4
|
+
const cacheService = require('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>');
|
|
5
|
+
<%_ } -%>
|
|
6
|
+
|
|
7
|
+
jest.mock('@/infrastructure/repositories/UserRepository');
|
|
8
|
+
<%_ if (caching !== 'None') { -%>
|
|
9
|
+
jest.mock('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>', () => ({
|
|
10
|
+
get: jest.fn(),
|
|
11
|
+
set: jest.fn(),
|
|
12
|
+
del: jest.fn()
|
|
13
|
+
}));
|
|
14
|
+
<%_ } -%>
|
|
15
|
+
|
|
16
|
+
describe('GetAllUsers UseCase', () => {
|
|
17
|
+
let getAllUsers;
|
|
18
|
+
let mockUserRepository;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockUserRepository = new UserRepository();
|
|
22
|
+
getAllUsers = new GetAllUsers(mockUserRepository);
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should retrieve all users', async () => {
|
|
27
|
+
const expectedUsers = [{ id: 1, name: 'Alice', email: 'alice@example.com' }];
|
|
28
|
+
mockUserRepository.getUsers.mockResolvedValue(expectedUsers);
|
|
29
|
+
<%_ if (caching !== 'None') { -%>
|
|
30
|
+
cacheService.get.mockResolvedValue(null); // Cache miss
|
|
31
|
+
<%_ } -%>
|
|
32
|
+
const result = await getAllUsers.execute();
|
|
33
|
+
|
|
34
|
+
expect(mockUserRepository.getUsers).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(result).toEqual(expectedUsers);
|
|
36
|
+
<%_ if (caching !== 'None') { -%>
|
|
37
|
+
expect(cacheService.set).toHaveBeenCalled();
|
|
38
|
+
<%_ } -%>
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
<%_ if (caching !== 'None') { -%>
|
|
42
|
+
it('should return from cache if available', async () => {
|
|
43
|
+
const cachedUsers = [{ id: 1, name: 'Cached', email: 'cached@example.com' }];
|
|
44
|
+
cacheService.get.mockResolvedValue(cachedUsers);
|
|
45
|
+
|
|
46
|
+
const result = await getAllUsers.execute();
|
|
47
|
+
|
|
48
|
+
expect(mockUserRepository.getUsers).not.toHaveBeenCalled();
|
|
49
|
+
expect(result).toEqual(cachedUsers);
|
|
50
|
+
});
|
|
51
|
+
<%_ } -%>
|
|
52
|
+
|
|
53
|
+
it('should throw an error if repository fails', async () => {
|
|
54
|
+
const error = new Error('Database error');
|
|
55
|
+
mockUserRepository.getUsers.mockRejectedValue(error);
|
|
56
|
+
<%_ if (caching !== 'None') { -%>
|
|
57
|
+
cacheService.get.mockResolvedValue(null); // Cache miss
|
|
58
|
+
<%_ } -%>
|
|
59
|
+
await expect(getAllUsers.execute()).rejects.toThrow(error);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BadRequestError } from '@/errors/BadRequestError';
|
|
2
|
+
import { ApiError } from '@/errors/ApiError';
|
|
3
|
+
import { HTTP_STATUS } from '@/utils/httpCodes';
|
|
4
|
+
|
|
5
|
+
describe('BadRequestError', () => {
|
|
6
|
+
it('should extend ApiError', () => {
|
|
7
|
+
const error = new BadRequestError();
|
|
8
|
+
expect(error).toBeInstanceOf(ApiError);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should have default message "Bad request"', () => {
|
|
12
|
+
const error = new BadRequestError();
|
|
13
|
+
expect(error.message).toBe('Bad request');
|
|
14
|
+
expect(error.statusCode).toBe(HTTP_STATUS.BAD_REQUEST);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should accept a custom message', () => {
|
|
18
|
+
const error = new BadRequestError('Custom bad request');
|
|
19
|
+
expect(error.message).toBe('Custom bad request');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NotFoundError } from '@/errors/NotFoundError';
|
|
2
|
+
import { ApiError } from '@/errors/ApiError';
|
|
3
|
+
import { HTTP_STATUS } from '@/utils/httpCodes';
|
|
4
|
+
|
|
5
|
+
describe('NotFoundError', () => {
|
|
6
|
+
it('should extend ApiError', () => {
|
|
7
|
+
const error = new NotFoundError();
|
|
8
|
+
expect(error).toBeInstanceOf(ApiError);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should have default message "Resource not found"', () => {
|
|
12
|
+
const error = new NotFoundError();
|
|
13
|
+
expect(error.message).toBe('Resource not found');
|
|
14
|
+
expect(error.statusCode).toBe(HTTP_STATUS.NOT_FOUND);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should accept a custom message', () => {
|
|
18
|
+
const error = new NotFoundError('User not found');
|
|
19
|
+
expect(error.message).toBe('User not found');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import express
|
|
1
|
+
import express from 'express';
|
|
2
2
|
import cors from 'cors';
|
|
3
3
|
import helmet from 'helmet';
|
|
4
4
|
import hpp from 'hpp';
|
|
5
5
|
import rateLimit from 'express-rate-limit';
|
|
6
6
|
import logger from '@/infrastructure/log/logger';
|
|
7
7
|
import morgan from 'morgan';
|
|
8
|
-
import { errorMiddleware } from '@/utils/
|
|
9
|
-
|
|
8
|
+
import { errorMiddleware } from '@/utils/errorMiddleware';
|
|
9
|
+
import { setupGracefulShutdown } from '@/utils/gracefulShutdown';
|
|
10
|
+
import healthRoutes from '@/interfaces/routes/healthRoute';
|
|
10
11
|
<% if (communication === 'REST APIs') { -%>
|
|
12
|
+
import userRoutes from '@/interfaces/routes/userRoutes';
|
|
11
13
|
import swaggerUi from 'swagger-ui-express';
|
|
12
14
|
import swaggerSpecs from '@/config/swagger';<% } -%>
|
|
13
15
|
<%_ if (communication === 'Kafka') { -%>import { KafkaService } from '@/infrastructure/messaging/kafkaClient';<%_ } -%>
|
|
@@ -56,15 +58,13 @@ app.use('/api/users', userRoutes);
|
|
|
56
58
|
<%_ if (communication === 'REST APIs') { -%>
|
|
57
59
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
58
60
|
<%_ } -%>
|
|
59
|
-
app.
|
|
60
|
-
res.json({ status: 'UP' });
|
|
61
|
-
});
|
|
61
|
+
app.use('/health', healthRoutes);
|
|
62
62
|
|
|
63
63
|
// Start Server Logic
|
|
64
64
|
const startServer = async () => {
|
|
65
65
|
<%_ if (communication === 'GraphQL') { -%>
|
|
66
66
|
// GraphQL Setup
|
|
67
|
-
const
|
|
67
|
+
const apolloServer = new ApolloServer<MyContext>({
|
|
68
68
|
typeDefs,
|
|
69
69
|
resolvers,
|
|
70
70
|
plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
|
|
@@ -88,14 +88,16 @@ const startServer = async () => {
|
|
|
88
88
|
return formattedError;
|
|
89
89
|
},
|
|
90
90
|
});
|
|
91
|
-
await
|
|
92
|
-
app.use('/graphql', expressMiddleware(
|
|
91
|
+
await apolloServer.start();
|
|
92
|
+
app.use('/graphql', expressMiddleware(apolloServer, { context: gqlContext }));
|
|
93
93
|
<%_ } -%>
|
|
94
94
|
app.use(errorMiddleware);
|
|
95
|
-
|
|
95
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
96
|
+
const kafkaService = new KafkaService();
|
|
97
|
+
<%_ } -%>
|
|
98
|
+
const server = app.listen(port, () => {
|
|
96
99
|
logger.info(`Server running on port ${port}`);
|
|
97
100
|
<%_ if (communication === 'Kafka') { -%>
|
|
98
|
-
const kafkaService = new KafkaService();
|
|
99
101
|
kafkaService.connect().then(() => {
|
|
100
102
|
logger.info('Kafka connected');
|
|
101
103
|
kafkaService.sendMessage('test-topic', 'Hello Kafka from Clean Arch TS!');
|
|
@@ -104,6 +106,8 @@ const startServer = async () => {
|
|
|
104
106
|
});
|
|
105
107
|
<%_ } -%>
|
|
106
108
|
});
|
|
109
|
+
|
|
110
|
+
setupGracefulShutdown(server<% if(communication === 'Kafka') { %>, kafkaService<% } %>);
|
|
107
111
|
};
|
|
108
112
|
|
|
109
113
|
<%_ if (database !== 'None') { -%>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
jest.mock('winston-daily-rotate-file');
|
|
2
|
+
jest.mock('winston', () => {
|
|
3
|
+
const mockLogger = {
|
|
4
|
+
add: jest.fn(),
|
|
5
|
+
info: jest.fn(),
|
|
6
|
+
error: jest.fn(),
|
|
7
|
+
warn: jest.fn()
|
|
8
|
+
};
|
|
9
|
+
const format = {
|
|
10
|
+
combine: jest.fn(),
|
|
11
|
+
timestamp: jest.fn(),
|
|
12
|
+
json: jest.fn(),
|
|
13
|
+
simple: jest.fn()
|
|
14
|
+
};
|
|
15
|
+
const transports = {
|
|
16
|
+
Console: jest.fn(),
|
|
17
|
+
DailyRotateFile: jest.fn()
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
format,
|
|
21
|
+
transports,
|
|
22
|
+
createLogger: jest.fn().mockReturnValue(mockLogger)
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
<% if (architecture === 'MVC') { -%>
|
|
27
|
+
import logger from '@/utils/logger';
|
|
28
|
+
<% } else { -%>
|
|
29
|
+
import logger from '@/infrastructure/log/logger';
|
|
30
|
+
<% } -%>
|
|
31
|
+
|
|
32
|
+
describe('Logger', () => {
|
|
33
|
+
it('should export a logger instance', () => {
|
|
34
|
+
expect(logger).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should have info method', () => {
|
|
38
|
+
expect(typeof logger.info).toBe('function');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should have error method', () => {
|
|
42
|
+
expect(typeof logger.error).toBe('function');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should call info', () => {
|
|
46
|
+
logger.info('test message');
|
|
47
|
+
expect(logger.info).toHaveBeenCalledWith('test message');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should call error', () => {
|
|
51
|
+
logger.error('test error');
|
|
52
|
+
expect(logger.error).toHaveBeenCalledWith('test error');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should use JSON format in production environment', () => {
|
|
56
|
+
const winston = require('winston');
|
|
57
|
+
jest.resetModules();
|
|
58
|
+
process.env.NODE_ENV = 'production';
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
60
|
+
require('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>');
|
|
61
|
+
expect(winston.format.json).toHaveBeenCalled();
|
|
62
|
+
process.env.NODE_ENV = 'test';
|
|
63
|
+
});
|
|
64
|
+
});
|
package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
2
|
+
import UserModel from '@/infrastructure/database/models/User';
|
|
3
|
+
|
|
4
|
+
// Mock DB Model Database Layer
|
|
5
|
+
jest.mock('@/infrastructure/database/models/User');
|
|
6
|
+
|
|
7
|
+
describe('UserRepository', () => {
|
|
8
|
+
let userRepository: UserRepository;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
userRepository = new UserRepository();
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('save', () => {
|
|
16
|
+
it('should save and return a newly created user (Happy Path)', async () => {
|
|
17
|
+
// Arrange
|
|
18
|
+
const payload = { id: '', name: 'TestUser', email: 'test@example.com' };
|
|
19
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
20
|
+
const mockDbRecord = { _id: { toString: () => '1' }, name: 'TestUser', email: 'test@example.com' };
|
|
21
|
+
(UserModel.create as jest.Mock).mockResolvedValue(mockDbRecord);
|
|
22
|
+
<%_ } else if (database === 'None') { -%>
|
|
23
|
+
(UserModel.create as jest.Mock).mockResolvedValue(payload);
|
|
24
|
+
<%_ } else { -%>
|
|
25
|
+
const mockDbRecord = { id: '1', name: 'TestUser', email: 'test@example.com' };
|
|
26
|
+
(UserModel.create as jest.Mock).mockResolvedValue(mockDbRecord);
|
|
27
|
+
<%_ } -%>
|
|
28
|
+
|
|
29
|
+
// Act
|
|
30
|
+
const result = await userRepository.save(payload);
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
<%_ if (database === 'None') { -%>
|
|
34
|
+
expect(result.name).toEqual(payload.name)
|
|
35
|
+
<%_ } else { -%>
|
|
36
|
+
expect(result).toEqual({ id: '1', name: 'TestUser', email: 'test@example.com' });
|
|
37
|
+
expect(UserModel.create).toHaveBeenCalledWith({ name: payload.name, email: payload.email });
|
|
38
|
+
<%_ } -%>
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should throw an error when DB fails explicitly (Edge Case)', async () => {
|
|
42
|
+
<%_ if (database === 'None') { -%>
|
|
43
|
+
// Mocks do not naturally fail
|
|
44
|
+
<%_ } else { -%>
|
|
45
|
+
// Arrange
|
|
46
|
+
const payload = { id: '', name: 'FailUser', email: 'fail@example.com' };
|
|
47
|
+
const error = new Error('DB Connection Refused');
|
|
48
|
+
(UserModel.create as jest.Mock).mockRejectedValue(error);
|
|
49
|
+
|
|
50
|
+
// Act & Assert
|
|
51
|
+
await expect(userRepository.save(payload)).rejects.toThrow(error);
|
|
52
|
+
<%_ } -%>
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('getUsers', () => {
|
|
57
|
+
it('should return a list of mapped UserEntities (Happy Path)', async () => {
|
|
58
|
+
// Arrange
|
|
59
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
60
|
+
const mockDbRecords = [{ _id: { toString: () => '1' }, name: 'User1', email: 'user1@example.com' }];
|
|
61
|
+
(UserModel.find as jest.Mock).mockResolvedValue(mockDbRecords);
|
|
62
|
+
<%_ } else if (database === 'None') { -%>
|
|
63
|
+
const mockData = [{ id: '1', name: 'User1', email: 'user1@example.com' }];
|
|
64
|
+
(UserModel.find as jest.Mock).mockResolvedValue(mockData);
|
|
65
|
+
<%_ } else { -%>
|
|
66
|
+
const mockDbRecords = [{ id: '1', name: 'User1', email: 'user1@example.com' }];
|
|
67
|
+
(UserModel.findAll as jest.Mock).mockResolvedValue(mockDbRecords);
|
|
68
|
+
<%_ } -%>
|
|
69
|
+
|
|
70
|
+
// Act
|
|
71
|
+
const result = await userRepository.getUsers();
|
|
72
|
+
|
|
73
|
+
// Assert
|
|
74
|
+
expect(result).toHaveLength(1);
|
|
75
|
+
expect(result[0]).toEqual({ id: '1', name: 'User1', email: 'user1@example.com' });
|
|
76
|
+
<%_ if (database !== 'None') { -%>
|
|
77
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
78
|
+
expect(UserModel.find).toHaveBeenCalled();
|
|
79
|
+
<%_ } else { -%>
|
|
80
|
+
expect(UserModel.findAll).toHaveBeenCalled();
|
|
81
|
+
<%_ } -%>
|
|
82
|
+
<%_ } -%>
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.ts.ejs
CHANGED
|
@@ -7,8 +7,7 @@ export class UserRepository {
|
|
|
7
7
|
const newUser = await UserModel.create({ name: user.name, email: user.email });
|
|
8
8
|
return { id: newUser._id.toString(), name: newUser.name, email: newUser.email };
|
|
9
9
|
<%_ } else if (database === 'None') { -%>
|
|
10
|
-
const newUser =
|
|
11
|
-
UserModel.mockData.push(newUser);
|
|
10
|
+
const newUser = await UserModel.create(user);
|
|
12
11
|
return newUser;
|
|
13
12
|
<%_ } else { -%>
|
|
14
13
|
const newUser = await UserModel.create({ name: user.name, email: user.email });
|
|
@@ -25,7 +24,7 @@ export class UserRepository {
|
|
|
25
24
|
email: user.email
|
|
26
25
|
}));
|
|
27
26
|
<%_ } else if (database === 'None') { -%>
|
|
28
|
-
return UserModel.
|
|
27
|
+
return await UserModel.find();
|
|
29
28
|
<%_ } else { -%>
|
|
30
29
|
const users = await UserModel.findAll();
|
|
31
30
|
return users.map(user => ({
|