nodejs-quickstart-structure 2.1.0 β 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 +11 -0
- package/README.md +40 -46
- package/lib/modules/config-files.js +6 -0
- package/package.json +1 -1
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +11 -1
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.spec.js.ejs +2 -0
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +1 -1
- package/templates/clean-architecture/js/src/interfaces/graphql/context.spec.js.ejs +19 -3
- package/templates/clean-architecture/js/src/usecases/CreateUser.spec.js.ejs +9 -1
- package/templates/clean-architecture/js/src/usecases/DeleteUser.spec.js.ejs +9 -1
- package/templates/clean-architecture/js/src/usecases/GetAllUsers.spec.js.ejs +9 -1
- package/templates/clean-architecture/js/src/usecases/UpdateUser.spec.js.ejs +9 -1
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +16 -1
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +17 -3
- package/templates/clean-architecture/ts/src/interfaces/graphql/context.spec.ts.ejs +21 -3
- package/templates/clean-architecture/ts/src/usecases/createUser.spec.ts.ejs +9 -1
- package/templates/clean-architecture/ts/src/usecases/deleteUser.spec.ts.ejs +9 -1
- package/templates/clean-architecture/ts/src/usecases/getAllUsers.spec.ts.ejs +9 -1
- package/templates/clean-architecture/ts/src/usecases/getUserById.spec.ts.ejs +9 -1
- package/templates/clean-architecture/ts/src/usecases/updateUser.spec.ts.ejs +9 -1
- package/templates/common/auth/js/controllers/authController.js.ejs +11 -9
- package/templates/common/auth/ts/controllers/authController.ts.ejs +7 -5
- package/templates/common/babel.config.js.ejs +5 -0
- 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/mongoose.spec.js.ejs +2 -0
- package/templates/common/database/ts/mongoose.spec.ts.ejs +2 -0
- package/templates/common/jest.config.js.ejs +2 -0
- package/templates/common/package.json.ejs +5 -3
- package/templates/common/tsconfig.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
2
2
|
|
|
3
|
+
## [2.1.1] - 2026-04-24
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Template Security Hardening**: Resolved missing buffer bounds check vulnerability in `uuid` (GHSA-w5hq-g745-h8pq) by adding `"uuid": "^14.0.0"` to template `overrides`.
|
|
7
|
+
- **Template Formatting**: Fixed an issue in `package.json.ejs` causing unexpected empty lines between generated dependencies.
|
|
8
|
+
- **Unit Test Stability**: Resolved persistent `SyntaxError` and `TypeError` in Jest unit tests across all architecture permutations by implementing ESM-compatible transformation patterns for `uuid` and standardizing on `jest.clearAllMocks()` with factory mocks.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Documentation Refinement**: Remade `README.md` and removed emojis for a cleaner, more professional, text-only presentation across the VitePress-based site.
|
|
12
|
+
|
|
3
13
|
## [2.1.0] - 2026-04-20
|
|
4
14
|
|
|
15
|
+
|
|
5
16
|
### Added
|
|
6
17
|
- **Pluggable JWT Authentication**: Introduced a robust, production-ready authentication system.
|
|
7
18
|
- Supports **Access & Refresh Token Rotation** for enhanced security.
|
package/README.md
CHANGED
|
@@ -6,41 +6,35 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/nodejs-quickstart-structure)
|
|
7
7
|
[](https://opensource.org/licenses/ISC)
|
|
8
8
|
|
|
9
|
-
### π Real-world Adoption
|
|
10
|
-
| **Metric** | **Insight** |
|
|
11
|
-
| :--- | :--- |
|
|
12
|
-
| π₯ **4,000+** | Downloads on npm |
|
|
13
|
-
| π **1,500+** | Recent GitHub Clones |
|
|
14
|
-
| π **Trusted by** | Devs from **Google**, **Viblo**, and global tech teams |
|
|
15
9
|
|
|
16
10
|
---
|
|
17
11
|
|
|
18
12
|
A powerful ecosystem to scaffold production-ready Node.js microservices with built-in best practices. Choose between **MVC** or **Clean Architecture**, **JavaScript** or **TypeScript**, and your preferred tech stack in seconds.
|
|
19
13
|
|
|
20
|
-
##
|
|
14
|
+
## Table of Contents
|
|
21
15
|
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
16
|
+
- [Quick Start](#choose-your-journey)
|
|
17
|
+
- [What's New](#whats-new-in-v21-the-authentication-release)
|
|
18
|
+
- [Key Features](#key-features)
|
|
19
|
+
- [Professional Standards](#professional-standards)
|
|
20
|
+
- [5,280+ Project Combinations](#5280-project-combinations)
|
|
21
|
+
- [Configuration Options](#configuration-options)
|
|
22
|
+
- [Generated Project Structure](#generated-project-structure)
|
|
23
|
+
- [Documentation](#documentation)
|
|
24
|
+
- [Support & Roadmap](#support--roadmap)
|
|
31
25
|
|
|
32
|
-
##
|
|
26
|
+
## Choose Your Journey
|
|
33
27
|
|
|
34
|
-
| **Path A: Next-Gen Web UI** (Recommended
|
|
28
|
+
| **Path A: Next-Gen Web UI** (Recommended) | **Path B: Interactive CLI** |
|
|
35
29
|
| :--- | :--- |
|
|
36
30
|
| <a href="https://paudang.github.io/nodejs-quickstart-structure/#configurator"><img src="docs/public/v2-preview.png" width="100%" alt="UI Preview"></a> | <img src="docs/demo.gif" width="100%" alt="CLI Demo"> |
|
|
37
|
-
| [Try Visual Configurator β](https://paudang.github.io/nodejs-quickstart-structure/#configurator) | [See CLI Commands β](
|
|
38
|
-
|
|
|
39
|
-
|
|
|
31
|
+
| [Try Visual Configurator β](https://paudang.github.io/nodejs-quickstart-structure/#configurator) | [See CLI Commands β](#path-b-interactive-cli) |
|
|
32
|
+
| **Visual Preview**: Real-time folder simulation. | **Fast & Direct**: Quickly scaffold in terminal. |
|
|
33
|
+
| **Zero-Prompt**: Paste a tailored command. | **AI-Ready**: Generates `.cursorrules`. |
|
|
40
34
|
|
|
41
35
|
---
|
|
42
36
|
|
|
43
|
-
###
|
|
37
|
+
### Path B: Interactive CLI
|
|
44
38
|
**Scaffold your project directly from your terminal in seconds.**
|
|
45
39
|
|
|
46
40
|
```bash
|
|
@@ -56,21 +50,21 @@ nodejs-quickstart init
|
|
|
56
50
|
|
|
57
51
|
---
|
|
58
52
|
|
|
59
|
-
##
|
|
53
|
+
## What's New in v2.1 (The Authentication Release)
|
|
60
54
|
|
|
61
55
|
The v2.1.0 release is a major leap forward, turning the generator into a **Community Standard**:
|
|
62
56
|
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
57
|
+
- **Pluggable JWT Authentication**: Production-ready access & refresh token patterns with automatic PM2/Environment configuration.
|
|
58
|
+
- **AI-Native Foundation**: Built-in `.cursorrules` optimized for **Cursor** & AI agentsβprojects are "Born to be Autonomously Coded."
|
|
59
|
+
- **Next-Gen Web UI**: A browser-based visual project simulator with real-time folder previews.
|
|
60
|
+
- **Enterprise Clean Architecture**: High-fidelity structure for professional Microservices (TS/JS).
|
|
61
|
+
- **Hardened Security**: Integrated Snyk & SonarCloud logic in core templates.
|
|
62
|
+
- **Zero-Prompt Workflow**: Generate projects with a single CLI command.
|
|
69
63
|
|
|
70
64
|
---
|
|
71
65
|
|
|
72
66
|
|
|
73
|
-
##
|
|
67
|
+
## Key Features
|
|
74
68
|
|
|
75
69
|
- **Interactive CLI**: Smooth, guided configuration process.
|
|
76
70
|
- **Multiple Architectures**: Supports both **MVC** and **Clean Architecture**.
|
|
@@ -79,24 +73,24 @@ nodejs-quickstart init
|
|
|
79
73
|
- **Communication Patterns**: Supports **REST**, **GraphQL** (Apollo), and **Kafka** (Event-driven).
|
|
80
74
|
- **Multi-layer Caching**: Integrated **Redis** or built-in **Memory Cache**.
|
|
81
75
|
- **Pluggable Authentication**: Built-in **JWT** support (Refresh/Access tokens).
|
|
82
|
-
- **AI-Native Optimized**: specifically designed for **Cursor** and AI agents, including built-in `.cursorrules` and Agent Skill prompts.
|
|
76
|
+
- **AI-Native Optimized**: specifically designed for **Cursor** and AI agents, including built-in `.cursorrules` and Agent Skill prompts.
|
|
83
77
|
|
|
84
78
|
---
|
|
85
79
|
|
|
86
|
-
##
|
|
80
|
+
## Professional Standards
|
|
87
81
|
|
|
88
82
|
We don't just generate boilerplate; we generate **production-ready** foundations. Every project includes:
|
|
89
83
|
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
84
|
+
- **Code Quality**: Pre-configured `Eslint` and `Prettier`.
|
|
85
|
+
- **Enterprise Security**: Integrated **Snyk (SCA)**, **SonarCloud (SAST)**, `Helmet`, `HPP`, and Rate-Limiting.
|
|
86
|
+
- **Robust Error Handling**: Centralized global error middleware with custom error classes (`ApiError`, `NotFoundError`, etc.) β consistent across REST & GraphQL.
|
|
87
|
+
- **Testing Excellence**: Integrated `Jest` and `Supertest` with **>80% Unit Test coverage** out of the box.
|
|
88
|
+
- **DevOps & CI/CD**: Optimized **Multi-Stage Dockerfiles**, health checks, infrastructure retry logic, and workflows for **GitHub Actions**, **Jenkins**, **GitLab CI**, **CircleCI**, and **Bitbucket Pipelines**.
|
|
89
|
+
- **Scalable Deployment**: Integrated **PM2 Ecosystem** config for zero-downtime reloads.
|
|
96
90
|
|
|
97
91
|
---
|
|
98
92
|
|
|
99
|
-
##
|
|
93
|
+
## 5,280+ Project Combinations
|
|
100
94
|
|
|
101
95
|
The CLI supports a massive number of configurations to fit your exact needs:
|
|
102
96
|
|
|
@@ -110,7 +104,7 @@ The CLI supports a massive number of configurations to fit your exact needs:
|
|
|
110
104
|
|
|
111
105
|
---
|
|
112
106
|
|
|
113
|
-
##
|
|
107
|
+
## Configuration Options
|
|
114
108
|
|
|
115
109
|
The CLI will guide you through:
|
|
116
110
|
1. **Project Name**
|
|
@@ -126,7 +120,7 @@ The CLI will guide you through:
|
|
|
126
120
|
|
|
127
121
|
---
|
|
128
122
|
|
|
129
|
-
##
|
|
123
|
+
## Generated Project Structure
|
|
130
124
|
Depending on your choices, the structure adapts. Here is a **TypeScript + Clean Architecture** preview:
|
|
131
125
|
|
|
132
126
|
```text
|
|
@@ -148,13 +142,13 @@ Depending on your choices, the structure adapts. Here is a **TypeScript + Clean
|
|
|
148
142
|
|
|
149
143
|
---
|
|
150
144
|
|
|
151
|
-
##
|
|
145
|
+
## Documentation
|
|
152
146
|
|
|
153
147
|
For full guides, architecture deep-dives, and feature references, visit our **[Official Documentation Site](https://paudang.github.io/nodejs-quickstart-structure/guide/getting-started.html)**.
|
|
154
148
|
|
|
155
149
|
---
|
|
156
150
|
|
|
157
|
-
##
|
|
151
|
+
## Support & Roadmap
|
|
158
152
|
|
|
159
153
|
### Support the Project
|
|
160
154
|
If this tool helped you build your project faster, please support us:
|
|
@@ -163,11 +157,11 @@ If this tool helped you build your project faster, please support us:
|
|
|
163
157
|
|
|
164
158
|
### Roadmap
|
|
165
159
|
Track our progress and vote for features on our public board:
|
|
166
|
-
|
|
160
|
+
**[View our Public Roadmap on Trello](https://trello.com/b/TPTo8ylF/nodejs-quickstart-structure-product)**
|
|
167
161
|
|
|
168
162
|
---
|
|
169
163
|
|
|
170
|
-
##
|
|
164
|
+
## Why Star us?
|
|
171
165
|
|
|
172
166
|
We are on a mission to build the best AI-Native Node.js scaffolding experience. Your star is not just a "like"βit's a vote of confidence that helps us:
|
|
173
167
|
|
|
@@ -175,7 +169,7 @@ We are on a mission to build the best AI-Native Node.js scaffolding experience.
|
|
|
175
169
|
2. **AI Model Awareness**: Popular repositories are weighted higher by AI coding assistants (Cursor, Copilot, etc.), making the generated code even better.
|
|
176
170
|
3. **Open Source Sustainability**: It motivates us to keep building and shipping "Enterprise-Grade" features for free.
|
|
177
171
|
|
|
178
|
-
If this tool saved you hours of work, **[please give us a Star!](https://github.com/paudang/nodejs-quickstart-structure)**
|
|
172
|
+
If this tool saved you hours of work, **[please give us a Star!](https://github.com/paudang/nodejs-quickstart-structure)**
|
|
179
173
|
|
|
180
174
|
---
|
|
181
175
|
|
|
@@ -53,6 +53,12 @@ export const renderProfessionalConfig = async (templatesDir, targetDir, config)
|
|
|
53
53
|
const jestE2eContent = ejs.render(jestE2eTemplate, { ...config });
|
|
54
54
|
await fs.writeFile(path.join(targetDir, 'jest.e2e.config.js'), jestE2eContent);
|
|
55
55
|
|
|
56
|
+
if (config.language === 'JavaScript') {
|
|
57
|
+
const babelTemplate = await fs.readFile(path.join(templatesDir, 'common', 'babel.config.js.ejs'), 'utf-8');
|
|
58
|
+
const babelContent = ejs.render(babelTemplate, { ...config });
|
|
59
|
+
await fs.writeFile(path.join(targetDir, 'babel.config.js'), babelContent);
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
// 1. Setup Husky pre-commit (Always for Professional Standard)
|
|
57
63
|
const huskyDir = path.join(targetDir, '.husky');
|
|
58
64
|
await fs.ensureDir(huskyDir);
|
package/package.json
CHANGED
package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
const UserRepository = require('@/infrastructure/repositories/UserRepository');
|
|
2
2
|
const UserModel = require('@/infrastructure/database/models/User');
|
|
3
3
|
|
|
4
|
-
jest.mock('@/infrastructure/database/models/User')
|
|
4
|
+
jest.mock('@/infrastructure/database/models/User', () => ({
|
|
5
|
+
create: jest.fn(),
|
|
6
|
+
findAll: jest.fn(),
|
|
7
|
+
findByPk: jest.fn(),
|
|
8
|
+
update: jest.fn(),
|
|
9
|
+
destroy: jest.fn(),
|
|
10
|
+
find: jest.fn(),
|
|
11
|
+
findById: jest.fn(),
|
|
12
|
+
findByIdAndUpdate: jest.fn(),
|
|
13
|
+
findByIdAndDelete: jest.fn(),
|
|
14
|
+
}));
|
|
5
15
|
|
|
6
16
|
describe('UserRepository', () => {
|
|
7
17
|
let userRepository;
|
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
<% if (auth.includes('JWT')) { %>
|
|
2
|
-
|
|
1
|
+
<% if (auth.includes('JWT')) { %>
|
|
2
|
+
const JwtService = require('@/infrastructure/auth/jwtService');
|
|
3
|
+
jest.mock('@/infrastructure/auth/jwtService', () => ({
|
|
4
|
+
verifyToken: jest.fn(),
|
|
5
|
+
generateToken: jest.fn(),
|
|
6
|
+
generateRefreshToken: jest.fn(),
|
|
7
|
+
verifyRefreshToken: jest.fn(),
|
|
8
|
+
decodeToken: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
<% } %>
|
|
3
11
|
const { gqlContext } = require('@/interfaces/graphql/context');
|
|
4
12
|
const { resolvers } = require('@/interfaces/graphql/index');
|
|
5
13
|
const { typeDefs } = require('@/interfaces/graphql/typeDefs/index');
|
|
6
14
|
|
|
7
|
-
jest.mock('@/infrastructure/repositories/UserRepository')
|
|
15
|
+
jest.mock('@/infrastructure/repositories/UserRepository', () => {
|
|
16
|
+
return jest.fn().mockImplementation(() => ({
|
|
17
|
+
save: jest.fn(),
|
|
18
|
+
findById: jest.fn(),
|
|
19
|
+
getUsers: jest.fn(),
|
|
20
|
+
update: jest.fn(),
|
|
21
|
+
delete: jest.fn()
|
|
22
|
+
}));
|
|
23
|
+
});
|
|
8
24
|
|
|
9
25
|
describe('GraphQL Context', () => {
|
|
10
26
|
afterEach(() => {
|
|
@@ -4,7 +4,15 @@ const UserRepository = require('@/infrastructure/repositories/UserRepository');
|
|
|
4
4
|
const cacheService = require('<% 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
|
+
return 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(),
|
|
@@ -4,7 +4,15 @@ const UserRepository = require('@/infrastructure/repositories/UserRepository');
|
|
|
4
4
|
const cacheService = require('<% 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
|
+
return 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(),
|
|
@@ -18,7 +18,15 @@ const UserRepository = require('@/infrastructure/repositories/UserRepository');
|
|
|
18
18
|
const cacheService = require('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>');
|
|
19
19
|
<%_ } -%>
|
|
20
20
|
|
|
21
|
-
jest.mock('@/infrastructure/repositories/UserRepository')
|
|
21
|
+
jest.mock('@/infrastructure/repositories/UserRepository', () => {
|
|
22
|
+
return jest.fn().mockImplementation(() => ({
|
|
23
|
+
save: jest.fn(),
|
|
24
|
+
findById: jest.fn(),
|
|
25
|
+
getUsers: jest.fn(),
|
|
26
|
+
update: jest.fn(),
|
|
27
|
+
delete: jest.fn()
|
|
28
|
+
}));
|
|
29
|
+
});
|
|
22
30
|
<%_ if (caching !== 'None') { -%>
|
|
23
31
|
jest.mock('<% if (caching === "Redis") { %>@/infrastructure/caching/redisClient<% } else { %>@/infrastructure/caching/memoryCache<% } %>', () => ({
|
|
24
32
|
get: jest.fn(),
|
|
@@ -4,7 +4,15 @@ const UserRepository = require('@/infrastructure/repositories/UserRepository');
|
|
|
4
4
|
const cacheService = require('<% 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
|
+
return 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(),
|
package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs
CHANGED
|
@@ -2,7 +2,22 @@ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
|
2
2
|
import UserModel from '@/infrastructure/database/models/User';
|
|
3
3
|
|
|
4
4
|
// Mock DB Model Database Layer
|
|
5
|
-
jest.mock('@/infrastructure/database/models/User')
|
|
5
|
+
jest.mock('@/infrastructure/database/models/User', () => {
|
|
6
|
+
return {
|
|
7
|
+
__esModule: true,
|
|
8
|
+
default: {
|
|
9
|
+
create: jest.fn(),
|
|
10
|
+
findAll: jest.fn(),
|
|
11
|
+
findByPk: jest.fn(),
|
|
12
|
+
update: jest.fn(),
|
|
13
|
+
destroy: jest.fn(),
|
|
14
|
+
find: jest.fn(),
|
|
15
|
+
findById: jest.fn(),
|
|
16
|
+
findByIdAndUpdate: jest.fn(),
|
|
17
|
+
findByIdAndDelete: jest.fn(),
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
});
|
|
6
21
|
|
|
7
22
|
describe('UserRepository', () => {
|
|
8
23
|
let userRepository: UserRepository;
|
package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs
CHANGED
|
@@ -12,12 +12,26 @@ import UpdateUser from '@/usecases/updateUser';
|
|
|
12
12
|
import DeleteUser from '@/usecases/deleteUser';
|
|
13
13
|
|
|
14
14
|
// Mock dependencies
|
|
15
|
-
jest.mock('@/infrastructure/repositories/UserRepository')
|
|
15
|
+
jest.mock('@/infrastructure/repositories/UserRepository', () => ({
|
|
16
|
+
UserRepository: jest.fn().mockImplementation(() => ({
|
|
17
|
+
save: jest.fn(),
|
|
18
|
+
findById: jest.fn(),
|
|
19
|
+
getUsers: jest.fn(),
|
|
20
|
+
update: jest.fn(),
|
|
21
|
+
delete: jest.fn()
|
|
22
|
+
}))
|
|
23
|
+
}));
|
|
16
24
|
jest.mock('@/usecases/createUser');
|
|
17
25
|
jest.mock('@/usecases/getAllUsers');
|
|
18
26
|
jest.mock('@/usecases/updateUser');
|
|
19
27
|
jest.mock('@/usecases/deleteUser');
|
|
20
|
-
jest.mock('@/
|
|
28
|
+
jest.mock('@/usecases/getUserById');
|
|
29
|
+
jest.mock('@/infrastructure/log/logger', () => ({
|
|
30
|
+
info: jest.fn(),
|
|
31
|
+
error: jest.fn(),
|
|
32
|
+
warn: jest.fn(),
|
|
33
|
+
debug: jest.fn()
|
|
34
|
+
}));
|
|
21
35
|
<%_ if (communication === 'Kafka') { -%>
|
|
22
36
|
jest.mock('@/infrastructure/messaging/kafkaClient', () => ({
|
|
23
37
|
kafkaService: {
|
|
@@ -45,7 +59,7 @@ describe('UserController (Clean Architecture)', () => {
|
|
|
45
59
|
|
|
46
60
|
beforeEach(() => {
|
|
47
61
|
// Clear all mocks
|
|
48
|
-
jest.
|
|
62
|
+
jest.clearAllMocks();
|
|
49
63
|
|
|
50
64
|
userController = new UserController();
|
|
51
65
|
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { Request } from 'express';
|
|
2
|
-
<% if (auth.includes('JWT')) { %>
|
|
3
|
-
|
|
2
|
+
<% if (auth.includes('JWT')) { %>
|
|
3
|
+
import { JwtService } from '@/infrastructure/auth/jwtService';
|
|
4
|
+
jest.mock('@/infrastructure/auth/jwtService', () => ({
|
|
5
|
+
JwtService: {
|
|
6
|
+
generateToken: jest.fn(),
|
|
7
|
+
verifyToken: jest.fn(),
|
|
8
|
+
generateRefreshToken: jest.fn(),
|
|
9
|
+
verifyRefreshToken: jest.fn(),
|
|
10
|
+
decodeToken: jest.fn(),
|
|
11
|
+
}
|
|
12
|
+
}));
|
|
13
|
+
<% } %>
|
|
4
14
|
import { gqlContext } from '@/interfaces/graphql/context';
|
|
5
15
|
|
|
6
|
-
jest.mock('@/infrastructure/repositories/UserRepository')
|
|
16
|
+
jest.mock('@/infrastructure/repositories/UserRepository', () => ({
|
|
17
|
+
UserRepository: jest.fn().mockImplementation(() => ({
|
|
18
|
+
save: jest.fn(),
|
|
19
|
+
findById: jest.fn(),
|
|
20
|
+
getUsers: jest.fn(),
|
|
21
|
+
update: jest.fn(),
|
|
22
|
+
delete: jest.fn()
|
|
23
|
+
}))
|
|
24
|
+
}));
|
|
7
25
|
|
|
8
26
|
describe('GraphQL Context', () => {
|
|
9
27
|
afterEach(() => {
|
|
@@ -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(),
|
|
@@ -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(),
|
|
@@ -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(),
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import GetUserById from '@/usecases/getUserById';
|
|
2
2
|
import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
3
3
|
|
|
4
|
-
jest.mock('@/infrastructure/repositories/UserRepository')
|
|
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
|
+
}));
|
|
5
13
|
|
|
6
14
|
<% if (caching === 'Redis') { -%>
|
|
7
15
|
jest.mock('@/infrastructure/caching/redisClient', () => ({
|
|
@@ -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(),
|
|
@@ -45,16 +45,16 @@ class AuthController {
|
|
|
45
45
|
const refreshJti = JwtService.decodeToken(refreshToken)?.jti;
|
|
46
46
|
const accessToken = JwtService.generateToken({ id: userId, email: user.email, sid: refreshJti });
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
<%_ if (caching !== 'None') { -%>
|
|
49
49
|
const cacheKey = `refresh_tokens:${userId}`;
|
|
50
50
|
const activeTokens = await cacheService.get(cacheKey) || [];
|
|
51
51
|
activeTokens.push(refreshJti);
|
|
52
52
|
await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
|
|
53
|
-
|
|
53
|
+
<% } else { %>
|
|
54
54
|
const activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
55
55
|
activeTokens.push(refreshJti);
|
|
56
56
|
JwtService.activeRefreshTokens.set(userId, activeTokens);
|
|
57
|
-
|
|
57
|
+
<%_ } -%>
|
|
58
58
|
|
|
59
59
|
res.json({ token: accessToken, accessToken, refreshToken });
|
|
60
60
|
} catch (error) {
|
|
@@ -78,7 +78,7 @@ class AuthController {
|
|
|
78
78
|
const userId = String(decoded.id);
|
|
79
79
|
const incomingJti = decoded.jti;
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
<%_ if (caching !== 'None') { -%>
|
|
82
82
|
const cacheKey = `refresh_tokens:${userId}`;
|
|
83
83
|
let activeTokens = await cacheService.get(cacheKey) || [];
|
|
84
84
|
|
|
@@ -95,7 +95,7 @@ class AuthController {
|
|
|
95
95
|
|
|
96
96
|
activeTokens.push(newRefreshJti);
|
|
97
97
|
await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
|
|
98
|
-
|
|
98
|
+
<% } else { %>
|
|
99
99
|
let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
100
100
|
|
|
101
101
|
if (!activeTokens.includes(incomingJti)) {
|
|
@@ -111,7 +111,7 @@ class AuthController {
|
|
|
111
111
|
|
|
112
112
|
activeTokens.push(newRefreshJti);
|
|
113
113
|
JwtService.activeRefreshTokens.set(userId, activeTokens);
|
|
114
|
-
|
|
114
|
+
<%_ } -%>
|
|
115
115
|
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
|
|
116
116
|
} catch (error) {
|
|
117
117
|
logger.error('Refresh token error:', error);
|
|
@@ -131,13 +131,15 @@ class AuthController {
|
|
|
131
131
|
|
|
132
132
|
if (decodedAccess && decodedAccess.jti && decodedAccess.exp) {
|
|
133
133
|
const remainingTime = Math.max(0, decodedAccess.exp - Math.floor(Date.now() / 1000));
|
|
134
|
-
|
|
134
|
+
<%_ if (caching !== 'None') { -%>
|
|
135
135
|
if (remainingTime > 0) {
|
|
136
136
|
await cacheService.set(`blacklist:${decodedAccess.jti}`, true, remainingTime);
|
|
137
|
-
}
|
|
137
|
+
}
|
|
138
|
+
<% } else { %>
|
|
138
139
|
if (remainingTime > 0) {
|
|
139
140
|
JwtService.blacklistedTokens.set(decodedAccess.jti, Date.now() + remainingTime * 1000);
|
|
140
|
-
}
|
|
141
|
+
}
|
|
142
|
+
<%_ } -%>
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
const { refreshToken } = req.body;
|
|
@@ -93,7 +93,7 @@ export class AuthController {
|
|
|
93
93
|
|
|
94
94
|
activeTokens.push(newRefreshJti!);
|
|
95
95
|
await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
|
|
96
|
-
<% } else {
|
|
96
|
+
<% } else { %>
|
|
97
97
|
let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
98
98
|
|
|
99
99
|
if (!activeTokens.includes(incomingJti)) {
|
|
@@ -130,13 +130,15 @@ export class AuthController {
|
|
|
130
130
|
|
|
131
131
|
if (decodedAccess && decodedAccess.jti && decodedAccess.exp) {
|
|
132
132
|
const remainingTime = Math.max(0, decodedAccess.exp - Math.floor(Date.now() / 1000));
|
|
133
|
-
|
|
133
|
+
<%_ if (caching !== 'None') { -%>
|
|
134
134
|
if (remainingTime > 0) {
|
|
135
135
|
await cacheService.set(`blacklist:${decodedAccess.jti}`, true, remainingTime);
|
|
136
|
-
}
|
|
136
|
+
}
|
|
137
|
+
<% } else { %>
|
|
137
138
|
if (remainingTime > 0) {
|
|
138
139
|
JwtService.blacklistedTokens.set(decodedAccess.jti, Date.now() + remainingTime * 1000);
|
|
139
|
-
}
|
|
140
|
+
}
|
|
141
|
+
<%_ } -%>
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
const { refreshToken } = req.body;
|
|
@@ -149,7 +151,7 @@ export class AuthController {
|
|
|
149
151
|
let activeTokens = await cacheService.get<string[]>(cacheKey) || [];
|
|
150
152
|
activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
|
|
151
153
|
await cacheService.set(cacheKey, activeTokens, 7 * 24 * 60 * 60);
|
|
152
|
-
<%
|
|
154
|
+
<% } else { %>
|
|
153
155
|
let activeTokens = JwtService.activeRefreshTokens.get(userId) || [];
|
|
154
156
|
activeTokens = activeTokens.filter(t => t !== decodedRefresh.jti);
|
|
155
157
|
JwtService.activeRefreshTokens.set(userId, activeTokens);<% } %>
|
|
@@ -5,6 +5,8 @@ jest.mock('mongoose', () => ({
|
|
|
5
5
|
const mockLogger = {
|
|
6
6
|
info: jest.fn(),
|
|
7
7
|
error: jest.fn(),
|
|
8
|
+
warn: jest.fn(),
|
|
9
|
+
debug: jest.fn(),
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
jest.mock('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>', () => mockLogger);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
testEnvironment: 'node',
|
|
3
|
+
testTimeout: 30000,
|
|
3
4
|
coverageDirectory: 'coverage',
|
|
4
5
|
collectCoverageFrom: ['src/**/*.{js,ts}'],
|
|
5
6
|
testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.spec.ts', '**/*.spec.js'],
|
|
7
|
+
transformIgnorePatterns: ['/node_modules/(?!.*uuid)'],
|
|
6
8
|
testPathIgnorePatterns: ['/node_modules/', '/tests/e2e/'],
|
|
7
9
|
<% if (language === 'TypeScript') { %>preset: 'ts-jest',<% } %>
|
|
8
10
|
moduleNameMapper: {
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"braces": "^3.0.3",
|
|
30
30
|
"picomatch": "^4.0.4",
|
|
31
31
|
"lodash": "^4.17.23",
|
|
32
|
-
"debounce": "^1.2.1"
|
|
32
|
+
"debounce": "^1.2.1",
|
|
33
|
+
"uuid": "^14.0.0"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"express": "^4.18.2",
|
|
@@ -58,7 +59,6 @@
|
|
|
58
59
|
"bcryptjs": "^2.4.3",
|
|
59
60
|
<% } -%>
|
|
60
61
|
"cors": "^2.8.5",
|
|
61
|
-
|
|
62
62
|
"helmet": "^7.1.0",
|
|
63
63
|
"hpp": "^0.2.3",
|
|
64
64
|
"express-rate-limit": "^7.1.5",
|
|
@@ -92,7 +92,6 @@
|
|
|
92
92
|
"cpx2": "^8.0.0"<% } %><% if (auth.includes('JWT')) { %>,
|
|
93
93
|
"@types/jsonwebtoken": "^9.0.6",
|
|
94
94
|
"@types/bcryptjs": "^2.4.6"<% } %><% } %>,
|
|
95
|
-
|
|
96
95
|
"eslint": "^10.1.0",
|
|
97
96
|
"@eslint/js": "^9.20.0",
|
|
98
97
|
"globals": "^15.14.0",
|
|
@@ -115,6 +114,9 @@
|
|
|
115
114
|
"tsc-alias": "^1.8.10",
|
|
116
115
|
"@types/supertest": "^6.0.2"<% } else { %>,
|
|
117
116
|
"jest": "^29.7.0",
|
|
117
|
+
"babel-jest": "^29.7.0",
|
|
118
|
+
"@babel/core": "^7.24.0",
|
|
119
|
+
"@babel/preset-env": "^7.24.0",
|
|
118
120
|
"wait-on": "^7.2.0",
|
|
119
121
|
"supertest": "^7.1.3"<% } %>
|
|
120
122
|
},
|