nodejs-quickstart-structure 1.13.0 → 1.15.1
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 +32 -0
- package/README.md +4 -3
- package/bin/index.js +84 -80
- package/lib/generator.js +28 -4
- package/lib/modules/app-setup.js +111 -19
- package/lib/modules/caching-setup.js +13 -0
- package/lib/modules/config-files.js +50 -62
- package/lib/modules/database-setup.js +35 -30
- package/lib/modules/kafka-setup.js +78 -10
- package/package.json +8 -4
- 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/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/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/errorMiddleware.ts.ejs +1 -2
- package/templates/common/.cursorrules.ejs +60 -0
- package/templates/common/.dockerignore +2 -0
- package/templates/common/.gitlab-ci.yml.ejs +5 -5
- package/templates/common/Jenkinsfile.ejs +1 -1
- package/templates/common/README.md.ejs +11 -1
- package/templates/common/_github/workflows/ci.yml +7 -4
- package/templates/common/caching/js/memoryCache.spec.js.ejs +101 -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/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.spec.js.ejs +70 -0
- package/templates/common/health/ts/healthRoute.spec.ts.ejs +76 -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 +9 -5
- 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 +1 -1
- package/templates/common/package.json.ejs +0 -3
- package/templates/common/prompts/add-feature.md.ejs +26 -0
- package/templates/common/prompts/project-context.md.ejs +43 -0
- package/templates/common/prompts/troubleshoot.md.ejs +28 -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/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 +1 -1
- 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/routes/api.spec.ts.ejs +40 -0
- package/templates/mvc/ts/src/utils/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 -24
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<% if (communication !== 'GraphQL') { -%>
|
|
2
|
+
import { Request, Response, NextFunction } from 'express';
|
|
3
|
+
import { HTTP_STATUS } from '@/utils/httpCodes';
|
|
4
|
+
<% } -%>
|
|
5
|
+
import { UserController } from '@/interfaces/controllers/userController';
|
|
6
|
+
import CreateUser from '@/usecases/createUser';
|
|
7
|
+
import GetAllUsers from '@/usecases/getAllUsers';
|
|
8
|
+
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
jest.mock('@/infrastructure/repositories/UserRepository');
|
|
11
|
+
jest.mock('@/usecases/createUser');
|
|
12
|
+
jest.mock('@/usecases/getAllUsers');
|
|
13
|
+
jest.mock('@/infrastructure/log/logger');
|
|
14
|
+
|
|
15
|
+
describe('UserController (Clean Architecture)', () => {
|
|
16
|
+
let userController: UserController;
|
|
17
|
+
let mockCreateUserUseCase: jest.Mocked<CreateUser>;
|
|
18
|
+
let mockGetAllUsersUseCase: jest.Mocked<GetAllUsers>;
|
|
19
|
+
<% if (communication !== 'GraphQL') { -%>
|
|
20
|
+
let mockRequest: Partial<Request>;
|
|
21
|
+
let mockResponse: Partial<Response>;
|
|
22
|
+
let mockNext: NextFunction;
|
|
23
|
+
<% } -%>
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
// Clear all mocks
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
|
|
29
|
+
userController = new UserController();
|
|
30
|
+
|
|
31
|
+
// Retrieve the mocked instances created inside UserController constructor
|
|
32
|
+
mockCreateUserUseCase = (CreateUser as jest.Mock).mock.instances[0] as jest.Mocked<CreateUser>;
|
|
33
|
+
mockGetAllUsersUseCase = (GetAllUsers as jest.Mock).mock.instances[0] as jest.Mocked<GetAllUsers>;
|
|
34
|
+
|
|
35
|
+
<% if (communication !== 'GraphQL') { -%>
|
|
36
|
+
mockRequest = {};
|
|
37
|
+
mockResponse = {
|
|
38
|
+
json: jest.fn(),
|
|
39
|
+
status: jest.fn().mockReturnThis(),
|
|
40
|
+
};
|
|
41
|
+
mockNext = jest.fn();
|
|
42
|
+
<% } -%>
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('getUsers', () => {
|
|
46
|
+
it('should return successfully (Happy Path)', async () => {
|
|
47
|
+
// Arrange
|
|
48
|
+
const usersMock = [{ id: '1', name: 'Test', email: 'test@example.com' }];
|
|
49
|
+
mockGetAllUsersUseCase.execute.mockResolvedValue(usersMock);
|
|
50
|
+
|
|
51
|
+
// Act
|
|
52
|
+
<% if (communication === 'GraphQL') { -%>
|
|
53
|
+
const result = await userController.getUsers();
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(result).toEqual(usersMock);
|
|
57
|
+
<% } else { -%>
|
|
58
|
+
await userController.getUsers(mockRequest as Request, mockResponse as Response, mockNext);
|
|
59
|
+
|
|
60
|
+
// Assert
|
|
61
|
+
expect(mockResponse.json).toHaveBeenCalledWith(usersMock);
|
|
62
|
+
<% } -%>
|
|
63
|
+
expect(mockGetAllUsersUseCase.execute).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle errors correctly (Error Handling)', async () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
const error = new Error('UseCase Error');
|
|
69
|
+
mockGetAllUsersUseCase.execute.mockRejectedValue(error);
|
|
70
|
+
|
|
71
|
+
// Act & Assert
|
|
72
|
+
<% if (communication === 'GraphQL') { -%>
|
|
73
|
+
await expect(userController.getUsers()).rejects.toThrow(error);
|
|
74
|
+
<% } else { -%>
|
|
75
|
+
await userController.getUsers(mockRequest as Request, mockResponse as Response, mockNext);
|
|
76
|
+
expect(mockNext).toHaveBeenCalledWith(error);
|
|
77
|
+
<% } -%>
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle non-Error objects in catch block', async () => {
|
|
81
|
+
// Arrange
|
|
82
|
+
const error = 'String Error';
|
|
83
|
+
mockGetAllUsersUseCase.execute.mockRejectedValue(error);
|
|
84
|
+
|
|
85
|
+
// Act & Assert
|
|
86
|
+
<% if (communication === 'GraphQL') { -%>
|
|
87
|
+
await expect(userController.getUsers()).rejects.toEqual(error);
|
|
88
|
+
<% } else { -%>
|
|
89
|
+
await userController.getUsers(mockRequest as Request, mockResponse as Response, mockNext);
|
|
90
|
+
expect(mockNext).toHaveBeenCalledWith(error);
|
|
91
|
+
<% } -%>
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('createUser', () => {
|
|
96
|
+
it('should successfully create a new user (Happy Path)', async () => {
|
|
97
|
+
// Arrange
|
|
98
|
+
const payload = { name: 'Alice', email: 'alice@example.com' };
|
|
99
|
+
<% if (communication === 'GraphQL') { -%>
|
|
100
|
+
const dataArg = payload;
|
|
101
|
+
<% } else { -%>
|
|
102
|
+
mockRequest.body = payload;
|
|
103
|
+
<% } -%>
|
|
104
|
+
const expectedUser = { id: '1', ...payload };
|
|
105
|
+
|
|
106
|
+
mockCreateUserUseCase.execute.mockResolvedValue(expectedUser);
|
|
107
|
+
|
|
108
|
+
// Act
|
|
109
|
+
<% if (communication === 'GraphQL') { -%>
|
|
110
|
+
const result = await userController.createUser(dataArg);
|
|
111
|
+
|
|
112
|
+
// Assert
|
|
113
|
+
expect(result).toEqual(expectedUser);
|
|
114
|
+
<% } else { -%>
|
|
115
|
+
await userController.createUser(mockRequest as Request, mockResponse as Response, mockNext);
|
|
116
|
+
|
|
117
|
+
// Assert
|
|
118
|
+
expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.CREATED);
|
|
119
|
+
expect(mockResponse.json).toHaveBeenCalledWith(expectedUser);
|
|
120
|
+
<% } -%>
|
|
121
|
+
expect(mockCreateUserUseCase.execute).toHaveBeenCalledWith(payload.name, payload.email);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle errors when creation fails (Error Handling)', async () => {
|
|
125
|
+
// Arrange
|
|
126
|
+
const error = new Error('Creation Error');
|
|
127
|
+
const payload = { name: 'Bob', email: 'bob@example.com' };
|
|
128
|
+
<% if (communication === 'GraphQL') { -%>
|
|
129
|
+
const dataArg = payload;
|
|
130
|
+
<% } else { -%>
|
|
131
|
+
mockRequest.body = payload;
|
|
132
|
+
<% } -%>
|
|
133
|
+
|
|
134
|
+
mockCreateUserUseCase.execute.mockRejectedValue(error);
|
|
135
|
+
|
|
136
|
+
// Act & Assert
|
|
137
|
+
<% if (communication === 'GraphQL') { -%>
|
|
138
|
+
await expect(userController.createUser(dataArg)).rejects.toThrow(error);
|
|
139
|
+
<% } else { -%>
|
|
140
|
+
await userController.createUser(mockRequest as Request, mockResponse as Response, mockNext);
|
|
141
|
+
expect(mockNext).toHaveBeenCalledWith(error);
|
|
142
|
+
<% } -%>
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle non-Error objects in catch block when creation fails', async () => {
|
|
146
|
+
// Arrange
|
|
147
|
+
const error = 'Creation String Error';
|
|
148
|
+
const payload = { name: 'Bob', email: 'bob@example.com' };
|
|
149
|
+
<% if (communication === 'GraphQL') { -%>
|
|
150
|
+
const dataArg = payload;
|
|
151
|
+
<% } else { -%>
|
|
152
|
+
mockRequest.body = payload;
|
|
153
|
+
<% } -%>
|
|
154
|
+
|
|
155
|
+
mockCreateUserUseCase.execute.mockRejectedValue(error);
|
|
156
|
+
|
|
157
|
+
// Act & Assert
|
|
158
|
+
<% if (communication === 'GraphQL') { -%>
|
|
159
|
+
await expect(userController.createUser(dataArg)).rejects.toEqual(error);
|
|
160
|
+
<% } else { -%>
|
|
161
|
+
await userController.createUser(mockRequest as Request, mockResponse as Response, mockNext);
|
|
162
|
+
expect(mockNext).toHaveBeenCalledWith(error);
|
|
163
|
+
<% } -%>
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { gqlContext } from '@/interfaces/graphql/context';
|
|
2
|
+
import { Request } from 'express';
|
|
3
|
+
import { resolvers } from '@/interfaces/graphql/resolvers';
|
|
4
|
+
import { typeDefs } from '@/interfaces/graphql/typeDefs';
|
|
5
|
+
|
|
6
|
+
jest.mock('@/infrastructure/repositories/UserRepository');
|
|
7
|
+
|
|
8
|
+
describe('GraphQL Context', () => {
|
|
9
|
+
it('should exercise GraphQL index entry points', () => {
|
|
10
|
+
expect(resolvers).toBeDefined();
|
|
11
|
+
expect(typeDefs).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
it('should return context with token when authorization header is present', async () => {
|
|
14
|
+
const mockRequest = {
|
|
15
|
+
headers: {
|
|
16
|
+
authorization: 'Bearer token123',
|
|
17
|
+
},
|
|
18
|
+
} as Request;
|
|
19
|
+
|
|
20
|
+
const context = await gqlContext({ req: mockRequest });
|
|
21
|
+
expect(context.token).toBe('Bearer token123');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should return context with empty token when authorization header is missing', async () => {
|
|
25
|
+
const mockRequest = {
|
|
26
|
+
headers: {},
|
|
27
|
+
} as Request;
|
|
28
|
+
|
|
29
|
+
const context = await gqlContext({ req: mockRequest });
|
|
30
|
+
expect(context.token).toBe('');
|
|
31
|
+
});
|
|
32
|
+
});
|
package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { userResolvers } from '@/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 {
|
|
8
|
+
UserController: jest.fn().mockImplementation(() => ({
|
|
9
|
+
getUsers: (...args: unknown[]) => mockGetUsers(...args),
|
|
10
|
+
createUser: (...args: unknown[]) => mockCreateUser(...args)
|
|
11
|
+
}))
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('User Resolvers', () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('Query.getAllUsers', () => {
|
|
21
|
+
it('should return all users', async () => {
|
|
22
|
+
const result = await userResolvers.Query.getAllUsers();
|
|
23
|
+
expect(result).toEqual([{ id: '1', name: 'John Doe', email: 'john@example.com' }]);
|
|
24
|
+
expect(mockGetUsers).toHaveBeenCalledTimes(1);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('Mutation.createUser', () => {
|
|
29
|
+
it('should create and return a new user', async () => {
|
|
30
|
+
const result = await userResolvers.Mutation.createUser(null, { name: 'Jane', email: 'jane@example.com' });
|
|
31
|
+
expect(result).toEqual({ id: '1', name: 'Jane', email: 'jane@example.com' });
|
|
32
|
+
expect(mockCreateUser).toHaveBeenCalledWith({ name: 'Jane', email: 'jane@example.com' });
|
|
33
|
+
expect(mockCreateUser).toHaveBeenCalledTimes(1);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
37
|
+
describe('User.id', () => {
|
|
38
|
+
it('should return parent.id if available', () => {
|
|
39
|
+
const parent = { id: '123' };
|
|
40
|
+
const result = userResolvers.User.id(parent as { id?: string; _id?: unknown });
|
|
41
|
+
expect(result).toBe('123');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should fallback to parent._id if id is not available', () => {
|
|
45
|
+
const parent = { _id: '456' };
|
|
46
|
+
const result = userResolvers.User.id(parent as { id?: string; _id?: unknown });
|
|
47
|
+
expect(result).toBe('456');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
<%_ } -%>
|
|
51
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import express, { Express } from 'express';
|
|
3
|
+
import router from '@/interfaces/routes/userRoutes';
|
|
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 {
|
|
10
|
+
UserController: jest.fn().mockImplementation(() => ({
|
|
11
|
+
getUsers: (...args: unknown[]) => mockGetUsers(...args),
|
|
12
|
+
createUser: (...args: unknown[]) => mockCreateUser(...args)
|
|
13
|
+
}))
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('UserRoutes', () => {
|
|
18
|
+
let app: Express;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
app = express();
|
|
22
|
+
app.use(express.json());
|
|
23
|
+
app.use('/users', router);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('POST /users should call controller.createUser', async () => {
|
|
27
|
+
await request(app)
|
|
28
|
+
.post('/users')
|
|
29
|
+
.send({ name: 'Test', email: 'test@example.com' });
|
|
30
|
+
|
|
31
|
+
expect(mockCreateUser).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('GET /users should call controller.getUsers', async () => {
|
|
35
|
+
await request(app)
|
|
36
|
+
.get('/users');
|
|
37
|
+
|
|
38
|
+
expect(mockGetUsers).toHaveBeenCalledTimes(1);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import CreateUser from '@/usecases/createUser';
|
|
2
|
+
import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
3
|
+
<%_ if (caching !== 'None') { -%>
|
|
4
|
+
import cacheService from '<% 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: CreateUser;
|
|
18
|
+
let mockUserRepository: jest.Mocked<UserRepository>;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockUserRepository = new UserRepository() as jest.Mocked<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 as any);
|
|
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,63 @@
|
|
|
1
|
+
import GetAllUsers from '@/usecases/getAllUsers';
|
|
2
|
+
import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
3
|
+
<%_ if (caching !== 'None') { -%>
|
|
4
|
+
import cacheService from '<% 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: GetAllUsers;
|
|
18
|
+
let mockUserRepository: jest.Mocked<UserRepository>;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockUserRepository = new UserRepository() as jest.Mocked<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 as any);
|
|
29
|
+
<%_ if (caching !== 'None') { -%>
|
|
30
|
+
(cacheService.get as jest.Mock).mockResolvedValue(null);
|
|
31
|
+
<%_ } %>
|
|
32
|
+
|
|
33
|
+
const result = await getAllUsers.execute();
|
|
34
|
+
|
|
35
|
+
expect(mockUserRepository.getUsers).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(result).toEqual(expectedUsers);
|
|
37
|
+
<%_ if (caching !== 'None') { -%>
|
|
38
|
+
expect(cacheService.set).toHaveBeenCalled();
|
|
39
|
+
<%_ } %>
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
<%_ if (caching !== 'None') { -%>
|
|
43
|
+
it('should return from cache if available', async () => {
|
|
44
|
+
const cachedUsers = [{ id: 1, name: 'Cached', email: 'cached@example.com' }];
|
|
45
|
+
(cacheService.get as jest.Mock).mockResolvedValue(cachedUsers);
|
|
46
|
+
|
|
47
|
+
const result = await getAllUsers.execute();
|
|
48
|
+
|
|
49
|
+
expect(mockUserRepository.getUsers).not.toHaveBeenCalled();
|
|
50
|
+
expect(result).toEqual(cachedUsers);
|
|
51
|
+
});
|
|
52
|
+
<%_ } -%>
|
|
53
|
+
|
|
54
|
+
it('should throw an error if repository fails', async () => {
|
|
55
|
+
const error = new Error('Database error');
|
|
56
|
+
mockUserRepository.getUsers.mockRejectedValue(error);
|
|
57
|
+
<%_ if (caching !== 'None') { -%>
|
|
58
|
+
(cacheService.get as jest.Mock).mockResolvedValue(null);
|
|
59
|
+
<%_ } -%>
|
|
60
|
+
|
|
61
|
+
await expect(getAllUsers.execute()).rejects.toThrow(error);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -3,8 +3,7 @@ import logger from '@/infrastructure/log/logger';
|
|
|
3
3
|
import { ApiError } from '@/errors/ApiError';
|
|
4
4
|
import { HTTP_STATUS } from '@/utils/httpCodes';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
export const errorMiddleware = (err: Error, req: Request, res: Response, next: unknown) => {
|
|
6
|
+
export const errorMiddleware = (err: Error, req: Request, res: Response, _next: unknown) => {
|
|
8
7
|
let error = err;
|
|
9
8
|
|
|
10
9
|
if (!(error instanceof ApiError)) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Cursor AI Coding Rules for <%= projectName %>
|
|
2
|
+
|
|
3
|
+
## Project Context
|
|
4
|
+
You are an expert working on **<%= projectName %>**.
|
|
5
|
+
- **Project Goal**: [Replace this with your business logic, e.g., E-commerce API]
|
|
6
|
+
- **Language**: <%= language %>
|
|
7
|
+
- **Architecture**: <%= architecture %>
|
|
8
|
+
- **Database**: <%= database %>
|
|
9
|
+
- **Communication**: <%= communication %>
|
|
10
|
+
|
|
11
|
+
## Excluded Files/Folders
|
|
12
|
+
When indexing or searching the workspace, ignore the following paths to prevent context pollution:
|
|
13
|
+
- `node_modules/`
|
|
14
|
+
- `dist/`
|
|
15
|
+
- `build/`
|
|
16
|
+
- `coverage/`
|
|
17
|
+
- `.git/`
|
|
18
|
+
|
|
19
|
+
## Strict Rules
|
|
20
|
+
|
|
21
|
+
### 1. Testing First
|
|
22
|
+
- Every new service or controller method MUST have a test file in `tests/`.
|
|
23
|
+
- **Coverage Gate**: Aim for > 70% coverage (Statement/Line/Function/Branch).
|
|
24
|
+
- **Format**: Use Jest with the AAA (Arrange, Act, Assert) pattern.
|
|
25
|
+
- **Isolation**: Mock external dependencies (DB, Redis, etc.) using `jest.mock()`.
|
|
26
|
+
|
|
27
|
+
### 2. Error Handling
|
|
28
|
+
- Do NOT use generic `Error`.
|
|
29
|
+
- Use custom classes from `src/errors/` (e.g., `ApiError`, `NotFoundError`, `BadRequestError`).
|
|
30
|
+
<% if (language === 'TypeScript') { -%>
|
|
31
|
+
- Use `HTTP_STATUS` constants from `@/utils/httpCodes` for status codes.
|
|
32
|
+
<% } else { -%>
|
|
33
|
+
- Use `HTTP_STATUS` constants from `../utils/httpCodes.js` for status codes.
|
|
34
|
+
<% } -%>
|
|
35
|
+
|
|
36
|
+
### 3. File Naming & Style
|
|
37
|
+
- **Controllers**: camelCase (e.g., `userController.<% if (language === 'TypeScript') { %>ts<% } else { %>js<% } %>`).
|
|
38
|
+
- **Services**: camelCase (e.g., `userService.<% if (language === 'TypeScript') { %>ts<% } else { %>js<% } %>`).
|
|
39
|
+
- **Routes**: camelCase (e.g., `userRoutes.<% if (language === 'TypeScript') { %>ts<% } else { %>js<% } %>`).
|
|
40
|
+
<% if (language === 'TypeScript') { -%>
|
|
41
|
+
- **Imports**: Use path aliases (e.g., `@/services/...`) instead of relative paths.
|
|
42
|
+
- **Typing**: Ensure strong typing for interfaces and DTOs. Do not use `any` unless absolutely necessary.
|
|
43
|
+
<% } else { -%>
|
|
44
|
+
- **Imports**: Use relative paths as dictated by the directory structure.
|
|
45
|
+
<% } -%>
|
|
46
|
+
|
|
47
|
+
### 4. Architecture Standards
|
|
48
|
+
<% if (architecture === 'Clean Architecture') { -%>
|
|
49
|
+
- Enforce strict separation of concerns:
|
|
50
|
+
- `domain`: Entities and enterprise business rules.
|
|
51
|
+
- `usecases`: Application business rules.
|
|
52
|
+
- `interfaces`: Controllers and Routes.
|
|
53
|
+
- `infrastructure`: Frameworks, Database, Caching, and Web Server.
|
|
54
|
+
- Dependencies point inward toward the `domain`.
|
|
55
|
+
<% } else { -%>
|
|
56
|
+
- Enforce MVC standards:
|
|
57
|
+
- `models`: Data layer.
|
|
58
|
+
- `controllers`: Request handlers and business logic.
|
|
59
|
+
- `routes`: Define endpoints routing to controllers.
|
|
60
|
+
<% } -%>
|
|
@@ -12,24 +12,24 @@ cache:
|
|
|
12
12
|
|
|
13
13
|
install_dependencies:
|
|
14
14
|
stage: .pre
|
|
15
|
-
image: node:
|
|
15
|
+
image: node:22-alpine
|
|
16
16
|
script:
|
|
17
17
|
- npm ci
|
|
18
18
|
|
|
19
19
|
lint_code:
|
|
20
20
|
stage: lint
|
|
21
|
-
image: node:
|
|
21
|
+
image: node:22-alpine
|
|
22
22
|
script:
|
|
23
23
|
- npm run lint
|
|
24
24
|
|
|
25
25
|
run_tests:
|
|
26
26
|
stage: test
|
|
27
|
-
image: node:
|
|
27
|
+
image: node:22-alpine
|
|
28
28
|
script:
|
|
29
|
-
- npm test
|
|
29
|
+
- npm run test:coverage
|
|
30
30
|
|
|
31
31
|
build_app:
|
|
32
32
|
stage: build
|
|
33
|
-
image: node:
|
|
33
|
+
image: node:22-alpine
|
|
34
34
|
script:
|
|
35
35
|
- npm run build --if-present
|
|
@@ -217,4 +217,14 @@ docker-compose down
|
|
|
217
217
|
- **Helmet**: Sets secure HTTP headers.
|
|
218
218
|
- **CORS**: Configured for cross-origin requests.
|
|
219
219
|
- **Rate Limiting**: Protects against DDoS / Brute-force.
|
|
220
|
-
- **HPP**: Prevents HTTP Parameter Pollution attacks.
|
|
220
|
+
- **HPP**: Prevents HTTP Parameter Pollution attacks.
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
## 🤖 AI-Native Development
|
|
224
|
+
|
|
225
|
+
This project is "AI-Ready" out of the box. We have pre-configured industry-leading AI context files to bridge the gap between "Generated Code" and "AI-Assisted Development."
|
|
226
|
+
|
|
227
|
+
- **Magic Defaults**: We've automatically tailored your AI context to focus on **<%= projectName %>** and its specific architectural stack (<%= architecture %>, <%= database %>, etc.).
|
|
228
|
+
- **Use Cursor?** We've configured **`.cursorrules`** at the root. It enforces project standards (70% coverage, MVC/Clean) directly within the editor.
|
|
229
|
+
- *Pro-tip*: You can customize the `Project Goal` placeholder in `.cursorrules` to help the AI understand your specific business logic!
|
|
230
|
+
- **Use ChatGPT/Gemini/Claude?** Check the **`prompts/`** directory. It contains highly-specialized Agent Skill templates. You can copy-paste these into any LLM to give it a "Senior Developer" understanding of your codebase immediately.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
name: Node.js CI
|
|
2
2
|
|
|
3
|
+
env:
|
|
4
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
|
5
|
+
|
|
3
6
|
on:
|
|
4
7
|
push:
|
|
5
8
|
branches: [ "main" ]
|
|
@@ -13,13 +16,13 @@ jobs:
|
|
|
13
16
|
|
|
14
17
|
strategy:
|
|
15
18
|
matrix:
|
|
16
|
-
node-version: [
|
|
19
|
+
node-version: [20.x, 22.x]
|
|
17
20
|
|
|
18
21
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@v4
|
|
20
23
|
|
|
21
24
|
- name: Use Node.js ${{ matrix.node-version }}
|
|
22
|
-
uses: actions/setup-node@
|
|
25
|
+
uses: actions/setup-node@v4
|
|
23
26
|
with:
|
|
24
27
|
node-version: ${{ matrix.node-version }}
|
|
25
28
|
cache: 'npm'
|
|
@@ -31,7 +34,7 @@ jobs:
|
|
|
31
34
|
run: npm run lint
|
|
32
35
|
|
|
33
36
|
- name: Run Tests
|
|
34
|
-
run: npm test
|
|
37
|
+
run: npm run test:coverage
|
|
35
38
|
|
|
36
39
|
- name: Build
|
|
37
40
|
run: npm run build --if-present
|