nodejs-quickstart-structure 2.1.1 → 2.2.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 +16 -0
- package/README.md +12 -17
- package/bin/index.js +1 -0
- package/lib/generator.js +1 -1
- package/lib/modules/app-setup.js +16 -0
- package/lib/modules/auth-setup.js +46 -4
- package/lib/prompts.js +44 -4
- package/package.json +3 -2
- package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -2
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +27 -0
- package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +24 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +3 -1
- package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -2
- package/templates/clean-architecture/ts/src/domain/user.ts.ejs +14 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +24 -0
- package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +43 -45
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +5 -5
- package/templates/common/.env.example.ejs +10 -0
- package/templates/common/README.md.ejs +65 -14
- package/templates/common/auth/js/controllers/authController.js.ejs +326 -13
- package/templates/common/auth/js/controllers/authController.spec.js.ejs +237 -51
- package/templates/common/auth/js/middleware/authMiddleware.js.ejs +10 -6
- package/templates/common/auth/js/routes/authRoutes.js.ejs +11 -0
- package/templates/common/auth/js/services/jwtService.js.ejs +3 -3
- package/templates/common/auth/js/services/jwtService.spec.js.ejs +30 -0
- package/templates/common/auth/js/services/socialAuthService.js.ejs +175 -0
- package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +194 -0
- package/templates/common/auth/js/usecases/SocialLoginUseCase.js.ejs +114 -0
- package/templates/common/auth/js/usecases/SocialLoginUseCase.spec.js.ejs +143 -0
- package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +344 -64
- package/templates/common/auth/ts/controllers/authController.ts.ejs +341 -9
- package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +10 -6
- package/templates/common/auth/ts/routes/authRoutes.ts.ejs +11 -0
- package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +18 -0
- package/templates/common/auth/ts/services/jwtService.ts.ejs +3 -3
- package/templates/common/auth/ts/services/socialAuthService.spec.ts.ejs +187 -0
- package/templates/common/auth/ts/services/socialAuthService.ts.ejs +189 -0
- package/templates/common/auth/ts/usecases/SocialLoginUseCase.spec.ts.ejs +143 -0
- package/templates/common/auth/ts/usecases/SocialLoginUseCase.ts.ejs +117 -0
- package/templates/common/database/js/models/User.js.ejs +13 -5
- package/templates/common/database/js/models/User.js.mongoose.ejs +15 -1
- package/templates/common/database/ts/models/User.ts.ejs +23 -7
- package/templates/common/database/ts/models/User.ts.mongoose.ejs +18 -2
- package/templates/common/docker-compose.yml.ejs +21 -0
- package/templates/common/ecosystem.config.js.ejs +10 -0
- package/templates/common/jest.config.js.ejs +1 -1
- package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
- package/templates/common/package.json.ejs +2 -0
- package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +13 -1
- package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +13 -1
- package/templates/common/swagger.yml.ejs +62 -3
- package/templates/common/views/ejs/login.ejs.ejs +84 -0
- package/templates/common/views/ejs/signup.ejs.ejs +84 -0
- package/templates/common/views/pug/login.pug.ejs +78 -0
- package/templates/common/views/pug/signup.pug.ejs +78 -0
- package/templates/db/mysql/V1__Initial_Setup.sql.ejs +3 -1
- package/templates/db/postgres/V1__Initial_Setup.sql.ejs +3 -1
- package/templates/mvc/js/src/config/env.js.ejs +12 -2
- package/templates/mvc/js/src/controllers/userController.js.ejs +1 -1
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -3
- package/templates/mvc/ts/src/config/env.ts.ejs +12 -2
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +1 -0
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +5 -5
- package/templates/mvc/ts/src/index.ts.ejs +2 -1
- package/templates/clean-architecture/ts/src/domain/user.ts +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
2
|
+
|
|
3
|
+
## [2.2.0] - 2026-05-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Social Login Support**: Integrated Google and GitHub authentication into the Clean Architecture and MVC templates.
|
|
7
|
+
- **Expanded Validation Matrix**: Scaled the mathematical validation matrix to **7,920+ unique project scenarios**, ensuring 100% template rendering accuracy across all permutations.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Clean Architecture Restructuring**: Moved use cases from `src/domain/usecases` to `src/usecases` (Application Layer) to strictly follow architectural boundaries.
|
|
11
|
+
- **Improved EJS Alignment**: Optimized all templates with whitespace-slurping tags (`<%_`, `_%>`) to eliminate redundant blank lines in generated code.
|
|
12
|
+
- **Environment Validation**: Added social authentication credentials to Zod-based environment schemas.
|
|
2
13
|
|
|
14
|
+
## [2.1.2] - 2026-04-27
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Security Hardening**: Updated `postcss` to `^8.5.10` in project overrides to resolve potential security vulnerabilities and improve build stability.
|
|
18
|
+
|
|
3
19
|
## [2.1.1] - 2026-04-24
|
|
4
20
|
|
|
5
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A powerful ecosystem to scaffold production-ready Node.js microservices with bui
|
|
|
17
17
|
- [What's New](#whats-new-in-v21-the-authentication-release)
|
|
18
18
|
- [Key Features](#key-features)
|
|
19
19
|
- [Professional Standards](#professional-standards)
|
|
20
|
-
- [
|
|
20
|
+
- [7,920+ Project Combinations](#7920-project-combinations)
|
|
21
21
|
- [Configuration Options](#configuration-options)
|
|
22
22
|
- [Generated Project Structure](#generated-project-structure)
|
|
23
23
|
- [Documentation](#documentation)
|
|
@@ -50,20 +50,15 @@ nodejs-quickstart init
|
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
-
## What's New in v2.
|
|
53
|
+
## What's New in v2.2 (The Social Auth Release)
|
|
54
54
|
|
|
55
|
-
The v2.
|
|
55
|
+
The v2.2.0 release brings enterprise-grade identity management to your microservices:
|
|
56
56
|
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
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.
|
|
57
|
+
- **OAuth2 Social Login**: Seamlessly integrate **Google** and **GitHub** authentication with automatic user provisioning and JWT session linking.
|
|
58
|
+
- **Massive Matrix Expansion**: Now supporting **7,920+ unique project scenarios**, mathematically validated for template consistency.
|
|
63
59
|
|
|
64
60
|
---
|
|
65
61
|
|
|
66
|
-
|
|
67
62
|
## Key Features
|
|
68
63
|
|
|
69
64
|
- **Interactive CLI**: Smooth, guided configuration process.
|
|
@@ -72,7 +67,7 @@ nodejs-quickstart init
|
|
|
72
67
|
- **Database Ready**: Pre-configured for **MySQL**, **PostgreSQL**, or **MongoDB**.
|
|
73
68
|
- **Communication Patterns**: Supports **REST**, **GraphQL** (Apollo), and **Kafka** (Event-driven).
|
|
74
69
|
- **Multi-layer Caching**: Integrated **Redis** or built-in **Memory Cache**.
|
|
75
|
-
- **Pluggable Authentication**: Built-in **JWT**
|
|
70
|
+
- **Pluggable Authentication**: Built-in **JWT** and **OAuth2 (Google/GitHub)** support with Access/Refresh token rotation.
|
|
76
71
|
- **AI-Native Optimized**: specifically designed for **Cursor** and AI agents, including built-in `.cursorrules` and Agent Skill prompts.
|
|
77
72
|
|
|
78
73
|
---
|
|
@@ -90,14 +85,14 @@ We don't just generate boilerplate; we generate **production-ready** foundations
|
|
|
90
85
|
|
|
91
86
|
---
|
|
92
87
|
|
|
93
|
-
##
|
|
88
|
+
## 7,920+ Project Combinations
|
|
94
89
|
|
|
95
90
|
The CLI supports a massive number of configurations to fit your exact needs:
|
|
96
91
|
|
|
97
|
-
- **
|
|
98
|
-
- **MVC Architecture**:
|
|
99
|
-
- **Clean Architecture**:
|
|
100
|
-
- **
|
|
92
|
+
- **720 Core Combinations**:
|
|
93
|
+
- **MVC Architecture**: 540 variants (Languages × View Engines × Databases × Communication Patterns × Caching × Auth)
|
|
94
|
+
- **Clean Architecture**: 180 variants (Languages × Databases × Communication Patterns × Caching × Auth)
|
|
95
|
+
- **7,920+ Total Scenarios**:
|
|
101
96
|
- Every combination can be generated across 5 CI/CD providers.
|
|
102
97
|
- Optional **Enterprise-Grade Security Hardening** doubles the scenarios.
|
|
103
98
|
- Every single scenario is verified to be compatible with our **80% Coverage Threshold** policy.
|
|
@@ -114,7 +109,7 @@ The CLI will guide you through:
|
|
|
114
109
|
5. **Database**: `MySQL` | `PostgreSQL` | `MongoDB`
|
|
115
110
|
6. **Communication**: `REST` | `GraphQL` | `Kafka`
|
|
116
111
|
7. **Caching**: `None` | `Redis` | `Memory Cache`
|
|
117
|
-
8. **Auth**: `None` | `JWT`
|
|
112
|
+
8. **Auth**: `None` | `JWT` | `OAuth2 (Google/GitHub) + JWT`
|
|
118
113
|
9. **CI/CD**: `GitHub Actions` | `Jenkins` | `GitLab CI` | `CircleCI` | `Bitbucket Pipelines`
|
|
119
114
|
10. **Security**: (Optional) Snyk & SonarCloud Hardening
|
|
120
115
|
|
package/bin/index.js
CHANGED
|
@@ -34,6 +34,7 @@ program
|
|
|
34
34
|
.option('--no-include-security', 'Exclude Enterprise Security Hardening')
|
|
35
35
|
.option('--caching <type>', 'Caching Layer (None/Redis/Memory Cache)')
|
|
36
36
|
.option('--auth <modes...>', 'Authentication Modes (None, JWT)')
|
|
37
|
+
.option('--social-auth <providers...>', 'Social Authentication Providers (None, Google, GitHub)')
|
|
37
38
|
.option('--advanced-options', 'Enable Advanced Options')
|
|
38
39
|
.option('--no-advanced-options', 'Disable Advanced Options')
|
|
39
40
|
.action(async (options) => {
|
package/lib/generator.js
CHANGED
|
@@ -112,7 +112,7 @@ export const generateProject = async (config) => {
|
|
|
112
112
|
Language: ${language}
|
|
113
113
|
Database: ${config.database}
|
|
114
114
|
Communication: ${config.communication}${config.caching && config.caching !== 'None' ? `\n Caching: ${config.caching}` : ''}
|
|
115
|
-
Authentication: ${config.auth.join('
|
|
115
|
+
Authentication: ${[...(config.socialAuth || []).filter(s => s !== 'None'), ...config.auth.filter(a => a !== 'None')].join(' - ') || 'None'}
|
|
116
116
|
|
|
117
117
|
----------------------------------------------------
|
|
118
118
|
✨ High-Quality Standards Applied:
|
package/lib/modules/app-setup.js
CHANGED
|
@@ -175,6 +175,22 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
|
|
|
175
175
|
await fs.remove(path.join(targetDir, 'src/interfaces/routes', `${routeName}.ejs`));
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// --- Render All Domain Entities ---
|
|
179
|
+
const domainDir = path.join(targetDir, 'src/domain');
|
|
180
|
+
if (await fs.pathExists(domainDir)) {
|
|
181
|
+
const domainFiles = await fs.readdir(domainDir);
|
|
182
|
+
for (const file of domainFiles) {
|
|
183
|
+
if (file.endsWith('.ejs')) {
|
|
184
|
+
const templatePathObj = path.join(domainDir, file);
|
|
185
|
+
const destPathObj = path.join(domainDir, file.replace('.ejs', ''));
|
|
186
|
+
const template = await fs.readFile(templatePathObj, 'utf-8');
|
|
187
|
+
const content = ejs.render(template, { ...config });
|
|
188
|
+
await fs.writeFile(destPathObj, content);
|
|
189
|
+
await fs.remove(templatePathObj);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
178
194
|
// --- Render All Use Cases ---
|
|
179
195
|
const useCaseDir = path.join(targetDir, 'src/usecases');
|
|
180
196
|
if (await fs.pathExists(useCaseDir)) {
|
|
@@ -13,6 +13,10 @@ export const setupAuth = async (templatesDir, targetDir, config) => {
|
|
|
13
13
|
// 1. JWT Service
|
|
14
14
|
await renderAuthComponent(authSource, targetDir, 'services', `jwtService.${langExt}`, config);
|
|
15
15
|
|
|
16
|
+
if (config.socialAuth && config.socialAuth.filter(a => a !== 'None').length > 0) {
|
|
17
|
+
await renderAuthComponent(authSource, targetDir, 'services', `socialAuthService.${langExt}`, config);
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
// 2. Auth Middleware
|
|
17
21
|
await renderAuthComponent(authSource, targetDir, 'middleware', `authMiddleware.${langExt}`, config);
|
|
18
22
|
|
|
@@ -22,7 +26,12 @@ export const setupAuth = async (templatesDir, targetDir, config) => {
|
|
|
22
26
|
// 4. Auth Routes
|
|
23
27
|
await renderAuthComponent(authSource, targetDir, 'routes', `authRoutes.${langExt}`, config);
|
|
24
28
|
|
|
25
|
-
// 5.
|
|
29
|
+
// 5. Social Login Use Case (Clean Architecture only)
|
|
30
|
+
if (architecture === 'Clean Architecture' && config.socialAuth && config.socialAuth.filter(a => a !== 'None').length > 0) {
|
|
31
|
+
await renderAuthComponent(authSource, targetDir, 'usecases', `SocialLoginUseCase.${langExt}`, config);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 6. MVC Views (if applicable)
|
|
26
35
|
if (architecture === 'MVC' && viewEngine && viewEngine !== 'None') {
|
|
27
36
|
const engine = viewEngine.toLowerCase();
|
|
28
37
|
const views = ['login', 'signup'];
|
|
@@ -37,7 +46,7 @@ export const setupAuth = async (templatesDir, targetDir, config) => {
|
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
//
|
|
49
|
+
// 7. Restructuring for Clean Architecture
|
|
41
50
|
if (architecture === 'Clean Architecture') {
|
|
42
51
|
await restructureAuthForCleanArch(targetDir, langExt);
|
|
43
52
|
}
|
|
@@ -82,6 +91,21 @@ async function restructureAuthForCleanArch(targetDir, ext) {
|
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
|
|
94
|
+
if (await fs.pathExists(path.join(targetDir, `src/services/socialAuthService.${ext}`))) {
|
|
95
|
+
await fs.move(
|
|
96
|
+
path.join(targetDir, `src/services/socialAuthService.${ext}`),
|
|
97
|
+
path.join(targetDir, `src/infrastructure/auth/socialAuthService.${ext}`),
|
|
98
|
+
{ overwrite: true }
|
|
99
|
+
);
|
|
100
|
+
if (await fs.pathExists(path.join(targetDir, `tests/unit/services/socialAuthService.spec.${ext}`))) {
|
|
101
|
+
await fs.move(
|
|
102
|
+
path.join(targetDir, `tests/unit/services/socialAuthService.spec.${ext}`),
|
|
103
|
+
path.join(targetDir, `tests/unit/infrastructure/auth/socialAuthService.spec.${ext}`),
|
|
104
|
+
{ overwrite: true }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
// Controllers -> Interfaces/Controllers/auth
|
|
86
110
|
await fs.ensureDir(path.join(targetDir, 'src/interfaces/controllers/auth'));
|
|
87
111
|
await fs.ensureDir(path.join(targetDir, 'tests/unit/interfaces/controllers/auth'));
|
|
@@ -128,10 +152,28 @@ async function restructureAuthForCleanArch(targetDir, ext) {
|
|
|
128
152
|
);
|
|
129
153
|
}
|
|
130
154
|
|
|
155
|
+
// UseCases -> UseCases/auth
|
|
156
|
+
if (await fs.pathExists(path.join(targetDir, `src/usecases/SocialLoginUseCase.${ext}`))) {
|
|
157
|
+
await fs.ensureDir(path.join(targetDir, 'src/usecases/auth'));
|
|
158
|
+
await fs.ensureDir(path.join(targetDir, 'tests/unit/usecases/auth'));
|
|
159
|
+
await fs.move(
|
|
160
|
+
path.join(targetDir, `src/usecases/SocialLoginUseCase.${ext}`),
|
|
161
|
+
path.join(targetDir, `src/usecases/auth/socialLoginUseCase.${ext}`),
|
|
162
|
+
{ overwrite: true }
|
|
163
|
+
);
|
|
164
|
+
if (await fs.pathExists(path.join(targetDir, `tests/unit/usecases/SocialLoginUseCase.spec.${ext}`))) {
|
|
165
|
+
await fs.move(
|
|
166
|
+
path.join(targetDir, `tests/unit/usecases/SocialLoginUseCase.spec.${ext}`),
|
|
167
|
+
path.join(targetDir, `tests/unit/usecases/auth/socialLoginUseCase.spec.${ext}`),
|
|
168
|
+
{ overwrite: true }
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
131
173
|
// Cleanup empty dirs in src and tests/unit
|
|
132
174
|
const dirsToCleanup = [
|
|
133
|
-
'src/services', 'src/controllers', 'src/middleware', 'src/routes',
|
|
134
|
-
'tests/unit/services', 'tests/unit/controllers', 'tests/unit/middleware'
|
|
175
|
+
'src/services', 'src/controllers', 'src/middleware', 'src/routes', 'src/usecases',
|
|
176
|
+
'tests/unit/services', 'tests/unit/controllers', 'tests/unit/middleware', 'tests/unit/usecases'
|
|
135
177
|
];
|
|
136
178
|
for (const dir of dirsToCleanup) {
|
|
137
179
|
const fullDir = path.join(targetDir, dir);
|
package/lib/prompts.js
CHANGED
|
@@ -96,9 +96,9 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
type: 'select',
|
|
99
|
-
name: '
|
|
99
|
+
name: 'authChoice',
|
|
100
100
|
message: 'Select Authentication Mode:',
|
|
101
|
-
choices: ['None', 'JWT'],
|
|
101
|
+
choices: ['None', 'JWT', 'Google - Github - JWT'],
|
|
102
102
|
default: 'None',
|
|
103
103
|
when: (answers) => {
|
|
104
104
|
const advanced = options.advancedOptions !== undefined ? options.advancedOptions : answers.advancedOptions;
|
|
@@ -119,11 +119,37 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
119
119
|
result.advancedOptions = result.advancedOptions === 'Yes';
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
//
|
|
122
|
+
// Map authChoice to auth and socialAuth if not provided via options
|
|
123
|
+
if (!options.auth && result.authChoice) {
|
|
124
|
+
const choice = result.authChoice.toLowerCase();
|
|
125
|
+
if (choice.includes('google') || choice.includes('github')) {
|
|
126
|
+
result.auth = ['JWT'];
|
|
127
|
+
result.socialAuth = ['Google', 'GitHub'];
|
|
128
|
+
} else if (choice.includes('jwt')) {
|
|
129
|
+
result.auth = ['JWT'];
|
|
130
|
+
result.socialAuth = ['None'];
|
|
131
|
+
} else {
|
|
132
|
+
result.auth = ['None'];
|
|
133
|
+
result.socialAuth = ['None'];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Cleanup the temporary authChoice
|
|
138
|
+
delete result.authChoice;
|
|
139
|
+
|
|
140
|
+
// Normalize auth to array if it's a string from the options
|
|
123
141
|
if (typeof result.auth === 'string') {
|
|
124
142
|
result.auth = [result.auth];
|
|
125
143
|
}
|
|
126
144
|
|
|
145
|
+
// Map friendly CLI strings to actual values
|
|
146
|
+
if (result.auth && (result.auth.includes('Google - Github - JWT') || result.auth.includes('Google - GitHub - JWT'))) {
|
|
147
|
+
result.auth = ['JWT'];
|
|
148
|
+
if (!result.socialAuth || result.socialAuth.includes('None')) {
|
|
149
|
+
result.socialAuth = ['Google', 'GitHub'];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
127
153
|
// Default auth if not provided
|
|
128
154
|
if (!result.auth) {
|
|
129
155
|
result.auth = ['None'];
|
|
@@ -134,6 +160,20 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
134
160
|
result.auth = result.auth.filter(a => a !== 'None');
|
|
135
161
|
}
|
|
136
162
|
|
|
163
|
+
// Normalize socialAuth to array from options
|
|
164
|
+
if (typeof result.socialAuth === 'string') {
|
|
165
|
+
result.socialAuth = [result.socialAuth];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Default socialAuth if not provided
|
|
169
|
+
if (!result.socialAuth) {
|
|
170
|
+
result.socialAuth = ['None'];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Filter out 'None' if providers are selected
|
|
174
|
+
if (result.socialAuth.includes('Google') || result.socialAuth.includes('GitHub') || result.socialAuth.includes('Github')) {
|
|
175
|
+
result.socialAuth = result.socialAuth.filter(a => a !== 'None');
|
|
176
|
+
}
|
|
177
|
+
|
|
137
178
|
return result;
|
|
138
179
|
};
|
|
139
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodejs-quickstart-structure",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The ultimate nodejs quickstart structure CLI to scaffold Node.js microservices with MVC or Clean Architecture",
|
|
6
6
|
"main": "bin/index.js",
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
},
|
|
65
65
|
"overrides": {
|
|
66
66
|
"esbuild": "^0.25.0",
|
|
67
|
-
"vite": "^6.4.2"
|
|
67
|
+
"vite": "^6.4.2",
|
|
68
|
+
"postcss": "^8.5.10"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
71
|
"snyk": "^1.1303.2",
|
|
@@ -33,12 +33,22 @@ const envSchema = z.object({
|
|
|
33
33
|
<%_ if (communication === 'Kafka') { -%>
|
|
34
34
|
KAFKA_BROKER: z.string(),
|
|
35
35
|
<%_ } -%>
|
|
36
|
-
<%_ if (auth.includes('JWT')) {
|
|
36
|
+
<%_ if (auth.includes('JWT')) { _%>
|
|
37
37
|
JWT_SECRET: z.string(),
|
|
38
38
|
JWT_EXPIRES_IN: z.string().default('15m'),
|
|
39
39
|
JWT_REFRESH_SECRET: z.string().default('your-secret-refresh-key'),
|
|
40
40
|
JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
|
|
41
|
-
<%_
|
|
41
|
+
<%_ if (socialAuth && socialAuth.includes('Google')) { _%>
|
|
42
|
+
GOOGLE_CLIENT_ID: z.string(),
|
|
43
|
+
GOOGLE_CLIENT_SECRET: z.string(),
|
|
44
|
+
GOOGLE_CALLBACK_URL: z.string().optional(),
|
|
45
|
+
<%_ } _%>
|
|
46
|
+
<%_ if (socialAuth && socialAuth.includes('GitHub')) { _%>
|
|
47
|
+
GITHUB_CLIENT_ID: z.string(),
|
|
48
|
+
GITHUB_CLIENT_SECRET: z.string(),
|
|
49
|
+
GITHUB_CALLBACK_URL: z.string().optional(),
|
|
50
|
+
<%_ } _%>
|
|
51
|
+
<%_ } _%>
|
|
42
52
|
});
|
|
43
53
|
|
|
44
54
|
const _env = envSchema.safeParse(process.env);
|
package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs
CHANGED
|
@@ -38,6 +38,33 @@ class UserRepository {
|
|
|
38
38
|
<%_ } -%>
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
async findByEmail(email) {
|
|
42
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
43
|
+
const user = await UserModel.findOne({ email });
|
|
44
|
+
if (!user) return null;
|
|
45
|
+
return {
|
|
46
|
+
id: user._id.toString(),
|
|
47
|
+
name: user.name,
|
|
48
|
+
email: user.email,
|
|
49
|
+
googleId: user.googleId,
|
|
50
|
+
githubId: user.githubId
|
|
51
|
+
};
|
|
52
|
+
<%_ } else if (database === 'None') { -%>
|
|
53
|
+
return await UserModel.findOne({ email });
|
|
54
|
+
<%_ } else { -%>
|
|
55
|
+
const user = await UserModel.findOne({ where: { email } });
|
|
56
|
+
if (!user) return null;
|
|
57
|
+
return {
|
|
58
|
+
id: user.id,
|
|
59
|
+
name: user.name,
|
|
60
|
+
email: user.email,
|
|
61
|
+
googleId: user.googleId,
|
|
62
|
+
githubId: user.githubId
|
|
63
|
+
};
|
|
64
|
+
<%_ } -%>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
41
68
|
async getUsers() {
|
|
42
69
|
<%_ if (database === 'None') { -%>
|
|
43
70
|
return await UserModel.find();
|
package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs
CHANGED
|
@@ -5,6 +5,7 @@ jest.mock('@/infrastructure/database/models/User', () => ({
|
|
|
5
5
|
create: jest.fn(),
|
|
6
6
|
findAll: jest.fn(),
|
|
7
7
|
findByPk: jest.fn(),
|
|
8
|
+
findOne: jest.fn(),
|
|
8
9
|
update: jest.fn(),
|
|
9
10
|
destroy: jest.fn(),
|
|
10
11
|
find: jest.fn(),
|
|
@@ -83,6 +84,29 @@ describe('UserRepository', () => {
|
|
|
83
84
|
});
|
|
84
85
|
});
|
|
85
86
|
|
|
87
|
+
describe('findByEmail', () => {
|
|
88
|
+
it('should find and return a user by email', async () => {
|
|
89
|
+
const email = 'test@example.com';
|
|
90
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
91
|
+
const mockDbRecord = { _id: '1', name: 'TestUser', email: 'test@example.com' };
|
|
92
|
+
<%_ } else { -%>
|
|
93
|
+
const mockDbRecord = { id: '1', name: 'TestUser', email: 'test@example.com' };
|
|
94
|
+
<%_ } -%>
|
|
95
|
+
UserModel.findOne.mockResolvedValue(mockDbRecord);
|
|
96
|
+
|
|
97
|
+
const result = await userRepository.findByEmail(email);
|
|
98
|
+
|
|
99
|
+
expect(result.email).toBe(email);
|
|
100
|
+
expect(UserModel.findOne).toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return null if email not found', async () => {
|
|
104
|
+
UserModel.findOne.mockResolvedValue(null);
|
|
105
|
+
const result = await userRepository.findByEmail('not@found.com');
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
86
110
|
describe('getUsers', () => {
|
|
87
111
|
it('should return a list of mapped UserEntities (Happy Path)', async () => {
|
|
88
112
|
<%_ if (database === 'MongoDB') { -%>
|
|
@@ -35,7 +35,9 @@ const startServer = async () => {
|
|
|
35
35
|
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
36
36
|
app.use('/api', apiRoutes);
|
|
37
37
|
<%_ } -%>
|
|
38
|
-
<%_ if (auth.includes('JWT')) { -%>
|
|
38
|
+
<%_ if (auth.includes('JWT')) { -%>
|
|
39
|
+
app.use('/api/auth', authRoutes);
|
|
40
|
+
<%_ } -%>
|
|
39
41
|
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
40
42
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
41
43
|
<%_ } -%>
|
|
@@ -34,12 +34,22 @@ const envSchema = z.object({
|
|
|
34
34
|
<%_ if (communication === 'Kafka') { -%>
|
|
35
35
|
KAFKA_BROKER: z.string(),
|
|
36
36
|
<%_ } -%>
|
|
37
|
-
<%_ if (auth.includes('JWT')) {
|
|
37
|
+
<%_ if (auth.includes('JWT')) { _%>
|
|
38
38
|
JWT_SECRET: z.string(),
|
|
39
39
|
JWT_EXPIRES_IN: z.string().default('15m'),
|
|
40
40
|
JWT_REFRESH_SECRET: z.string().default('your-secret-refresh-key'),
|
|
41
41
|
JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
|
|
42
|
-
<%_
|
|
42
|
+
<%_ if (socialAuth && socialAuth.includes('Google')) { _%>
|
|
43
|
+
GOOGLE_CLIENT_ID: z.string(),
|
|
44
|
+
GOOGLE_CLIENT_SECRET: z.string(),
|
|
45
|
+
GOOGLE_CALLBACK_URL: z.string().optional(),
|
|
46
|
+
<%_ } _%>
|
|
47
|
+
<%_ if (socialAuth && socialAuth.includes('GitHub')) { _%>
|
|
48
|
+
GITHUB_CLIENT_ID: z.string(),
|
|
49
|
+
GITHUB_CLIENT_SECRET: z.string(),
|
|
50
|
+
GITHUB_CALLBACK_URL: z.string().optional(),
|
|
51
|
+
<%_ } _%>
|
|
52
|
+
<%_ } _%>
|
|
43
53
|
});
|
|
44
54
|
|
|
45
55
|
const _env = envSchema.safeParse(process.env);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class User {
|
|
2
|
+
constructor(
|
|
3
|
+
public id: number | string | null,
|
|
4
|
+
public name: string,
|
|
5
|
+
public email: string,
|
|
6
|
+
public password?: string | null,
|
|
7
|
+
<%_ if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('Google')) { _%>
|
|
8
|
+
public googleId?: string | null,
|
|
9
|
+
<%_ } _%>
|
|
10
|
+
<%_ if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('GitHub')) { _%>
|
|
11
|
+
public githubId?: string | null,
|
|
12
|
+
<%_ } _%>
|
|
13
|
+
) { }
|
|
14
|
+
}
|
package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs
CHANGED
|
@@ -9,6 +9,7 @@ jest.mock('@/infrastructure/database/models/User', () => {
|
|
|
9
9
|
create: jest.fn(),
|
|
10
10
|
findAll: jest.fn(),
|
|
11
11
|
findByPk: jest.fn(),
|
|
12
|
+
findOne: jest.fn(),
|
|
12
13
|
update: jest.fn(),
|
|
13
14
|
destroy: jest.fn(),
|
|
14
15
|
find: jest.fn(),
|
|
@@ -115,6 +116,29 @@ describe('UserRepository', () => {
|
|
|
115
116
|
});
|
|
116
117
|
});
|
|
117
118
|
|
|
119
|
+
describe('findByEmail', () => {
|
|
120
|
+
it('should find and return a user by email', async () => {
|
|
121
|
+
const email = 'test@example.com';
|
|
122
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
123
|
+
const mockDbRecord = { _id: { toString: () => '1' }, name: 'TestUser', email: 'test@example.com' };
|
|
124
|
+
<%_ } else { -%>
|
|
125
|
+
const mockDbRecord = { id: '1', name: 'TestUser', email: 'test@example.com' };
|
|
126
|
+
<%_ } -%>
|
|
127
|
+
(UserModel.findOne as jest.Mock).mockResolvedValue(mockDbRecord);
|
|
128
|
+
|
|
129
|
+
const result = await userRepository.findByEmail(email);
|
|
130
|
+
|
|
131
|
+
expect(result?.email).toBe(email);
|
|
132
|
+
expect(UserModel.findOne).toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should return null if email not found', async () => {
|
|
136
|
+
(UserModel.findOne as jest.Mock).mockResolvedValue(null);
|
|
137
|
+
const result = await userRepository.findByEmail('not@found.com');
|
|
138
|
+
expect(result).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
118
142
|
describe('getUsers', () => {
|
|
119
143
|
it('should return a list of mapped UserEntities (Happy Path)', async () => {
|
|
120
144
|
// Arrange
|
package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs
CHANGED
|
@@ -3,87 +3,74 @@ import UserModel from '@/infrastructure/database/models/User';
|
|
|
3
3
|
|
|
4
4
|
export class UserRepository {
|
|
5
5
|
async save(user: UserEntity): Promise<UserEntity> {
|
|
6
|
+
const userData = {
|
|
7
|
+
name: user.name,
|
|
8
|
+
email: user.email,
|
|
9
|
+
<% if (auth.includes('JWT')) { %>password: user.password,<% } %>
|
|
10
|
+
<% if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('Google')) { %>googleId: user.googleId,<% } %>
|
|
11
|
+
<% if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('GitHub')) { %>githubId: user.githubId,<% } %>
|
|
12
|
+
};
|
|
13
|
+
|
|
6
14
|
<%_ if (database === 'MongoDB') { -%>
|
|
7
|
-
const newUser = await UserModel.create(
|
|
8
|
-
|
|
9
|
-
email: user.email,
|
|
10
|
-
<% if (auth.includes('JWT')) { %>password: user.password<% } %>
|
|
11
|
-
});
|
|
12
|
-
return { id: newUser._id.toString(), name: newUser.name, email: newUser.email };
|
|
15
|
+
const newUser = await UserModel.create(userData);
|
|
16
|
+
return this.mapToEntity(newUser);
|
|
13
17
|
<%_ } else if (database === 'None') { -%>
|
|
14
|
-
const newUser = await UserModel.create(
|
|
15
|
-
|
|
16
|
-
email: user.email,
|
|
17
|
-
<% if (auth.includes('JWT')) { %>password: user.password<% } %>
|
|
18
|
-
});
|
|
19
|
-
return { id: newUser.id, name: newUser.name, email: newUser.email };
|
|
18
|
+
const newUser = await UserModel.create(userData);
|
|
19
|
+
return this.mapToEntity(newUser);
|
|
20
20
|
<%_ } else { -%>
|
|
21
|
-
const newUser = await UserModel.create(
|
|
22
|
-
|
|
23
|
-
email: user.email,
|
|
24
|
-
<% if (auth.includes('JWT')) { %>password: user.password<% } %>
|
|
25
|
-
});
|
|
26
|
-
return { id: newUser.id, name: newUser.name, email: newUser.email };
|
|
21
|
+
const newUser = await UserModel.create(userData);
|
|
22
|
+
return this.mapToEntity(newUser);
|
|
27
23
|
<%_ } -%>
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
async findById(id: number | string): Promise<UserEntity | null> {
|
|
31
27
|
<%_ if (database === 'MongoDB') { -%>
|
|
32
28
|
const user = await UserModel.findById(id);
|
|
33
|
-
if (!user) return null;
|
|
34
|
-
return { id: user._id.toString(), name: user.name, email: user.email };
|
|
35
|
-
<%_ } else if (database === 'None') { -%>
|
|
36
|
-
const user = await UserModel.findByPk(id);
|
|
37
|
-
if (!user) return null;
|
|
38
|
-
return { id: user.id, name: user.name, email: user.email };
|
|
39
29
|
<%_ } else { -%>
|
|
40
30
|
const user = await UserModel.findByPk(id);
|
|
41
|
-
if (!user) return null;
|
|
42
|
-
return { id: user.id, name: user.name, email: user.email };
|
|
43
31
|
<%_ } -%>
|
|
32
|
+
if (!user) return null;
|
|
33
|
+
return this.mapToEntity(user);
|
|
44
34
|
}
|
|
45
35
|
|
|
36
|
+
async findByEmail(email: string): Promise<UserEntity | null> {
|
|
37
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
38
|
+
const user = await UserModel.findOne({ email });
|
|
39
|
+
<%_ } else if (database === 'None') { -%>
|
|
40
|
+
const user = await UserModel.findOne({ email });
|
|
41
|
+
<%_ } else { -%>
|
|
42
|
+
const user = await UserModel.findOne({ where: { email } });
|
|
43
|
+
<%_ } -%>
|
|
44
|
+
if (!user) return null;
|
|
45
|
+
return this.mapToEntity(user);
|
|
46
|
+
}
|
|
46
47
|
|
|
47
48
|
async getUsers(): Promise<UserEntity[]> {
|
|
48
49
|
<%_ if (database === 'MongoDB') { -%>
|
|
49
50
|
const users = await UserModel.find();
|
|
50
|
-
return users.map(user => ({
|
|
51
|
-
id: user._id.toString(),
|
|
52
|
-
name: user.name,
|
|
53
|
-
email: user.email
|
|
54
|
-
}));
|
|
55
51
|
<%_ } else if (database === 'None') { -%>
|
|
56
52
|
const users = await UserModel.find();
|
|
57
|
-
return users.map(user => ({
|
|
58
|
-
id: user.id,
|
|
59
|
-
name: user.name,
|
|
60
|
-
email: user.email
|
|
61
|
-
}));
|
|
62
53
|
<%_ } else { -%>
|
|
63
54
|
const users = await UserModel.findAll();
|
|
64
|
-
return users.map(user => ({
|
|
65
|
-
id: user.id,
|
|
66
|
-
name: user.name,
|
|
67
|
-
email: user.email
|
|
68
|
-
}));
|
|
69
55
|
<%_ } -%>
|
|
56
|
+
return users.map(user => this.mapToEntity(user));
|
|
70
57
|
}
|
|
71
58
|
|
|
72
59
|
async update(id: number | string, data: Partial<UserEntity>): Promise<UserEntity | null> {
|
|
73
60
|
<%_ if (database === 'MongoDB') { -%>
|
|
74
61
|
const user = await UserModel.findByIdAndUpdate(id, data, { new: true });
|
|
75
62
|
if (!user) return null;
|
|
76
|
-
return
|
|
63
|
+
return this.mapToEntity(user);
|
|
77
64
|
<%_ } else if (database === 'None') { -%>
|
|
78
65
|
const { id: _, ...updateData } = data;
|
|
79
|
-
const user = await UserModel.update(id, updateData as
|
|
66
|
+
const user = await UserModel.update(id, updateData as Record<string, unknown>);
|
|
80
67
|
if (!user) return null;
|
|
81
|
-
return
|
|
68
|
+
return this.mapToEntity(user);
|
|
82
69
|
<%_ } else { -%>
|
|
83
70
|
const user = await UserModel.findByPk(id);
|
|
84
71
|
if (!user) return null;
|
|
85
72
|
await user.update(data);
|
|
86
|
-
return
|
|
73
|
+
return this.mapToEntity(user);
|
|
87
74
|
<%_ } -%>
|
|
88
75
|
}
|
|
89
76
|
|
|
@@ -100,4 +87,15 @@ export class UserRepository {
|
|
|
100
87
|
return true;
|
|
101
88
|
<%_ } -%>
|
|
102
89
|
}
|
|
90
|
+
|
|
91
|
+
private mapToEntity(user: { id?: string | number; _id?: { toString(): string }; name: string; email: string; password?: string | null; googleId?: string | null; githubId?: string | null }): UserEntity {
|
|
92
|
+
return {
|
|
93
|
+
id: user.id || user._id?.toString(),
|
|
94
|
+
name: user.name,
|
|
95
|
+
email: user.email,
|
|
96
|
+
password: user.password,
|
|
97
|
+
<% if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('Google')) { %>googleId: user.googleId,<% } %>
|
|
98
|
+
<% if (typeof socialAuth !== 'undefined' && socialAuth && socialAuth.includes('GitHub')) { %>githubId: user.githubId,<% } %>
|
|
99
|
+
} as UserEntity;
|
|
100
|
+
}
|
|
103
101
|
}
|