opticore-feature-component 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.
@@ -0,0 +1,1950 @@
1
+ // src/core/core.ts
2
+ import process4 from "process";
3
+ import fs4 from "fs";
4
+ import path6 from "path";
5
+ import { isCancel as isCancel4 } from "@clack/prompts";
6
+
7
+ // src/utils/welcomeMessage.utils.ts
8
+ import gradient from "gradient-string";
9
+ function UWelcomeMessage() {
10
+ return console.log(gradient("cyan", "pink", "orange")(
11
+ "\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E\n\u2502 \u2502\n\u2502 Welcome to Feature Component CLI \u2502\n\u2502 \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F\n"
12
+ ));
13
+ }
14
+
15
+ // src/utils/choiceFeatureCleanModule.ts
16
+ import colors2 from "ansi-colors";
17
+ import { isCancel } from "@clack/prompts";
18
+
19
+ // src/utils/prompt.utils.ts
20
+ import { confirm, select, text } from "@clack/prompts";
21
+ import colors from "ansi-colors";
22
+
23
+ // src/utils/fsModule.utils.ts
24
+ import fs from "fs";
25
+ var UFsModule = class {
26
+ static createDirectoryRecursively(dirPath) {
27
+ try {
28
+ fs.mkdirSync(dirPath, { recursive: true });
29
+ } catch (e) {
30
+ console.error(e.message);
31
+ }
32
+ }
33
+ static createFile(filePath, fileContent) {
34
+ try {
35
+ fs.writeFileSync(filePath, fileContent);
36
+ } catch (e) {
37
+ console.error(e.message);
38
+ }
39
+ }
40
+ static removeDir(directory, recursive, force) {
41
+ try {
42
+ if (fs.existsSync(directory)) {
43
+ fs.rmSync(directory, { recursive, force });
44
+ }
45
+ } catch (e) {
46
+ console.error(`Error removing folder: ${e.message}`, e);
47
+ process.exit(0);
48
+ }
49
+ }
50
+ };
51
+
52
+ // src/utils/prompt.utils.ts
53
+ import { LoggerCore } from "opticore-logger";
54
+ import { HttpStatusCode } from "opticore-http-response";
55
+ var UPrompt = class {
56
+ static logger = new LoggerCore();
57
+ static async text(message, placeholder, validate) {
58
+ return await text(
59
+ {
60
+ message,
61
+ placeholder,
62
+ validate
63
+ }
64
+ );
65
+ }
66
+ static async confirm(message, defaultValue = true) {
67
+ return await confirm({
68
+ message,
69
+ initialValue: defaultValue
70
+ });
71
+ }
72
+ static cancelOperation(controllerPath) {
73
+ console.log(`${colors.bgRed(`${colors.white("Operation cancelled.")}`)}`);
74
+ if (controllerPath) {
75
+ try {
76
+ UFsModule.removeDir(controllerPath, true, true);
77
+ this.logger.info({
78
+ title: "DIR_REMOVED",
79
+ message: `Removed controller directory: ${controllerPath}`
80
+ });
81
+ } catch (error) {
82
+ this.logger.error({
83
+ errorType: error.name,
84
+ httpCodeValue: HttpStatusCode.INTERNAL_SERVER_ERROR,
85
+ message: error.message,
86
+ stackTrace: error.stack,
87
+ title: "FAILED_TO_REMOVE_CONTROLLER_DIR"
88
+ });
89
+ }
90
+ }
91
+ process.exit(0);
92
+ }
93
+ static async select(message, defaultValue, options) {
94
+ let optSelected = await select({
95
+ message,
96
+ initialValue: defaultValue,
97
+ options
98
+ });
99
+ return optSelected[0];
100
+ }
101
+ };
102
+
103
+ // src/utils/choiceFeatureCleanModule.ts
104
+ async function choiceFeatureCleanModule() {
105
+ const featureSelected = await UPrompt.select(
106
+ "Please choose the creation principle for your feature :",
107
+ ["opticore_principle"],
108
+ [
109
+ { label: "OptiCoreJs CLEAN Module", value: ["opticore_clean_module"], hint: "recommended" },
110
+ { label: "Custom feature", value: ["custom_feature"] }
111
+ ]
112
+ );
113
+ if (isCancel(featureSelected)) {
114
+ console.log(`${colors2.bgRed(`${colors2.white("Operation cancelled.")}`)}`);
115
+ process.exit(0);
116
+ } else {
117
+ return featureSelected;
118
+ }
119
+ }
120
+
121
+ // src/utils/constants/customFeatureInfoMessage.constant.ts
122
+ import colors3 from "ansi-colors";
123
+ import process2 from "process";
124
+ function customFeatureInfo() {
125
+ console.log(`${colors3.bgCyan(`${colors3.white("\nYou will have to create and configure your created modules manually.")}`)}`);
126
+ process2.exit(0);
127
+ }
128
+
129
+ // src/utils/customFeatureInfo.utils.ts
130
+ import colors4 from "ansi-colors";
131
+ function UCustomFeatureInfo(choice) {
132
+ console.log(`You have chosen : ${choice === "opticore_clean_module" ? colors4.cyan("OptiCoreJs CLEAN Module") : colors4.cyan("Custom feature")}
133
+ `);
134
+ console.log(`Clean module contains the following files :`);
135
+ console.log(`${colors4.cyan("application")}`);
136
+ console.log(` \u2570\u2500\u2500${colors4.bgBlackBright(`${colors4.white("{controller, service, validator}")}`)}`);
137
+ console.log(`${colors4.cyan("domain")}`);
138
+ console.log(` \u2570\u2500\u2500${colors4.bgBlackBright(`${colors4.white("{constants, entities, enums, interfaces}")}`)}`);
139
+ console.log(`${colors4.cyan("infrastructures")}`);
140
+ console.log(` \u2570\u2500\u2500${colors4.bgBlackBright(`${colors4.white("{api, middleware}")}`)}`);
141
+ console.log(`${colors4.cyan("persistence")}`);
142
+ console.log(` \u2570\u2500\u2500${colors4.bgBlackBright(`${colors4.white("{cache, contrats, repositories, usecases}")}`)}`);
143
+ console.log(`${colors4.cyan("file")} : route of feature named ${colors4.cyan("featureName.router")}
144
+ `);
145
+ console.log(colors4.cyan("Now let's start create a feature !"));
146
+ }
147
+
148
+ // src/services/featureName.service.ts
149
+ var _featureName = async () => {
150
+ return await UPrompt.text(
151
+ "Enter feature's name :",
152
+ "login",
153
+ (value) => {
154
+ let pattern = new RegExp("^[a-z][A-Za-z]+$");
155
+ if (!value) {
156
+ return "Please enter a feature's name.";
157
+ }
158
+ if (!pattern.test(value)) {
159
+ return "Please enter a valide feature's name.";
160
+ }
161
+ }
162
+ );
163
+ };
164
+
165
+ // src/services/base.service.ts
166
+ import path from "path";
167
+ import fs2 from "fs";
168
+ import colors5 from "ansi-colors";
169
+ import process3 from "process";
170
+ var BaseService = class {
171
+ static _isSubdirectoryExists(subdirectory) {
172
+ const subdirectoryString = typeof subdirectory === "symbol" ? subdirectory.toString() : String(subdirectory);
173
+ const subdirectoryPath = path.join(process3.cwd(), "src", "features", subdirectoryString);
174
+ try {
175
+ const stats = fs2.statSync(subdirectoryPath);
176
+ return stats.isDirectory();
177
+ } catch (error) {
178
+ return false;
179
+ }
180
+ }
181
+ static _featureFounded(feature) {
182
+ console.log(`${colors5.bgCyan(`${colors5.white(`${colors5.bold(`feature ${feature} is already exists.`)}`)}`)}`);
183
+ process3.exit(0);
184
+ }
185
+ static _checkFeatureDir() {
186
+ console.error(`${colors5.bgRed(`${colors5.white("Check if directory src/features exists, and try again.")}`)}`);
187
+ process3.exit();
188
+ }
189
+ static _dirFounded(feature) {
190
+ console.error(`${colors5.bgRed(`${colors5.white(`The ${colors5.bgCyan(`${colors5.bold(`feature ${feature} is already exists.`)}`)}`)}`)}`);
191
+ process3.exit();
192
+ }
193
+ static _cancelOperation(controllerPath) {
194
+ console.log(`${colors5.bgRed(`${colors5.white("Operation cancelled.")}`)}`);
195
+ UFsModule.removeDir(controllerPath, true, true);
196
+ process3.exit(0);
197
+ }
198
+ static async _createFeatureModuleDir(featureDir, feature) {
199
+ let ora = (await import("ora")).default;
200
+ const spinner = ora("Feature's name creation...").start();
201
+ UFsModule.createDirectoryRecursively(featureDir);
202
+ spinner.succeed(`Your feature"s name ${colors5.bgCyan(colors5.white(`${feature}`))} has been created successfully.
203
+ `);
204
+ return { featureDir, feature };
205
+ }
206
+ static async _createApplicationDirComponent(feature) {
207
+ let application = "";
208
+ application = `${feature}/application`;
209
+ UFsModule.createDirectoryRecursively(application);
210
+ return application;
211
+ }
212
+ static async _createDomainDirComponent(feature) {
213
+ let domain = "";
214
+ domain = `${feature}/domain`;
215
+ UFsModule.createDirectoryRecursively(domain);
216
+ return domain;
217
+ }
218
+ static async _createInfraDirComponent(feature) {
219
+ let infrastructure = "";
220
+ infrastructure = `${feature}/infrastructure`;
221
+ UFsModule.createDirectoryRecursively(infrastructure);
222
+ return infrastructure;
223
+ }
224
+ static async _createPersistenceDirComponent(feature) {
225
+ let persistence = "";
226
+ persistence = `${feature}/persistence`;
227
+ UFsModule.createDirectoryRecursively(persistence);
228
+ return persistence;
229
+ }
230
+ };
231
+
232
+ // src/services/controllerType.service.ts
233
+ async function _controllerType(messageText, defaultValue, options) {
234
+ return await UPrompt.select(
235
+ messageText,
236
+ [defaultValue],
237
+ options
238
+ );
239
+ }
240
+
241
+ // src/core/components/application/createController.application.ts
242
+ import { isCancel as isCancel3 } from "@clack/prompts";
243
+
244
+ // src/core/domains/constants/cleanType.constant.ts
245
+ var CCleanType = {
246
+ "clean_component_by_step": "clean_component_by_step",
247
+ "full_clean_component": "full_clean_component",
248
+ "simple_component": "simple_component"
249
+ };
250
+
251
+ // src/templates/simpleComponent/simpleComponent.template.ts
252
+ import path3 from "path";
253
+ import colors7 from "ansi-colors";
254
+
255
+ // src/utils/featureComponentGenerator.utils.ts
256
+ import path2 from "path";
257
+ import colors6 from "ansi-colors";
258
+ import fs3 from "fs";
259
+ import { LoggerCore as LoggerCore2 } from "opticore-logger";
260
+
261
+ // src/templates/cleanComponents/controller.template.ts
262
+ var generateControllerTemplate = (controllerName, featureName, methods) => {
263
+ const capitalized = capitalize(featureName);
264
+ const useCaseName = `${capitalized}UseCase`;
265
+ const presenterName = `${capitalized}Presenter`;
266
+ const methodsCode = methods.map((method) => generateMethod(method, featureName, capitalized)).join("\n");
267
+ const errorHandler = errorHandlerAppend(capitalized);
268
+ return `import { Request, Response } from "express";
269
+ import { ResponseHandler, HttpStatusCode, IResponseHandlerSuccessData } from "opticore-http-response";
270
+ import { ${useCaseName} } from "../../../application/use-cases/${featureName}.usecase";
271
+ import { ${presenterName} } from "../presenters/${featureName}.presenter";
272
+ import { ${capitalized}Repository } from "../repositories/${featureName}.repository";
273
+
274
+ /**
275
+ * ${controllerName} \u2014 HTTP Controller (Infrastructure Layer)
276
+ *
277
+ * Handles Express Request/Response only.
278
+ * Delegates all business logic to the use case.
279
+ * Uses the presenter to format the output.
280
+ *
281
+ * Wiring: Controller \u2192 UseCase \u2192 Repository (all injected here manually).
282
+ * For larger projects, replace manual wiring with a DI container (e.g., tsyringe, awilix).
283
+ */
284
+ export class ${controllerName} {
285
+ private static buildUseCase(): ${useCaseName} {
286
+ const repository = new ${capitalized}Repository();
287
+ return new ${useCaseName}(repository);
288
+ }
289
+
290
+ private static buildPresenter(): ${presenterName} {
291
+ return new ${presenterName}();
292
+ }
293
+
294
+ ${methodsCode}
295
+ ${errorHandler}
296
+ }
297
+ `;
298
+ };
299
+ var generateMethod = (method, featureName, capitalized) => {
300
+ const lower = method.toLowerCase();
301
+ if (lower.includes("findall") || lower.includes("getall")) {
302
+ return generateFindAllMethod(method, featureName, capitalized);
303
+ }
304
+ if (lower.includes("findbyid") || lower.includes("getbyid") || lower.includes("getone")) {
305
+ return generateFindByIdMethod(method, featureName, capitalized);
306
+ }
307
+ if (lower.includes("create") || lower.includes("add")) {
308
+ return generateCreateMethod(method, featureName, capitalized);
309
+ }
310
+ if (lower.includes("update") || lower.includes("edit")) {
311
+ return generateUpdateMethod(method, featureName, capitalized);
312
+ }
313
+ if (lower.includes("delete") || lower.includes("remove")) {
314
+ return generateDeleteMethod(method, featureName, capitalized);
315
+ }
316
+ return `
317
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
318
+ try {
319
+ const useCase = ${capitalized}Controller.buildUseCase();
320
+ const presenter = ${capitalized}Controller.buildPresenter();
321
+ // TODO: Implement ${method} logic
322
+ // const result = await useCase.${method}(...);
323
+ return ResponseHandler.success(presenter.presentOne({} as any), "success", HttpStatusCode.OK);
324
+ } catch (error) {
325
+ return ${capitalized}Controller.handleError(error);
326
+ }
327
+ }
328
+ `;
329
+ };
330
+ var generateFindAllMethod = (method, featureName, capitalized) => `
331
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
332
+ try {
333
+ const useCase = ${capitalized}Controller.buildUseCase();
334
+ const presenter = ${capitalized}Controller.buildPresenter();
335
+
336
+ const results = await useCase.findAll();
337
+ return ResponseHandler.success(presenter.presentMany(results), "success", HttpStatusCode.OK);
338
+ } catch (error) {
339
+ return ${capitalized}Controller.handleError(error);
340
+ }
341
+ }
342
+ `;
343
+ var generateFindByIdMethod = (method, featureName, capitalized) => `
344
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
345
+ try {
346
+ const { id } = req.params;
347
+ const useCase = ${capitalized}Controller.buildUseCase();
348
+ const presenter = ${capitalized}Controller.buildPresenter();
349
+
350
+ const result = await useCase.findById(id);
351
+ if (!result) {
352
+ return ResponseHandler.error(\`Not found: \${id}\`, HttpStatusCode.NOT_FOUND);
353
+ }
354
+ return ResponseHandler.success(presenter.presentOne(result), "success", HttpStatusCode.OK);
355
+ } catch (error) {
356
+ return ${capitalized}Controller.handleError(error);
357
+ }
358
+ }
359
+ `;
360
+ var generateCreateMethod = (method, featureName, capitalized) => `
361
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
362
+ try {
363
+ const dto = req.body; // TODO: Validate with your validation layer
364
+ const useCase = ${capitalized}Controller.buildUseCase();
365
+ const presenter = ${capitalized}Controller.buildPresenter();
366
+
367
+ const result = await useCase.create(dto);
368
+ return ResponseHandler.success(presenter.presentOne(result), "created", HttpStatusCode.CREATED);
369
+ } catch (error) {
370
+ return ${capitalized}Controller.handleError(error);
371
+ }
372
+ }
373
+ `;
374
+ var generateUpdateMethod = (method, featureName, capitalized) => `
375
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
376
+ try {
377
+ const { id } = req.params;
378
+ const dto = { ...req.body, id }; // TODO: Validate with your validation layer
379
+ const useCase = ${capitalized}Controller.buildUseCase();
380
+ const presenter = ${capitalized}Controller.buildPresenter();
381
+
382
+ const result = await useCase.update(dto);
383
+ if (!result) {
384
+ return ResponseHandler.error(\`Not found: \${id}\`, HttpStatusCode.NOT_FOUND);
385
+ }
386
+ return ResponseHandler.success(presenter.presentOne(result), "success", HttpStatusCode.OK);
387
+ } catch (error) {
388
+ return ${capitalized}Controller.handleError(error);
389
+ }
390
+ }
391
+ `;
392
+ var generateDeleteMethod = (method, featureName, capitalized) => `
393
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
394
+ try {
395
+ const { id } = req.params;
396
+ const useCase = ${capitalized}Controller.buildUseCase();
397
+
398
+ const deleted = await useCase.delete(id);
399
+ if (!deleted) {
400
+ return ResponseHandler.error(\`Not found: \${id}\`, HttpStatusCode.NOT_FOUND);
401
+ }
402
+ return ResponseHandler.success(null, "deleted", HttpStatusCode.NO_CONTENT);
403
+ } catch (error) {
404
+ return ${capitalized}Controller.handleError(error);
405
+ }
406
+ }
407
+ `;
408
+ var errorHandlerAppend = (capitalized) => `
409
+ private static handleError(error: unknown) {
410
+ const message = error instanceof Error ? error.message : "Internal server error";
411
+ return ResponseHandler.error(message, HttpStatusCode.INTERNAL_SERVER_ERROR);
412
+ }
413
+ `;
414
+ var capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
415
+
416
+ // src/templates/cleanComponents/entity.template.ts
417
+ var generateEntityTemplate = (entityName, featureName) => {
418
+ const capitalized = capitalize2(featureName);
419
+ return `/**
420
+ * ${entityName} \u2014 Domain Entity
421
+ * Represents the core business object for the "${featureName}" feature.
422
+ * No framework dependency, pure business logic only.
423
+ */
424
+ export class ${entityName} {
425
+ private readonly _id: string;
426
+ private _createdAt: Date;
427
+ private _updatedAt: Date;
428
+
429
+ constructor(
430
+ id: string,
431
+ // TODO: Add your business properties here
432
+ createdAt?: Date,
433
+ updatedAt?: Date,
434
+ ) {
435
+ this._id = id;
436
+ this._createdAt = createdAt ?? new Date();
437
+ this._updatedAt = updatedAt ?? new Date();
438
+
439
+ this.validate();
440
+ }
441
+
442
+ // -------------------------------------------------------------------------
443
+ // Getters
444
+ // -------------------------------------------------------------------------
445
+
446
+ get id(): string {
447
+ return this._id;
448
+ }
449
+
450
+ get createdAt(): Date {
451
+ return this._createdAt;
452
+ }
453
+
454
+ get updatedAt(): Date {
455
+ return this._updatedAt;
456
+ }
457
+
458
+ // -------------------------------------------------------------------------
459
+ // Business methods (behavior belongs to the entity)
460
+ // -------------------------------------------------------------------------
461
+
462
+ /**
463
+ * Marks the entity as updated.
464
+ * Call this whenever a business mutation occurs.
465
+ */
466
+ public touch(): void {
467
+ this._updatedAt = new Date();
468
+ }
469
+
470
+ /**
471
+ * Returns a plain object snapshot of the entity.
472
+ * Useful for persistence or presentation mapping.
473
+ */
474
+ public toSnapshot(): Record<string, unknown> {
475
+ return {
476
+ id: this._id,
477
+ createdAt: this._createdAt,
478
+ updatedAt: this._updatedAt,
479
+ };
480
+ }
481
+
482
+ // -------------------------------------------------------------------------
483
+ // Invariant validation (domain rules enforced at construction)
484
+ // -------------------------------------------------------------------------
485
+
486
+ private validate(): void {
487
+ if (!this._id || this._id.trim().length === 0) {
488
+ throw new Error(\`[${entityName}] id must not be empty.\`);
489
+ }
490
+ // TODO: Add your domain invariant checks here
491
+ }
492
+ }
493
+ `;
494
+ };
495
+ var capitalize2 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
496
+
497
+ // src/templates/cleanComponents/repositoryInterface.template.ts
498
+ var generateRepositoryInterfaceTemplate = (interfaceName, featureName) => {
499
+ const entityName = `${capitalize3(featureName)}Entity`;
500
+ const entityImport = `../../../domain/entities/${featureName}.entity`;
501
+ return `import { ${entityName} } from "${entityImport}";
502
+
503
+ /**
504
+ * ${interfaceName} \u2014 Repository Port (Application Layer)
505
+ *
506
+ * Defines the persistence contract. The infrastructure layer must implement this.
507
+ * The application layer depends on this abstraction \u2014 never on a concrete ORM or DB.
508
+ */
509
+ export interface ${interfaceName} {
510
+
511
+ /**
512
+ * Returns all ${featureName} entities.
513
+ */
514
+ findAll(): Promise<${entityName}[]>;
515
+
516
+ /**
517
+ * Finds a single ${featureName} by its unique identifier.
518
+ * Returns null if not found (avoid throwing for "not found" in repositories).
519
+ */
520
+ findById(id: string): Promise<${entityName} | null>;
521
+
522
+ /**
523
+ * Persists a new ${featureName} entity.
524
+ * Returns the created entity (with generated id if applicable).
525
+ */
526
+ create(entity: ${entityName}): Promise<${entityName}>;
527
+
528
+ /**
529
+ * Persists changes to an existing ${featureName} entity.
530
+ * Returns the updated entity, or null if not found.
531
+ */
532
+ update(entity: ${entityName}): Promise<${entityName} | null>;
533
+
534
+ /**
535
+ * Removes a ${featureName} entity by id.
536
+ * Returns true if deleted, false if not found.
537
+ */
538
+ delete(id: string): Promise<boolean>;
539
+ }
540
+ `;
541
+ };
542
+ var capitalize3 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
543
+
544
+ // src/templates/cleanComponents/repositoryImpl.template.ts
545
+ var generateRepositoryImplTemplate = (featureName, interfaceImportPath) => {
546
+ const capitalized = capitalize4(featureName);
547
+ const implName = `${capitalized}Repository`;
548
+ const interfaceName = `I${capitalized}Repository`;
549
+ const entityName = `${capitalized}Entity`;
550
+ const entityImport = `../../../domain/entities/${featureName}.entity`;
551
+ return `import { ${interfaceName} } from "${interfaceImportPath}";
552
+ import { ${entityName} } from "${entityImport}";
553
+
554
+ /**
555
+ * ${implName} \u2014 Repository Implementation (Infrastructure Layer)
556
+ *
557
+ * Provides the actual persistence logic for ${featureName}.
558
+ * Replace the in-memory store below with your ORM (TypeORM, Prisma, Mongoose, etc.).
559
+ * This class depends on the ${interfaceName} contract defined in the application layer.
560
+ */
561
+ export class ${implName} implements ${interfaceName} {
562
+
563
+ /**
564
+ * TODO: Inject your database client / ORM model here via the constructor.
565
+ * Example with TypeORM:
566
+ * constructor(private readonly repo: Repository<${capitalized}Model>) {}
567
+ *
568
+ * Example with Prisma:
569
+ * constructor(private readonly prisma: PrismaClient) {}
570
+ */
571
+
572
+ // -------------------------------------------------------------------------
573
+ // In-memory store (replace with real persistence)
574
+ // -------------------------------------------------------------------------
575
+ private store: Map<string, ${entityName}> = new Map();
576
+
577
+ async findAll(): Promise<${entityName}[]> {
578
+ // TODO: Replace with your ORM query
579
+ // Example (TypeORM): return this.repo.find();
580
+ // Example (Prisma): const rows = await this.prisma.${featureName}.findMany();
581
+ // return rows.map(row => this.toDomain(row));
582
+ return Array.from(this.store.values());
583
+ }
584
+
585
+ async findById(id: string): Promise<${entityName} | null> {
586
+ // TODO: Replace with your ORM query
587
+ // Example (TypeORM): return this.repo.findOneBy({ id }) ?? null;
588
+ // Example (Prisma): const row = await this.prisma.${featureName}.findUnique({ where: { id } });
589
+ // return row ? this.toDomain(row) : null;
590
+ return this.store.get(id) ?? null;
591
+ }
592
+
593
+ async create(entity: ${entityName}): Promise<${entityName}> {
594
+ // TODO: Replace with your ORM save
595
+ // Example (TypeORM): const model = this.repo.create(this.toPersistence(entity));
596
+ // return this.toDomain(await this.repo.save(model));
597
+ // Example (Prisma): const row = await this.prisma.${featureName}.create({ data: this.toPersistence(entity) });
598
+ // return this.toDomain(row);
599
+ this.store.set(entity.id, entity);
600
+ return entity;
601
+ }
602
+
603
+ async update(entity: ${entityName}): Promise<${entityName} | null> {
604
+ // TODO: Replace with your ORM update
605
+ if (!this.store.has(entity.id)) return null;
606
+ this.store.set(entity.id, entity);
607
+ return entity;
608
+ }
609
+
610
+ async delete(id: string): Promise<boolean> {
611
+ // TODO: Replace with your ORM delete
612
+ // Example (TypeORM): const result = await this.repo.delete(id);
613
+ // return (result.affected ?? 0) > 0;
614
+ return this.store.delete(id);
615
+ }
616
+
617
+ // -------------------------------------------------------------------------
618
+ // Private mappers (ORM model \u2194 Domain entity)
619
+ // -------------------------------------------------------------------------
620
+
621
+ /**
622
+ * Maps an ORM/DB row to a domain entity.
623
+ * TODO: Adapt field mapping to your actual persistence model.
624
+ */
625
+ private toDomain(row: any): ${entityName} {
626
+ return new ${entityName}(
627
+ row.id,
628
+ // TODO: Map your ORM fields here
629
+ row.createdAt,
630
+ row.updatedAt,
631
+ );
632
+ }
633
+
634
+ /**
635
+ * Maps a domain entity to an ORM/DB-friendly plain object.
636
+ * TODO: Adapt to your schema.
637
+ */
638
+ private toPersistence(entity: ${entityName}): Record<string, unknown> {
639
+ return {
640
+ id: entity.id,
641
+ // TODO: Map your domain fields here
642
+ createdAt: entity.createdAt,
643
+ updatedAt: entity.updatedAt,
644
+ };
645
+ }
646
+ }
647
+ `;
648
+ };
649
+ var capitalize4 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
650
+
651
+ // src/templates/cleanComponents/useCase.template.ts
652
+ var generateUseCaseTemplate = (useCaseName, featureName) => {
653
+ const capitalized = capitalize5(featureName);
654
+ const interfaceName = `I${capitalized}Repository`;
655
+ const entityName = `${capitalized}Entity`;
656
+ return `import { ${interfaceName} } from "../ports/repositories/${featureName}.repository.interface";
657
+ import { ${entityName} } from "../../domain/entities/${featureName}.entity";
658
+ import {
659
+ Create${capitalized}Dto,
660
+ Update${capitalized}Dto,
661
+ ${capitalized}ResponseDto,
662
+ ${capitalized}DtoMapper,
663
+ } from "../dtos/${featureName}.dto";
664
+
665
+ /**
666
+ * ${useCaseName} \u2014 Application Use Case
667
+ *
668
+ * Orchestrates business operations for the "${featureName}" feature.
669
+ * It depends ONLY on the repository port (interface), never on a concrete implementation.
670
+ * The concrete repository is injected at the infrastructure level (DI container or manual wiring).
671
+ */
672
+ export class ${useCaseName} {
673
+
674
+ constructor(
675
+ private readonly repository: ${interfaceName},
676
+ ) {}
677
+
678
+ // -------------------------------------------------------------------------
679
+ // Query methods (read)
680
+ // -------------------------------------------------------------------------
681
+
682
+ async findAll(): Promise<${capitalized}ResponseDto[]> {
683
+ const entities = await this.repository.findAll();
684
+ return ${capitalized}DtoMapper.toResponseList(entities);
685
+ }
686
+
687
+ async findById(id: string): Promise<${capitalized}ResponseDto | null> {
688
+ const entity = await this.repository.findById(id);
689
+ if (!entity) return null;
690
+ return ${capitalized}DtoMapper.toResponse(entity);
691
+ }
692
+
693
+ // -------------------------------------------------------------------------
694
+ // Command methods (write)
695
+ // -------------------------------------------------------------------------
696
+
697
+ async create(dto: Create${capitalized}Dto): Promise<${capitalized}ResponseDto> {
698
+ // TODO: Map DTO to domain entity. Generate id using your preferred strategy (uuid, etc.)
699
+ const id = crypto.randomUUID();
700
+ const entity = new ${entityName}(
701
+ id,
702
+ // TODO: Pass your domain fields from dto
703
+ );
704
+
705
+ const saved = await this.repository.create(entity);
706
+ return ${capitalized}DtoMapper.toResponse(saved);
707
+ }
708
+
709
+ async update(dto: Update${capitalized}Dto): Promise<${capitalized}ResponseDto | null> {
710
+ const existing = await this.repository.findById(dto.id);
711
+ if (!existing) return null;
712
+
713
+ // TODO: Apply the update to the domain entity
714
+ // existing.updateName(dto.name);
715
+ existing.touch();
716
+
717
+ const updated = await this.repository.update(existing);
718
+ if (!updated) return null;
719
+ return ${capitalized}DtoMapper.toResponse(updated);
720
+ }
721
+
722
+ async delete(id: string): Promise<boolean> {
723
+ return this.repository.delete(id);
724
+ }
725
+ }
726
+ `;
727
+ };
728
+ var capitalize5 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
729
+
730
+ // src/templates/cleanComponents/dto.template.ts
731
+ var generateDtoTemplate = (featureName) => {
732
+ const capitalized = capitalize6(featureName);
733
+ return `/**
734
+ * ${capitalized}Dto \u2014 Data Transfer Objects (Application Layer)
735
+ *
736
+ * DTOs define the shape of data entering and leaving use cases.
737
+ * They decouple the domain model from HTTP requests/responses.
738
+ * Validation is handled at the infrastructure layer (e.g., in the controller).
739
+ */
740
+
741
+ // -------------------------------------------------------------------------
742
+ // Input DTOs (data coming IN to use cases)
743
+ // -------------------------------------------------------------------------
744
+
745
+ export interface Create${capitalized}Dto {
746
+ // TODO: Define the fields required to create a ${featureName}
747
+ // Example:
748
+ // name: string;
749
+ // email: string;
750
+ }
751
+
752
+ export interface Update${capitalized}Dto {
753
+ id: string;
754
+ // TODO: Define the fields allowed to be updated
755
+ // All fields should be optional (partial update pattern)
756
+ // Example:
757
+ // name?: string;
758
+ }
759
+
760
+ // -------------------------------------------------------------------------
761
+ // Output DTOs (data going OUT from use cases to the controller/presenter)
762
+ // -------------------------------------------------------------------------
763
+
764
+ export interface ${capitalized}ResponseDto {
765
+ id: string;
766
+ createdAt: Date;
767
+ updatedAt: Date;
768
+ // TODO: Mirror the fields you expose to clients
769
+ // Avoid leaking internal domain model details (e.g., private fields, passwords)
770
+ }
771
+
772
+ // -------------------------------------------------------------------------
773
+ // Mapper helper (Domain Entity \u2192 Response DTO)
774
+ // -------------------------------------------------------------------------
775
+
776
+ import { ${capitalized}Entity } from "../../domain/entities/${featureName}.entity";
777
+
778
+ export class ${capitalized}DtoMapper {
779
+
780
+ /**
781
+ * Maps a domain entity to a safe response DTO.
782
+ * Use this in your presenter or use case output.
783
+ */
784
+ static toResponse(entity: ${capitalized}Entity): ${capitalized}ResponseDto {
785
+ return {
786
+ id: entity.id,
787
+ createdAt: entity.createdAt,
788
+ updatedAt: entity.updatedAt,
789
+ // TODO: Map your additional fields here
790
+ };
791
+ }
792
+
793
+ /**
794
+ * Maps an array of entities to response DTOs.
795
+ */
796
+ static toResponseList(entities: ${capitalized}Entity[]): ${capitalized}ResponseDto[] {
797
+ return entities.map((entity) => this.toResponse(entity));
798
+ }
799
+ }
800
+ `;
801
+ };
802
+ var capitalize6 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
803
+
804
+ // src/templates/cleanComponents/presenter.template.ts
805
+ var generatePresenterInterfaceTemplate = (featureName) => {
806
+ const capitalized = capitalize7(featureName);
807
+ const interfaceName = `I${capitalized}Presenter`;
808
+ return `import { ${capitalized}ResponseDto } from "../../dtos/${featureName}.dto";
809
+
810
+ /**
811
+ * ${interfaceName} \u2014 Presenter Port (Application Layer)
812
+ *
813
+ * Defines how a use case result is formatted for the client.
814
+ * The infrastructure layer provides the concrete implementation
815
+ * (e.g., JSON serialization, HTTP envelopes, pagination wrappers).
816
+ */
817
+ export interface ${interfaceName} {
818
+
819
+ /**
820
+ * Formats a single ${featureName} response.
821
+ */
822
+ presentOne(data: ${capitalized}ResponseDto): unknown;
823
+
824
+ /**
825
+ * Formats a list of ${featureName} responses.
826
+ */
827
+ presentMany(data: ${capitalized}ResponseDto[]): unknown;
828
+
829
+ /**
830
+ * Formats a "not found" response.
831
+ */
832
+ presentNotFound(id?: string): unknown;
833
+ }
834
+ `;
835
+ };
836
+ var generatePresenterImplTemplate = (featureName) => {
837
+ const capitalized = capitalize7(featureName);
838
+ const implName = `${capitalized}Presenter`;
839
+ const interfaceName = `I${capitalized}Presenter`;
840
+ return `import { ${interfaceName} } from "../../../application/ports/presenters/${featureName}.presenter.interface";
841
+ import { ${capitalized}ResponseDto } from "../../../application/dtos/${featureName}.dto";
842
+
843
+ /**
844
+ * ${implName} \u2014 Presenter Implementation (Infrastructure Layer)
845
+ *
846
+ * Formats use case output into a client-facing structure.
847
+ * Keeps HTTP/JSON concerns OUT of the domain and application layers.
848
+ */
849
+ export class ${implName} implements ${interfaceName} {
850
+
851
+ presentOne(data: ${capitalized}ResponseDto): unknown {
852
+ return {
853
+ success: true,
854
+ data,
855
+ };
856
+ }
857
+
858
+ presentMany(data: ${capitalized}ResponseDto[]): unknown {
859
+ return {
860
+ success: true,
861
+ count: data.length,
862
+ data,
863
+ };
864
+ }
865
+
866
+ presentNotFound(id?: string): unknown {
867
+ return {
868
+ success: false,
869
+ message: id
870
+ ? \`${capitalized} with id "\${id}" was not found.\`
871
+ : \`${capitalized} not found.\`,
872
+ };
873
+ }
874
+ }
875
+ `;
876
+ };
877
+ var capitalize7 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
878
+
879
+ // src/templates/cleanComponents/router.template.ts
880
+ var resolveRoute = (methodName, featureName) => {
881
+ const lower = methodName.toLowerCase();
882
+ if (lower.includes("findall") || lower.includes("getall")) return { httpMethod: "get", path: `/${featureName}` };
883
+ if (lower.includes("findbyid") || lower.includes("getbyid") || lower.includes("getone")) return { httpMethod: "get", path: `/${featureName}/:id` };
884
+ if (lower.includes("create") || lower.includes("add")) return { httpMethod: "post", path: `/${featureName}` };
885
+ if (lower.includes("update") || lower.includes("edit")) return { httpMethod: "put", path: `/${featureName}/:id` };
886
+ if (lower.includes("delete") || lower.includes("remove")) return { httpMethod: "delete", path: `/${featureName}/:id` };
887
+ return { httpMethod: "get", path: `/${featureName}/${lower}` };
888
+ };
889
+ var generateRouterHandlerTemplate = (featureName, controllerMethods) => {
890
+ const capitalized = capitalize8(featureName);
891
+ const handlerName = `${capitalized}HandlerRouter`;
892
+ const controllerName = `${capitalized}Controller`;
893
+ const routesCode = (controllerMethods || []).map((method) => {
894
+ const { httpMethod, path: path7 } = resolveRoute(method, featureName);
895
+ return `{
896
+ path: \`${path7}\`,
897
+ method: "${httpMethod}",
898
+ middlewares: [],
899
+ handler: async (ctx: ICustomContext) => await ${controllerName}.${method}(ctx.req, ctx.res)
900
+ }`;
901
+ }).join(",\n");
902
+ return `import { OpticoreRouting, ICustomContext, IMultipleRouteDefinition } from "opticore-router";
903
+ import { ${controllerName} } from "../adapters/controllers/${featureName}.controller";
904
+
905
+ export const ${handlerName}: () => IMultipleRouteDefinition = () => {
906
+ return OpticoreRouting.routes(
907
+ ${controllerName},
908
+ [
909
+ ${routesCode.replace(/\n/g, "\n ")}
910
+ ]
911
+ );
912
+ };
913
+ `;
914
+ };
915
+ var generateRouterTemplate = (featureName) => {
916
+ const capitalized = capitalize8(featureName);
917
+ const handlerName = `${capitalized}HandlerRouter`;
918
+ const routerName = `${capitalized}Router`;
919
+ return `import { ${handlerName} } from "./${featureName}.router.handler";
920
+ import { TFeatureRoutes } from "opticore-router";
921
+
922
+ export const ${routerName}: TFeatureRoutes = {
923
+ routes: [
924
+ {
925
+ path: ${handlerName}().path,
926
+ handler: ${handlerName}().handler
927
+ }
928
+ ]
929
+ };
930
+ `;
931
+ };
932
+ var capitalize8 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
933
+
934
+ // src/templates/cleanComponents/event.template.ts
935
+ var generateEventTemplate = (featureName) => {
936
+ const capitalized = capitalize9(featureName);
937
+ return `/**
938
+ * ${capitalized}Event \u2014 Domain Events
939
+ *
940
+ * Domain events capture facts that happened in the domain.
941
+ * They are immutable, named in the past tense, and carry the data
942
+ * that was relevant at the moment the event occurred.
943
+ *
944
+ * Use cases:
945
+ * - Trigger side effects (send email, notify another bounded context)
946
+ * - Feed an event bus / message broker (RabbitMQ, Kafka, etc.)
947
+ * - Build an audit log or event sourcing stream
948
+ */
949
+
950
+ // -------------------------------------------------------------------------
951
+ // Base interface \u2014 all domain events share these fields
952
+ // -------------------------------------------------------------------------
953
+
954
+ export interface IDomainEvent {
955
+ /** Unique event identifier (uuid) */
956
+ readonly eventId: string;
957
+ /** When the event occurred */
958
+ readonly occurredAt: Date;
959
+ /** The aggregate that emitted this event */
960
+ readonly aggregateId: string;
961
+ }
962
+
963
+ // -------------------------------------------------------------------------
964
+ // Concrete events for the "${featureName}" feature
965
+ // -------------------------------------------------------------------------
966
+
967
+ export class ${capitalized}CreatedEvent implements IDomainEvent {
968
+ readonly eventId: string;
969
+ readonly occurredAt: Date;
970
+ readonly aggregateId: string;
971
+
972
+ constructor(
973
+ aggregateId: string,
974
+ // TODO: Add the fields that describe what was created
975
+ ) {
976
+ this.eventId = crypto.randomUUID();
977
+ this.occurredAt = new Date();
978
+ this.aggregateId = aggregateId;
979
+ }
980
+ }
981
+
982
+ export class ${capitalized}UpdatedEvent implements IDomainEvent {
983
+ readonly eventId: string;
984
+ readonly occurredAt: Date;
985
+ readonly aggregateId: string;
986
+
987
+ constructor(
988
+ aggregateId: string,
989
+ // TODO: Add the fields that changed
990
+ ) {
991
+ this.eventId = crypto.randomUUID();
992
+ this.occurredAt = new Date();
993
+ this.aggregateId = aggregateId;
994
+ }
995
+ }
996
+
997
+ export class ${capitalized}DeletedEvent implements IDomainEvent {
998
+ readonly eventId: string;
999
+ readonly occurredAt: Date;
1000
+ readonly aggregateId: string;
1001
+
1002
+ constructor(aggregateId: string) {
1003
+ this.eventId = crypto.randomUUID();
1004
+ this.occurredAt = new Date();
1005
+ this.aggregateId = aggregateId;
1006
+ }
1007
+ }
1008
+ `;
1009
+ };
1010
+ var capitalize9 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
1011
+
1012
+ // src/templates/cleanComponents/exception.template.ts
1013
+ var generateExceptionTemplate = (featureName) => {
1014
+ const capitalized = capitalize10(featureName);
1015
+ return `/**
1016
+ * ${capitalized}Exception \u2014 Domain Exceptions
1017
+ *
1018
+ * Domain exceptions represent violations of business rules specific to "${featureName}".
1019
+ * They are thrown inside the domain or use case layer and caught at the infrastructure
1020
+ * layer (controller) to be translated into appropriate HTTP responses.
1021
+ *
1022
+ * Rule: never import HTTP status codes or Express here \u2014 this is pure domain logic.
1023
+ */
1024
+
1025
+ // -------------------------------------------------------------------------
1026
+ // Base domain exception
1027
+ // -------------------------------------------------------------------------
1028
+
1029
+ export class ${capitalized}DomainException extends Error {
1030
+ /** Machine-readable error code for programmatic handling */
1031
+ readonly code: string;
1032
+
1033
+ constructor(message: string, code: string) {
1034
+ super(message);
1035
+ this.name = "${capitalized}DomainException";
1036
+ this.code = code;
1037
+
1038
+ // Maintains proper prototype chain in TypeScript
1039
+ Object.setPrototypeOf(this, new.target.prototype);
1040
+ }
1041
+ }
1042
+
1043
+ // -------------------------------------------------------------------------
1044
+ // Specific domain exceptions
1045
+ // -------------------------------------------------------------------------
1046
+
1047
+ /**
1048
+ * Thrown when a ${featureName} cannot be found by its identifier.
1049
+ * The controller should translate this to HTTP 404.
1050
+ */
1051
+ export class ${capitalized}NotFoundException extends ${capitalized}DomainException {
1052
+ constructor(id: string) {
1053
+ super(
1054
+ \`${capitalized} with id "\${id}" does not exist.\`,
1055
+ "${featureName.toUpperCase()}_NOT_FOUND",
1056
+ );
1057
+ this.name = "${capitalized}NotFoundException";
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * Thrown when an operation violates a uniqueness constraint.
1063
+ * The controller should translate this to HTTP 409.
1064
+ */
1065
+ export class ${capitalized}AlreadyExistsException extends ${capitalized}DomainException {
1066
+ constructor(field: string, value: string) {
1067
+ super(
1068
+ \`A ${featureName} with \${field} "\${value}" already exists.\`,
1069
+ "${featureName.toUpperCase()}_ALREADY_EXISTS",
1070
+ );
1071
+ this.name = "${capitalized}AlreadyExistsException";
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * Thrown when the provided data violates a business invariant.
1077
+ * The controller should translate this to HTTP 422.
1078
+ */
1079
+ export class ${capitalized}InvalidDataException extends ${capitalized}DomainException {
1080
+ constructor(reason: string) {
1081
+ super(
1082
+ \`Invalid ${featureName} data: \${reason}\`,
1083
+ "${featureName.toUpperCase()}_INVALID_DATA",
1084
+ );
1085
+ this.name = "${capitalized}InvalidDataException";
1086
+ }
1087
+ }
1088
+
1089
+ // -------------------------------------------------------------------------
1090
+ // Exception guard helper
1091
+ // -------------------------------------------------------------------------
1092
+
1093
+ /**
1094
+ * Type guard to check if an unknown error is a ${capitalized}DomainException.
1095
+ * Use this in your controller's catch block to map domain errors to HTTP responses.
1096
+ *
1097
+ * @example
1098
+ * try {
1099
+ * await useCase.create(dto);
1100
+ * } catch (error) {
1101
+ * if (is${capitalized}DomainException(error)) {
1102
+ * switch (error.code) {
1103
+ * case "${featureName.toUpperCase()}_NOT_FOUND": return res.status(404).json(...);
1104
+ * case "${featureName.toUpperCase()}_ALREADY_EXISTS": return res.status(409).json(...);
1105
+ * case "${featureName.toUpperCase()}_INVALID_DATA": return res.status(422).json(...);
1106
+ * }
1107
+ * }
1108
+ * // Unknown error \u2014 rethrow or return 500
1109
+ * throw error;
1110
+ * }
1111
+ */
1112
+ export const is${capitalized}DomainException = (error: unknown): error is ${capitalized}DomainException =>
1113
+ error instanceof ${capitalized}DomainException;
1114
+ `;
1115
+ };
1116
+ var capitalize10 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
1117
+
1118
+ // src/templates/cleanComponents/service.template.ts
1119
+ var generateServiceTemplate = (featureName) => {
1120
+ const capitalized = capitalize11(featureName);
1121
+ return `import { ${capitalized}ResponseDto } from "../../dtos/${featureName}.dto";
1122
+
1123
+ /**
1124
+ * I${capitalized}Service \u2014 Application Service Port
1125
+ *
1126
+ * Application services handle orchestration logic that sits ABOVE a single use case
1127
+ * but BELOW the HTTP layer. They are the right place for:
1128
+ *
1129
+ * - Cross-use-case workflows (e.g. "create + notify + log in one transaction")
1130
+ * - Business rules that span multiple aggregates
1131
+ * - Coordination with external systems (email, payment, 3rd-party APIs)
1132
+ * whose contracts are defined here as abstractions
1133
+ *
1134
+ * The infrastructure layer provides the concrete implementation.
1135
+ * Use cases depend on this interface \u2014 never on the concrete class.
1136
+ */
1137
+ export interface I${capitalized}Service {
1138
+
1139
+ /**
1140
+ * Full creation workflow for a ${featureName}.
1141
+ * May involve validation, persistence, event publishing, notifications, etc.
1142
+ *
1143
+ * @param data - Raw input (already validated by the controller)
1144
+ * @returns The created ${featureName} as a response DTO
1145
+ */
1146
+ handleCreate(data: Record<string, unknown>): Promise<${capitalized}ResponseDto>;
1147
+
1148
+ /**
1149
+ * Full update workflow for a ${featureName}.
1150
+ *
1151
+ * @param id - Identifier of the ${featureName} to update
1152
+ * @param data - Fields to update
1153
+ * @returns The updated ${featureName}, or null if not found
1154
+ */
1155
+ handleUpdate(id: string, data: Record<string, unknown>): Promise<${capitalized}ResponseDto | null>;
1156
+
1157
+ /**
1158
+ * Full deletion workflow for a ${featureName}.
1159
+ * May involve cleanup tasks (remove related files, revoke tokens, etc.)
1160
+ *
1161
+ * @param id - Identifier of the ${featureName} to delete
1162
+ * @returns true if deleted, false if not found
1163
+ */
1164
+ handleDelete(id: string): Promise<boolean>;
1165
+
1166
+ // TODO: Add feature-specific service methods below
1167
+ // Example:
1168
+ // sendWelcomeNotification(id: string): Promise<void>;
1169
+ // exportToCsv(filters: Record<string, unknown>): Promise<Buffer>;
1170
+ }
1171
+ `;
1172
+ };
1173
+ var capitalize11 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
1174
+
1175
+ // src/templates/simpleComponent/model.template.ts
1176
+ var generateModelTemplate = (featureName) => {
1177
+ const capitalized = capitalize12(featureName);
1178
+ const modelName = `${capitalized}Model`;
1179
+ return `/**
1180
+ * ${modelName} \u2014 Data Model
1181
+ * Plain data structure for the "${featureName}" feature.
1182
+ */
1183
+ export class ${modelName} {
1184
+ id: string;
1185
+ createdAt: Date;
1186
+ updatedAt: Date;
1187
+
1188
+ constructor(
1189
+ id: string,
1190
+ // TODO: Add your model properties here
1191
+ createdAt?: Date,
1192
+ updatedAt?: Date,
1193
+ ) {
1194
+ this.id = id;
1195
+ this.createdAt = createdAt ?? new Date();
1196
+ this.updatedAt = updatedAt ?? new Date();
1197
+ }
1198
+ }
1199
+ `;
1200
+ };
1201
+ var capitalize12 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
1202
+
1203
+ // src/templates/simpleComponent/simpleRepository.template.ts
1204
+ var generateSimpleRepositoryTemplate = (featureName) => {
1205
+ const capitalized = capitalize13(featureName);
1206
+ const repoName = `${capitalized}Repository`;
1207
+ const modelName = `${capitalized}Model`;
1208
+ return `import { ${modelName} } from "../models/${featureName}.model";
1209
+
1210
+ export class ${repoName} {
1211
+ private store: Map<string, ${modelName}> = new Map();
1212
+
1213
+ async findAll(): Promise<${modelName}[]> {
1214
+ // TODO: Replace with your ORM query
1215
+ return Array.from(this.store.values());
1216
+ }
1217
+
1218
+ async findById(id: string): Promise<${modelName} | null> {
1219
+ // TODO: Replace with your ORM query
1220
+ return this.store.get(id) ?? null;
1221
+ }
1222
+
1223
+ async create(model: ${modelName}): Promise<${modelName}> {
1224
+ // TODO: Replace with your ORM save
1225
+ this.store.set(model.id, model);
1226
+ return model;
1227
+ }
1228
+
1229
+ async update(model: ${modelName}): Promise<${modelName} | null> {
1230
+ // TODO: Replace with your ORM update
1231
+ if (!this.store.has(model.id)) return null;
1232
+ this.store.set(model.id, model);
1233
+ return model;
1234
+ }
1235
+
1236
+ async delete(id: string): Promise<boolean> {
1237
+ // TODO: Replace with your ORM delete
1238
+ return this.store.delete(id);
1239
+ }
1240
+ }
1241
+ `;
1242
+ };
1243
+ var capitalize13 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
1244
+
1245
+ // src/templates/simpleComponent/simpleService.template.ts
1246
+ var generateSimpleServiceTemplate = (featureName) => {
1247
+ const capitalized = capitalize14(featureName);
1248
+ const serviceName = `${capitalized}Service`;
1249
+ const repoName = `${capitalized}Repository`;
1250
+ const modelName = `${capitalized}Model`;
1251
+ return `import { ${repoName} } from "../repositories/${featureName}.repository";
1252
+ import { ${modelName} } from "../models/${featureName}.model";
1253
+
1254
+ export class ${serviceName} {
1255
+ private readonly repository: ${repoName};
1256
+
1257
+ constructor() {
1258
+ this.repository = new ${repoName}();
1259
+ }
1260
+
1261
+ async findAll(): Promise<${modelName}[]> {
1262
+ return this.repository.findAll();
1263
+ }
1264
+
1265
+ async findById(id: string): Promise<${modelName} | null> {
1266
+ return this.repository.findById(id);
1267
+ }
1268
+
1269
+ async create(data: Record<string, unknown>): Promise<${modelName}> {
1270
+ const model = new ${modelName}(String(Date.now()));
1271
+ // TODO: Map data fields onto model
1272
+ return this.repository.create(model);
1273
+ }
1274
+
1275
+ async update(id: string, data: Record<string, unknown>): Promise<${modelName} | null> {
1276
+ const existing = await this.repository.findById(id);
1277
+ if (!existing) return null;
1278
+ // TODO: Apply data fields onto existing model
1279
+ return this.repository.update(existing);
1280
+ }
1281
+
1282
+ async delete(id: string): Promise<boolean> {
1283
+ return this.repository.delete(id);
1284
+ }
1285
+ }
1286
+ `;
1287
+ };
1288
+ var capitalize14 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
1289
+
1290
+ // src/templates/simpleComponent/simpleController.template.ts
1291
+ var generateSimpleControllerTemplate = (controllerName, featureName, methods) => {
1292
+ const capitalized = capitalize15(featureName);
1293
+ const serviceName = `${capitalized}Service`;
1294
+ const methodsCode = methods.map((method) => generateMethod2(method, capitalized, serviceName)).join("\n");
1295
+ return `import { Request, Response } from "express";
1296
+ import { ResponseHandler, HttpStatusCode, IResponseHandlerSuccessData } from "opticore-http-response";
1297
+ import { ${serviceName} } from "../services/${featureName}.service";
1298
+
1299
+ export class ${controllerName} {
1300
+ private static buildService(): ${serviceName} {
1301
+ return new ${serviceName}();
1302
+ }
1303
+
1304
+ ${methodsCode}
1305
+ private static handleError(error: unknown) {
1306
+ const message = error instanceof Error ? error.message : "Internal server error";
1307
+ return ResponseHandler.error(message, HttpStatusCode.INTERNAL_SERVER_ERROR);
1308
+ }
1309
+ }
1310
+ `;
1311
+ };
1312
+ var generateMethod2 = (method, capitalized, serviceName) => {
1313
+ const lower = method.toLowerCase();
1314
+ if (lower.includes("findall") || lower.includes("getall")) return generateFindAllMethod2(method, capitalized);
1315
+ if (lower.includes("findbyid") || lower.includes("getbyid") || lower.includes("getone")) return generateFindByIdMethod2(method, capitalized);
1316
+ if (lower.includes("create") || lower.includes("add")) return generateCreateMethod2(method, capitalized);
1317
+ if (lower.includes("update") || lower.includes("edit")) return generateUpdateMethod2(method, capitalized);
1318
+ if (lower.includes("delete") || lower.includes("remove")) return generateDeleteMethod2(method, capitalized);
1319
+ return `
1320
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
1321
+ try {
1322
+ const service = ${capitalized}Controller.buildService();
1323
+ // TODO: Implement ${method} logic
1324
+ return ResponseHandler.success({}, "success", HttpStatusCode.OK);
1325
+ } catch (error) {
1326
+ return ${capitalized}Controller.handleError(error);
1327
+ }
1328
+ }
1329
+ `;
1330
+ };
1331
+ var generateFindAllMethod2 = (method, capitalized) => `
1332
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
1333
+ try {
1334
+ const service = ${capitalized}Controller.buildService();
1335
+ const results = await service.findAll();
1336
+ return ResponseHandler.success(results, "success", HttpStatusCode.OK);
1337
+ } catch (error) {
1338
+ return ${capitalized}Controller.handleError(error);
1339
+ }
1340
+ }
1341
+ `;
1342
+ var generateFindByIdMethod2 = (method, capitalized) => `
1343
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
1344
+ try {
1345
+ const { id } = req.params;
1346
+ const service = ${capitalized}Controller.buildService();
1347
+ const result = await service.findById(id);
1348
+ if (!result) {
1349
+ return ResponseHandler.error(\`Not found: \${id}\`, HttpStatusCode.NOT_FOUND);
1350
+ }
1351
+ return ResponseHandler.success(result, "success", HttpStatusCode.OK);
1352
+ } catch (error) {
1353
+ return ${capitalized}Controller.handleError(error);
1354
+ }
1355
+ }
1356
+ `;
1357
+ var generateCreateMethod2 = (method, capitalized) => `
1358
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
1359
+ try {
1360
+ const service = ${capitalized}Controller.buildService();
1361
+ const result = await service.create(req.body);
1362
+ return ResponseHandler.success(result, "created", HttpStatusCode.CREATED);
1363
+ } catch (error) {
1364
+ return ${capitalized}Controller.handleError(error);
1365
+ }
1366
+ }
1367
+ `;
1368
+ var generateUpdateMethod2 = (method, capitalized) => `
1369
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
1370
+ try {
1371
+ const { id } = req.params;
1372
+ const service = ${capitalized}Controller.buildService();
1373
+ const result = await service.update(id, req.body);
1374
+ if (!result) {
1375
+ return ResponseHandler.error(\`Not found: \${id}\`, HttpStatusCode.NOT_FOUND);
1376
+ }
1377
+ return ResponseHandler.success(result, "success", HttpStatusCode.OK);
1378
+ } catch (error) {
1379
+ return ${capitalized}Controller.handleError(error);
1380
+ }
1381
+ }
1382
+ `;
1383
+ var generateDeleteMethod2 = (method, capitalized) => `
1384
+ static async ${method}(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
1385
+ try {
1386
+ const { id } = req.params;
1387
+ const service = ${capitalized}Controller.buildService();
1388
+ const deleted = await service.delete(id);
1389
+ if (!deleted) {
1390
+ return ResponseHandler.error(\`Not found: \${id}\`, HttpStatusCode.NOT_FOUND);
1391
+ }
1392
+ return ResponseHandler.success(null, "deleted", HttpStatusCode.NO_CONTENT);
1393
+ } catch (error) {
1394
+ return ${capitalized}Controller.handleError(error);
1395
+ }
1396
+ }
1397
+ `;
1398
+ var capitalize15 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
1399
+
1400
+ // src/templates/simpleComponent/simpleRouter.template.ts
1401
+ var resolveRoute2 = (methodName, featureName) => {
1402
+ const lower = methodName.toLowerCase();
1403
+ if (lower.includes("findall") || lower.includes("getall")) return { httpMethod: "get", path: `/${featureName}` };
1404
+ if (lower.includes("findbyid") || lower.includes("getbyid") || lower.includes("getone")) return { httpMethod: "get", path: `/${featureName}/:id` };
1405
+ if (lower.includes("create") || lower.includes("add")) return { httpMethod: "post", path: `/${featureName}` };
1406
+ if (lower.includes("update") || lower.includes("edit")) return { httpMethod: "put", path: `/${featureName}/:id` };
1407
+ if (lower.includes("delete") || lower.includes("remove")) return { httpMethod: "delete", path: `/${featureName}/:id` };
1408
+ return { httpMethod: "get", path: `/${featureName}/${lower}` };
1409
+ };
1410
+ var generateSimpleRouterHandlerTemplate = (featureName, controllerMethods) => {
1411
+ const capitalized = capitalize16(featureName);
1412
+ const handlerName = `${capitalized}HandlerRouter`;
1413
+ const controllerName = `${capitalized}Controller`;
1414
+ const routesCode = (controllerMethods || []).map((method) => {
1415
+ const { httpMethod, path: path7 } = resolveRoute2(method, featureName);
1416
+ return `{
1417
+ path: \`${path7}\`,
1418
+ method: "${httpMethod}",
1419
+ middlewares: [],
1420
+ handler: async (ctx: ICustomContext) => await ${controllerName}.${method}(ctx.req, ctx.res)
1421
+ }`;
1422
+ }).join(",\n");
1423
+ return `import { OpticoreRouting, ICustomContext, IMultipleRouteDefinition } from "opticore-router";
1424
+ import { ${controllerName} } from "../controllers/${featureName}.controller";
1425
+
1426
+ export const ${handlerName}: () => IMultipleRouteDefinition = () => {
1427
+ return OpticoreRouting.routes(
1428
+ ${controllerName},
1429
+ [
1430
+ ${routesCode.replace(/\n/g, "\n ")}
1431
+ ]
1432
+ );
1433
+ };
1434
+ `;
1435
+ };
1436
+ var generateSimpleRouterTemplate = (featureName) => {
1437
+ const capitalized = capitalize16(featureName);
1438
+ const handlerName = `${capitalized}HandlerRouter`;
1439
+ const routerName = `${capitalized}Router`;
1440
+ return `import { ${handlerName} } from "./${featureName}.router.handler";
1441
+ import { TFeatureRoutes } from "opticore-router";
1442
+
1443
+ export const ${routerName}: TFeatureRoutes = {
1444
+ routes: [
1445
+ {
1446
+ path: ${handlerName}().path,
1447
+ handler: ${handlerName}().handler
1448
+ }
1449
+ ]
1450
+ };
1451
+ `;
1452
+ };
1453
+ var capitalize16 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
1454
+
1455
+ // src/utils/featureComponentGenerator.utils.ts
1456
+ var FeatureComponentGeneratorUtils = class {
1457
+ static logger = new LoggerCore2();
1458
+ static REGISTER_ROUTER_PATH = path2.join(process.cwd(), "src/app/router/register.router.ts");
1459
+ static structure = "clean";
1460
+ static setStructure(s) {
1461
+ this.structure = s;
1462
+ }
1463
+ /**
1464
+ * Creates the domain entity file.
1465
+ * File: domain/entities/<featureName>.entity.ts
1466
+ */
1467
+ static async createEntity(featureName, entityDir) {
1468
+ const entityName = `${this.capitalize(featureName)}Entity`;
1469
+ const filePath = path2.join(entityDir, `${featureName}.entity.ts`);
1470
+ UFsModule.createFile(filePath, generateEntityTemplate(entityName, featureName));
1471
+ console.log(colors6.green(`\u2705 Entity created: ${filePath}`));
1472
+ }
1473
+ /**
1474
+ * Creates the domain event file.
1475
+ * File: domain/events/<featureName>.event.ts
1476
+ */
1477
+ static async createEvent(featureName, eventDir) {
1478
+ const filePath = path2.join(eventDir, `${featureName}.event.ts`);
1479
+ UFsModule.createFile(filePath, generateEventTemplate(featureName));
1480
+ console.log(colors6.green(`\u2705 Event created: ${filePath}`));
1481
+ }
1482
+ /**
1483
+ * Creates the domain exception file.
1484
+ * File: domain/exceptions/<featureName>.exception.ts
1485
+ */
1486
+ static async createException(featureName, exceptionDir) {
1487
+ const filePath = path2.join(exceptionDir, `${featureName}.exception.ts`);
1488
+ UFsModule.createFile(filePath, generateExceptionTemplate(featureName));
1489
+ console.log(colors6.green(`\u2705 Exception created: ${filePath}`));
1490
+ }
1491
+ /**
1492
+ * Creates the repository port interface.
1493
+ * File: application/ports/repositories/<featureName>.repository.interface.ts
1494
+ */
1495
+ static async createRepositoryInterface(featureName, interfaceDir) {
1496
+ const interfaceName = `I${this.capitalize(featureName)}Repository`;
1497
+ const filePath = path2.join(interfaceDir, `${featureName}.repository.interface.ts`);
1498
+ UFsModule.createFile(filePath, generateRepositoryInterfaceTemplate(interfaceName, featureName));
1499
+ console.log(colors6.green(`\u2705 Repository interface created: ${filePath}`));
1500
+ }
1501
+ /**
1502
+ * Creates the presenter port interface.
1503
+ * File: application/ports/presenters/<featureName>.presenter.interface.ts
1504
+ */
1505
+ static async createPresenterInterface(featureName, presenterInterfaceDir) {
1506
+ const filePath = path2.join(presenterInterfaceDir, `${featureName}.presenter.interface.ts`);
1507
+ UFsModule.createFile(filePath, generatePresenterInterfaceTemplate(featureName));
1508
+ console.log(colors6.green(`\u2705 Presenter interface created: ${filePath}`));
1509
+ }
1510
+ /**
1511
+ * Creates the application service port interface.
1512
+ * File: application/ports/services/<featureName>.service.ts
1513
+ */
1514
+ static async createService(featureName, serviceDir) {
1515
+ const filePath = path2.join(serviceDir, `${featureName}.service.ts`);
1516
+ UFsModule.createFile(filePath, generateServiceTemplate(featureName));
1517
+ console.log(colors6.green(`\u2705 Service interface created: ${filePath}`));
1518
+ }
1519
+ /**
1520
+ * Creates the DTO file (input + output shapes + mapper).
1521
+ * File: application/dtos/<featureName>.dto.ts
1522
+ */
1523
+ static async createDto(featureName, dtoDir) {
1524
+ const filePath = path2.join(dtoDir, `${featureName}.dto.ts`);
1525
+ UFsModule.createFile(filePath, generateDtoTemplate(featureName));
1526
+ console.log(colors6.green(`\u2705 DTO created: ${filePath}`));
1527
+ }
1528
+ /**
1529
+ * Creates the use case file.
1530
+ * File: application/use-cases/<featureName>.usecase.ts
1531
+ */
1532
+ static async createUseCase(featureName, useCaseDir) {
1533
+ const useCaseName = `${this.capitalize(featureName)}UseCase`;
1534
+ const filePath = path2.join(useCaseDir, `${featureName}.usecase.ts`);
1535
+ UFsModule.createFile(filePath, generateUseCaseTemplate(useCaseName, featureName));
1536
+ console.log(colors6.green(`\u2705 UseCase created: ${filePath}`));
1537
+ }
1538
+ // =========================================================================
1539
+ // INFRASTRUCTURE LAYER
1540
+ // =========================================================================
1541
+ /**
1542
+ * Creates the concrete repository implementation.
1543
+ * File: infrastructure/adapters/repositories/<featureName>.repository.ts
1544
+ */
1545
+ static async createRepositoryImpl(featureName, repositoryImplDir) {
1546
+ const interfaceImportPath = `../../../application/ports/repositories/${featureName}.repository.interface`;
1547
+ const filePath = path2.join(repositoryImplDir, `${featureName}.repository.ts`);
1548
+ UFsModule.createFile(filePath, generateRepositoryImplTemplate(featureName, interfaceImportPath));
1549
+ console.log(colors6.green(`\u2705 Repository implementation created: ${filePath}`));
1550
+ }
1551
+ /**
1552
+ * Creates the concrete presenter implementation.
1553
+ * File: infrastructure/adapters/presenters/<featureName>.presenter.ts
1554
+ */
1555
+ static async createPresenterImpl(featureName, presenterImplDir) {
1556
+ const filePath = path2.join(presenterImplDir, `${featureName}.presenter.ts`);
1557
+ UFsModule.createFile(filePath, generatePresenterImplTemplate(featureName));
1558
+ console.log(colors6.green(`\u2705 Presenter implementation created: ${filePath}`));
1559
+ }
1560
+ /**
1561
+ * Creates the HTTP controller.
1562
+ * Prompts the user for method names, then generates the file.
1563
+ * File: infrastructure/adapters/controllers/<featureName>.controller.ts
1564
+ *
1565
+ * @returns The controller class name and the list of chosen methods.
1566
+ */
1567
+ static async createController(featureName, controllerDir) {
1568
+ const controllerName = `${this.capitalize(featureName)}Controller`;
1569
+ const filePath = path2.join(controllerDir, `${featureName}.controller.ts`);
1570
+ const wantsMethods = await UPrompt.confirm("Do you want to add methods to the controller?");
1571
+ let methods = [];
1572
+ let content;
1573
+ if (wantsMethods) {
1574
+ const methodsText = await UPrompt.text(
1575
+ "Enter the method names (comma separated):",
1576
+ "create, findAll, findById, update, delete",
1577
+ (v) => v ? void 0 : "You must enter at least one method."
1578
+ );
1579
+ methods = methodsText.split(",").map((m) => m.trim()).filter(Boolean);
1580
+ content = generateControllerTemplate(controllerName, featureName, methods);
1581
+ } else {
1582
+ content = `import { Request, Response } from "express";
1583
+
1584
+ export class ${controllerName} {}
1585
+ `;
1586
+ }
1587
+ UFsModule.createFile(filePath, content);
1588
+ console.log(colors6.green(`\u2705 Controller created: ${filePath}`));
1589
+ return { controllerName, methods };
1590
+ }
1591
+ /**
1592
+ * Creates the data model file (simple component).
1593
+ * File: models/<featureName>.model.ts
1594
+ */
1595
+ static async createModel(featureName, modelDir) {
1596
+ const filePath = path2.join(modelDir, `${featureName}.model.ts`);
1597
+ UFsModule.createFile(filePath, generateModelTemplate(featureName));
1598
+ console.log(colors6.green(`\u2705 Model created: ${filePath}`));
1599
+ }
1600
+ /**
1601
+ * Creates the simple repository (no interface, uses the model directly).
1602
+ * File: repositories/<featureName>.repository.ts
1603
+ */
1604
+ static async createSimpleRepository(featureName, repositoryDir) {
1605
+ const filePath = path2.join(repositoryDir, `${featureName}.repository.ts`);
1606
+ UFsModule.createFile(filePath, generateSimpleRepositoryTemplate(featureName));
1607
+ console.log(colors6.green(`\u2705 Repository created: ${filePath}`));
1608
+ }
1609
+ /**
1610
+ * Creates the simple concrete service (no interface, wires the repository).
1611
+ * File: services/<featureName>.service.ts
1612
+ */
1613
+ static async createSimpleService(featureName, serviceDir) {
1614
+ const filePath = path2.join(serviceDir, `${featureName}.service.ts`);
1615
+ UFsModule.createFile(filePath, generateSimpleServiceTemplate(featureName));
1616
+ console.log(colors6.green(`\u2705 Service created: ${filePath}`));
1617
+ }
1618
+ /**
1619
+ * Creates the simple controller (delegates to service, no use-case/presenter).
1620
+ * File: controllers/<featureName>.controller.ts
1621
+ */
1622
+ static async createSimpleController(featureName, controllerDir) {
1623
+ const controllerName = `${this.capitalize(featureName)}Controller`;
1624
+ const filePath = path2.join(controllerDir, `${featureName}.controller.ts`);
1625
+ const wantsMethods = await UPrompt.confirm("Do you want to add methods to the controller?");
1626
+ let methods = [];
1627
+ let content;
1628
+ if (wantsMethods) {
1629
+ const methodsText = await UPrompt.text(
1630
+ "Enter the method names (comma separated):",
1631
+ "create, findAll, findById, update, delete",
1632
+ (v) => v ? void 0 : "You must enter at least one method."
1633
+ );
1634
+ methods = methodsText.split(",").map((m) => m.trim()).filter(Boolean);
1635
+ content = generateSimpleControllerTemplate(controllerName, featureName, methods);
1636
+ } else {
1637
+ content = `import { Request, Response } from "express";
1638
+
1639
+ export class ${controllerName} {}
1640
+ `;
1641
+ }
1642
+ UFsModule.createFile(filePath, content);
1643
+ console.log(colors6.green(`\u2705 Controller created: ${filePath}`));
1644
+ return { controllerName, methods };
1645
+ }
1646
+ /**
1647
+ * Creates the route handler (HTTP verb + path → controller method bindings).
1648
+ * File: infrastructure/routes/<featureName>.router.handler.ts (clean)
1649
+ * routes/<featureName>.router.handler.ts (simple)
1650
+ */
1651
+ static async createRouterHandler(featureName, routerHandlerDir, controllerMethods) {
1652
+ const filePath = path2.join(routerHandlerDir, `${featureName}.router.handler.ts`);
1653
+ const content = this.structure === "simple" ? generateSimpleRouterHandlerTemplate(featureName, controllerMethods) : generateRouterHandlerTemplate(featureName, controllerMethods);
1654
+ UFsModule.createFile(filePath, content);
1655
+ console.log(colors6.green(`\u2705 Router Handler created: ${filePath}`));
1656
+ }
1657
+ /**
1658
+ * Creates the feature router and registers it in register.router.ts.
1659
+ * File: infrastructure/routes/<featureName>.router.ts (clean)
1660
+ * routes/<featureName>.router.ts (simple)
1661
+ */
1662
+ static async createRouter(featureName, routerDir) {
1663
+ const filePath = path2.join(routerDir, `${featureName}.router.ts`);
1664
+ const content = this.structure === "simple" ? generateSimpleRouterTemplate(featureName) : generateRouterTemplate(featureName);
1665
+ UFsModule.createFile(filePath, content);
1666
+ await this.updateRegisterRouter(featureName);
1667
+ console.log(colors6.green(`\u2705 Router created: ${filePath}`));
1668
+ }
1669
+ static capitalize(str) {
1670
+ return str.charAt(0).toUpperCase() + str.slice(1);
1671
+ }
1672
+ /**
1673
+ * Appends an import and registers the new router in register.router.ts.
1674
+ */
1675
+ static async updateRegisterRouter(featureName) {
1676
+ const routerName = `${this.capitalize(featureName)}Router`;
1677
+ const routerPath = this.structure === "simple" ? `../../features/${featureName}/routes/${featureName}.router` : `../../features/${featureName}/infrastructure/routes/${featureName}.router`;
1678
+ const importStatement = `import { ${routerName} } from "${routerPath}"`;
1679
+ try {
1680
+ if (!fs3.existsSync(this.REGISTER_ROUTER_PATH)) {
1681
+ console.log(
1682
+ colors6.yellow(`\u26A0\uFE0F register.router.ts not found at ${this.REGISTER_ROUTER_PATH}, skipping auto-registration.`)
1683
+ );
1684
+ return;
1685
+ }
1686
+ let content = fs3.readFileSync(this.REGISTER_ROUTER_PATH, "utf-8");
1687
+ if (content.includes(`import { ${routerName} } from`)) {
1688
+ console.log(colors6.cyan(`\u2139\uFE0F ${routerName} is already registered.`));
1689
+ return;
1690
+ }
1691
+ const lines = content.split("\n");
1692
+ let lastImportIndex = -1;
1693
+ for (let i = 0; i < lines.length; i++) {
1694
+ if (lines[i].trim().startsWith("import ")) lastImportIndex = i;
1695
+ }
1696
+ if (lastImportIndex !== -1) {
1697
+ lines.splice(lastImportIndex + 1, 0, importStatement);
1698
+ content = lines.join("\n");
1699
+ } else {
1700
+ content = importStatement + "\n" + content;
1701
+ }
1702
+ const ARRAY_OPEN = "registered([";
1703
+ const ARRAY_CLOSE = "]);";
1704
+ const startIdx = content.indexOf(ARRAY_OPEN);
1705
+ if (startIdx === -1) {
1706
+ console.log(colors6.yellow(`\u26A0\uFE0F Could not find 'registered([' in register.router.ts.`));
1707
+ return;
1708
+ }
1709
+ const endIdx = content.indexOf(ARRAY_CLOSE, startIdx + ARRAY_OPEN.length);
1710
+ if (endIdx === -1) {
1711
+ console.log(colors6.yellow(`\u26A0\uFE0F Could not find matching ']);' in register.router.ts.`));
1712
+ return;
1713
+ }
1714
+ const arrayContent = content.substring(startIdx + ARRAY_OPEN.length, endIdx);
1715
+ const existingRoutes = arrayContent.split("\n").map((l) => l.trim().replace(/,+$/, "")).filter(Boolean);
1716
+ if (!existingRoutes.includes(routerName)) {
1717
+ existingRoutes.push(routerName);
1718
+ }
1719
+ const indentedRoutes = existingRoutes.map((r) => ` ${r},`).join("\n");
1720
+ const newContent = content.substring(0, startIdx + ARRAY_OPEN.length) + `
1721
+ ${indentedRoutes}
1722
+ ` + content.substring(endIdx);
1723
+ fs3.writeFileSync(this.REGISTER_ROUTER_PATH, newContent, "utf-8");
1724
+ console.log(colors6.green(`\u2705 register.router.ts updated with ${routerName}.`));
1725
+ } catch (error) {
1726
+ console.error(colors6.red(`\u274C Failed to update register.router.ts: ${error.message}`));
1727
+ }
1728
+ }
1729
+ };
1730
+
1731
+ // src/templates/simpleComponent/simpleComponent.template.ts
1732
+ var simpleComponentTemplate = async (featureName) => {
1733
+ FeatureComponentGeneratorUtils.setStructure("simple");
1734
+ const basePath = path3.join("src", "features", featureName);
1735
+ const dirs = [
1736
+ path3.join(basePath, "routes"),
1737
+ path3.join(basePath, "controllers"),
1738
+ path3.join(basePath, "services"),
1739
+ path3.join(basePath, "repositories"),
1740
+ path3.join(basePath, "models")
1741
+ ];
1742
+ dirs.forEach((dir) => UFsModule.createDirectoryRecursively(dir));
1743
+ const paths = {
1744
+ modelsDir: path3.join(basePath, "models"),
1745
+ repositoriesDir: path3.join(basePath, "repositories"),
1746
+ servicesDir: path3.join(basePath, "services"),
1747
+ controllersDir: path3.join(basePath, "controllers"),
1748
+ routesDir: path3.join(basePath, "routes")
1749
+ };
1750
+ await FeatureComponentGeneratorUtils.createModel(featureName, paths.modelsDir);
1751
+ await FeatureComponentGeneratorUtils.createSimpleRepository(featureName, paths.repositoriesDir);
1752
+ await FeatureComponentGeneratorUtils.createSimpleService(featureName, paths.servicesDir);
1753
+ const controller = await FeatureComponentGeneratorUtils.createSimpleController(
1754
+ featureName,
1755
+ paths.controllersDir
1756
+ );
1757
+ await FeatureComponentGeneratorUtils.createRouterHandler(featureName, paths.routesDir, controller.methods);
1758
+ await FeatureComponentGeneratorUtils.createRouter(featureName, paths.routesDir);
1759
+ console.log(
1760
+ colors7.bgGreen(
1761
+ colors7.white(`
1762
+ \u{1F389} Feature "${featureName}" scaffolded with Simple Component!
1763
+ `)
1764
+ )
1765
+ );
1766
+ };
1767
+
1768
+ // src/templates/fullCleanComponent/fullCleanComponent.template.ts
1769
+ import path4 from "path";
1770
+ import colors8 from "ansi-colors";
1771
+ var fullCleanComponentTemplate = async (featureName) => {
1772
+ const basePath = path4.join("src", "features", featureName);
1773
+ const dirs = [
1774
+ // Domain
1775
+ path4.join(basePath, "domain", "entities"),
1776
+ path4.join(basePath, "domain", "events"),
1777
+ path4.join(basePath, "domain", "exceptions"),
1778
+ // Application
1779
+ path4.join(basePath, "application", "use-cases"),
1780
+ path4.join(basePath, "application", "dtos"),
1781
+ path4.join(basePath, "application", "ports", "repositories"),
1782
+ path4.join(basePath, "application", "ports", "presenters"),
1783
+ path4.join(basePath, "application", "ports", "services"),
1784
+ // Infrastructure
1785
+ path4.join(basePath, "infrastructure", "adapters", "controllers"),
1786
+ path4.join(basePath, "infrastructure", "adapters", "repositories"),
1787
+ path4.join(basePath, "infrastructure", "adapters", "presenters"),
1788
+ path4.join(basePath, "infrastructure", "routes")
1789
+ ];
1790
+ dirs.forEach((dir) => UFsModule.createDirectoryRecursively(dir));
1791
+ const paths = {
1792
+ // Domain
1793
+ entityDir: path4.join(basePath, "domain", "entities"),
1794
+ eventDir: path4.join(basePath, "domain", "events"),
1795
+ exceptionDir: path4.join(basePath, "domain", "exceptions"),
1796
+ // Application
1797
+ repositoryInterfaceDir: path4.join(basePath, "application", "ports", "repositories"),
1798
+ presenterInterfaceDir: path4.join(basePath, "application", "ports", "presenters"),
1799
+ serviceDir: path4.join(basePath, "application", "ports", "services"),
1800
+ dtoDir: path4.join(basePath, "application", "dtos"),
1801
+ useCaseDir: path4.join(basePath, "application", "use-cases"),
1802
+ // Infrastructure
1803
+ repositoryImplDir: path4.join(basePath, "infrastructure", "adapters", "repositories"),
1804
+ presenterImplDir: path4.join(basePath, "infrastructure", "adapters", "presenters"),
1805
+ controllerDir: path4.join(basePath, "infrastructure", "adapters", "controllers"),
1806
+ routerDir: path4.join(basePath, "infrastructure", "routes")
1807
+ };
1808
+ await FeatureComponentGeneratorUtils.createEntity(featureName, paths.entityDir);
1809
+ await FeatureComponentGeneratorUtils.createEvent(featureName, paths.eventDir);
1810
+ await FeatureComponentGeneratorUtils.createException(featureName, paths.exceptionDir);
1811
+ await FeatureComponentGeneratorUtils.createRepositoryInterface(featureName, paths.repositoryInterfaceDir);
1812
+ await FeatureComponentGeneratorUtils.createPresenterInterface(featureName, paths.presenterInterfaceDir);
1813
+ await FeatureComponentGeneratorUtils.createService(featureName, paths.serviceDir);
1814
+ await FeatureComponentGeneratorUtils.createDto(featureName, paths.dtoDir);
1815
+ await FeatureComponentGeneratorUtils.createUseCase(featureName, paths.useCaseDir);
1816
+ await FeatureComponentGeneratorUtils.createRepositoryImpl(featureName, paths.repositoryImplDir);
1817
+ await FeatureComponentGeneratorUtils.createPresenterImpl(featureName, paths.presenterImplDir);
1818
+ const controller = await FeatureComponentGeneratorUtils.createController(
1819
+ featureName,
1820
+ paths.controllerDir
1821
+ );
1822
+ await FeatureComponentGeneratorUtils.createRouterHandler(featureName, paths.routerDir, controller.methods);
1823
+ await FeatureComponentGeneratorUtils.createRouter(featureName, paths.routerDir);
1824
+ console.log(
1825
+ colors8.bgGreen(
1826
+ colors8.white(`
1827
+ \u{1F389} Feature "${featureName}" scaffolded with Clean Architecture!
1828
+ `)
1829
+ )
1830
+ );
1831
+ };
1832
+
1833
+ // src/templates/cleanComponentByStep/cleanComponentByStep.template.ts
1834
+ import path5 from "path";
1835
+ import colors9 from "ansi-colors";
1836
+ import { isCancel as isCancel2 } from "@clack/prompts";
1837
+ var cleanComponentByStepTemplate = async (featureName) => {
1838
+ const base = path5.join("src", "features", featureName);
1839
+ let created = 0;
1840
+ const ask = async (label) => {
1841
+ const answer = await UPrompt.confirm(label, false);
1842
+ if (isCancel2(answer)) {
1843
+ UFsModule.removeDir(base, true, true);
1844
+ process.exit(0);
1845
+ }
1846
+ return answer;
1847
+ };
1848
+ const write = (dir, filename) => {
1849
+ UFsModule.createDirectoryRecursively(dir);
1850
+ const filePath = path5.join(dir, filename);
1851
+ UFsModule.createFile(filePath, "");
1852
+ console.log(colors9.green(`\u2705 Created: ${filePath}`));
1853
+ created++;
1854
+ };
1855
+ console.log(colors9.bold(colors9.cyan("\n\u2500\u2500 Domain \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")));
1856
+ if (await ask(`Entity \u2192 ${featureName}.entity.ts`))
1857
+ write(path5.join(base, "domain", "entities"), `${featureName}.entity.ts`);
1858
+ if (await ask(`Event \u2192 ${featureName}.event.ts`))
1859
+ write(path5.join(base, "domain", "events"), `${featureName}.event.ts`);
1860
+ if (await ask(`Exception \u2192 ${featureName}.exception.ts`))
1861
+ write(path5.join(base, "domain", "exceptions"), `${featureName}.exception.ts`);
1862
+ console.log(colors9.bold(colors9.cyan("\n\u2500\u2500 Application \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")));
1863
+ if (await ask(`Repo Interface \u2192 ${featureName}.repository.interface.ts`))
1864
+ write(path5.join(base, "application", "ports", "repositories"), `${featureName}.repository.interface.ts`);
1865
+ if (await ask(`Presenter Port \u2192 ${featureName}.presenter.interface.ts`))
1866
+ write(path5.join(base, "application", "ports", "presenters"), `${featureName}.presenter.interface.ts`);
1867
+ if (await ask(`Service Port \u2192 ${featureName}.service.ts`))
1868
+ write(path5.join(base, "application", "ports", "services"), `${featureName}.service.ts`);
1869
+ if (await ask(`DTO \u2192 ${featureName}.dto.ts`))
1870
+ write(path5.join(base, "application", "dtos"), `${featureName}.dto.ts`);
1871
+ if (await ask(`Use Case \u2192 ${featureName}.usecase.ts`))
1872
+ write(path5.join(base, "application", "use-cases"), `${featureName}.usecase.ts`);
1873
+ console.log(colors9.bold(colors9.cyan("\n\u2500\u2500 Infrastructure \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")));
1874
+ if (await ask(`Repo Impl \u2192 ${featureName}.repository.ts`))
1875
+ write(path5.join(base, "infrastructure", "adapters", "repositories"), `${featureName}.repository.ts`);
1876
+ if (await ask(`Presenter Impl \u2192 ${featureName}.presenter.ts`))
1877
+ write(path5.join(base, "infrastructure", "adapters", "presenters"), `${featureName}.presenter.ts`);
1878
+ if (await ask(`Controller \u2192 ${featureName}.controller.ts`))
1879
+ write(path5.join(base, "infrastructure", "adapters", "controllers"), `${featureName}.controller.ts`);
1880
+ if (await ask(`Router Handler \u2192 ${featureName}.router.handler.ts`))
1881
+ write(path5.join(base, "infrastructure", "routes"), `${featureName}.router.handler.ts`);
1882
+ if (await ask(`Router \u2192 ${featureName}.router.ts`))
1883
+ write(path5.join(base, "infrastructure", "routes"), `${featureName}.router.ts`);
1884
+ if (created === 0) {
1885
+ console.log(colors9.yellow(`
1886
+ \u26A0\uFE0F No files were created for feature "${featureName}".`));
1887
+ } else {
1888
+ console.log(
1889
+ colors9.bgGreen(colors9.white(`
1890
+ \u{1F389} Feature "${featureName}" \u2014 ${created} file(s) created step by step.
1891
+ `))
1892
+ );
1893
+ }
1894
+ };
1895
+
1896
+ // src/core/components/application/createController.application.ts
1897
+ var createComponentApplication = async (featureDirectory, featureName) => {
1898
+ const askComponentType = await _controllerType(
1899
+ "Choose a type of component :",
1900
+ CCleanType.full_clean_component,
1901
+ [
1902
+ { label: "Simple component", value: [CCleanType.simple_component] },
1903
+ { label: "CLEAN Architecture component by step", value: [CCleanType.clean_component_by_step] },
1904
+ { label: "Full CLEAN Architecture component", value: [CCleanType.full_clean_component] }
1905
+ ]
1906
+ );
1907
+ if (isCancel3(askComponentType) === true || askComponentType === void 0) {
1908
+ BaseService._cancelOperation(featureDirectory);
1909
+ } else {
1910
+ switch (askComponentType) {
1911
+ case CCleanType.simple_component:
1912
+ await simpleComponentTemplate(featureName);
1913
+ break;
1914
+ case CCleanType.clean_component_by_step:
1915
+ await cleanComponentByStepTemplate(featureName);
1916
+ break;
1917
+ case CCleanType.full_clean_component:
1918
+ await fullCleanComponentTemplate(featureName);
1919
+ break;
1920
+ }
1921
+ }
1922
+ };
1923
+
1924
+ // src/core/core.ts
1925
+ (async () => {
1926
+ UWelcomeMessage();
1927
+ const featureChoice = await choiceFeatureCleanModule();
1928
+ featureChoice !== "opticore_clean_module" || featureChoice === void 0 ? customFeatureInfo() : UCustomFeatureInfo(featureChoice);
1929
+ const featureName = await _featureName();
1930
+ const directory = path6.join(process4.cwd(), "src", "features");
1931
+ const featureDirectory = path6.join(directory, featureName.toString());
1932
+ const featureCleanModule = BaseService._isSubdirectoryExists(featureName);
1933
+ if (featureCleanModule) {
1934
+ BaseService._featureFounded(featureName);
1935
+ }
1936
+ if (isCancel4(featureName)) {
1937
+ BaseService._cancelOperation(featureDirectory);
1938
+ } else {
1939
+ try {
1940
+ if (fs4.existsSync(directory)) {
1941
+ await createComponentApplication(featureDirectory, featureName);
1942
+ } else {
1943
+ BaseService._checkFeatureDir();
1944
+ }
1945
+ } catch (err) {
1946
+ console.error(err.message);
1947
+ process4.exit();
1948
+ }
1949
+ }
1950
+ })();