create-flex-stack 1.0.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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/index.js +330 -0
- package/dist/templates/cleanTemplate.js +697 -0
- package/dist/templates/frontendTemplate.js +855 -0
- package/dist/templates/hexagonalTemplate.js +855 -0
- package/dist/templates/layeredTemplate.js +745 -0
- package/dist/templates/modularTemplate.js +691 -0
- package/dist/templates/sharedTemplate.js +654 -0
- package/dist/templates/vmcTemplate.js +26 -0
- package/dist/types.js +1 -0
- package/dist/utils/generator.js +1120 -0
- package/package.json +46 -0
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { getTsConfig, getGitIgnore, getDockerFiles, getDotEnv, getReadme, getPackageJson, getRootPackageJson, getPrismaSchema, getMulterMiddleware, getJestConfig } from '../templates/sharedTemplate.js';
|
|
5
|
+
import { getLayeredFiles } from '../templates/layeredTemplate.js';
|
|
6
|
+
import { getVmcFiles } from '../templates/vmcTemplate.js';
|
|
7
|
+
import { getModularFiles } from '../templates/modularTemplate.js';
|
|
8
|
+
import { getCleanFiles } from '../templates/cleanTemplate.js';
|
|
9
|
+
import { getHexagonalFiles } from '../templates/hexagonalTemplate.js';
|
|
10
|
+
import { getFrontendFiles } from '../templates/frontendTemplate.js';
|
|
11
|
+
export async function generateProject(options, onStep) {
|
|
12
|
+
const targetDir = path.resolve(process.cwd(), options.projectName);
|
|
13
|
+
if (fs.existsSync(targetDir)) {
|
|
14
|
+
throw new Error(`Directory "${options.projectName}" already exists.`);
|
|
15
|
+
}
|
|
16
|
+
onStep('Creating project directories...', 'start');
|
|
17
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
18
|
+
const files = {};
|
|
19
|
+
if (options.frontend) {
|
|
20
|
+
// 1. MONOREPO STRUCTURE
|
|
21
|
+
files['package.json'] = getRootPackageJson(options.projectName);
|
|
22
|
+
files['.gitignore'] = getGitIgnore();
|
|
23
|
+
files['README.md'] = getReadme(options);
|
|
24
|
+
// Write Frontend files under frontend/
|
|
25
|
+
const frontFiles = getFrontendFiles(options);
|
|
26
|
+
for (const [relPath, content] of Object.entries(frontFiles)) {
|
|
27
|
+
files[`frontend/${relPath}`] = content;
|
|
28
|
+
}
|
|
29
|
+
// Write Backend files under backend/
|
|
30
|
+
files['backend/package.json'] = getPackageJson(options);
|
|
31
|
+
files['backend/tsconfig.json'] = getTsConfig(options);
|
|
32
|
+
files['backend/.gitignore'] = getGitIgnore();
|
|
33
|
+
files['backend/.env'] = getDotEnv(options);
|
|
34
|
+
files['backend/.env.example'] = getDotEnv(options);
|
|
35
|
+
files['backend/README.md'] = getReadme(options);
|
|
36
|
+
if (options.orm === 'prisma') {
|
|
37
|
+
files['backend/prisma/schema.prisma'] = getPrismaSchema(options);
|
|
38
|
+
}
|
|
39
|
+
if (options.docker) {
|
|
40
|
+
const { dockerfile, dockerCompose } = getDockerFiles(options);
|
|
41
|
+
files['backend/Dockerfile'] = dockerfile;
|
|
42
|
+
files['docker-compose.yml'] = dockerCompose; // Root compose
|
|
43
|
+
}
|
|
44
|
+
if (options.testing) {
|
|
45
|
+
files['backend/jest.config.json'] = getJestConfig();
|
|
46
|
+
}
|
|
47
|
+
// Backend Source
|
|
48
|
+
const srcFiles = getBackendSource(options);
|
|
49
|
+
for (const [relPath, content] of Object.entries(srcFiles)) {
|
|
50
|
+
files[`backend/${relPath}`] = content;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// 2. SINGLE BACKEND ONLY STRUCTURE
|
|
55
|
+
files['package.json'] = getPackageJson(options);
|
|
56
|
+
files['tsconfig.json'] = getTsConfig(options);
|
|
57
|
+
files['.gitignore'] = getGitIgnore();
|
|
58
|
+
files['.env'] = getDotEnv(options);
|
|
59
|
+
files['.env.example'] = getDotEnv(options);
|
|
60
|
+
files['README.md'] = getReadme(options);
|
|
61
|
+
if (options.orm === 'prisma') {
|
|
62
|
+
files['prisma/schema.prisma'] = getPrismaSchema(options);
|
|
63
|
+
}
|
|
64
|
+
if (options.docker) {
|
|
65
|
+
const { dockerfile, dockerCompose } = getDockerFiles(options);
|
|
66
|
+
files['Dockerfile'] = dockerfile;
|
|
67
|
+
files['docker-compose.yml'] = dockerCompose;
|
|
68
|
+
}
|
|
69
|
+
if (options.testing) {
|
|
70
|
+
files['jest.config.json'] = getJestConfig();
|
|
71
|
+
}
|
|
72
|
+
// Backend Source
|
|
73
|
+
const srcFiles = getBackendSource(options);
|
|
74
|
+
for (const [relPath, content] of Object.entries(srcFiles)) {
|
|
75
|
+
files[relPath] = content;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Write all files
|
|
79
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
80
|
+
const fullPath = path.join(targetDir, relPath);
|
|
81
|
+
const dir = path.dirname(fullPath);
|
|
82
|
+
if (!fs.existsSync(dir)) {
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
fs.writeFileSync(fullPath, content.trim() + '\n', 'utf8');
|
|
86
|
+
}
|
|
87
|
+
onStep('Directories and source code files generated.', 'stop');
|
|
88
|
+
// 3. Install dependencies
|
|
89
|
+
onStep('Installing project dependencies (this may take a few seconds)...', 'start');
|
|
90
|
+
try {
|
|
91
|
+
execSync('npm install', { cwd: targetDir, stdio: 'ignore' });
|
|
92
|
+
onStep('Dependencies installed successfully.', 'stop');
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
onStep('Failed to install dependencies automatically. Run "npm install" manually.', 'error');
|
|
96
|
+
}
|
|
97
|
+
// 4. Run Prisma client generation if applicable
|
|
98
|
+
if (options.orm === 'prisma') {
|
|
99
|
+
onStep('Generating Prisma client...', 'start');
|
|
100
|
+
const prismaDir = options.frontend ? path.join(targetDir, 'backend') : targetDir;
|
|
101
|
+
try {
|
|
102
|
+
execSync('npx prisma generate', { cwd: prismaDir, stdio: 'ignore' });
|
|
103
|
+
onStep('Prisma client generated successfully.', 'stop');
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
onStep('Failed to generate Prisma client automatically. Run "npx prisma generate" manually.', 'error');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 5. Initialize Git repository
|
|
110
|
+
onStep('Initializing Git repository...', 'start');
|
|
111
|
+
try {
|
|
112
|
+
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
|
|
113
|
+
execSync('git add .', { cwd: targetDir, stdio: 'ignore' });
|
|
114
|
+
execSync('git commit -m "Initial commit from create-flex-stack"', { cwd: targetDir, stdio: 'ignore' });
|
|
115
|
+
onStep('Git repository initialized and initial commit created.', 'stop');
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
onStep('Failed to initialize Git repository automatically.', 'stop');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function getBackendSource(options) {
|
|
122
|
+
let srcFiles = {};
|
|
123
|
+
const sourceArchitecture = options.architecture;
|
|
124
|
+
if (sourceArchitecture === 'vmc') {
|
|
125
|
+
srcFiles = getVmcFiles(options);
|
|
126
|
+
}
|
|
127
|
+
else if (sourceArchitecture === 'modular') {
|
|
128
|
+
srcFiles = getModularFiles(options);
|
|
129
|
+
}
|
|
130
|
+
else if (sourceArchitecture === 'clean') {
|
|
131
|
+
srcFiles = getCleanFiles(options);
|
|
132
|
+
}
|
|
133
|
+
else if (sourceArchitecture === 'hexagonal') {
|
|
134
|
+
srcFiles = getHexagonalFiles(options);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
srcFiles = getLayeredFiles(options);
|
|
138
|
+
}
|
|
139
|
+
if (!options.auth) {
|
|
140
|
+
srcFiles = applyNoAuthBackend(options, srcFiles);
|
|
141
|
+
}
|
|
142
|
+
// Dynamically inject File Upload middleware if toggled on
|
|
143
|
+
if (options.fileUpload) {
|
|
144
|
+
const uploadMiddlewareContent = getMulterMiddleware();
|
|
145
|
+
if (sourceArchitecture === 'clean') {
|
|
146
|
+
srcFiles['src/presentation/middlewares/upload.middleware.ts'] = uploadMiddlewareContent.replace('./errorHandler.js', './error.middleware.js');
|
|
147
|
+
}
|
|
148
|
+
else if (sourceArchitecture === 'hexagonal') {
|
|
149
|
+
srcFiles['src/adapters/inbound/web/middlewares/upload.middleware.ts'] = uploadMiddlewareContent.replace('./errorHandler.js', './error.middleware.js');
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
srcFiles['src/middlewares/upload.ts'] = uploadMiddlewareContent;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (options.testing) {
|
|
156
|
+
srcFiles['tests/health.test.ts'] = `describe('project scaffold', () => {
|
|
157
|
+
it('has a working test setup', () => {
|
|
158
|
+
expect(true).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
return srcFiles;
|
|
164
|
+
}
|
|
165
|
+
function applyNoAuthBackend(options, srcFiles) {
|
|
166
|
+
const files = { ...srcFiles };
|
|
167
|
+
for (const key of Object.keys(files)) {
|
|
168
|
+
if (key.includes('/auth') ||
|
|
169
|
+
key.includes('/rbac') ||
|
|
170
|
+
key.includes('/security/') ||
|
|
171
|
+
key.includes('password-hasher') ||
|
|
172
|
+
key.includes('token-service') ||
|
|
173
|
+
key.includes('login-user') ||
|
|
174
|
+
key.includes('register-user') ||
|
|
175
|
+
key.includes('refresh-token') ||
|
|
176
|
+
key.includes('auth.service') ||
|
|
177
|
+
key.includes('auth.use-cases')) {
|
|
178
|
+
delete files[key];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (options.architecture === 'vmc') {
|
|
182
|
+
addPublicLayeredLikeUsers(files, {
|
|
183
|
+
schemaPath: 'src/view/user.view.ts',
|
|
184
|
+
servicePath: 'src/model/user.model-service.ts',
|
|
185
|
+
controllerPath: 'src/controller/user.controller.ts',
|
|
186
|
+
routesPath: 'src/routes/user.routes.ts',
|
|
187
|
+
repositoryImport: '../model/user.repository.js',
|
|
188
|
+
serviceImport: '../model/user.model-service.js',
|
|
189
|
+
controllerImport: '../controller/user.controller.js',
|
|
190
|
+
schemaImport: '../view/user.view.js',
|
|
191
|
+
errorImport: '../middlewares/errorHandler.js',
|
|
192
|
+
repositoryPath: 'src/model/user.repository.ts',
|
|
193
|
+
repositoryClass: 'UserRepository',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else if (options.architecture === 'modular') {
|
|
197
|
+
addPublicLayeredLikeUsers(files, {
|
|
198
|
+
schemaPath: 'src/modules/users/users.dto.ts',
|
|
199
|
+
servicePath: 'src/modules/users/users.service.ts',
|
|
200
|
+
controllerPath: 'src/modules/users/users.controller.ts',
|
|
201
|
+
routesPath: 'src/modules/users/users.routes.ts',
|
|
202
|
+
repositoryPath: 'src/modules/users/users.repository.ts',
|
|
203
|
+
repositoryImport: './users.repository.js',
|
|
204
|
+
serviceImport: './users.service.js',
|
|
205
|
+
controllerImport: './users.controller.js',
|
|
206
|
+
schemaImport: './users.dto.js',
|
|
207
|
+
errorImport: '../../middlewares/errorHandler.js',
|
|
208
|
+
repositoryClass: 'UsersRepository',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else if (options.architecture === 'layered') {
|
|
212
|
+
addPublicLayeredLikeUsers(files, {
|
|
213
|
+
schemaPath: 'src/schemas/user.schema.ts',
|
|
214
|
+
servicePath: 'src/services/user.service.ts',
|
|
215
|
+
controllerPath: 'src/controllers/user.controller.ts',
|
|
216
|
+
routesPath: 'src/routes/user.routes.ts',
|
|
217
|
+
repositoryPath: 'src/repositories/user.repository.ts',
|
|
218
|
+
repositoryImport: '../repositories/user.repository.js',
|
|
219
|
+
serviceImport: '../services/user.service.js',
|
|
220
|
+
controllerImport: '../controllers/user.controller.js',
|
|
221
|
+
schemaImport: '../schemas/user.schema.js',
|
|
222
|
+
errorImport: '../middlewares/errorHandler.js',
|
|
223
|
+
repositoryClass: 'UserRepository',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else if (options.architecture === 'clean') {
|
|
227
|
+
addPublicCleanUsers(files, options);
|
|
228
|
+
}
|
|
229
|
+
else if (options.architecture === 'hexagonal') {
|
|
230
|
+
addPublicHexagonalUsers(files, options);
|
|
231
|
+
}
|
|
232
|
+
if (options.swagger) {
|
|
233
|
+
addPublicSwagger(files, options.architecture);
|
|
234
|
+
}
|
|
235
|
+
return files;
|
|
236
|
+
}
|
|
237
|
+
function addPublicSwagger(files, architecture) {
|
|
238
|
+
const swagger = `export const swaggerDocument = {
|
|
239
|
+
openapi: "3.0.0",
|
|
240
|
+
info: { title: "API Documentation", version: "1.0.0" },
|
|
241
|
+
paths: {
|
|
242
|
+
"/health": {
|
|
243
|
+
get: {
|
|
244
|
+
summary: "Health check",
|
|
245
|
+
responses: { "200": { description: "API is running" } }
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
"/users": {
|
|
249
|
+
get: {
|
|
250
|
+
summary: "List users",
|
|
251
|
+
responses: { "200": { description: "Users returned" } }
|
|
252
|
+
},
|
|
253
|
+
post: {
|
|
254
|
+
summary: "Create user",
|
|
255
|
+
requestBody: {
|
|
256
|
+
required: true,
|
|
257
|
+
content: {
|
|
258
|
+
"application/json": {
|
|
259
|
+
schema: {
|
|
260
|
+
type: "object",
|
|
261
|
+
required: ["email"],
|
|
262
|
+
properties: {
|
|
263
|
+
email: { type: "string", format: "email" },
|
|
264
|
+
name: { type: "string" },
|
|
265
|
+
role: { type: "string", enum: ["user", "admin"] }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
responses: { "201": { description: "User created" } }
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
"/users/{id}": {
|
|
275
|
+
get: {
|
|
276
|
+
summary: "Get user",
|
|
277
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
278
|
+
responses: { "200": { description: "User returned" }, "404": { description: "User not found" } }
|
|
279
|
+
},
|
|
280
|
+
put: {
|
|
281
|
+
summary: "Update user",
|
|
282
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
283
|
+
requestBody: {
|
|
284
|
+
required: true,
|
|
285
|
+
content: {
|
|
286
|
+
"application/json": {
|
|
287
|
+
schema: {
|
|
288
|
+
type: "object",
|
|
289
|
+
properties: {
|
|
290
|
+
email: { type: "string", format: "email" },
|
|
291
|
+
name: { type: "string" },
|
|
292
|
+
role: { type: "string", enum: ["user", "admin"] }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
responses: { "200": { description: "User updated" } }
|
|
299
|
+
},
|
|
300
|
+
delete: {
|
|
301
|
+
summary: "Delete user",
|
|
302
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
303
|
+
responses: { "204": { description: "User deleted" } }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
`;
|
|
309
|
+
if (architecture === 'hexagonal') {
|
|
310
|
+
files['src/adapters/inbound/web/docs/swagger.ts'] = swagger;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const serverPath = architecture === 'clean' ? 'src/presentation/express.server.ts' : 'src/app.ts';
|
|
314
|
+
const server = files[serverPath];
|
|
315
|
+
if (!server)
|
|
316
|
+
return;
|
|
317
|
+
files[serverPath] = server.replace(/const swaggerDocument = [\s\S]*?app\.use\('\/docs', swaggerUi\.serve, swaggerUi\.setup\(swaggerDocument\)\);/, `${swagger.replace('export const', 'const')}\napp.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));`);
|
|
318
|
+
}
|
|
319
|
+
function addPublicHexagonalUsers(files, options) {
|
|
320
|
+
files['src/domain/models/user.model.ts'] = `export class User {
|
|
321
|
+
constructor(
|
|
322
|
+
public readonly id: string,
|
|
323
|
+
public readonly email: string,
|
|
324
|
+
public readonly name: string | null,
|
|
325
|
+
public readonly role: string | null,
|
|
326
|
+
public readonly createdAt: Date
|
|
327
|
+
) {}
|
|
328
|
+
}
|
|
329
|
+
`;
|
|
330
|
+
files['src/ports/outbound/user-repository.port.ts'] = `import { User } from '../../domain/models/user.model.js';
|
|
331
|
+
|
|
332
|
+
export interface IUserRepositoryPort {
|
|
333
|
+
findByEmail(email: string): Promise<User | null>;
|
|
334
|
+
findById(id: string): Promise<User | null>;
|
|
335
|
+
create(user: Omit<User, 'id' | 'createdAt'>): Promise<User>;
|
|
336
|
+
update(id: string, user: Partial<User>): Promise<User>;
|
|
337
|
+
delete(id: string): Promise<void>;
|
|
338
|
+
findAll(): Promise<User[]>;
|
|
339
|
+
}
|
|
340
|
+
`;
|
|
341
|
+
files['src/ports/inbound/user-usecase.port.ts'] = `export interface ICreateUserPort {
|
|
342
|
+
execute(input: { email: string; name?: string; role?: string }): Promise<any>;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface IListUsersPort {
|
|
346
|
+
execute(): Promise<any[]>;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface IGetUserPort {
|
|
350
|
+
execute(userId: string): Promise<any>;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export interface IUpdateUserPort {
|
|
354
|
+
execute(userId: string, input: { email?: string; name?: string; role?: string }): Promise<any>;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface IDeleteUserPort {
|
|
358
|
+
execute(userId: string): Promise<void>;
|
|
359
|
+
}
|
|
360
|
+
`;
|
|
361
|
+
files['src/application/use-cases/user.use-cases.ts'] = `import { IUserRepositoryPort } from '../../ports/outbound/user-repository.port.js';
|
|
362
|
+
import { ICreateUserPort, IDeleteUserPort, IGetUserPort, IListUsersPort, IUpdateUserPort } from '../../ports/inbound/user-usecase.port.js';
|
|
363
|
+
|
|
364
|
+
export class CreateUserUseCase implements ICreateUserPort {
|
|
365
|
+
constructor(private userRepo: IUserRepositoryPort) {}
|
|
366
|
+
|
|
367
|
+
async execute(input: { email: string; name?: string; role?: string }) {
|
|
368
|
+
const existing = await this.userRepo.findByEmail(input.email);
|
|
369
|
+
if (existing) {
|
|
370
|
+
throw new Error('Email already exists');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const user = await this.userRepo.create({
|
|
374
|
+
email: input.email,
|
|
375
|
+
name: input.name || null,
|
|
376
|
+
role: input.role || null,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return user;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export class ListUsersUseCase implements IListUsersPort {
|
|
384
|
+
constructor(private userRepo: IUserRepositoryPort) {}
|
|
385
|
+
|
|
386
|
+
execute() {
|
|
387
|
+
return this.userRepo.findAll();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export class GetUserUseCase implements IGetUserPort {
|
|
392
|
+
constructor(private userRepo: IUserRepositoryPort) {}
|
|
393
|
+
|
|
394
|
+
async execute(userId: string) {
|
|
395
|
+
const user = await this.userRepo.findById(userId);
|
|
396
|
+
if (!user) {
|
|
397
|
+
throw new Error('User not found');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return user;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export class UpdateUserUseCase implements IUpdateUserPort {
|
|
405
|
+
constructor(private userRepo: IUserRepositoryPort) {}
|
|
406
|
+
|
|
407
|
+
execute(userId: string, input: { email?: string; name?: string; role?: string }) {
|
|
408
|
+
return this.userRepo.update(userId, input);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export class DeleteUserUseCase implements IDeleteUserPort {
|
|
413
|
+
constructor(private userRepo: IUserRepositoryPort) {}
|
|
414
|
+
|
|
415
|
+
execute(userId: string) {
|
|
416
|
+
return this.userRepo.delete(userId);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
`;
|
|
420
|
+
files['src/adapters/inbound/web/schemas/user.schema.ts'] = `import { z } from 'zod';
|
|
421
|
+
|
|
422
|
+
export const createUserSchema = z.object({
|
|
423
|
+
body: z.object({
|
|
424
|
+
email: z.string().email(),
|
|
425
|
+
name: z.string().min(2).optional(),
|
|
426
|
+
role: z.enum(['user', 'admin']).optional(),
|
|
427
|
+
}),
|
|
428
|
+
});
|
|
429
|
+
`;
|
|
430
|
+
files['src/adapters/inbound/web/handlers/user.handler.ts'] = `import { Request, Response, NextFunction } from 'express';
|
|
431
|
+
import { CreateUserUseCase, DeleteUserUseCase, GetUserUseCase, ListUsersUseCase, UpdateUserUseCase } from '../../../../application/use-cases/user.use-cases.js';
|
|
432
|
+
import { OrmUserRepository } from '../../../outbound/db/orm-user.repository.js';
|
|
433
|
+
|
|
434
|
+
const userRepo = new OrmUserRepository();
|
|
435
|
+
|
|
436
|
+
export class UserHandler {
|
|
437
|
+
async create(req: Request, res: Response, next: NextFunction) {
|
|
438
|
+
try {
|
|
439
|
+
const useCase = new CreateUserUseCase(userRepo);
|
|
440
|
+
const user = await useCase.execute(req.body);
|
|
441
|
+
res.status(201).json({ status: 'success', data: { user } });
|
|
442
|
+
} catch (err: any) {
|
|
443
|
+
res.status(400).json({ status: 'error', statusCode: 400, message: err.message });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async show(req: Request, res: Response, next: NextFunction) {
|
|
448
|
+
try {
|
|
449
|
+
const useCase = new GetUserUseCase(userRepo);
|
|
450
|
+
const user = await useCase.execute(req.params.id);
|
|
451
|
+
res.status(200).json({ status: 'success', data: { user } });
|
|
452
|
+
} catch (err: any) {
|
|
453
|
+
res.status(404).json({ status: 'error', statusCode: 404, message: err.message });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async list(req: Request, res: Response, next: NextFunction) {
|
|
458
|
+
try {
|
|
459
|
+
const useCase = new ListUsersUseCase(userRepo);
|
|
460
|
+
const users = await useCase.execute();
|
|
461
|
+
res.status(200).json({ status: 'success', data: { users } });
|
|
462
|
+
} catch (err: any) {
|
|
463
|
+
next(err);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async update(req: Request, res: Response, next: NextFunction) {
|
|
468
|
+
try {
|
|
469
|
+
const useCase = new UpdateUserUseCase(userRepo);
|
|
470
|
+
const user = await useCase.execute(req.params.id, req.body);
|
|
471
|
+
res.status(200).json({ status: 'success', data: { user } });
|
|
472
|
+
} catch (err: any) {
|
|
473
|
+
next(err);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async remove(req: Request, res: Response, next: NextFunction) {
|
|
478
|
+
try {
|
|
479
|
+
const useCase = new DeleteUserUseCase(userRepo);
|
|
480
|
+
await useCase.execute(req.params.id);
|
|
481
|
+
res.status(204).send();
|
|
482
|
+
} catch (err: any) {
|
|
483
|
+
next(err);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
`;
|
|
488
|
+
files['src/adapters/inbound/web/routes/user.routes.ts'] = `import { Router } from 'express';
|
|
489
|
+
import { UserHandler } from '../handlers/user.handler.js';
|
|
490
|
+
import { validate } from '../middlewares/validate.middleware.js';
|
|
491
|
+
import { createUserSchema } from '../schemas/user.schema.js';
|
|
492
|
+
|
|
493
|
+
const router = Router();
|
|
494
|
+
const handler = new UserHandler();
|
|
495
|
+
|
|
496
|
+
router.get('/', handler.list);
|
|
497
|
+
router.get('/:id', handler.show);
|
|
498
|
+
router.post('/', validate(createUserSchema), handler.create);
|
|
499
|
+
router.put('/:id', handler.update);
|
|
500
|
+
router.delete('/:id', handler.remove);
|
|
501
|
+
|
|
502
|
+
export default router;
|
|
503
|
+
`;
|
|
504
|
+
if (options.orm === 'typeorm') {
|
|
505
|
+
files['src/adapters/outbound/db/user.entity.ts'] = `import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm";
|
|
506
|
+
|
|
507
|
+
@Entity({ name: "users" })
|
|
508
|
+
export class UserEntity {
|
|
509
|
+
@PrimaryGeneratedColumn("uuid")
|
|
510
|
+
id!: string;
|
|
511
|
+
|
|
512
|
+
@Column({ unique: true })
|
|
513
|
+
email!: string;
|
|
514
|
+
|
|
515
|
+
@Column({ nullable: true })
|
|
516
|
+
name?: string;
|
|
517
|
+
|
|
518
|
+
@Column({ nullable: true })
|
|
519
|
+
role?: string;
|
|
520
|
+
|
|
521
|
+
@CreateDateColumn()
|
|
522
|
+
createdAt!: Date;
|
|
523
|
+
|
|
524
|
+
@UpdateDateColumn()
|
|
525
|
+
updatedAt!: Date;
|
|
526
|
+
}
|
|
527
|
+
`;
|
|
528
|
+
files['src/adapters/outbound/db/orm-user.repository.ts'] = `import { AppDataSource } from '../../../config/data-source.js';
|
|
529
|
+
import { UserEntity } from './user.entity.js';
|
|
530
|
+
import { User } from '../../../domain/models/user.model.js';
|
|
531
|
+
import { IUserRepositoryPort } from '../../../ports/outbound/user-repository.port.js';
|
|
532
|
+
|
|
533
|
+
export class OrmUserRepository implements IUserRepositoryPort {
|
|
534
|
+
private repo = AppDataSource.getRepository(UserEntity);
|
|
535
|
+
|
|
536
|
+
private mapToModel(u: UserEntity): User {
|
|
537
|
+
return new User(u.id, u.email, u.name ?? null, u.role ?? null, u.createdAt);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
541
|
+
const user = await this.repo.findOne({ where: { email } });
|
|
542
|
+
return user ? this.mapToModel(user) : null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async findById(id: string): Promise<User | null> {
|
|
546
|
+
const user = await this.repo.findOne({ where: { id } });
|
|
547
|
+
return user ? this.mapToModel(user) : null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
|
|
551
|
+
const created = this.repo.create({ ...user, name: user.name ?? undefined, role: user.role ?? undefined });
|
|
552
|
+
const saved = await this.repo.save(created);
|
|
553
|
+
return this.mapToModel(saved);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async update(id: string, user: Partial<User>): Promise<User> {
|
|
557
|
+
await this.repo.update(id, { ...user, name: user.name ?? undefined, role: user.role ?? undefined });
|
|
558
|
+
const updated = await this.repo.findOneByOrFail({ id });
|
|
559
|
+
return this.mapToModel(updated);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async delete(id: string): Promise<void> {
|
|
563
|
+
await this.repo.delete(id);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async findAll(): Promise<User[]> {
|
|
567
|
+
const users = await this.repo.find();
|
|
568
|
+
return users.map((user: UserEntity) => this.mapToModel(user));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
`;
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
files['src/adapters/outbound/db/orm-user.repository.ts'] = `import { PrismaClient } from '@prisma/client';
|
|
575
|
+
import { User } from '../../../domain/models/user.model.js';
|
|
576
|
+
import { IUserRepositoryPort } from '../../../ports/outbound/user-repository.port.js';
|
|
577
|
+
|
|
578
|
+
const prisma = new PrismaClient();
|
|
579
|
+
|
|
580
|
+
export class OrmUserRepository implements IUserRepositoryPort {
|
|
581
|
+
private mapToModel(u: any): User {
|
|
582
|
+
return new User(u.id, u.email, u.name, u.role, u.createdAt);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
586
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
587
|
+
return user ? this.mapToModel(user) : null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async findById(id: string): Promise<User | null> {
|
|
591
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
592
|
+
return user ? this.mapToModel(user) : null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
|
|
596
|
+
const created = await prisma.user.create({ data: user });
|
|
597
|
+
return this.mapToModel(created);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async update(id: string, user: Partial<User>): Promise<User> {
|
|
601
|
+
const updated = await prisma.user.update({ where: { id }, data: user });
|
|
602
|
+
return this.mapToModel(updated);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async delete(id: string): Promise<void> {
|
|
606
|
+
await prisma.user.delete({ where: { id } });
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async findAll(): Promise<User[]> {
|
|
610
|
+
const users = await prisma.user.findMany();
|
|
611
|
+
return users.map(user => this.mapToModel(user));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
`;
|
|
615
|
+
}
|
|
616
|
+
function addPublicCleanUsers(files, options) {
|
|
617
|
+
const repoImport = options.orm === 'typeorm'
|
|
618
|
+
? '../../infrastructure/repositories/typeorm-user.repository.impl.js'
|
|
619
|
+
: options.orm === 'mongoose'
|
|
620
|
+
? '../../infrastructure/repositories/mongoose-user.repository.impl.js'
|
|
621
|
+
: '../../infrastructure/repositories/prisma-user.repository.impl.js';
|
|
622
|
+
const repoClass = options.orm === 'typeorm'
|
|
623
|
+
? 'TypeOrmUserRepository'
|
|
624
|
+
: options.orm === 'mongoose'
|
|
625
|
+
? 'MongooseUserRepository'
|
|
626
|
+
: 'PrismaUserRepository';
|
|
627
|
+
files['src/domain/entities/user.entity.ts'] = `export class User {
|
|
628
|
+
constructor(
|
|
629
|
+
public readonly id: string,
|
|
630
|
+
public readonly email: string,
|
|
631
|
+
public readonly name: string | null,
|
|
632
|
+
public readonly role: string | null,
|
|
633
|
+
public readonly createdAt: Date
|
|
634
|
+
) {}
|
|
635
|
+
}
|
|
636
|
+
`;
|
|
637
|
+
files['src/application/interfaces/user-repository.interface.ts'] = `import { User } from '../../domain/entities/user.entity.js';
|
|
638
|
+
|
|
639
|
+
export interface IUserRepository {
|
|
640
|
+
findByEmail(email: string): Promise<User | null>;
|
|
641
|
+
findById(id: string): Promise<User | null>;
|
|
642
|
+
create(user: Omit<User, 'id' | 'createdAt'>): Promise<User>;
|
|
643
|
+
update(id: string, user: Partial<User>): Promise<User>;
|
|
644
|
+
delete(id: string): Promise<void>;
|
|
645
|
+
findAll(): Promise<User[]>;
|
|
646
|
+
}
|
|
647
|
+
`;
|
|
648
|
+
files['src/application/use-cases/user.use-cases.ts'] = `import { IUserRepository } from '../interfaces/user-repository.interface.js';
|
|
649
|
+
|
|
650
|
+
export class CreateUserUseCase {
|
|
651
|
+
constructor(private userRepository: IUserRepository) {}
|
|
652
|
+
|
|
653
|
+
async execute(input: { email: string; name?: string; role?: string }) {
|
|
654
|
+
const existing = await this.userRepository.findByEmail(input.email);
|
|
655
|
+
if (existing) {
|
|
656
|
+
throw new Error('Email already exists');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return this.userRepository.create({
|
|
660
|
+
email: input.email,
|
|
661
|
+
name: input.name || null,
|
|
662
|
+
role: input.role || null,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export class ListUsersUseCase {
|
|
668
|
+
constructor(private userRepository: IUserRepository) {}
|
|
669
|
+
|
|
670
|
+
execute() {
|
|
671
|
+
return this.userRepository.findAll();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export class GetUserUseCase {
|
|
676
|
+
constructor(private userRepository: IUserRepository) {}
|
|
677
|
+
|
|
678
|
+
async execute(id: string) {
|
|
679
|
+
const user = await this.userRepository.findById(id);
|
|
680
|
+
if (!user) {
|
|
681
|
+
throw new Error('User not found');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return user;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export class UpdateUserUseCase {
|
|
689
|
+
constructor(private userRepository: IUserRepository) {}
|
|
690
|
+
|
|
691
|
+
execute(id: string, input: { email?: string; name?: string; role?: string }) {
|
|
692
|
+
return this.userRepository.update(id, input);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export class DeleteUserUseCase {
|
|
697
|
+
constructor(private userRepository: IUserRepository) {}
|
|
698
|
+
|
|
699
|
+
execute(id: string) {
|
|
700
|
+
return this.userRepository.delete(id);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
`;
|
|
704
|
+
files['src/presentation/schemas/user.schema.ts'] = `import { z } from 'zod';
|
|
705
|
+
|
|
706
|
+
export const createUserSchema = z.object({
|
|
707
|
+
body: z.object({
|
|
708
|
+
email: z.string().email(),
|
|
709
|
+
name: z.string().min(2).optional(),
|
|
710
|
+
role: z.enum(['user', 'admin']).optional(),
|
|
711
|
+
}),
|
|
712
|
+
});
|
|
713
|
+
`;
|
|
714
|
+
files['src/presentation/controllers/user.controller.ts'] = `import { Request, Response, NextFunction } from 'express';
|
|
715
|
+
import { CreateUserUseCase, DeleteUserUseCase, GetUserUseCase, ListUsersUseCase, UpdateUserUseCase } from '../../application/use-cases/user.use-cases.js';
|
|
716
|
+
import { ${repoClass} } from '${repoImport}';
|
|
717
|
+
|
|
718
|
+
const userRepository = new ${repoClass}();
|
|
719
|
+
|
|
720
|
+
export class UserController {
|
|
721
|
+
async create(req: Request, res: Response, next: NextFunction) {
|
|
722
|
+
try {
|
|
723
|
+
const useCase = new CreateUserUseCase(userRepository);
|
|
724
|
+
const user = await useCase.execute(req.body);
|
|
725
|
+
res.status(201).json({ status: 'success', data: { user } });
|
|
726
|
+
} catch (err: any) {
|
|
727
|
+
res.status(400).json({ status: 'error', statusCode: 400, message: err.message });
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async show(req: Request, res: Response, next: NextFunction) {
|
|
732
|
+
try {
|
|
733
|
+
const useCase = new GetUserUseCase(userRepository);
|
|
734
|
+
const user = await useCase.execute(req.params.id);
|
|
735
|
+
res.status(200).json({ status: 'success', data: { user } });
|
|
736
|
+
} catch (err: any) {
|
|
737
|
+
res.status(404).json({ status: 'error', statusCode: 404, message: err.message });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async list(req: Request, res: Response, next: NextFunction) {
|
|
742
|
+
try {
|
|
743
|
+
const useCase = new ListUsersUseCase(userRepository);
|
|
744
|
+
const users = await useCase.execute();
|
|
745
|
+
res.status(200).json({ status: 'success', data: { users } });
|
|
746
|
+
} catch (err) {
|
|
747
|
+
next(err);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async update(req: Request, res: Response, next: NextFunction) {
|
|
752
|
+
try {
|
|
753
|
+
const useCase = new UpdateUserUseCase(userRepository);
|
|
754
|
+
const user = await useCase.execute(req.params.id, req.body);
|
|
755
|
+
res.status(200).json({ status: 'success', data: { user } });
|
|
756
|
+
} catch (err) {
|
|
757
|
+
next(err);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async remove(req: Request, res: Response, next: NextFunction) {
|
|
762
|
+
try {
|
|
763
|
+
const useCase = new DeleteUserUseCase(userRepository);
|
|
764
|
+
await useCase.execute(req.params.id);
|
|
765
|
+
res.status(204).send();
|
|
766
|
+
} catch (err) {
|
|
767
|
+
next(err);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
`;
|
|
772
|
+
files['src/presentation/routes/user.routes.ts'] = `import { Router } from 'express';
|
|
773
|
+
import { UserController } from '../controllers/user.controller.js';
|
|
774
|
+
import { validate } from '../middlewares/validate.middleware.js';
|
|
775
|
+
import { createUserSchema } from '../schemas/user.schema.js';
|
|
776
|
+
|
|
777
|
+
const router = Router();
|
|
778
|
+
const controller = new UserController();
|
|
779
|
+
|
|
780
|
+
router.get('/', controller.list);
|
|
781
|
+
router.get('/:id', controller.show);
|
|
782
|
+
router.post('/', validate(createUserSchema), controller.create);
|
|
783
|
+
router.put('/:id', controller.update);
|
|
784
|
+
router.delete('/:id', controller.remove);
|
|
785
|
+
|
|
786
|
+
export default router;
|
|
787
|
+
`;
|
|
788
|
+
addPublicCleanRepository(files, options);
|
|
789
|
+
}
|
|
790
|
+
function addPublicCleanRepository(files, options) {
|
|
791
|
+
const orm = options.orm;
|
|
792
|
+
if (orm === 'typeorm') {
|
|
793
|
+
const type = options.database === 'mysql' ? 'mysql' : options.database === 'sqlite' ? 'sqlite' : 'postgres';
|
|
794
|
+
const databaseLine = type === 'sqlite' ? ' database: "dev.db",' : ' url: process.env.DATABASE_URL,';
|
|
795
|
+
files['src/infrastructure/database/connection.ts'] = `import "reflect-metadata";
|
|
796
|
+
import { DataSource } from "typeorm";
|
|
797
|
+
import { UserEntity } from "../repositories/user.entity.js";
|
|
798
|
+
|
|
799
|
+
export const AppDataSource = new DataSource({
|
|
800
|
+
type: "${type}",
|
|
801
|
+
${databaseLine}
|
|
802
|
+
synchronize: true,
|
|
803
|
+
logging: false,
|
|
804
|
+
entities: [UserEntity],
|
|
805
|
+
});
|
|
806
|
+
`;
|
|
807
|
+
files['src/infrastructure/repositories/user.entity.ts'] = `import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm";
|
|
808
|
+
|
|
809
|
+
@Entity({ name: "users" })
|
|
810
|
+
export class UserEntity {
|
|
811
|
+
@PrimaryGeneratedColumn("uuid")
|
|
812
|
+
id!: string;
|
|
813
|
+
|
|
814
|
+
@Column({ unique: true })
|
|
815
|
+
email!: string;
|
|
816
|
+
|
|
817
|
+
@Column({ nullable: true })
|
|
818
|
+
name?: string;
|
|
819
|
+
|
|
820
|
+
@Column({ nullable: true })
|
|
821
|
+
role?: string;
|
|
822
|
+
|
|
823
|
+
@CreateDateColumn()
|
|
824
|
+
createdAt!: Date;
|
|
825
|
+
|
|
826
|
+
@UpdateDateColumn()
|
|
827
|
+
updatedAt!: Date;
|
|
828
|
+
}
|
|
829
|
+
`;
|
|
830
|
+
files['src/infrastructure/repositories/typeorm-user.repository.impl.ts'] = `import { AppDataSource } from '../database/connection.js';
|
|
831
|
+
import { UserEntity } from './user.entity.js';
|
|
832
|
+
import { User } from '../../domain/entities/user.entity.js';
|
|
833
|
+
import { IUserRepository } from '../../application/interfaces/user-repository.interface.js';
|
|
834
|
+
|
|
835
|
+
export class TypeOrmUserRepository implements IUserRepository {
|
|
836
|
+
private repo = AppDataSource.getRepository(UserEntity);
|
|
837
|
+
|
|
838
|
+
private mapToEntity(u: UserEntity): User {
|
|
839
|
+
return new User(u.id, u.email, u.name ?? null, u.role ?? null, u.createdAt);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
843
|
+
const user = await this.repo.findOne({ where: { email } });
|
|
844
|
+
return user ? this.mapToEntity(user) : null;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async findById(id: string): Promise<User | null> {
|
|
848
|
+
const user = await this.repo.findOne({ where: { id } });
|
|
849
|
+
return user ? this.mapToEntity(user) : null;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
|
|
853
|
+
const created = this.repo.create({ ...user, name: user.name ?? undefined, role: user.role ?? undefined });
|
|
854
|
+
const saved = await this.repo.save(created);
|
|
855
|
+
return this.mapToEntity(saved);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async update(id: string, user: Partial<User>): Promise<User> {
|
|
859
|
+
await this.repo.update(id, { ...user, name: user.name ?? undefined, role: user.role ?? undefined });
|
|
860
|
+
const updated = await this.repo.findOneByOrFail({ id });
|
|
861
|
+
return this.mapToEntity(updated);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async delete(id: string): Promise<void> {
|
|
865
|
+
await this.repo.delete(id);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async findAll(): Promise<User[]> {
|
|
869
|
+
const users = await this.repo.find();
|
|
870
|
+
return users.map((user: UserEntity) => this.mapToEntity(user));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
`;
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (orm === 'mongoose') {
|
|
877
|
+
files['src/infrastructure/repositories/mongoose-user.repository.impl.ts'] = `import { UserModel } from '../models/user.model.js';
|
|
878
|
+
import { User } from '../../domain/entities/user.entity.js';
|
|
879
|
+
import { IUserRepository } from '../../application/interfaces/user-repository.interface.js';
|
|
880
|
+
|
|
881
|
+
export class MongooseUserRepository implements IUserRepository {
|
|
882
|
+
private mapToEntity(doc: any): User {
|
|
883
|
+
return new User(doc._id.toString(), doc.email, doc.name ?? null, doc.role ?? null, doc.createdAt);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
887
|
+
const doc = await UserModel.findOne({ email });
|
|
888
|
+
return doc ? this.mapToEntity(doc) : null;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async findById(id: string): Promise<User | null> {
|
|
892
|
+
const doc = await UserModel.findById(id);
|
|
893
|
+
return doc ? this.mapToEntity(doc) : null;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
|
|
897
|
+
const doc = await UserModel.create(user);
|
|
898
|
+
return this.mapToEntity(doc);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async update(id: string, user: Partial<User>): Promise<User> {
|
|
902
|
+
const doc = await UserModel.findByIdAndUpdate(id, user, { new: true });
|
|
903
|
+
if (!doc) throw new Error('User not found');
|
|
904
|
+
return this.mapToEntity(doc);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async delete(id: string): Promise<void> {
|
|
908
|
+
await UserModel.findByIdAndDelete(id);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async findAll(): Promise<User[]> {
|
|
912
|
+
const docs = await UserModel.find();
|
|
913
|
+
return docs.map(doc => this.mapToEntity(doc));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
`;
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
files['src/infrastructure/repositories/prisma-user.repository.impl.ts'] = `import { PrismaClient } from '@prisma/client';
|
|
920
|
+
import { User } from '../../domain/entities/user.entity.js';
|
|
921
|
+
import { IUserRepository } from '../../application/interfaces/user-repository.interface.js';
|
|
922
|
+
|
|
923
|
+
const prisma = new PrismaClient();
|
|
924
|
+
|
|
925
|
+
export class PrismaUserRepository implements IUserRepository {
|
|
926
|
+
private mapToEntity(dbUser: any): User {
|
|
927
|
+
return new User(dbUser.id, dbUser.email, dbUser.name, dbUser.role, dbUser.createdAt);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
931
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
932
|
+
return user ? this.mapToEntity(user) : null;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async findById(id: string): Promise<User | null> {
|
|
936
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
937
|
+
return user ? this.mapToEntity(user) : null;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async create(user: Omit<User, 'id' | 'createdAt'>): Promise<User> {
|
|
941
|
+
const created = await prisma.user.create({ data: user });
|
|
942
|
+
return this.mapToEntity(created);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async update(id: string, user: Partial<User>): Promise<User> {
|
|
946
|
+
const updated = await prisma.user.update({ where: { id }, data: user });
|
|
947
|
+
return this.mapToEntity(updated);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async delete(id: string): Promise<void> {
|
|
951
|
+
await prisma.user.delete({ where: { id } });
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async findAll(): Promise<User[]> {
|
|
955
|
+
const users = await prisma.user.findMany();
|
|
956
|
+
return users.map(user => this.mapToEntity(user));
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
`;
|
|
960
|
+
}
|
|
961
|
+
function addPublicLayeredLikeUsers(files, paths) {
|
|
962
|
+
files[paths.repositoryPath] = `import { PrismaClient } from '@prisma/client';
|
|
963
|
+
|
|
964
|
+
const prisma = new PrismaClient();
|
|
965
|
+
|
|
966
|
+
export class ${paths.repositoryClass} {
|
|
967
|
+
async findByEmail(email: string) {
|
|
968
|
+
return prisma.user.findUnique({ where: { email } });
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async findById(id: string) {
|
|
972
|
+
return prisma.user.findUnique({ where: { id } });
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async create(data: { email: string; name?: string | null; role?: string | null }) {
|
|
976
|
+
return prisma.user.create({ data });
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async update(id: string, data: { email?: string; name?: string | null; role?: string | null }) {
|
|
980
|
+
return prisma.user.update({ where: { id }, data });
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async delete(id: string) {
|
|
984
|
+
return prisma.user.delete({ where: { id } });
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async findAll() {
|
|
988
|
+
return prisma.user.findMany();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
`;
|
|
992
|
+
files[paths.schemaPath] = `import { z } from 'zod';
|
|
993
|
+
|
|
994
|
+
export const createUserSchema = z.object({
|
|
995
|
+
body: z.object({
|
|
996
|
+
email: z.string().email('Invalid email address'),
|
|
997
|
+
name: z.string().min(2, 'Name must be at least 2 characters').optional(),
|
|
998
|
+
role: z.enum(['user', 'admin']).optional(),
|
|
999
|
+
}),
|
|
1000
|
+
});
|
|
1001
|
+
`;
|
|
1002
|
+
files[paths.servicePath] = `import { ${paths.repositoryClass} } from '${paths.repositoryImport}';
|
|
1003
|
+
import { AppError } from '${paths.errorImport}';
|
|
1004
|
+
|
|
1005
|
+
const userRepository = new ${paths.repositoryClass}();
|
|
1006
|
+
|
|
1007
|
+
export class UserService {
|
|
1008
|
+
async create(data: any) {
|
|
1009
|
+
const existing = await userRepository.findByEmail(data.email);
|
|
1010
|
+
if (existing) {
|
|
1011
|
+
throw new AppError(400, 'Email already exists');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const user = await userRepository.create(data);
|
|
1015
|
+
return {
|
|
1016
|
+
id: user.id,
|
|
1017
|
+
email: user.email,
|
|
1018
|
+
name: user.name,
|
|
1019
|
+
role: user.role,
|
|
1020
|
+
createdAt: user.createdAt,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async getById(id: string) {
|
|
1025
|
+
const user = await userRepository.findById(id);
|
|
1026
|
+
if (!user) {
|
|
1027
|
+
throw new AppError(404, 'User not found');
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
return {
|
|
1031
|
+
id: user.id,
|
|
1032
|
+
email: user.email,
|
|
1033
|
+
name: user.name,
|
|
1034
|
+
role: user.role,
|
|
1035
|
+
createdAt: user.createdAt,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async getAllUsers() {
|
|
1040
|
+
return userRepository.findAll();
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async update(id: string, data: any) {
|
|
1044
|
+
return userRepository.update(id, data);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async remove(id: string) {
|
|
1048
|
+
await userRepository.delete(id);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
`;
|
|
1052
|
+
files[paths.controllerPath] = `import { Request, Response, NextFunction } from 'express';
|
|
1053
|
+
import { UserService } from '${paths.serviceImport}';
|
|
1054
|
+
|
|
1055
|
+
const userService = new UserService();
|
|
1056
|
+
|
|
1057
|
+
export class UserController {
|
|
1058
|
+
async create(req: Request, res: Response, next: NextFunction) {
|
|
1059
|
+
try {
|
|
1060
|
+
const user = await userService.create(req.body);
|
|
1061
|
+
res.status(201).json({ status: 'success', data: { user } });
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
next(err);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async show(req: Request, res: Response, next: NextFunction) {
|
|
1068
|
+
try {
|
|
1069
|
+
const user = await userService.getById(req.params.id);
|
|
1070
|
+
res.status(200).json({ status: 'success', data: { user } });
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
next(err);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async list(req: Request, res: Response, next: NextFunction) {
|
|
1077
|
+
try {
|
|
1078
|
+
const users = await userService.getAllUsers();
|
|
1079
|
+
res.status(200).json({ status: 'success', data: { users } });
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
next(err);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
async update(req: Request, res: Response, next: NextFunction) {
|
|
1086
|
+
try {
|
|
1087
|
+
const user = await userService.update(req.params.id, req.body);
|
|
1088
|
+
res.status(200).json({ status: 'success', data: { user } });
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
next(err);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
async remove(req: Request, res: Response, next: NextFunction) {
|
|
1095
|
+
try {
|
|
1096
|
+
await userService.remove(req.params.id);
|
|
1097
|
+
res.status(204).send();
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
next(err);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
`;
|
|
1104
|
+
files[paths.routesPath] = `import { Router } from 'express';
|
|
1105
|
+
import { UserController } from '${paths.controllerImport}';
|
|
1106
|
+
import { validate } from '${paths.routesPath.includes('/modules/') ? '../../middlewares/validate.js' : '../middlewares/validate.js'}';
|
|
1107
|
+
import { createUserSchema } from '${paths.schemaImport}';
|
|
1108
|
+
|
|
1109
|
+
const router = Router();
|
|
1110
|
+
const controller = new UserController();
|
|
1111
|
+
|
|
1112
|
+
router.get('/', controller.list);
|
|
1113
|
+
router.get('/:id', controller.show);
|
|
1114
|
+
router.post('/', validate(createUserSchema), controller.create);
|
|
1115
|
+
router.put('/:id', controller.update);
|
|
1116
|
+
router.delete('/:id', controller.remove);
|
|
1117
|
+
|
|
1118
|
+
export default router;
|
|
1119
|
+
`;
|
|
1120
|
+
}
|