nodejs-quickstart-structure 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +17 -4
  2. package/README.md +2 -1
  3. package/bin/index.js +93 -92
  4. package/lib/generator.js +1 -1
  5. package/lib/modules/caching-setup.js +76 -73
  6. package/lib/modules/config-files.js +4 -0
  7. package/lib/modules/kafka-setup.js +249 -191
  8. package/lib/modules/project-setup.js +1 -0
  9. package/package.json +13 -2
  10. package/templates/clean-architecture/js/src/errors/BadRequestError.js +11 -10
  11. package/templates/clean-architecture/js/src/errors/BadRequestError.spec.js.ejs +22 -21
  12. package/templates/clean-architecture/js/src/errors/NotFoundError.js +11 -10
  13. package/templates/clean-architecture/js/src/errors/NotFoundError.spec.js.ejs +22 -21
  14. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +69 -39
  15. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +142 -81
  16. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +1 -1
  17. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +156 -75
  18. package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +234 -138
  19. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +27 -21
  20. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +66 -49
  21. package/templates/clean-architecture/js/src/interfaces/graphql/typeDefs/user.types.js.ejs +19 -17
  22. package/templates/clean-architecture/js/src/interfaces/routes/api.js +12 -10
  23. package/templates/clean-architecture/js/src/usecases/DeleteUser.js +11 -0
  24. package/templates/clean-architecture/js/src/usecases/DeleteUser.spec.js.ejs +47 -0
  25. package/templates/clean-architecture/js/src/usecases/UpdateUser.js +11 -0
  26. package/templates/clean-architecture/js/src/usecases/UpdateUser.spec.js.ejs +48 -0
  27. package/templates/clean-architecture/js/src/utils/errorMessages.js +14 -0
  28. package/templates/clean-architecture/ts/src/errors/BadRequestError.spec.ts.ejs +22 -21
  29. package/templates/clean-architecture/ts/src/errors/BadRequestError.ts +9 -8
  30. package/templates/clean-architecture/ts/src/errors/NotFoundError.spec.ts.ejs +22 -21
  31. package/templates/clean-architecture/ts/src/errors/NotFoundError.ts +9 -8
  32. package/templates/clean-architecture/ts/src/index.ts.ejs +1 -1
  33. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +175 -85
  34. package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +74 -0
  35. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +331 -185
  36. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +173 -84
  37. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +68 -51
  38. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +29 -21
  39. package/templates/clean-architecture/ts/src/interfaces/graphql/typeDefs/user.types.ts.ejs +17 -15
  40. package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +13 -11
  41. package/templates/clean-architecture/ts/src/usecases/deleteUser.spec.ts.ejs +47 -0
  42. package/templates/clean-architecture/ts/src/usecases/deleteUser.ts +9 -0
  43. package/templates/clean-architecture/ts/src/usecases/updateUser.spec.ts.ejs +48 -0
  44. package/templates/clean-architecture/ts/src/usecases/updateUser.ts +9 -0
  45. package/templates/clean-architecture/ts/src/utils/errorMessages.ts +12 -0
  46. package/templates/common/.gitattributes +46 -0
  47. package/templates/common/.snyk.ejs +45 -0
  48. package/templates/common/Dockerfile +17 -9
  49. package/templates/common/README.md.ejs +295 -263
  50. package/templates/common/caching/clean/js/DeleteUser.js.ejs +27 -0
  51. package/templates/common/caching/clean/js/UpdateUser.js.ejs +27 -0
  52. package/templates/common/caching/clean/ts/deleteUser.ts.ejs +24 -0
  53. package/templates/common/caching/clean/ts/updateUser.ts.ejs +25 -0
  54. package/templates/common/caching/ts/memoryCache.ts.ejs +73 -64
  55. package/templates/common/caching/ts/redisClient.ts.ejs +89 -80
  56. package/templates/common/database/js/models/User.js.ejs +79 -53
  57. package/templates/common/database/js/models/User.js.mongoose.ejs +23 -19
  58. package/templates/common/database/js/models/User.spec.js.ejs +94 -84
  59. package/templates/common/database/ts/models/User.spec.ts.ejs +100 -84
  60. package/templates/common/database/ts/models/User.ts.ejs +87 -61
  61. package/templates/common/database/ts/models/User.ts.mongoose.ejs +30 -25
  62. package/templates/common/health/js/healthRoute.js.ejs +50 -47
  63. package/templates/common/health/ts/healthRoute.ts.ejs +49 -46
  64. package/templates/common/jest.e2e.config.js.ejs +8 -8
  65. package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -30
  66. package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +12 -11
  67. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +44 -31
  68. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +86 -49
  69. package/templates/common/kafka/js/services/kafkaService.js.ejs +93 -93
  70. package/templates/common/kafka/js/utils/kafkaEvents.js.ejs +7 -0
  71. package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -51
  72. package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +12 -11
  73. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +86 -49
  74. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +38 -25
  75. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +95 -95
  76. package/templates/common/kafka/ts/utils/kafkaEvents.ts.ejs +5 -0
  77. package/templates/common/package.json.ejs +10 -2
  78. package/templates/common/shutdown/js/gracefulShutdown.js.ejs +65 -61
  79. package/templates/common/shutdown/js/gracefulShutdown.spec.js.ejs +149 -160
  80. package/templates/common/shutdown/ts/gracefulShutdown.spec.ts.ejs +179 -158
  81. package/templates/common/shutdown/ts/gracefulShutdown.ts.ejs +59 -55
  82. package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +120 -49
  83. package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +120 -49
  84. package/templates/common/swagger.yml.ejs +118 -66
  85. package/templates/db/mysql/V1__Initial_Setup.sql.ejs +10 -9
  86. package/templates/db/postgres/V1__Initial_Setup.sql.ejs +10 -9
  87. package/templates/mvc/js/src/controllers/userController.js.ejs +246 -105
  88. package/templates/mvc/js/src/controllers/userController.spec.js.ejs +481 -209
  89. package/templates/mvc/js/src/errors/BadRequestError.js +11 -10
  90. package/templates/mvc/js/src/errors/BadRequestError.spec.js.ejs +22 -21
  91. package/templates/mvc/js/src/errors/NotFoundError.js +11 -10
  92. package/templates/mvc/js/src/errors/NotFoundError.spec.js.ejs +22 -21
  93. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +25 -19
  94. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +64 -47
  95. package/templates/mvc/js/src/graphql/typeDefs/user.types.js.ejs +19 -17
  96. package/templates/mvc/js/src/index.js.ejs +1 -1
  97. package/templates/mvc/js/src/routes/api.js +10 -8
  98. package/templates/mvc/js/src/routes/api.spec.js.ejs +41 -36
  99. package/templates/mvc/js/src/utils/errorMessages.js +14 -0
  100. package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +481 -203
  101. package/templates/mvc/ts/src/controllers/userController.ts.ejs +248 -107
  102. package/templates/mvc/ts/src/errors/BadRequestError.spec.ts.ejs +22 -21
  103. package/templates/mvc/ts/src/errors/BadRequestError.ts +9 -8
  104. package/templates/mvc/ts/src/errors/NotFoundError.spec.ts.ejs +27 -21
  105. package/templates/mvc/ts/src/errors/NotFoundError.ts +9 -8
  106. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +68 -51
  107. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +29 -21
  108. package/templates/mvc/ts/src/graphql/typeDefs/user.types.ts.ejs +17 -15
  109. package/templates/mvc/ts/src/index.ts.ejs +156 -153
  110. package/templates/mvc/ts/src/routes/api.spec.ts.ejs +59 -40
  111. package/templates/mvc/ts/src/routes/api.ts +12 -10
  112. package/templates/mvc/ts/src/utils/errorMessages.ts +12 -0
  113. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.ts.ejs +0 -37
@@ -0,0 +1,47 @@
1
+ const DeleteUser = require('@/usecases/DeleteUser');
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('DeleteUser UseCase', () => {
17
+ let deleteUser;
18
+ let mockUserRepository;
19
+
20
+ beforeEach(() => {
21
+ mockUserRepository = new UserRepository();
22
+ deleteUser = new DeleteUser(mockUserRepository);
23
+ jest.clearAllMocks();
24
+ });
25
+
26
+ it('should delete and return the user', async () => {
27
+ const id = 1;
28
+ const expectedResult = { id, name: 'Deleted User', email: 'test@test.com' };
29
+
30
+ mockUserRepository.delete.mockResolvedValue(expectedResult);
31
+
32
+ const result = await deleteUser.execute(id);
33
+
34
+ expect(mockUserRepository.delete).toHaveBeenCalledWith(id);
35
+ expect(result).toEqual(expectedResult);
36
+ <%_ if (caching !== 'None') { -%>
37
+ expect(cacheService.del).toHaveBeenCalledWith('users:all');
38
+ <%_ } %>
39
+ });
40
+
41
+ it('should throw an error if repository fails', async () => {
42
+ const error = new Error('Database error');
43
+ mockUserRepository.delete.mockRejectedValue(error);
44
+
45
+ await expect(deleteUser.execute(1)).rejects.toThrow(error);
46
+ });
47
+ });
@@ -0,0 +1,11 @@
1
+ class UpdateUser {
2
+ constructor(userRepository) {
3
+ this.userRepository = userRepository;
4
+ }
5
+
6
+ async execute(id, data) {
7
+ return this.userRepository.update(id, data);
8
+ }
9
+ }
10
+
11
+ module.exports = UpdateUser;
@@ -0,0 +1,48 @@
1
+ const UpdateUser = require('@/usecases/UpdateUser');
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('UpdateUser UseCase', () => {
17
+ let updateUser;
18
+ let mockUserRepository;
19
+
20
+ beforeEach(() => {
21
+ mockUserRepository = new UserRepository();
22
+ updateUser = new UpdateUser(mockUserRepository);
23
+ jest.clearAllMocks();
24
+ });
25
+
26
+ it('should update and return the user', async () => {
27
+ const id = 1;
28
+ const data = { name: 'Updated Name' };
29
+ const expectedUser = { id, name: 'Updated Name', email: 'test@test.com' };
30
+
31
+ mockUserRepository.update.mockResolvedValue(expectedUser);
32
+
33
+ const result = await updateUser.execute(id, data);
34
+
35
+ expect(mockUserRepository.update).toHaveBeenCalledWith(id, data);
36
+ expect(result).toEqual(expectedUser);
37
+ <%_ if (caching !== 'None') { -%>
38
+ expect(cacheService.del).toHaveBeenCalledWith('users:all');
39
+ <%_ } %>
40
+ });
41
+
42
+ it('should throw an error if repository fails', async () => {
43
+ const error = new Error('Database error');
44
+ mockUserRepository.update.mockRejectedValue(error);
45
+
46
+ await expect(updateUser.execute(1, { name: 'Test' })).rejects.toThrow(error);
47
+ });
48
+ });
@@ -0,0 +1,14 @@
1
+ const ERROR_MESSAGES = {
2
+ USER_NOT_FOUND: 'User not found',
3
+ RESOURCE_NOT_FOUND: 'Resource not found',
4
+ INVALID_USER_DATA: 'Invalid user event data',
5
+ INTERNAL_SERVER_ERROR: 'Internal Server Error',
6
+ BAD_REQUEST: 'Bad Request',
7
+ FETCH_USERS_ERROR: 'Error fetching users',
8
+ CREATE_USER_ERROR: 'Error creating user',
9
+ UPDATE_USER_ERROR: 'Error updating user',
10
+ DELETE_USER_ERROR: 'Error deleting user',
11
+ DATABASE_PING_FAILED: 'Health Check Database Ping Failed',
12
+ };
13
+
14
+ module.exports = ERROR_MESSAGES;
@@ -1,21 +1,22 @@
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
- });
1
+ import { BadRequestError } from '@/errors/BadRequestError';
2
+ import { ApiError } from '@/errors/ApiError';
3
+ import { HTTP_STATUS } from '@/utils/httpCodes';
4
+ import { ERROR_MESSAGES } from '@/utils/errorMessages';
5
+
6
+ describe('BadRequestError', () => {
7
+ it('should extend ApiError', () => {
8
+ const error = new BadRequestError();
9
+ expect(error).toBeInstanceOf(ApiError);
10
+ });
11
+
12
+ it('should have default message "Bad Request"', () => {
13
+ const error = new BadRequestError();
14
+ expect(error.message).toBe(ERROR_MESSAGES.BAD_REQUEST);
15
+ expect(error.statusCode).toBe(HTTP_STATUS.BAD_REQUEST);
16
+ });
17
+
18
+ it('should accept a custom message', () => {
19
+ const error = new BadRequestError('Custom bad request');
20
+ expect(error.message).toBe('Custom bad request');
21
+ });
22
+ });
@@ -1,8 +1,9 @@
1
- import { ApiError } from '@/errors/ApiError';
2
- import { HTTP_STATUS } from '@/utils/httpCodes';
3
-
4
- export class BadRequestError extends ApiError {
5
- constructor(message = 'Bad request') {
6
- super(HTTP_STATUS.BAD_REQUEST, message);
7
- }
8
- }
1
+ import { ApiError } from '@/errors/ApiError';
2
+ import { HTTP_STATUS } from '@/utils/httpCodes';
3
+ import { ERROR_MESSAGES } from '@/utils/errorMessages';
4
+
5
+ export class BadRequestError extends ApiError {
6
+ constructor(message: string = ERROR_MESSAGES.BAD_REQUEST) {
7
+ super(HTTP_STATUS.BAD_REQUEST, message);
8
+ }
9
+ }
@@ -1,21 +1,22 @@
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
+ import { NotFoundError } from '@/errors/NotFoundError';
2
+ import { ApiError } from '@/errors/ApiError';
3
+ import { HTTP_STATUS } from '@/utils/httpCodes';
4
+ import { ERROR_MESSAGES } from '@/utils/errorMessages';
5
+
6
+ describe('NotFoundError', () => {
7
+ it('should extend ApiError', () => {
8
+ const error = new NotFoundError();
9
+ expect(error).toBeInstanceOf(ApiError);
10
+ });
11
+
12
+ it('should have default message "Resource not found"', () => {
13
+ const error = new NotFoundError();
14
+ expect(error.message).toBe(ERROR_MESSAGES.RESOURCE_NOT_FOUND);
15
+ expect(error.statusCode).toBe(HTTP_STATUS.NOT_FOUND);
16
+ });
17
+
18
+ it('should accept a custom message', () => {
19
+ const error = new NotFoundError(ERROR_MESSAGES.USER_NOT_FOUND);
20
+ expect(error.message).toBe(ERROR_MESSAGES.USER_NOT_FOUND);
21
+ });
22
+ });
@@ -1,8 +1,9 @@
1
- import { ApiError } from '@/errors/ApiError';
2
- import { HTTP_STATUS } from '@/utils/httpCodes';
3
-
4
- export class NotFoundError extends ApiError {
5
- constructor(message = 'Resource not found') {
6
- super(HTTP_STATUS.NOT_FOUND, message);
7
- }
8
- }
1
+ import { ApiError } from '@/errors/ApiError';
2
+ import { HTTP_STATUS } from '@/utils/httpCodes';
3
+ import { ERROR_MESSAGES } from '@/utils/errorMessages';
4
+
5
+ export class NotFoundError extends ApiError {
6
+ constructor(message: string = ERROR_MESSAGES.RESOURCE_NOT_FOUND) {
7
+ super(HTTP_STATUS.NOT_FOUND, message);
8
+ }
9
+ }
@@ -16,7 +16,7 @@ import swaggerSpecs from '@/config/swagger';<% } %>
16
16
  <%_ if (communication === 'Kafka') { -%>import { kafkaService } from '@/infrastructure/messaging/kafkaClient';<%_ } -%>
17
17
  <%_ if (communication === 'GraphQL') { -%>
18
18
  import { ApolloServer } from '@apollo/server';
19
- import { expressMiddleware } from '@apollo/server/express4';
19
+ import { expressMiddleware } from '@as-integrations/express4';
20
20
  import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
21
21
  import { unwrapResolverError } from '@apollo/server/errors';
22
22
  import { ApiError } from '@/errors/ApiError';
@@ -1,85 +1,175 @@
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
- });
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
+
86
+ describe('update', () => {
87
+ it('should update and return the user (Happy Path)', async () => {
88
+ // Arrange
89
+ const id = '1';
90
+ const data = { name: 'Updated' };
91
+ const expectedUser = { id: '1', name: 'Updated', email: 'test@example.com' };
92
+
93
+ <%_ if (database === 'MongoDB') { -%>
94
+ const mockDbRecord = { _id: { toString: () => '1' }, name: 'Updated', email: 'test@example.com' };
95
+ (UserModel.findByIdAndUpdate as jest.Mock).mockResolvedValue(mockDbRecord);
96
+ <%_ } else if (database === 'None') { -%>
97
+ (UserModel.update as jest.Mock).mockResolvedValue(expectedUser);
98
+ <%_ } else { -%>
99
+ const mockDbRecord = { id: '1', name: 'Updated', email: 'test@example.com', update: jest.fn().mockResolvedValue(true) };
100
+ (UserModel.findByPk as jest.Mock).mockResolvedValue(mockDbRecord);
101
+ <%_ } -%>
102
+
103
+ // Act
104
+ const result = await userRepository.update(id, data);
105
+
106
+ // Assert
107
+ expect(result?.name).toEqual(data.name);
108
+ <%_ if (database !== 'None') { -%>
109
+ <%_ if (database === 'MongoDB') { -%>
110
+ expect(UserModel.findByIdAndUpdate).toHaveBeenCalled();
111
+ <%_ } else { -%>
112
+ expect(UserModel.findByPk).toHaveBeenCalled();
113
+ <%_ } -%>
114
+ <%_ } -%>
115
+ });
116
+
117
+ it('should return null when user not found (Error Handling)', async () => {
118
+ // Arrange
119
+ const id = '999';
120
+ <%_ if (database === 'MongoDB') { -%>
121
+ (UserModel.findByIdAndUpdate as jest.Mock).mockResolvedValue(null);
122
+ <%_ } else if (database === 'None') { -%>
123
+ (UserModel.update as jest.Mock).mockResolvedValue(null);
124
+ <%_ } else { -%>
125
+ (UserModel.findByPk as jest.Mock).mockResolvedValue(null);
126
+ <%_ } -%>
127
+
128
+ // Act
129
+ const result = await userRepository.update(id, { name: 'Fail' });
130
+
131
+ // Assert
132
+ expect(result).toBeNull();
133
+ });
134
+ });
135
+
136
+ describe('delete', () => {
137
+ it('should successfully delete a user (Happy Path)', async () => {
138
+ // Arrange
139
+ const id = '1';
140
+
141
+ <%_ if (database === 'MongoDB') { -%>
142
+ (UserModel.findByIdAndDelete as jest.Mock).mockResolvedValue(true);
143
+ <%_ } else if (database === 'None') { -%>
144
+ (UserModel.destroy as jest.Mock).mockResolvedValue(true);
145
+ <%_ } else { -%>
146
+ const mockDbRecord = { id: '1', destroy: jest.fn().mockResolvedValue(true) };
147
+ (UserModel.findByPk as jest.Mock).mockResolvedValue(mockDbRecord);
148
+ <%_ } -%>
149
+
150
+ // Act
151
+ const result = await userRepository.delete(id);
152
+
153
+ // Assert
154
+ expect(result).toBe(true);
155
+ });
156
+
157
+ it('should return false when user not found during deletion (Error Handling)', async () => {
158
+ // Arrange
159
+ const id = '999';
160
+ <%_ if (database === 'MongoDB') { -%>
161
+ (UserModel.findByIdAndDelete as jest.Mock).mockResolvedValue(null);
162
+ <%_ } else if (database === 'None') { -%>
163
+ (UserModel.destroy as jest.Mock).mockResolvedValue(false);
164
+ <%_ } else { -%>
165
+ (UserModel.findByPk as jest.Mock).mockResolvedValue(null);
166
+ <%_ } -%>
167
+
168
+ // Act
169
+ const result = await userRepository.delete(id);
170
+
171
+ // Assert
172
+ expect(result).toBe(false);
173
+ });
174
+ });
175
+ });
@@ -0,0 +1,74 @@
1
+ import { User as UserEntity } from '@/domain/user';
2
+ import UserModel from '@/infrastructure/database/models/User';
3
+
4
+ export class UserRepository {
5
+ async save(user: UserEntity): Promise<UserEntity> {
6
+ <%_ if (database === 'MongoDB') { -%>
7
+ const newUser = await UserModel.create({ name: user.name, email: user.email });
8
+ return { id: newUser._id.toString(), name: newUser.name, email: newUser.email };
9
+ <%_ } else if (database === 'None') { -%>
10
+ const newUser = await UserModel.create({ name: user.name, email: user.email });
11
+ return { id: newUser.id, name: newUser.name, email: newUser.email };
12
+ <%_ } else { -%>
13
+ const newUser = await UserModel.create({ name: user.name, email: user.email });
14
+ return { id: newUser.id, name: newUser.name, email: newUser.email };
15
+ <%_ } -%>
16
+ }
17
+
18
+ async getUsers(): Promise<UserEntity[]> {
19
+ <%_ if (database === 'MongoDB') { -%>
20
+ const users = await UserModel.find();
21
+ return users.map(user => ({
22
+ id: user._id.toString(),
23
+ name: user.name,
24
+ email: user.email
25
+ }));
26
+ <%_ } else if (database === 'None') { -%>
27
+ const users = await UserModel.find();
28
+ return users.map(user => ({
29
+ id: user.id,
30
+ name: user.name,
31
+ email: user.email
32
+ }));
33
+ <%_ } else { -%>
34
+ const users = await UserModel.findAll();
35
+ return users.map(user => ({
36
+ id: user.id,
37
+ name: user.name,
38
+ email: user.email
39
+ }));
40
+ <%_ } -%>
41
+ }
42
+
43
+ async update(id: number | string, data: Partial<UserEntity>): Promise<UserEntity | null> {
44
+ <%_ if (database === 'MongoDB') { -%>
45
+ const user = await UserModel.findByIdAndUpdate(id, data, { new: true });
46
+ if (!user) return null;
47
+ return { id: user._id.toString(), name: user.name, email: user.email };
48
+ <%_ } else if (database === 'None') { -%>
49
+ const { id: _, ...updateData } = data;
50
+ const user = await UserModel.update(id, updateData as Parameters<typeof UserModel.update>[1]);
51
+ if (!user) return null;
52
+ return { id: user.id, name: user.name, email: user.email };
53
+ <%_ } else { -%>
54
+ const user = await UserModel.findByPk(id);
55
+ if (!user) return null;
56
+ await user.update(data);
57
+ return { id: user.id || 0, name: user.name, email: user.email };
58
+ <%_ } -%>
59
+ }
60
+
61
+ async delete(id: number | string): Promise<boolean> {
62
+ <%_ if (database === 'MongoDB') { -%>
63
+ const result = await UserModel.findByIdAndDelete(id);
64
+ return !!result;
65
+ <%_ } else if (database === 'None') { -%>
66
+ return await UserModel.destroy(id);
67
+ <%_ } else { -%>
68
+ const user = await UserModel.findByPk(id);
69
+ if (!user) return false;
70
+ await user.destroy();
71
+ return true;
72
+ <%_ } -%>
73
+ }
74
+ }