nodejs-quickstart-structure 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +4 -3
- package/lib/generator.js +17 -3
- package/lib/modules/app-setup.js +111 -19
- package/lib/modules/caching-setup.js +13 -0
- package/lib/modules/config-files.js +25 -62
- package/lib/modules/database-setup.js +35 -30
- package/lib/modules/kafka-setup.js +78 -10
- package/package.json +1 -2
- package/templates/clean-architecture/js/src/errors/BadRequestError.js +1 -1
- package/templates/clean-architecture/js/src/errors/BadRequestError.spec.js.ejs +21 -0
- package/templates/clean-architecture/js/src/errors/NotFoundError.js +1 -1
- package/templates/clean-architecture/js/src/errors/NotFoundError.spec.js.ejs +21 -0
- package/templates/clean-architecture/js/src/infrastructure/log/logger.spec.js.ejs +63 -0
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +2 -3
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +81 -0
- package/templates/clean-architecture/js/src/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/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/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
|
@@ -13,24 +13,68 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
13
13
|
// Render Kafka Service with dynamic logger path
|
|
14
14
|
const kafkaServiceFileName = `kafkaService.${langExt}`;
|
|
15
15
|
const kafkaServiceTemplate = path.join(targetDir, 'src', 'services', `${kafkaServiceFileName}.ejs`);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
let configPath = '../config/kafka';
|
|
16
|
+
// Render Kafka Service Spec
|
|
17
|
+
const kafkaSpecFileName = `kafkaService.spec.${langExt}`;
|
|
18
|
+
const kafkaSpecTemplate = path.join(targetDir, 'src', 'services', `${kafkaSpecFileName}.ejs`);
|
|
20
19
|
|
|
20
|
+
if (await fs.pathExists(kafkaServiceTemplate)) {
|
|
21
|
+
let serviceLoggerPath, serviceConfigPath;
|
|
21
22
|
if (language === 'TypeScript') {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
serviceLoggerPath = architecture === 'Clean Architecture' ? '@/infrastructure/log/logger' : '@/utils/logger';
|
|
24
|
+
serviceConfigPath = architecture === 'Clean Architecture' ? '@/infrastructure/config/kafka' : '@/config/kafka';
|
|
25
|
+
} else {
|
|
26
|
+
serviceLoggerPath = architecture === 'Clean Architecture' ? '../../infrastructure/log/logger' : '../utils/logger';
|
|
27
|
+
serviceConfigPath = architecture === 'Clean Architecture' ? '../../infrastructure/config/kafka' : '../config/kafka';
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
const content = ejs.render(await fs.readFile(kafkaServiceTemplate, 'utf-8'), { loggerPath, configPath });
|
|
30
|
+
const content = ejs.render(await fs.readFile(kafkaServiceTemplate, 'utf-8'), { ...config, loggerPath: serviceLoggerPath, configPath: serviceConfigPath });
|
|
27
31
|
await fs.writeFile(path.join(targetDir, 'src', 'services', kafkaServiceFileName), content);
|
|
28
32
|
await fs.remove(kafkaServiceTemplate);
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
if (await fs.pathExists(kafkaSpecTemplate)) {
|
|
36
|
+
let specLoggerPath, specConfigPath, specServicePath;
|
|
37
|
+
if (language === 'TypeScript') {
|
|
38
|
+
specLoggerPath = architecture === 'Clean Architecture' ? '@/infrastructure/log/logger' : '@/utils/logger';
|
|
39
|
+
specConfigPath = architecture === 'Clean Architecture' ? '@/infrastructure/config/kafka' : '@/config/kafka';
|
|
40
|
+
specServicePath = architecture === 'Clean Architecture' ? '@/infrastructure/messaging/kafkaClient' : '@/services/kafkaService';
|
|
41
|
+
} else {
|
|
42
|
+
// For JS tests, we use @/ to ensure resolution after move to tests/
|
|
43
|
+
specLoggerPath = architecture === 'Clean Architecture' ? '@/infrastructure/log/logger' : '@/utils/logger';
|
|
44
|
+
specConfigPath = architecture === 'Clean Architecture' ? '@/infrastructure/config/kafka' : '@/config/kafka';
|
|
45
|
+
specServicePath = architecture === 'Clean Architecture' ? '@/infrastructure/messaging/kafkaClient' : '@/services/kafkaService';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const specContent = ejs.render(await fs.readFile(kafkaSpecTemplate, 'utf-8'), { ...config, loggerPath: specLoggerPath, configPath: specConfigPath, servicePath: specServicePath });
|
|
49
|
+
await fs.writeFile(path.join(targetDir, 'src', 'services', kafkaSpecFileName), specContent);
|
|
50
|
+
await fs.remove(kafkaSpecTemplate);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Render Kafka Config Spec
|
|
54
|
+
const kafkaConfigSpecFileName = `kafka.spec.${langExt}`;
|
|
55
|
+
const kafkaConfigSpecTemplate = path.join(templatesDir, 'common', 'kafka', langExt, 'config', `${kafkaConfigSpecFileName}.ejs`);
|
|
56
|
+
if (await fs.pathExists(kafkaConfigSpecTemplate)) {
|
|
57
|
+
const specContent = ejs.render(await fs.readFile(kafkaConfigSpecTemplate, 'utf-8'), { ...config });
|
|
58
|
+
let specTarget;
|
|
59
|
+
if (architecture === 'MVC') {
|
|
60
|
+
specTarget = path.join(targetDir, 'tests', 'config', kafkaConfigSpecFileName);
|
|
61
|
+
} else {
|
|
62
|
+
specTarget = path.join(targetDir, 'tests', 'infrastructure', 'config', kafkaConfigSpecFileName);
|
|
63
|
+
}
|
|
64
|
+
await fs.ensureDir(path.dirname(specTarget));
|
|
65
|
+
await fs.writeFile(specTarget, specContent);
|
|
66
|
+
|
|
67
|
+
// Remove the template from src in targetDir to avoid double processing by processAllTests
|
|
68
|
+
const targetSpecTemplate = path.join(targetDir, 'src', 'config', `${kafkaConfigSpecFileName}.ejs`);
|
|
69
|
+
if (await fs.pathExists(targetSpecTemplate)) {
|
|
70
|
+
await fs.remove(targetSpecTemplate);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
31
74
|
if (architecture === 'Clean Architecture') {
|
|
32
75
|
// Clean Architecture Restructuring
|
|
33
76
|
await fs.ensureDir(path.join(targetDir, 'src/infrastructure/messaging'));
|
|
77
|
+
await fs.ensureDir(path.join(targetDir, 'tests/infrastructure/messaging'));
|
|
34
78
|
await fs.ensureDir(path.join(targetDir, 'src/infrastructure/config'));
|
|
35
79
|
|
|
36
80
|
const serviceExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
@@ -42,6 +86,15 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
42
86
|
{ overwrite: true }
|
|
43
87
|
);
|
|
44
88
|
|
|
89
|
+
// Move Spec to Tests/Infrastructure/Messaging
|
|
90
|
+
if (await fs.pathExists(path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`))) {
|
|
91
|
+
await fs.move(
|
|
92
|
+
path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`),
|
|
93
|
+
path.join(targetDir, `tests/infrastructure/messaging/kafkaClient.spec.${serviceExt}`),
|
|
94
|
+
{ overwrite: true }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
45
98
|
// Move Config to Infrastructure/Config
|
|
46
99
|
// Note: Check if config path exists before moving, though copy above should have put it there
|
|
47
100
|
if (await fs.pathExists(path.join(targetDir, `src/config/kafka.${serviceExt}`))) {
|
|
@@ -57,12 +110,27 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
57
110
|
|
|
58
111
|
// Remove REST-specific folders (Interfaces) - Note: routes is kept for health endpoint
|
|
59
112
|
await fs.remove(path.join(targetDir, 'src/interfaces/controllers'));
|
|
113
|
+
await fs.remove(path.join(targetDir, 'tests/interfaces/controllers'));
|
|
60
114
|
|
|
61
115
|
// Original logic removed src/config entirely, but now we use it for Zod env validation in TS.
|
|
62
116
|
// We will no longer delete it.
|
|
63
|
-
} else if (architecture === 'MVC'
|
|
64
|
-
|
|
65
|
-
|
|
117
|
+
} else if (architecture === 'MVC') {
|
|
118
|
+
const serviceExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
119
|
+
// Move Spec to Tests/Services
|
|
120
|
+
if (await fs.pathExists(path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`))) {
|
|
121
|
+
await fs.ensureDir(path.join(targetDir, 'tests/services'));
|
|
122
|
+
await fs.move(
|
|
123
|
+
path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`),
|
|
124
|
+
path.join(targetDir, `tests/services/kafkaService.spec.${serviceExt}`),
|
|
125
|
+
{ overwrite: true }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!config.viewEngine || config.viewEngine === 'None') {
|
|
130
|
+
// MVC Cleanup for Kafka Worker (No views) - Note: routes is kept for health endpoint
|
|
131
|
+
await fs.remove(path.join(targetDir, 'src/controllers'));
|
|
132
|
+
await fs.remove(path.join(targetDir, 'tests/controllers'));
|
|
133
|
+
}
|
|
66
134
|
}
|
|
67
135
|
};
|
|
68
136
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodejs-quickstart-structure",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A CLI to scaffold Node.js microservices with MVC or Clean Architecture",
|
|
6
6
|
"main": "bin/index.js",
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
"bin",
|
|
45
45
|
"lib",
|
|
46
46
|
"templates",
|
|
47
|
-
"docs",
|
|
48
47
|
"README.md",
|
|
49
48
|
"CHANGELOG.md"
|
|
50
49
|
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { BadRequestError } = require('@/errors/BadRequestError');
|
|
2
|
+
const { ApiError } = require('@/errors/ApiError');
|
|
3
|
+
const HTTP_STATUS = require('@/utils/httpCodes');
|
|
4
|
+
|
|
5
|
+
describe('BadRequestError', () => {
|
|
6
|
+
it('should extend ApiError', () => {
|
|
7
|
+
const error = new BadRequestError();
|
|
8
|
+
expect(error).toBeInstanceOf(ApiError);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should have default message "Bad request"', () => {
|
|
12
|
+
const error = new BadRequestError();
|
|
13
|
+
expect(error.message).toBe('Bad request');
|
|
14
|
+
expect(error.statusCode).toBe(HTTP_STATUS.BAD_REQUEST);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should accept a custom message', () => {
|
|
18
|
+
const error = new BadRequestError('Custom bad request');
|
|
19
|
+
expect(error.message).toBe('Custom bad request');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { NotFoundError } = require('@/errors/NotFoundError');
|
|
2
|
+
const { ApiError } = require('@/errors/ApiError');
|
|
3
|
+
const HTTP_STATUS = require('@/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
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
jest.mock('winston-daily-rotate-file');
|
|
2
|
+
jest.mock('winston', () => {
|
|
3
|
+
const mockLogger = {
|
|
4
|
+
add: jest.fn(),
|
|
5
|
+
info: jest.fn(),
|
|
6
|
+
error: jest.fn(),
|
|
7
|
+
warn: jest.fn()
|
|
8
|
+
};
|
|
9
|
+
const format = {
|
|
10
|
+
combine: jest.fn(),
|
|
11
|
+
timestamp: jest.fn(),
|
|
12
|
+
json: jest.fn(),
|
|
13
|
+
simple: jest.fn()
|
|
14
|
+
};
|
|
15
|
+
const transports = {
|
|
16
|
+
Console: jest.fn(),
|
|
17
|
+
DailyRotateFile: jest.fn()
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
format,
|
|
21
|
+
transports,
|
|
22
|
+
createLogger: jest.fn().mockReturnValue(mockLogger)
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
<% if (architecture === 'MVC') { -%>
|
|
27
|
+
const logger = require('@/utils/logger');
|
|
28
|
+
<% } else { -%>
|
|
29
|
+
const logger = require('@/infrastructure/log/logger');
|
|
30
|
+
<% } -%>
|
|
31
|
+
|
|
32
|
+
describe('Logger', () => {
|
|
33
|
+
it('should export a logger instance', () => {
|
|
34
|
+
expect(logger).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should have info method', () => {
|
|
38
|
+
expect(typeof logger.info).toBe('function');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should have error method', () => {
|
|
42
|
+
expect(typeof logger.error).toBe('function');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should call info', () => {
|
|
46
|
+
logger.info('test message');
|
|
47
|
+
expect(logger.info).toHaveBeenCalledWith('test message');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should call error', () => {
|
|
51
|
+
logger.error('test error');
|
|
52
|
+
expect(logger.error).toHaveBeenCalledWith('test error');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should use JSON format in production environment', () => {
|
|
56
|
+
const winston = require('winston');
|
|
57
|
+
jest.resetModules();
|
|
58
|
+
process.env.NODE_ENV = 'production';
|
|
59
|
+
require('@/infrastructure/log/logger');
|
|
60
|
+
expect(winston.format.json).toHaveBeenCalled();
|
|
61
|
+
process.env.NODE_ENV = 'test';
|
|
62
|
+
});
|
|
63
|
+
});
|
package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs
CHANGED
|
@@ -3,8 +3,7 @@ const UserModel = require('../database/models/User');
|
|
|
3
3
|
class UserRepository {
|
|
4
4
|
async save(user) {
|
|
5
5
|
<%_ if (database === 'None') { -%>
|
|
6
|
-
const newUser =
|
|
7
|
-
UserModel.mockData.push(newUser);
|
|
6
|
+
const newUser = await UserModel.create(user);
|
|
8
7
|
return newUser;
|
|
9
8
|
<%_ } else { -%>
|
|
10
9
|
const newUser = await UserModel.create({ name: user.name, email: user.email });
|
|
@@ -18,7 +17,7 @@ class UserRepository {
|
|
|
18
17
|
|
|
19
18
|
async getUsers() {
|
|
20
19
|
<%_ if (database === 'None') { -%>
|
|
21
|
-
return UserModel.
|
|
20
|
+
return await UserModel.find();
|
|
22
21
|
<%_ } else if (database === 'MongoDB') { -%>
|
|
23
22
|
const users = await UserModel.find();
|
|
24
23
|
return users.map(user => ({
|
package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const UserRepository = require('@/infrastructure/repositories/UserRepository');
|
|
2
|
+
const UserModel = require('@/infrastructure/database/models/User');
|
|
3
|
+
|
|
4
|
+
jest.mock('@/infrastructure/database/models/User');
|
|
5
|
+
|
|
6
|
+
describe('UserRepository', () => {
|
|
7
|
+
let userRepository;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
userRepository = new UserRepository();
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('save', () => {
|
|
15
|
+
it('should save and return a newly created user (Happy Path)', async () => {
|
|
16
|
+
const userData = { name: 'New User', email: 'new@example.com' };
|
|
17
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
18
|
+
const savedData = { _id: 'mock-id', ...userData };
|
|
19
|
+
UserModel.create.mockResolvedValue(savedData);
|
|
20
|
+
<%_ } else if (database === 'None') { -%>
|
|
21
|
+
UserModel.create.mockResolvedValue(userData);
|
|
22
|
+
<%_ } else { -%>
|
|
23
|
+
const savedData = { <%= database === "MongoDB" ? "_id" : "id" %>: '<%= database === "MongoDB" ? "mock-id" : "1" %>', ...userData };
|
|
24
|
+
UserModel.create.mockResolvedValue(savedData);
|
|
25
|
+
<%_ } -%>
|
|
26
|
+
|
|
27
|
+
const result = await userRepository.save(userData);
|
|
28
|
+
|
|
29
|
+
<%_ if (database === 'None') { -%>
|
|
30
|
+
expect(result.name).toEqual(userData.name);
|
|
31
|
+
<%_ } else { -%>
|
|
32
|
+
expect(UserModel.create).toHaveBeenCalledWith(userData);
|
|
33
|
+
expect(result).toEqual({ id: '<%= database === "MongoDB" ? "mock-id" : "1" %>', ...userData });
|
|
34
|
+
<%_ } -%>
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should throw an error when DB fails explicitly (Edge Case)', async () => {
|
|
38
|
+
<%_ if (database === 'None') { -%>
|
|
39
|
+
// mock data logic doesn't throw naturally
|
|
40
|
+
<%_ } else { -%>
|
|
41
|
+
const error = new Error('DB Error');
|
|
42
|
+
UserModel.create.mockRejectedValue(error);
|
|
43
|
+
|
|
44
|
+
await expect(userRepository.save({})).rejects.toThrow('DB Error');
|
|
45
|
+
<%_ } -%>
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getUsers', () => {
|
|
50
|
+
it('should return a list of mapped UserEntities (Happy Path)', async () => {
|
|
51
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
52
|
+
const mockUsers = [
|
|
53
|
+
{ _id: '1', name: 'User 1', email: 'u1@test.com' },
|
|
54
|
+
{ _id: '2', name: 'User 2', email: 'u2@test.com' }
|
|
55
|
+
];
|
|
56
|
+
UserModel.find.mockResolvedValue(mockUsers);
|
|
57
|
+
<%_ } else if (database === 'None') { -%>
|
|
58
|
+
const mockUsers = [{ id: '1', name: 'User 1', email: 'u1@test.com' }];
|
|
59
|
+
UserModel.find.mockResolvedValue(mockUsers);
|
|
60
|
+
<%_ } else { -%>
|
|
61
|
+
const mockUsers = [
|
|
62
|
+
{ <%= database === "MongoDB" ? "_id" : "id" %>: '1', name: 'User 1', email: 'u1@test.com' },
|
|
63
|
+
{ <%= database === "MongoDB" ? "_id" : "id" %>: '2', name: 'User 2', email: 'u2@test.com' }
|
|
64
|
+
];
|
|
65
|
+
UserModel.<%= database === "MongoDB" ? "find" : "findAll" %>.mockResolvedValue(mockUsers);
|
|
66
|
+
<%_ } -%>
|
|
67
|
+
|
|
68
|
+
const result = await userRepository.getUsers();
|
|
69
|
+
|
|
70
|
+
<%_ if (database !== 'None') { -%>
|
|
71
|
+
expect(UserModel.<%= database === 'MongoDB' ? 'find' : 'findAll' %>).toHaveBeenCalled();
|
|
72
|
+
<%_ } -%>
|
|
73
|
+
expect(result).toEqual([
|
|
74
|
+
{ id: '1', name: 'User 1', email: 'u1@test.com' }
|
|
75
|
+
<%_ if (database !== 'None') { -%>
|
|
76
|
+
, { id: '2', name: 'User 2', email: 'u2@test.com' }
|
|
77
|
+
<%_ } -%>
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -33,10 +33,14 @@ class UserController {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
<% } else { -%>
|
|
36
|
-
getUsers(req, res, next) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
async getUsers(req, res, next) {
|
|
37
|
+
try {
|
|
38
|
+
const users = await this.getAllUsersUseCase.execute();
|
|
39
|
+
res.json(users);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.error('Error getting users:', error);
|
|
42
|
+
next(error);
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
async createUser(req, res, next) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const UserController = require('@/interfaces/controllers/userController');
|
|
2
|
+
const CreateUser = require('@/usecases/CreateUser');
|
|
3
|
+
const GetAllUsers = require('@/usecases/GetAllUsers');
|
|
4
|
+
|
|
5
|
+
jest.mock('@/usecases/CreateUser');
|
|
6
|
+
jest.mock('@/usecases/GetAllUsers');
|
|
7
|
+
|
|
8
|
+
describe('UserController (Clean Architecture)', () => {
|
|
9
|
+
let userController;
|
|
10
|
+
let mockCreateUserUseCase;
|
|
11
|
+
let mockGetAllUsersUseCase;
|
|
12
|
+
<%_ if (communication !== 'GraphQL') { -%>
|
|
13
|
+
let mockRequest;
|
|
14
|
+
let mockResponse;
|
|
15
|
+
let mockNext;
|
|
16
|
+
<%_ } -%>
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
|
|
21
|
+
userController = new UserController();
|
|
22
|
+
|
|
23
|
+
// Retrieve the mocked instances created inside UserController constructor
|
|
24
|
+
mockCreateUserUseCase = CreateUser.mock.instances[0];
|
|
25
|
+
mockGetAllUsersUseCase = GetAllUsers.mock.instances[0];
|
|
26
|
+
|
|
27
|
+
<%_ if (communication !== 'GraphQL') { -%>
|
|
28
|
+
mockRequest = {
|
|
29
|
+
body: {}
|
|
30
|
+
};
|
|
31
|
+
mockResponse = {
|
|
32
|
+
status: jest.fn().mockReturnThis(),
|
|
33
|
+
json: jest.fn()
|
|
34
|
+
};
|
|
35
|
+
mockNext = jest.fn();
|
|
36
|
+
<%_ } -%>
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('getUsers', () => {
|
|
40
|
+
it('should return successfully (Happy Path)', async () => {
|
|
41
|
+
const usersMock = [{ id: '1', name: 'Test', email: 'test@example.com' }];
|
|
42
|
+
mockGetAllUsersUseCase.execute.mockResolvedValue(usersMock);
|
|
43
|
+
|
|
44
|
+
<%_ if (communication === 'GraphQL') { -%>
|
|
45
|
+
const result = await userController.getUsers();
|
|
46
|
+
expect(result).toEqual(usersMock);
|
|
47
|
+
<%_ } else { -%>
|
|
48
|
+
await userController.getUsers(mockRequest, mockResponse, mockNext);
|
|
49
|
+
expect(mockResponse.json).toHaveBeenCalledWith(usersMock);
|
|
50
|
+
<%_ } -%>
|
|
51
|
+
expect(mockGetAllUsersUseCase.execute).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle errors correctly (Error Handling)', async () => {
|
|
55
|
+
const error = new Error('UseCase Error');
|
|
56
|
+
mockGetAllUsersUseCase.execute.mockRejectedValue(error);
|
|
57
|
+
|
|
58
|
+
<%_ if (communication === 'GraphQL') { -%>
|
|
59
|
+
await expect(userController.getUsers()).rejects.toThrow(error);
|
|
60
|
+
<%_ } else { -%>
|
|
61
|
+
await userController.getUsers(mockRequest, mockResponse, mockNext);
|
|
62
|
+
expect(mockNext).toHaveBeenCalledWith(error);
|
|
63
|
+
<%_ } -%>
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('createUser', () => {
|
|
68
|
+
it('should successfully create a new user (Happy Path)', async () => {
|
|
69
|
+
const payload = { name: 'Alice', email: 'alice@example.com' };
|
|
70
|
+
<%_ if (communication === 'GraphQL') { -%>
|
|
71
|
+
const dataArg = payload;
|
|
72
|
+
<%_ } else { -%>
|
|
73
|
+
mockRequest.body = payload;
|
|
74
|
+
<%_ } -%>
|
|
75
|
+
const expectedUser = { id: '1', ...payload };
|
|
76
|
+
|
|
77
|
+
mockCreateUserUseCase.execute.mockResolvedValue(expectedUser);
|
|
78
|
+
|
|
79
|
+
<%_ if (communication === 'GraphQL') { -%>
|
|
80
|
+
const result = await userController.createUser(dataArg);
|
|
81
|
+
expect(result).toEqual(expectedUser);
|
|
82
|
+
<%_ } else { -%>
|
|
83
|
+
await userController.createUser(mockRequest, mockResponse, mockNext);
|
|
84
|
+
expect(mockResponse.status).toHaveBeenCalledWith(201);
|
|
85
|
+
expect(mockResponse.json).toHaveBeenCalledWith(expectedUser);
|
|
86
|
+
<%_ } -%>
|
|
87
|
+
expect(mockCreateUserUseCase.execute).toHaveBeenCalledWith(payload.name, payload.email);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle errors when creation fails (Error Handling)', async () => {
|
|
91
|
+
const error = new Error('Creation Error');
|
|
92
|
+
mockCreateUserUseCase.execute.mockRejectedValue(error);
|
|
93
|
+
|
|
94
|
+
<%_ if (communication === 'GraphQL') { -%>
|
|
95
|
+
await expect(userController.createUser({ name: 'Bob', email: 'bob@example.com' })).rejects.toThrow(error);
|
|
96
|
+
<%_ } else { -%>
|
|
97
|
+
await userController.createUser(mockRequest, mockResponse, mockNext);
|
|
98
|
+
expect(mockNext).toHaveBeenCalledWith(error);
|
|
99
|
+
<%_ } -%>
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { gqlContext } = require('@/interfaces/graphql/context');
|
|
2
|
+
const { resolvers } = require('@/interfaces/graphql/resolvers');
|
|
3
|
+
const { typeDefs } = require('@/interfaces/graphql/typeDefs');
|
|
4
|
+
|
|
5
|
+
jest.mock('@/infrastructure/repositories/UserRepository');
|
|
6
|
+
|
|
7
|
+
describe('GraphQL Context', () => {
|
|
8
|
+
it('should exercise GraphQL index entry points', () => {
|
|
9
|
+
expect(resolvers).toBeDefined();
|
|
10
|
+
expect(typeDefs).toBeDefined();
|
|
11
|
+
});
|
|
12
|
+
it('should return context with token when authorization header is present', async () => {
|
|
13
|
+
const mockRequest = {
|
|
14
|
+
headers: {
|
|
15
|
+
authorization: 'Bearer token123',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const context = await gqlContext({ req: mockRequest });
|
|
20
|
+
expect(context.token).toBe('Bearer token123');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return context with empty token when authorization header is missing', async () => {
|
|
24
|
+
const mockRequest = {
|
|
25
|
+
headers: {},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const context = await gqlContext({ req: mockRequest });
|
|
29
|
+
expect(context.token).toBe('');
|
|
30
|
+
});
|
|
31
|
+
});
|
package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.spec.js.ejs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { userResolvers } = require('@/interfaces/graphql/resolvers/user.resolvers');
|
|
2
|
+
|
|
3
|
+
const mockGetUsers = jest.fn().mockResolvedValue([{ id: '1', name: 'John Doe', email: 'john@example.com' }]);
|
|
4
|
+
const mockCreateUser = jest.fn().mockResolvedValue({ id: '1', name: 'Jane', email: 'jane@example.com' });
|
|
5
|
+
|
|
6
|
+
jest.mock('@/interfaces/controllers/userController', () => {
|
|
7
|
+
return jest.fn().mockImplementation(() => ({
|
|
8
|
+
getUsers: (...args) => mockGetUsers(...args),
|
|
9
|
+
createUser: (...args) => mockCreateUser(...args)
|
|
10
|
+
}));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('User Resolvers', () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('Query.getAllUsers', () => {
|
|
19
|
+
it('should return all users', async () => {
|
|
20
|
+
const result = await userResolvers.Query.getAllUsers();
|
|
21
|
+
expect(result).toEqual([{ id: '1', name: 'John Doe', email: 'john@example.com' }]);
|
|
22
|
+
expect(mockGetUsers).toHaveBeenCalledTimes(1);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('Mutation.createUser', () => {
|
|
27
|
+
it('should create and return a new user', async () => {
|
|
28
|
+
const result = await userResolvers.Mutation.createUser(null, { name: 'Jane', email: 'jane@example.com' });
|
|
29
|
+
expect(result).toEqual({ id: '1', name: 'Jane', email: 'jane@example.com' });
|
|
30
|
+
expect(mockCreateUser).toHaveBeenCalledWith({ name: 'Jane', email: 'jane@example.com' });
|
|
31
|
+
expect(mockCreateUser).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
35
|
+
describe('User.id', () => {
|
|
36
|
+
it('should return parent.id if available', () => {
|
|
37
|
+
const parent = { id: '123' };
|
|
38
|
+
const result = userResolvers.User.id(parent);
|
|
39
|
+
expect(result).toBe('123');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should fallback to parent._id if id is not available', () => {
|
|
43
|
+
const parent = { _id: '456' };
|
|
44
|
+
const result = userResolvers.User.id(parent);
|
|
45
|
+
expect(result).toBe('456');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
<%_ } -%>
|
|
49
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const request = require('supertest');
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const router = require('@/interfaces/routes/api');
|
|
4
|
+
|
|
5
|
+
const mockGetUsers = jest.fn().mockImplementation((req, res) => res.status(200).json([{ id: '1', name: 'John Doe' }]));
|
|
6
|
+
const mockCreateUser = jest.fn().mockImplementation((req, res) => res.status(201).json({ id: '1', name: 'Test' }));
|
|
7
|
+
|
|
8
|
+
jest.mock('@/interfaces/controllers/userController', () => {
|
|
9
|
+
return jest.fn().mockImplementation(() => ({
|
|
10
|
+
getUsers: (...args) => mockGetUsers(...args),
|
|
11
|
+
createUser: (...args) => mockCreateUser(...args)
|
|
12
|
+
}));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('ApiRoutes', () => {
|
|
16
|
+
let app;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
app = express();
|
|
20
|
+
app.use(express.json());
|
|
21
|
+
app.use('/api', router);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('POST /api/users should call controller.createUser', async () => {
|
|
25
|
+
await request(app)
|
|
26
|
+
.post('/api/users')
|
|
27
|
+
.send({ name: 'Test', email: 'test@example.com' });
|
|
28
|
+
|
|
29
|
+
expect(mockCreateUser).toHaveBeenCalledTimes(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('GET /api/users should call controller.getUsers', async () => {
|
|
33
|
+
await request(app)
|
|
34
|
+
.get('/api/users');
|
|
35
|
+
|
|
36
|
+
expect(mockGetUsers).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const CreateUser = require('@/usecases/CreateUser');
|
|
2
|
+
const UserRepository = require('@/infrastructure/repositories/UserRepository');
|
|
3
|
+
<%_ if (caching !== 'None') { -%>
|
|
4
|
+
const cacheService = require('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>');
|
|
5
|
+
<%_ } -%>
|
|
6
|
+
|
|
7
|
+
jest.mock('@/infrastructure/repositories/UserRepository');
|
|
8
|
+
<%_ if (caching !== 'None') { -%>
|
|
9
|
+
jest.mock('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>', () => ({
|
|
10
|
+
get: jest.fn(),
|
|
11
|
+
set: jest.fn(),
|
|
12
|
+
del: jest.fn()
|
|
13
|
+
}));
|
|
14
|
+
<%_ } -%>
|
|
15
|
+
|
|
16
|
+
describe('CreateUser UseCase', () => {
|
|
17
|
+
let createUser;
|
|
18
|
+
let mockUserRepository;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockUserRepository = new UserRepository();
|
|
22
|
+
createUser = new CreateUser(mockUserRepository);
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should create and save a new user', async () => {
|
|
27
|
+
const name = 'Test User';
|
|
28
|
+
const email = 'test@example.com';
|
|
29
|
+
const expectedResult = { id: 1, name, email };
|
|
30
|
+
|
|
31
|
+
mockUserRepository.save.mockResolvedValue(expectedResult);
|
|
32
|
+
|
|
33
|
+
const result = await createUser.execute(name, email);
|
|
34
|
+
|
|
35
|
+
expect(mockUserRepository.save).toHaveBeenCalledTimes(1);
|
|
36
|
+
const savedUser = mockUserRepository.save.mock.calls[0][0];
|
|
37
|
+
expect(savedUser.name).toBe(name);
|
|
38
|
+
expect(savedUser.email).toBe(email);
|
|
39
|
+
expect(result).toEqual(expectedResult);
|
|
40
|
+
<%_ if (caching !== 'None') { -%>
|
|
41
|
+
expect(cacheService.del).toHaveBeenCalledWith('users:all');
|
|
42
|
+
<%_ } -%>
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should throw an error if repository fails', async () => {
|
|
46
|
+
const error = new Error('Database error');
|
|
47
|
+
mockUserRepository.save.mockRejectedValue(error);
|
|
48
|
+
|
|
49
|
+
await expect(createUser.execute('Test', 'test@test.com')).rejects.toThrow(error);
|
|
50
|
+
});
|
|
51
|
+
});
|