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.
Files changed (66) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +12 -17
  3. package/bin/index.js +1 -0
  4. package/lib/generator.js +1 -1
  5. package/lib/modules/app-setup.js +16 -0
  6. package/lib/modules/auth-setup.js +46 -4
  7. package/lib/prompts.js +44 -4
  8. package/package.json +3 -2
  9. package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +12 -2
  10. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.js.ejs +27 -0
  11. package/templates/clean-architecture/js/src/infrastructure/repositories/UserRepository.spec.js.ejs +24 -0
  12. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +3 -1
  13. package/templates/clean-architecture/ts/src/config/env.ts.ejs +12 -2
  14. package/templates/clean-architecture/ts/src/domain/user.ts.ejs +14 -0
  15. package/templates/clean-architecture/ts/src/infrastructure/repositories/UserRepository.spec.ts.ejs +24 -0
  16. package/templates/clean-architecture/ts/src/infrastructure/repositories/userRepository.ts.ejs +43 -45
  17. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  18. package/templates/common/.env.example.ejs +10 -0
  19. package/templates/common/README.md.ejs +65 -14
  20. package/templates/common/auth/js/controllers/authController.js.ejs +326 -13
  21. package/templates/common/auth/js/controllers/authController.spec.js.ejs +237 -51
  22. package/templates/common/auth/js/middleware/authMiddleware.js.ejs +10 -6
  23. package/templates/common/auth/js/routes/authRoutes.js.ejs +11 -0
  24. package/templates/common/auth/js/services/jwtService.js.ejs +3 -3
  25. package/templates/common/auth/js/services/jwtService.spec.js.ejs +30 -0
  26. package/templates/common/auth/js/services/socialAuthService.js.ejs +175 -0
  27. package/templates/common/auth/js/services/socialAuthService.spec.js.ejs +194 -0
  28. package/templates/common/auth/js/usecases/SocialLoginUseCase.js.ejs +114 -0
  29. package/templates/common/auth/js/usecases/SocialLoginUseCase.spec.js.ejs +143 -0
  30. package/templates/common/auth/ts/controllers/authController.spec.ts.ejs +344 -64
  31. package/templates/common/auth/ts/controllers/authController.ts.ejs +341 -9
  32. package/templates/common/auth/ts/middleware/authMiddleware.ts.ejs +10 -6
  33. package/templates/common/auth/ts/routes/authRoutes.ts.ejs +11 -0
  34. package/templates/common/auth/ts/services/jwtService.spec.ts.ejs +18 -0
  35. package/templates/common/auth/ts/services/jwtService.ts.ejs +3 -3
  36. package/templates/common/auth/ts/services/socialAuthService.spec.ts.ejs +187 -0
  37. package/templates/common/auth/ts/services/socialAuthService.ts.ejs +189 -0
  38. package/templates/common/auth/ts/usecases/SocialLoginUseCase.spec.ts.ejs +143 -0
  39. package/templates/common/auth/ts/usecases/SocialLoginUseCase.ts.ejs +117 -0
  40. package/templates/common/database/js/models/User.js.ejs +13 -5
  41. package/templates/common/database/js/models/User.js.mongoose.ejs +15 -1
  42. package/templates/common/database/ts/models/User.ts.ejs +23 -7
  43. package/templates/common/database/ts/models/User.ts.mongoose.ejs +18 -2
  44. package/templates/common/docker-compose.yml.ejs +21 -0
  45. package/templates/common/ecosystem.config.js.ejs +10 -0
  46. package/templates/common/jest.config.js.ejs +1 -1
  47. package/templates/common/kafka/js/services/kafkaService.js.ejs +1 -1
  48. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +1 -1
  49. package/templates/common/package.json.ejs +2 -0
  50. package/templates/common/src/tests/e2e/e2e.users.test.js.ejs +13 -1
  51. package/templates/common/src/tests/e2e/e2e.users.test.ts.ejs +13 -1
  52. package/templates/common/swagger.yml.ejs +62 -3
  53. package/templates/common/views/ejs/login.ejs.ejs +84 -0
  54. package/templates/common/views/ejs/signup.ejs.ejs +84 -0
  55. package/templates/common/views/pug/login.pug.ejs +78 -0
  56. package/templates/common/views/pug/signup.pug.ejs +78 -0
  57. package/templates/db/mysql/V1__Initial_Setup.sql.ejs +3 -1
  58. package/templates/db/postgres/V1__Initial_Setup.sql.ejs +3 -1
  59. package/templates/mvc/js/src/config/env.js.ejs +12 -2
  60. package/templates/mvc/js/src/controllers/userController.js.ejs +1 -1
  61. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -3
  62. package/templates/mvc/ts/src/config/env.ts.ejs +12 -2
  63. package/templates/mvc/ts/src/controllers/userController.ts.ejs +1 -0
  64. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +5 -5
  65. package/templates/mvc/ts/src/index.ts.ejs +2 -1
  66. 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
- - [5,280+ Project Combinations](#5280-project-combinations)
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.1 (The Authentication Release)
53
+ ## What's New in v2.2 (The Social Auth Release)
54
54
 
55
- The v2.1.0 release is a major leap forward, turning the generator into a **Community Standard**:
55
+ The v2.2.0 release brings enterprise-grade identity management to your microservices:
56
56
 
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.
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** support (Refresh/Access tokens).
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
- ## 5,280+ Project Combinations
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
- - **480 Core Combinations**:
98
- - **MVC Architecture**: 360 variants (Languages × View Engines × Databases × Communication Patterns × Caching × Auth)
99
- - **Clean Architecture**: 120 variants (Languages × Databases × Communication Patterns × Caching × Auth)
100
- - **5,280+ Total Scenarios**:
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:
@@ -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. MVC Views (if applicable)
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
- // 6. Restructuring for Clean Architecture
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: 'auth',
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
- // Normalize auth to array if it's a string from the list prompt
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.1.1",
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);
@@ -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();
@@ -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')) { -%>app.use('/api/auth', authRoutes);<%_ } -%>
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
+ }
@@ -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
@@ -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
- name: user.name,
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
- name: user.name,
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
- name: user.name,
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 { id: user._id.toString(), name: user.name, email: user.email };
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 Parameters<typeof UserModel.update>[1]);
66
+ const user = await UserModel.update(id, updateData as Record<string, unknown>);
80
67
  if (!user) return null;
81
- return { id: user.id, name: user.name, email: user.email };
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 { id: user.id || 0, name: user.name, email: user.email };
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
  }