nodejs-quickstart-structure 2.0.1 → 2.1.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 +25 -0
- package/README.md +64 -66
- package/bin/index.js +5 -2
- package/lib/generator.js +10 -4
- package/lib/modules/app-setup.js +76 -6
- package/lib/modules/auth-setup.js +143 -0
- package/lib/modules/caching-setup.js +8 -1
- package/lib/modules/config-files.js +6 -0
- package/lib/modules/database-setup.js +2 -1
- package/lib/modules/project-setup.js +1 -0
- package/lib/prompts.js +39 -0
- package/package.json +5 -4
- package/templates/clean-architecture/js/src/domain/models/User.js +3 -1
- package/templates/clean-architecture/js/src/index.js.ejs +2 -0
- package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -3
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +25 -2
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +38 -1
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +3 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.spec.js.ejs +51 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/swagger.spec.js.ejs +14 -0
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +41 -4
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +70 -5
- package/templates/clean-architecture/js/src/interfaces/graphql/context.js.ejs +13 -6
- package/templates/clean-architecture/js/src/interfaces/graphql/context.spec.js.ejs +55 -22
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +10 -5
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs +32 -10
- package/templates/clean-architecture/js/src/interfaces/graphql/typeDefs/user.types.js.ejs +1 -1
- package/templates/clean-architecture/js/src/interfaces/routes/api.js.ejs +15 -0
- package/templates/clean-architecture/js/src/interfaces/routes/api.spec.js.ejs +4 -0
- package/templates/clean-architecture/js/src/usecases/CreateUser.js.ejs +34 -0
- package/templates/clean-architecture/js/src/usecases/CreateUser.spec.js.ejs +12 -3
- package/templates/clean-architecture/js/src/usecases/DeleteUser.js.ejs +27 -0
- package/templates/clean-architecture/js/src/usecases/DeleteUser.spec.js.ejs +9 -1
- package/templates/clean-architecture/js/src/usecases/GetAllUsers.js.ejs +36 -0
- package/templates/clean-architecture/js/src/usecases/GetAllUsers.spec.js.ejs +23 -1
- package/templates/clean-architecture/js/src/usecases/GetUserById.js.ejs +36 -0
- package/templates/clean-architecture/js/src/usecases/GetUserById.spec.js.ejs +48 -0
- package/templates/clean-architecture/js/src/usecases/UpdateUser.js.ejs +28 -0
- package/templates/clean-architecture/js/src/usecases/UpdateUser.spec.js.ejs +9 -1
- package/templates/clean-architecture/js/src/utils/errorMessages.js +1 -0
- package/templates/clean-architecture/js/src/utils/httpCodes.js +2 -0
- package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -3
- package/templates/clean-architecture/ts/src/domain/user.ts +3 -1
- package/templates/clean-architecture/ts/src/index.ts.ejs +4 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +71 -10
- package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +32 -3
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +43 -9
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +57 -15
- package/templates/clean-architecture/ts/src/interfaces/graphql/context.spec.ts.ejs +57 -24
- package/templates/clean-architecture/ts/src/interfaces/graphql/context.ts.ejs +14 -8
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.spec.ts.ejs +33 -10
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +15 -5
- package/templates/clean-architecture/ts/src/interfaces/graphql/typeDefs/user.types.ts.ejs +1 -1
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.spec.ts.ejs +9 -1
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts.ejs +16 -0
- package/templates/clean-architecture/ts/src/usecases/createUser.spec.ts.ejs +12 -3
- package/templates/clean-architecture/ts/src/usecases/createUser.ts.ejs +35 -0
- package/templates/clean-architecture/ts/src/usecases/deleteUser.spec.ts.ejs +10 -1
- package/templates/clean-architecture/ts/src/usecases/deleteUser.ts.ejs +24 -0
- package/templates/clean-architecture/ts/src/usecases/getAllUsers.spec.ts.ejs +9 -1
- package/templates/clean-architecture/ts/src/usecases/getAllUsers.ts.ejs +21 -0
- package/templates/clean-architecture/ts/src/usecases/getUserById.spec.ts.ejs +55 -0
- package/templates/clean-architecture/ts/src/usecases/getUserById.ts.ejs +23 -0
- package/templates/clean-architecture/ts/src/usecases/updateUser.spec.ts.ejs +10 -1
- package/templates/clean-architecture/ts/src/usecases/updateUser.ts.ejs +25 -0
- package/templates/clean-architecture/ts/src/utils/errorMessages.ts +1 -0
- package/templates/clean-architecture/ts/src/utils/httpCodes.ts +1 -0
- package/templates/common/.cursorrules.ejs +9 -0
- package/templates/common/.env.example.ejs +17 -10
- package/templates/common/README.md.ejs +63 -18
- package/templates/common/auth/js/controllers/authController.js.ejs +170 -0
- package/templates/common/auth/js/controllers/authController.spec.js.ejs +148 -0
- package/templates/common/auth/js/middleware/authMiddleware.js.ejs +58 -0
- package/templates/common/auth/js/middleware/authMiddleware.spec.js.ejs +108 -0
- package/templates/common/auth/js/routes/authRoutes.js.ejs +16 -0
- package/templates/common/auth/js/services/jwtService.js.ejs +54 -0
- package/templates/common/auth/js/services/jwtService.spec.js.ejs +84 -0
- package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +161 -0
- package/templates/common/auth/ts/controllers/authController.ts.ejs +167 -0
- package/templates/common/auth/ts/middleware/authMiddleware.spec.ts.ejs +128 -0
- package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +59 -0
- package/templates/common/auth/ts/routes/authRoutes.ts.ejs +20 -0
- package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +89 -0
- package/templates/common/auth/ts/services/jwtService.ts.ejs +60 -0
- package/templates/common/babel.config.js.ejs +5 -0
- package/templates/common/caching/clean/js/CreateUser.js.ejs +14 -5
- package/templates/common/caching/clean/js/DeleteUser.js.ejs +2 -1
- package/templates/common/caching/clean/js/GetUserById.js.ejs +39 -0
- package/templates/common/caching/clean/js/UpdateUser.js.ejs +2 -1
- package/templates/common/caching/clean/ts/createUser.ts.ejs +14 -6
- package/templates/common/caching/clean/ts/deleteUser.ts.ejs +2 -1
- package/templates/common/caching/clean/ts/getUserById.ts.ejs +32 -0
- package/templates/common/caching/clean/ts/updateUser.ts.ejs +2 -1
- package/templates/common/caching/js/memoryCache.spec.js.ejs +2 -0
- package/templates/common/caching/js/redisClient.spec.js.ejs +2 -0
- package/templates/common/caching/ts/memoryCache.spec.ts.ejs +2 -0
- package/templates/common/caching/ts/redisClient.spec.ts.ejs +2 -0
- package/templates/common/database/js/models/User.js.ejs +14 -1
- package/templates/common/database/js/models/User.js.mongoose.ejs +7 -0
- package/templates/common/database/js/models/User.spec.js.ejs +12 -0
- package/templates/common/database/js/mongoose.spec.js.ejs +2 -0
- package/templates/common/database/ts/models/User.spec.ts.ejs +10 -0
- package/templates/common/database/ts/models/User.ts.ejs +17 -0
- package/templates/common/database/ts/models/User.ts.mongoose.ejs +8 -0
- package/templates/common/database/ts/mongoose.spec.ts.ejs +2 -0
- package/templates/common/docker-compose.yml.ejs +12 -0
- package/templates/common/ecosystem.config.js.ejs +9 -3
- package/templates/common/eslint.config.mjs.ejs +3 -0
- package/templates/common/jest.config.js.ejs +13 -9
- package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +1 -1
- package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
- package/templates/common/migrations/init.js.ejs +5 -4
- package/templates/common/package.json.ejs +11 -2
- package/templates/common/prompts/project-context.md.ejs +8 -1
- package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +149 -107
- package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +88 -47
- package/templates/common/swagger.yml.ejs +148 -0
- package/templates/common/tsconfig.eslint.json +15 -0
- package/templates/common/tsconfig.json +3 -1
- package/templates/common/views/ejs/index.ejs +264 -30
- package/templates/common/views/ejs/login.ejs.ejs +244 -0
- package/templates/common/views/ejs/signup.ejs.ejs +282 -0
- package/templates/common/views/pug/index.pug +269 -38
- package/templates/common/views/pug/login.pug.ejs +195 -0
- package/templates/common/views/pug/signup.pug.ejs +241 -0
- package/templates/db/mysql/V1__Initial_Setup.sql.ejs +6 -0
- package/templates/db/postgres/V1__Initial_Setup.sql.ejs +6 -0
- package/templates/mvc/js/src/config/env.js.ejs +12 -3
- package/templates/mvc/js/src/controllers/userController.js.ejs +29 -5
- package/templates/mvc/js/src/controllers/userController.spec.js.ejs +27 -12
- package/templates/mvc/js/src/graphql/context.js.ejs +14 -3
- package/templates/mvc/js/src/graphql/context.spec.js.ejs +36 -21
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +10 -5
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.spec.js.ejs +32 -10
- package/templates/mvc/js/src/graphql/typeDefs/user.types.js.ejs +1 -1
- package/templates/mvc/js/src/index.js.ejs +16 -3
- package/templates/mvc/js/src/routes/api.js.ejs +14 -0
- package/templates/mvc/js/src/routes/api.spec.js.ejs +3 -0
- package/templates/mvc/js/src/utils/errorMessages.js +1 -0
- package/templates/mvc/js/src/utils/httpCodes.js +1 -0
- package/templates/mvc/ts/src/config/env.ts.ejs +12 -3
- package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +95 -7
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +68 -11
- package/templates/mvc/ts/src/graphql/context.spec.ts.ejs +36 -23
- package/templates/mvc/ts/src/graphql/context.ts.ejs +15 -6
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.spec.ts.ejs +32 -10
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +15 -5
- package/templates/mvc/ts/src/graphql/typeDefs/user.types.ts.ejs +1 -1
- package/templates/mvc/ts/src/index.ts.ejs +15 -3
- package/templates/mvc/ts/src/routes/api.spec.ts.ejs +6 -0
- package/templates/mvc/ts/src/routes/api.ts.ejs +15 -0
- package/templates/mvc/ts/src/utils/errorMessages.ts +1 -0
- package/templates/mvc/ts/src/utils/httpCodes.ts +1 -0
- package/templates/clean-architecture/js/src/interfaces/routes/api.js +0 -12
- package/templates/clean-architecture/js/src/usecases/CreateUser.js +0 -14
- package/templates/clean-architecture/js/src/usecases/DeleteUser.js +0 -11
- package/templates/clean-architecture/js/src/usecases/GetAllUsers.js +0 -12
- package/templates/clean-architecture/js/src/usecases/UpdateUser.js +0 -11
- package/templates/clean-architecture/ts/src/interfaces/routes/userRoutes.ts +0 -13
- package/templates/clean-architecture/ts/src/usecases/createUser.ts +0 -13
- package/templates/clean-architecture/ts/src/usecases/deleteUser.ts +0 -9
- package/templates/clean-architecture/ts/src/usecases/getAllUsers.ts +0 -10
- package/templates/clean-architecture/ts/src/usecases/updateUser.ts +0 -9
- package/templates/mvc/js/src/routes/api.js +0 -10
- package/templates/mvc/ts/src/routes/api.ts +0 -12
|
@@ -10,7 +10,7 @@ export const userTypes = `#graphql
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
type Mutation {
|
|
13
|
-
createUser(name: String!, email: String
|
|
13
|
+
createUser(name: String!, email: String!<%_ if (auth.some(a => a !=='None')) { _%>, password: String!<%_ } _%>): User
|
|
14
14
|
updateUser(id: ID!, name: String, email: String): User
|
|
15
15
|
deleteUser(id: ID!): Boolean
|
|
16
16
|
}
|
|
@@ -4,12 +4,20 @@ import router from '@/interfaces/routes/userRoutes';
|
|
|
4
4
|
|
|
5
5
|
const mockGetUsers = jest.fn().mockImplementation((req, res) => res.status(200).json([{ id: '1', name: 'John Doe' }]));
|
|
6
6
|
const mockCreateUser = jest.fn().mockImplementation((req, res) => res.status(201).json({ id: '1', name: 'Test' }));
|
|
7
|
+
const mockGetUserById = jest.fn().mockImplementation((req, res) => res.status(200).json({ id: '1', name: 'Test' }));
|
|
8
|
+
|
|
9
|
+
<% if (auth.includes('JWT')) { -%>
|
|
10
|
+
jest.mock('@/infrastructure/webserver/middleware/authMiddleware', () => ({
|
|
11
|
+
authMiddleware: (req: any, res: any, next: any) => next()
|
|
12
|
+
}));
|
|
13
|
+
<% } -%>
|
|
7
14
|
|
|
8
15
|
jest.mock('@/interfaces/controllers/userController', () => {
|
|
9
16
|
return {
|
|
10
17
|
UserController: jest.fn().mockImplementation(() => ({
|
|
11
18
|
getUsers: (...args: unknown[]) => mockGetUsers(...args),
|
|
12
|
-
createUser: (...args: unknown[]) => mockCreateUser(...args)
|
|
19
|
+
createUser: (...args: unknown[]) => mockCreateUser(...args),
|
|
20
|
+
getUserById: (...args: unknown[]) => mockGetUserById(...args)
|
|
13
21
|
}))
|
|
14
22
|
};
|
|
15
23
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { UserController } from '@/interfaces/controllers/userController';
|
|
3
|
+
<%_ if (auth.includes('JWT')) { _%>
|
|
4
|
+
import { authMiddleware } from '@/infrastructure/webserver/middleware/authMiddleware';
|
|
5
|
+
<%_ } _%>
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
const userController = new UserController();
|
|
9
|
+
|
|
10
|
+
router.post('/', (req: Request, res: Response, next: NextFunction) => userController.createUser(req, res, next));
|
|
11
|
+
router.get('/', <% if (auth.includes('JWT')) { %>authMiddleware, <% } %>(req: Request, res: Response, next: NextFunction) => userController.getUsers(req, res, next));
|
|
12
|
+
router.get('/:id', <% if (auth.includes('JWT')) { %>authMiddleware, <% } %>(req: Request, res: Response, next: NextFunction) => userController.getUserById(req, res, next));
|
|
13
|
+
router.patch('/:id', <% if (auth.includes('JWT')) { %>authMiddleware, <% } %>(req: Request, res: Response, next: NextFunction) => userController.updateUser(req, res, next));
|
|
14
|
+
router.delete('/:id', <% if (auth.includes('JWT')) { %>authMiddleware, <% } %>(req: Request, res: Response, next: NextFunction) => userController.deleteUser(req, res, next));
|
|
15
|
+
|
|
16
|
+
export default router;
|
|
@@ -4,7 +4,15 @@ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
|
4
4
|
import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
|
|
5
5
|
<%_ } -%>
|
|
6
6
|
|
|
7
|
-
jest.mock('@/infrastructure/repositories/UserRepository')
|
|
7
|
+
jest.mock('@/infrastructure/repositories/UserRepository', () => ({
|
|
8
|
+
UserRepository: jest.fn().mockImplementation(() => ({
|
|
9
|
+
save: jest.fn(),
|
|
10
|
+
findById: jest.fn(),
|
|
11
|
+
getUsers: jest.fn(),
|
|
12
|
+
update: jest.fn(),
|
|
13
|
+
delete: jest.fn()
|
|
14
|
+
}))
|
|
15
|
+
}));
|
|
8
16
|
<%_ if (caching !== 'None') { -%>
|
|
9
17
|
jest.mock('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>', () => ({
|
|
10
18
|
get: jest.fn(),
|
|
@@ -26,11 +34,12 @@ describe('CreateUser UseCase', () => {
|
|
|
26
34
|
it('should create and save a new user', async () => {
|
|
27
35
|
const name = 'Test User';
|
|
28
36
|
const email = 'test@example.com';
|
|
37
|
+
const password = 'password123';
|
|
29
38
|
const expectedResult = { id: 1, name, email };
|
|
30
39
|
|
|
31
40
|
mockUserRepository.save.mockResolvedValue(expectedResult as any);
|
|
32
41
|
|
|
33
|
-
const result = await createUser.execute(name, email);
|
|
42
|
+
const result = await createUser.execute(name, email, password);
|
|
34
43
|
|
|
35
44
|
expect(mockUserRepository.save).toHaveBeenCalledTimes(1);
|
|
36
45
|
const savedUser = mockUserRepository.save.mock.calls[0][0];
|
|
@@ -46,6 +55,6 @@ describe('CreateUser UseCase', () => {
|
|
|
46
55
|
const error = new Error('Database error');
|
|
47
56
|
mockUserRepository.save.mockRejectedValue(error);
|
|
48
57
|
|
|
49
|
-
await expect(createUser.execute('Test', 'test@test.com')).rejects.toThrow(error);
|
|
58
|
+
await expect(createUser.execute('Test', 'test@test.com', 'pwd')).rejects.toThrow(error);
|
|
50
59
|
});
|
|
51
60
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { User } from '@/domain/user';
|
|
2
|
+
import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
3
|
+
<% if (auth.includes('JWT')) { -%>
|
|
4
|
+
import bcrypt from 'bcryptjs';
|
|
5
|
+
<% } -%>
|
|
6
|
+
<% if (caching !== 'None') { -%>
|
|
7
|
+
import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
|
|
8
|
+
<% } -%>
|
|
9
|
+
<% if (caching !== 'None') { -%>
|
|
10
|
+
import logger from '@/infrastructure/log/logger';
|
|
11
|
+
<% } -%>
|
|
12
|
+
|
|
13
|
+
export default class CreateUser {
|
|
14
|
+
constructor(private userRepository: UserRepository) {}
|
|
15
|
+
|
|
16
|
+
async execute(name: string, email: string, password?: string) {
|
|
17
|
+
let finalPassword = password;
|
|
18
|
+
<% if (auth.includes('JWT')) { -%>
|
|
19
|
+
if (password) {
|
|
20
|
+
finalPassword = await bcrypt.hash(password, 10);
|
|
21
|
+
}
|
|
22
|
+
<% } -%>
|
|
23
|
+
const user = new User(null, name, email, finalPassword);
|
|
24
|
+
const savedUser = await this.userRepository.save(user);
|
|
25
|
+
<% if (caching !== 'None') { -%>
|
|
26
|
+
try {
|
|
27
|
+
await cacheService.del('users:all');
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logger.warn('Failed to clear cache after user creation:', error);
|
|
30
|
+
}
|
|
31
|
+
<% } -%>
|
|
32
|
+
|
|
33
|
+
return savedUser;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -4,7 +4,15 @@ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
|
4
4
|
import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
|
|
5
5
|
<%_ } -%>
|
|
6
6
|
|
|
7
|
-
jest.mock('@/infrastructure/repositories/UserRepository')
|
|
7
|
+
jest.mock('@/infrastructure/repositories/UserRepository', () => ({
|
|
8
|
+
UserRepository: jest.fn().mockImplementation(() => ({
|
|
9
|
+
save: jest.fn(),
|
|
10
|
+
findById: jest.fn(),
|
|
11
|
+
getUsers: jest.fn(),
|
|
12
|
+
update: jest.fn(),
|
|
13
|
+
delete: jest.fn()
|
|
14
|
+
}))
|
|
15
|
+
}));
|
|
8
16
|
<%_ if (caching !== 'None') { -%>
|
|
9
17
|
jest.mock('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>', () => ({
|
|
10
18
|
get: jest.fn(),
|
|
@@ -35,6 +43,7 @@ describe('DeleteUser UseCase', () => {
|
|
|
35
43
|
expect(result).toEqual(expectedResult);
|
|
36
44
|
<%_ if (caching !== 'None') { -%>
|
|
37
45
|
expect(cacheService.del).toHaveBeenCalledWith('users:all');
|
|
46
|
+
expect(cacheService.del).toHaveBeenCalledWith(`user:${id}`);
|
|
38
47
|
<%_ } %>
|
|
39
48
|
});
|
|
40
49
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
2
|
+
<%_ if (caching !== 'None') { _%>
|
|
3
|
+
import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
|
|
4
|
+
import logger from '@/infrastructure/log/logger';
|
|
5
|
+
<%_ } _%>
|
|
6
|
+
|
|
7
|
+
export default class DeleteUser {
|
|
8
|
+
constructor(private userRepository: UserRepository) {}
|
|
9
|
+
|
|
10
|
+
async execute(id: number | string) {
|
|
11
|
+
const result = await this.userRepository.delete(id);
|
|
12
|
+
<%_ if (caching !== 'None') { -%>
|
|
13
|
+
if (result) {
|
|
14
|
+
try {
|
|
15
|
+
await cacheService.del('users:all');
|
|
16
|
+
await cacheService.del(`user:${id}`);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
logger.warn('Failed to clear cache after user deletion:', error);
|
|
19
|
+
}
|
|
20
|
+
}<%_ } -%>
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -4,7 +4,15 @@ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
|
4
4
|
import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
|
|
5
5
|
<%_ } -%>
|
|
6
6
|
|
|
7
|
-
jest.mock('@/infrastructure/repositories/UserRepository')
|
|
7
|
+
jest.mock('@/infrastructure/repositories/UserRepository', () => ({
|
|
8
|
+
UserRepository: jest.fn().mockImplementation(() => ({
|
|
9
|
+
save: jest.fn(),
|
|
10
|
+
findById: jest.fn(),
|
|
11
|
+
getUsers: jest.fn(),
|
|
12
|
+
update: jest.fn(),
|
|
13
|
+
delete: jest.fn()
|
|
14
|
+
}))
|
|
15
|
+
}));
|
|
8
16
|
<%_ if (caching !== 'None') { -%>
|
|
9
17
|
jest.mock('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>', () => ({
|
|
10
18
|
get: jest.fn(),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
2
|
+
<%_ if (caching !== 'None') { _%>
|
|
3
|
+
import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
|
|
4
|
+
<%_ } _%>
|
|
5
|
+
|
|
6
|
+
export default class GetAllUsers {
|
|
7
|
+
constructor(private userRepository: UserRepository) {}
|
|
8
|
+
|
|
9
|
+
async execute() {
|
|
10
|
+
<%_ if (caching !== 'None') { -%>
|
|
11
|
+
const cachedUsers = await cacheService.get('users:all');
|
|
12
|
+
if (cachedUsers) return cachedUsers;
|
|
13
|
+
<%_ } -%>
|
|
14
|
+
const users = await this.userRepository.getUsers();
|
|
15
|
+
<%_ if (caching !== 'None') { -%>
|
|
16
|
+
await cacheService.set('users:all', users, 3600); // Cache for 1 hour
|
|
17
|
+
<%_ } -%>
|
|
18
|
+
|
|
19
|
+
return users;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import GetUserById from '@/usecases/getUserById';
|
|
2
|
+
import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
3
|
+
|
|
4
|
+
jest.mock('@/infrastructure/repositories/UserRepository', () => ({
|
|
5
|
+
UserRepository: jest.fn().mockImplementation(() => ({
|
|
6
|
+
save: jest.fn(),
|
|
7
|
+
findById: jest.fn(),
|
|
8
|
+
getUsers: jest.fn(),
|
|
9
|
+
update: jest.fn(),
|
|
10
|
+
delete: jest.fn()
|
|
11
|
+
}))
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
<% if (caching === 'Redis') { -%>
|
|
15
|
+
jest.mock('@/infrastructure/caching/redisClient', () => ({
|
|
16
|
+
get: jest.fn(),
|
|
17
|
+
set: jest.fn(),
|
|
18
|
+
del: jest.fn()
|
|
19
|
+
}));
|
|
20
|
+
<% } else if (caching === 'Memory Cache') { -%>
|
|
21
|
+
jest.mock('@/infrastructure/caching/memoryCache', () => ({
|
|
22
|
+
get: jest.fn(),
|
|
23
|
+
set: jest.fn(),
|
|
24
|
+
del: jest.fn()
|
|
25
|
+
}));
|
|
26
|
+
<% } -%>
|
|
27
|
+
|
|
28
|
+
describe('GetUserById Use Case', () => {
|
|
29
|
+
let getUserById: GetUserById;
|
|
30
|
+
let userRepository: jest.Mocked<UserRepository>;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
userRepository = new UserRepository() as jest.Mocked<UserRepository>;
|
|
34
|
+
getUserById = new GetUserById(userRepository);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return a user if found', async () => {
|
|
38
|
+
const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' };
|
|
39
|
+
userRepository.findById.mockResolvedValue(mockUser);
|
|
40
|
+
|
|
41
|
+
const result = await getUserById.execute('1');
|
|
42
|
+
|
|
43
|
+
expect(result).toEqual(mockUser);
|
|
44
|
+
expect(userRepository.findById).toHaveBeenCalledWith('1');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return null if user is not found', async () => {
|
|
48
|
+
userRepository.findById.mockResolvedValue(null);
|
|
49
|
+
|
|
50
|
+
const result = await getUserById.execute('999');
|
|
51
|
+
|
|
52
|
+
expect(result).toBeNull();
|
|
53
|
+
expect(userRepository.findById).toHaveBeenCalledWith('999');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
2
|
+
<%_ if (caching !== 'None') { _%>
|
|
3
|
+
import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
|
|
4
|
+
<%_ } _%>
|
|
5
|
+
|
|
6
|
+
export default class GetUserById {
|
|
7
|
+
constructor(private userRepository: UserRepository) {}
|
|
8
|
+
|
|
9
|
+
async execute(id: string | number) {
|
|
10
|
+
<%_ if (caching !== 'None') { -%>
|
|
11
|
+
const cacheKey = `user:${id}`;
|
|
12
|
+
const cachedUser = await cacheService.get(cacheKey);
|
|
13
|
+
if (cachedUser) return cachedUser;
|
|
14
|
+
<%_ } -%>
|
|
15
|
+
const user = await this.userRepository.findById(id);
|
|
16
|
+
<%_ if (caching !== 'None') { -%>
|
|
17
|
+
if (user) {
|
|
18
|
+
await cacheService.set(`user:${id}`, user, 3600); // Cache for 1 hour
|
|
19
|
+
}<%_ } -%>
|
|
20
|
+
|
|
21
|
+
return user;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -4,7 +4,15 @@ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
|
4
4
|
import cacheService from '<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>';
|
|
5
5
|
<%_ } -%>
|
|
6
6
|
|
|
7
|
-
jest.mock('@/infrastructure/repositories/UserRepository')
|
|
7
|
+
jest.mock('@/infrastructure/repositories/UserRepository', () => ({
|
|
8
|
+
UserRepository: jest.fn().mockImplementation(() => ({
|
|
9
|
+
save: jest.fn(),
|
|
10
|
+
findById: jest.fn(),
|
|
11
|
+
getUsers: jest.fn(),
|
|
12
|
+
update: jest.fn(),
|
|
13
|
+
delete: jest.fn()
|
|
14
|
+
}))
|
|
15
|
+
}));
|
|
8
16
|
<%_ if (caching !== 'None') { -%>
|
|
9
17
|
jest.mock('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>', () => ({
|
|
10
18
|
get: jest.fn(),
|
|
@@ -36,6 +44,7 @@ describe('UpdateUser UseCase', () => {
|
|
|
36
44
|
expect(result).toEqual(expectedUser);
|
|
37
45
|
<%_ if (caching !== 'None') { -%>
|
|
38
46
|
expect(cacheService.del).toHaveBeenCalledWith('users:all');
|
|
47
|
+
expect(cacheService.del).toHaveBeenCalledWith(`user:${id}`);
|
|
39
48
|
<%_ } %>
|
|
40
49
|
});
|
|
41
50
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { User } from '@/domain/user';
|
|
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
|
+
import logger from '@/infrastructure/log/logger';
|
|
6
|
+
<%_ } _%>
|
|
7
|
+
|
|
8
|
+
export default class UpdateUser {
|
|
9
|
+
constructor(private userRepository: UserRepository) {}
|
|
10
|
+
|
|
11
|
+
async execute(id: number | string, data: Partial<User>) {
|
|
12
|
+
const user = await this.userRepository.update(id, data);
|
|
13
|
+
<%_ if (caching !== 'None') { -%>
|
|
14
|
+
if (user) {
|
|
15
|
+
try {
|
|
16
|
+
await cacheService.del('users:all');
|
|
17
|
+
await cacheService.del(`user:${id}`);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
logger.warn('Failed to clear cache after user update:', error);
|
|
20
|
+
}
|
|
21
|
+
}<%_ } -%>
|
|
22
|
+
|
|
23
|
+
return user;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -5,6 +5,7 @@ export const ERROR_MESSAGES = {
|
|
|
5
5
|
INTERNAL_SERVER_ERROR: 'Internal Server Error',
|
|
6
6
|
BAD_REQUEST: 'Bad Request',
|
|
7
7
|
FETCH_USERS_ERROR: 'Error fetching users',
|
|
8
|
+
FETCH_USER_ERROR: 'Error fetching user',
|
|
8
9
|
CREATE_USER_ERROR: 'Error creating user',
|
|
9
10
|
UPDATE_USER_ERROR: 'Error updating user',
|
|
10
11
|
DELETE_USER_ERROR: 'Error deleting user',
|
|
@@ -58,3 +58,12 @@ When indexing or searching the workspace, ignore the following paths to prevent
|
|
|
58
58
|
- `controllers`: Request handlers and business logic.
|
|
59
59
|
- `routes`: Define endpoints routing to controllers.
|
|
60
60
|
<% } -%>
|
|
61
|
+
|
|
62
|
+
### 5. Security & Authentication
|
|
63
|
+
<% if (auth.includes('JWT')) { -%>
|
|
64
|
+
- **Protected Routes**: When adding new routes, consider if they need `authMiddleware`.
|
|
65
|
+
- **User Identity**: Extract user info from `req.user` (populated by middleware).
|
|
66
|
+
- **Sensitive Data**: Never return passwords in API responses (ensure `.toJSON()` or DTOs handle this).
|
|
67
|
+
<% } -%>
|
|
68
|
+
- **Input Validation**: Always validate user input using schemas or controller-level checks.
|
|
69
|
+
- **Sanitization**: Ensure inputs are sanitized before being used in DB queries (though our ORMs like Sequelize/Mongoose handle much of this).
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Application
|
|
2
2
|
PORT=3000
|
|
3
3
|
NODE_ENV=development
|
|
4
|
-
|
|
5
4
|
<%_ if (database !== 'None') { -%>
|
|
5
|
+
|
|
6
6
|
# Database
|
|
7
7
|
<%_ if (database === 'MySQL') { -%>
|
|
8
8
|
DB_HOST=localhost
|
|
@@ -10,32 +10,39 @@ DB_PORT=3306
|
|
|
10
10
|
DB_USER=root
|
|
11
11
|
DB_PASSWORD=root
|
|
12
12
|
DB_NAME=<%= dbName %>
|
|
13
|
-
<%
|
|
13
|
+
<% } -%>
|
|
14
14
|
<%_ if (database === 'PostgreSQL') { -%>
|
|
15
15
|
DB_HOST=localhost
|
|
16
16
|
DB_PORT=5432
|
|
17
17
|
DB_USER=postgres
|
|
18
18
|
DB_PASSWORD=root
|
|
19
19
|
DB_NAME=<%= dbName %>
|
|
20
|
-
<%
|
|
20
|
+
<% } -%>
|
|
21
21
|
<%_ if (database === 'MongoDB') { -%>
|
|
22
22
|
DB_HOST=localhost
|
|
23
23
|
DB_PORT=27017
|
|
24
24
|
DB_NAME=<%= dbName %>
|
|
25
|
-
<%
|
|
26
|
-
<%
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
<% } -%>
|
|
26
|
+
<% } -%>
|
|
29
27
|
<%_ if (communication === 'Kafka') { -%>
|
|
28
|
+
|
|
30
29
|
# Communication
|
|
31
30
|
KAFKA_BROKER=localhost:9093
|
|
32
31
|
KAFKA_CLIENT_ID=<%= projectName %>
|
|
33
32
|
KAFKA_GROUP_ID=<%= projectName %>-group
|
|
34
|
-
<%
|
|
35
|
-
|
|
33
|
+
<% } -%>
|
|
36
34
|
<%_ if (caching === 'Redis') { -%>
|
|
35
|
+
|
|
37
36
|
# Caching
|
|
38
37
|
REDIS_HOST=localhost
|
|
39
38
|
REDIS_PORT=6379
|
|
40
39
|
REDIS_PASSWORD=
|
|
41
|
-
<%
|
|
40
|
+
<% } -%>
|
|
41
|
+
<%_ if (auth.includes('JWT')) { -%>
|
|
42
|
+
|
|
43
|
+
# Authentication
|
|
44
|
+
JWT_SECRET=your_jwt_secret_key_here_change_it
|
|
45
|
+
JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here_change_it
|
|
46
|
+
JWT_EXPIRES_IN=15m
|
|
47
|
+
JWT_REFRESH_EXPIRES_IN=7d
|
|
48
|
+
<% } -%>
|
|
@@ -29,6 +29,7 @@ This project follows a strict **7-Step Production-Ready Process** to ensure qual
|
|
|
29
29
|
|
|
30
30
|
- **Architecture**: <%= architecture %> (<% if (architecture === 'Clean Architecture') { %>Domain, UseCases, Infrastructure<% } else { %>MVC Pattern<% } %>).
|
|
31
31
|
- **Database**: <%= database %> <% if (database !== 'None') { %>(via <%= database === 'MongoDB' ? 'Mongoose' : 'Sequelize' %>)<% } %>.
|
|
32
|
+
<% if (auth.includes('JWT')) { %>- **Authentication**: JWT-based Auth (Sign Up, Login, Protected Routes).<% } %>
|
|
32
33
|
- **Security**: Helmet, CORS, Rate Limiting, HPP, Snyk SCA.
|
|
33
34
|
- **Quality**: 80%+ Test Coverage, Eslint, Prettier, Husky.
|
|
34
35
|
- **DevOps**: Multi-stage Docker, CI/CD ready (GitHub/GitLab/Jenkins/Bitbucket/CircleCI).
|
|
@@ -95,6 +96,21 @@ Microservices communication handled via **Kafka**.
|
|
|
95
96
|
API is exposed via **GraphQL**.
|
|
96
97
|
The Apollo Sandbox UI for API exploration and documentation is available natively, fully embedded for offline development:
|
|
97
98
|
- **URL**: `http://localhost:3000/graphql` (Dynamic based on PORT)
|
|
99
|
+
|
|
100
|
+
<%_ if (auth.includes('JWT')) { _%>
|
|
101
|
+
### 🔐 Authorization
|
|
102
|
+
To access protected resolvers (Query: `getAllUsers`, Mutations: `updateUser`, `deleteUser`), you must provide a valid JWT token in the headers.
|
|
103
|
+
|
|
104
|
+
**How to authenticate:**
|
|
105
|
+
1. Use the REST login endpoint: `POST /api/auth/login` (with your email and password).
|
|
106
|
+
2. Copy the `accessToken` from the response.
|
|
107
|
+
3. In the Apollo Sandbox "HTTP Headers" tab (bottom section), add:
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"Authorization": "Bearer YOUR_ACCESS_TOKEN_HERE"
|
|
111
|
+
}
|
|
112
|
+
```<%_ } _%>
|
|
113
|
+
|
|
98
114
|
If you are opening `http://localhost:3000/graphql` in your browser, you can directly run the following in the Apollo Sandbox UI:
|
|
99
115
|
|
|
100
116
|
**Query to get all users:**
|
|
@@ -111,7 +127,7 @@ query GetAllUsers {
|
|
|
111
127
|
**Mutation to create a user:**
|
|
112
128
|
```graphql
|
|
113
129
|
mutation CreateUser {
|
|
114
|
-
createUser(name: "John Doe", email: "john@example.com") {
|
|
130
|
+
createUser(name: "John Doe", email: "john@example.com"<%_ if (auth.includes('JWT')) { _%>, password: "yourpassword123"<%_ } _%>) {
|
|
115
131
|
id
|
|
116
132
|
name
|
|
117
133
|
email
|
|
@@ -119,7 +135,7 @@ mutation CreateUser {
|
|
|
119
135
|
}
|
|
120
136
|
```
|
|
121
137
|
|
|
122
|
-
**Mutation to update a user:**
|
|
138
|
+
**Mutation to update a user (Protected):**
|
|
123
139
|
```graphql
|
|
124
140
|
mutation UpdateUser {
|
|
125
141
|
updateUser(id: "1", name: "John Updated") {
|
|
@@ -130,7 +146,7 @@ mutation UpdateUser {
|
|
|
130
146
|
}
|
|
131
147
|
```
|
|
132
148
|
|
|
133
|
-
**Mutation to delete a user:**
|
|
149
|
+
**Mutation to delete a user (Protected):**
|
|
134
150
|
```graphql
|
|
135
151
|
mutation DeleteUser {
|
|
136
152
|
deleteUser(id: "1")
|
|
@@ -143,32 +159,61 @@ A Swagger UI for API documentation is available at:
|
|
|
143
159
|
|
|
144
160
|
### 🛣️ User Endpoints:
|
|
145
161
|
- `GET /api/users`: List all users.
|
|
162
|
+
- `GET /api/users/:id`: Get a user by ID.
|
|
146
163
|
- `POST /api/users`: Create a new user.
|
|
147
164
|
- `PATCH /api/users/:id`: Partially update a user.
|
|
148
165
|
- `DELETE /api/users/:id`: Delete a user (Soft Delete).
|
|
149
|
-
<%_
|
|
150
|
-
|
|
166
|
+
<%_ if (auth.includes('JWT')) { %>
|
|
167
|
+
### 🔐 Auth Endpoints:
|
|
168
|
+
- `POST /api/auth/login`: Exchange credentials for a short-lived `accessToken` and a long-lived `refreshToken`.
|
|
169
|
+
- `POST /api/auth/refresh`: Submit a `refreshToken` to receive a new pair of tokens. (Includes theft-detection logic).
|
|
170
|
+
- `POST /api/auth/logout`: Revoke (blacklist) the active `accessToken` and delete the `refreshToken`.
|
|
171
|
+
- `POST /api/users`: Acts as Sign Up when password is provided.
|
|
172
|
+
*Note: To access protected user endpoints (GET/PATCH/DELETE), include `Authorization: Bearer <your_accessToken>` in the headers.*
|
|
173
|
+
<% } -%>
|
|
174
|
+
<% } -%>
|
|
151
175
|
<% if (communication === 'Kafka') { -%>
|
|
152
176
|
## 📡 Testing Kafka Asynchronous Flow
|
|
153
177
|
This project demonstrates a production-ready Kafka flow:
|
|
154
|
-
1. **Producer**: When a user is created via the API, a `USER_CREATED`
|
|
155
|
-
2. **Consumer**: `WelcomeEmailConsumer` listens to `user-topic` and simulates sending an email.
|
|
178
|
+
1. **Producer**: When a user is created, updated, or deleted via the API, a corresponding event (`USER_CREATED`, `USER_UPDATED`, `USER_DELETED`) is sent to `user-topic`.
|
|
179
|
+
2. **Consumer**: `WelcomeEmailConsumer` listens to `user-topic` and simulates processing (e.g., sending an email on creation).
|
|
156
180
|
|
|
157
181
|
### How to verify:
|
|
158
182
|
1. Ensure infrastructure is running: `docker-compose up -d<% if (database !== 'None') { %> db<% } %><% if (caching === 'Redis') { %> redis<% } %><% if (communication === 'Kafka') { %> kafka<% } %>`
|
|
159
183
|
2. Start the app: `npm run dev`
|
|
160
|
-
3. Trigger
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
184
|
+
3. Trigger internal events:
|
|
185
|
+
|
|
186
|
+
**Create User (Sign Up - Public):**
|
|
187
|
+
```bash
|
|
188
|
+
curl -X POST http://localhost:3000/api/users \
|
|
189
|
+
-H "Content-Type: application/json" \
|
|
190
|
+
-d '{"name": "Kafka Tester", "email": "kafka@example.com"<%_ if (auth.includes('JWT')) { _%>, "password": "password123"<%_ } _%>}'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
<%_ if (auth.includes('JWT')) { _%>
|
|
194
|
+
**Update User (Protected - Requires Auth):**
|
|
195
|
+
1. Login to get token: `POST /api/auth/login`
|
|
196
|
+
2. Update user with token:
|
|
197
|
+
```bash
|
|
198
|
+
curl -X PATCH http://localhost:3000/api/users/1 \
|
|
199
|
+
-H "Authorization: Bearer <YOUR_TOKEN>" \
|
|
200
|
+
-H "Content-Type: application/json" \
|
|
201
|
+
-d '{"name": "Updated Name"}'
|
|
202
|
+
```
|
|
203
|
+
<%_ } else { _%>
|
|
204
|
+
**Update User:**
|
|
205
|
+
```bash
|
|
206
|
+
curl -X PATCH http://localhost:3000/api/users/1 \
|
|
207
|
+
-H "Content-Type: application/json" \
|
|
208
|
+
-d '{"name": "Updated Name"}'
|
|
209
|
+
```
|
|
210
|
+
<%_ } _%>
|
|
166
211
|
4. Observe the logs:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
212
|
+
```text
|
|
213
|
+
[Kafka] Producer: Sent USER_CREATED event for 'kafka@example.com'
|
|
214
|
+
[Kafka] Consumer: Received USER_CREATED.
|
|
215
|
+
[Kafka] Consumer: 📧 Sending welcome email to 'kafka@example.com'... Done!
|
|
216
|
+
```
|
|
172
217
|
|
|
173
218
|
### 🛠️ Kafka Troubleshooting
|
|
174
219
|
If the connection or events are failing:
|