├── .gitignore ├── .npmignore ├── .npmrc ├── README.md ├── bin └── scaffold.js ├── lib ├── commands │ ├── CommandCreateAdapter.ts │ ├── CommandCreateAdapterSimple.ts │ ├── CommandCreateController.ts │ ├── CommandCreateEntity.ts │ ├── CommandCreateInterface.ts │ ├── CommandCreateInterfaceResource.ts │ ├── CommandCreateService.ts │ ├── CommandCreateServiceResource.ts │ ├── CommandInit.ts │ └── CommandUtils.ts ├── contracts │ └── SingletonInterace.ts ├── index.ts ├── templates │ ├── AdaptersTemplate.ts │ ├── DatabaseTemplate.ts │ ├── ModelsTemplate.ts │ ├── ProjectInitTemplate.ts │ └── SingletonGenerateTemplate.ts ├── types │ └── SingletonTypes.ts └── utils │ ├── constants.ts │ ├── emojis.ts │ ├── helpers.ts │ ├── messages.ts │ └── paths.ts ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .idea/* 107 | .DS_Store 108 | 109 | data 110 | package-lock.json 111 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | assets/ 3 | .idea/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture Scaffold 2 | 3 | This CLI creates the structure of a NodeJs and TypeScript project based on clean architecture to build REST full APIs, it comes with the initial configuration of an Express application as a NodeJs framework and this is located in the **`application layer`**. 4 | 5 | - [Clean Architecture Scaffold](#clean-architecture-scaffold) 6 | - [Implementation of the plugin](#Implementation-of-the-plugin) 7 | - [Tasks](#tasks) 8 | - [Project Generation](#project-generation) 9 | - [Model Generation](#model-generation) 10 | - [Interface Generation](#interface-generation) 11 | - [Interface Resource Generation](#interface-resource-generation) 12 | - [Service Generation](#service-generation) 13 | - [Service Resource Generation](#service-resource-generation) 14 | - [Adapter ORM Generation](#adapter-orm-generation) 15 | - [Singleton Instances](#singleton-instances) 16 | - [Adapter Simple Generation](#adapter-simple-generation) 17 | - [Controller Generate](#controller-generation) 18 | - [Decorators](#decorators) 19 | - [Example of use case](#example-of-use-case) 20 | 21 | # Implementation of the plugin 22 | 23 | We install the plugin globally in our computer, to be able to access the commands that generate the tasks. 24 | the tasks. 25 | 26 | ```shell 27 | npm i -g @tsclean/scaffold 28 | ``` 29 | 30 | # Tasks 31 | 32 | ## Project Generation 33 | 34 | 1. We generate the project structure with the command **`scaffold create:project`**, which receives one parameter **`--name`**. 35 | 36 | ```shell 37 | scaffold create:project --name=[project name] 38 | ``` 39 | 40 | ```shell 41 | cd project name 42 | ``` 43 | 44 | ## Model Generation 45 | 46 | 1. The **`scaffold create:entity`** command will generate a model in the **`domain layer [models]`**, this task has **`--name`** as parameter and this is required. 47 | The name must have a middle hyphen in case it is compound. 48 | 49 | Example: **`--name=user`** 50 | 51 | ```shell 52 | scaffold create:entity --name=user 53 | ``` 54 | 55 | ## Interface Generation 56 | 57 | 1. The **`scaffold create:interface`** command generates an interface, the location of the file is according to the 58 | component where it is required. where it is required. The name must have a hyphen in case it is a compound name. 59 | 60 | Example: **`--name=user, --name=user-detail, --name=post-comments-user.`** 61 | 62 | ```shell 63 | scaffold create:interface --name=user-detail --path=entities 64 | ``` 65 | 66 | ```shell 67 | scaffold create:interface --name=user-detail --path=service 68 | ``` 69 | 70 | ```shell 71 | scaffold create:interface --name=user-detail --path=infra 72 | ``` 73 | 74 | ## Interface Resource Generation 75 | 76 | 1. The command **`scaffold create:interface-resource`** generates an interface, this task has **`--name`** and **`--resource`** as parameters this is required. 77 | The name must be in lower case, as it is in the model. 78 | 79 | Note: It is recommended to create resource type interfaces when an entity is expected to be used in the contract implementation. 80 | 81 | Example: **`--name=user`** 82 | 83 | ```shell 84 | scaffold create:interface-resource --name=user --resource 85 | ``` 86 | 87 | ## Service Generation 88 | 89 | 1. The **`scaffold create:service`** command will generate the interface and the service that implements it in the **`domain layer [use-cases]`**, this task has **`--name`** as parameter and this is required. The name must be hyphenated if it is a compound name. 90 | 91 | Example: **`--name=user, --name=user-detail, --name=post-comments-user.`** 92 | 93 | ```shell 94 | scaffold create:service --name=user 95 | ``` 96 | 97 | ## Service Resource Generation 98 | 99 | 1. The **`scaffold create:service-resource`** command will generate the interface and the service that implements it in the **`domain layer [use-cases]`**, 100 | this task has **`--name`** as parameter and **`--resource`** this is required. The name must be in lower case, as it is in the model. 101 | 102 | Example: **`--name=user --resource`** 103 | 104 | ```shell 105 | scaffold create:service-resource --name=user --resource 106 | ``` 107 | 108 | ## Adapter ORM Generation 109 | 110 | 1. The **`scaffold create:adapter-orm`** command will generate an adapter in the **`infrastructure layer`**, 111 | this task has **`--name`** and **`--orm`** as parameters this is required. The name of the **`--manager`** parameter corresponds to the database manager. 112 | After the adapter is generated, the provider must be included in the app.ts file and then the name of the provider in the corresponding service must be passed through the constructor. 113 | 114 | Example: **`--name=user --orm=sequelize --manager=mysql`** 115 | 116 | 2. By convention the plugin handles names in singular, this helps to create additional code that benefits each component. 117 | In this case when you create the adapter with the name that matches the entity in the domain models folder, it does the automatic import in all the component of the adapter. 118 | 119 | - command to generate a `mysql' adapter with sequelize orm. 120 | 121 | ```shell 122 | scaffold create:adapter-orm --name=user --orm=sequelize --manager=mysql 123 | ``` 124 | 125 | - command to generate a postgres adapter with sequelize orm 126 | 127 | ```shell 128 | scaffold create:adapter-orm --name=user --orm=sequelize --manager=pg 129 | ``` 130 | 131 | - command to generate the mongoose orm. 132 | 133 | ```shell 134 | scaffold create:adapter-orm --name=user --orm=mongo --manager=mongoose 135 | ``` 136 | 137 | ## Singleton Instances 138 | 139 | In this version we add the management of the connections to the databases by means of the Singleton Pattern to create the instances, this was done in the `index.ts` file. These instances are added in the `singletonInitializers` array when the database connection adapter is created. 140 | 141 | Example: When the adapter is created for postgres, the Singleton class is generated to make the connection to the database and the function is added to initialize it. 142 | 143 | ```typescript 144 | // src/application/config/pg-instance.ts 145 | import { Sequelize } from 'sequelize-typescript' 146 | import { Logger } from '@tsclean/core' 147 | import { CONFIG_PG } from '@/application/config/environment' 148 | import { UserModelPg } from '@/infrastructure/driven-adapters/adapters/orm/sequelize/models/user-pg' 149 | 150 | /** 151 | * Class that generates a connection instance for Pg using the Singleton pattern 152 | */ 153 | export class PgConfiguration { 154 | /** Private logger instance for logging purposes */ 155 | private logger: Logger 156 | 157 | /** Private static instance variable to implement the Singleton pattern */ 158 | private static instance: PgConfiguration 159 | 160 | /** Sequelize instance for managing the PostgreSQL database connection */ 161 | public sequelize: Sequelize 162 | 163 | /** Private constructor to ensure that only one instance is created */ 164 | private constructor() { 165 | /** Initialize the logger with the class name */ 166 | this.logger = new Logger(PgConfiguration.name) 167 | 168 | /** Create a new Sequelize instance with the provided configuration */ 169 | this.sequelize = new Sequelize( 170 | CONFIG_PG.database, 171 | CONFIG_PG.user, 172 | CONFIG_PG.password, 173 | { 174 | host: CONFIG_PG.host, 175 | dialect: 'postgres', 176 | /** This array contains all the system models that are used for Pg. */ 177 | models: [UserModelPg] 178 | } 179 | ) 180 | } 181 | 182 | /** Method to get the instance of the class, following the Singleton pattern */ 183 | public static getInstance(): PgConfiguration { 184 | if (!PgConfiguration.instance) { 185 | PgConfiguration.instance = new PgConfiguration() 186 | } 187 | return PgConfiguration.instance 188 | } 189 | 190 | /** Asynchronous method to manage the PostgreSQL database connection */ 191 | public async managerConnectionPg(): Promise { 192 | try { 193 | /** Attempt to authenticate the connection to the database */ 194 | await this.sequelize.authenticate() 195 | /** Log a success message if the authentication is successful */ 196 | this.logger.log( 197 | `Connection successfully to database of Pg: ${CONFIG_PG.database}` 198 | ) 199 | } catch (error) { 200 | /** Log an error message if the authentication fails */ 201 | this.logger.error('Failed to connect to Pg', error) 202 | } 203 | } 204 | } 205 | ``` 206 | 207 | Note: Here you can declare all the Singleton instances that the application uses. 208 | 209 | ```typescript 210 | // src/application/singleton.ts 211 | import { PgConfiguration } from '@/application/config/pg-instance' 212 | 213 | /** 214 | * This array has all the singleton instances of the application 215 | */ 216 | export const singletonInitializers: Array<() => Promise> = [ 217 | async () => { 218 | const pgConfig = PgConfiguration.getInstance() 219 | await pgConfig.managerConnectionPg() 220 | } 221 | ] 222 | ``` 223 | 224 | ## Adapter Simple Generation 225 | 226 | 1. The **`scaffold create:adapter`** command will generate an adapter simple in the **`infrastructure layer`**, 227 | this task has **`--name`** as parameter and this is required. 228 | 229 | ```shell 230 | scaffold create:adapter --name=jwt 231 | ``` 232 | 233 | ## Controller Generation 234 | 235 | 1. The **`scaffold create:controller`** command will generate a controller in the **`infrastructure layer`**, 236 | this task has **`--name`** as parameter and this is required. The name must have a hyphen in case it is a compound name. 237 | 238 | Example: **`--name=user, --name=user-detail, --name=post-comments-user.`** 239 | 240 | ```shell 241 | scaffold create:controller --name=user-detail 242 | ``` 243 | 244 | ## Decorators 245 | 246 | Decorators allow us to add annotations and metadata or change the behavior of classes, properties, methods, parameters and accessors. 247 | 248 | `@Services` Decorator to inject the logic of this class as a service. 249 | 250 | ```typescript 251 | @Services 252 | export class UserServiceImpl {} 253 | ``` 254 | 255 | `@Adapter` Decorator to keep the reference of an interface and to be able to apply the SOLID principle of Dependency Inversion. 256 | 257 | ```typescript 258 | // Constant to have the interface reference. 259 | export const USER_REPOSITORY = 'USER_REPOSITORY' 260 | 261 | export interface IUserRepository { 262 | save: (data: T) => Promise 263 | } 264 | 265 | @service 266 | export class UserServiceImpl { 267 | constructor( 268 | @Adapter(USER_REPOSITORY) 269 | private readonly userRespository: IUserRepository 270 | ) {} 271 | } 272 | ``` 273 | 274 | `@Mapping` Decorator that allows us to create the path of an end point. 275 | 276 | ```typescript 277 | @Mapping('api/v1/users') 278 | export class UserController {} 279 | ``` 280 | 281 | #### Decorators HTTP 282 | 283 | `@Get()` Decorator to solve a request for a specific resource. 284 | 285 | `@Post()` Decorator used to send an entity to a specific resource. 286 | 287 | `@Put()` Decorator that replaces the current representations of the target resource with the payload of the request. 288 | 289 | `@Delete()` Decorator that deletes the specific resource. 290 | 291 | `@Params()` Decorator to read the parameters specified in a method. 292 | 293 | `@Body()` Decorator that passes the payload of a method in the request. 294 | 295 | ```typescript 296 | @Mapping('api/v1/users') 297 | export class UserController { 298 | @Get() 299 | getAllUsers() {} 300 | 301 | @Get(':id') 302 | getByIdUser(@Params() id: string | number) {} 303 | 304 | @Post() 305 | saveUser(@Body() data: T) {} 306 | 307 | @Put(':id') 308 | updateByIdUser(@Params() id: string | number, @Body data: T) {} 309 | 310 | @Delete(':id') 311 | deleteByIdUser(@Params() id: string | number) {} 312 | } 313 | ``` 314 | 315 | ## Example of use case 316 | 317 | - Create a user in the store 318 | 319 | 1. Create the project. 320 | 321 | ```shell 322 | scaffold create:project --name=store 323 | ``` 324 | 325 | 2. Create entity user 326 | 327 | ```shell 328 | scaffold create:entity --name=user 329 | ``` 330 | 331 | ```typescript 332 | // src/domain/entities/user.ts 333 | export type UserEntity = { 334 | id: string | number 335 | name: string 336 | email: string 337 | } 338 | 339 | export type AddUserParams = Omit 340 | ``` 341 | 342 | 3. Create the contract to create the user. 343 | 344 | ```shell 345 | scaffold create:interface --name=user --path=entities 346 | ``` 347 | 348 | ```typescript 349 | // src/domain/entities/contracts/user-repository.ts 350 | import { AddUserParams, UserModel } from '@/domain/entities/user' 351 | 352 | export const USER_REPOSITORY = 'USER_REPOSITORY' 353 | 354 | export interface IUserRepository { 355 | save: (data: AddUserParams) => Promise 356 | } 357 | ``` 358 | 359 | 4. Create services user 360 | 361 | ```shell 362 | scaffold create:service --name=user 363 | ``` 364 | 365 | ```typescript 366 | // src/domain/use-cases/user-service.ts 367 | import { AddUserParams, UserEntity } from '@/domain/entities/user' 368 | 369 | export const USER_SERVICE = 'USER_SERVICE' 370 | 371 | export interface IUserService { 372 | save: (data: AddUserParams) => Promise 373 | } 374 | ``` 375 | 376 | ```typescript 377 | // src/domain/use-cases/impl/user-service-impl.ts 378 | import { Adapter, Service } from '@tsclean/core' 379 | import { IUserService } from '@/domain/use-cases/user-service' 380 | import { UserModel } from '@/domain/models/user' 381 | import { 382 | IUserRepository, 383 | USER_REPOSITORY 384 | } from '@/domain/models/contracts/user-repository' 385 | 386 | @Service() 387 | export class UserServiceImpl implements IUserService { 388 | constructor( 389 | // Decorator to keep the reference of the Interface, by means of the constant. 390 | @Adapter(USER_REPOSITORY) 391 | private readonly userRepository: IUserRepository 392 | ) {} 393 | 394 | /** 395 | * Method to send the data to the repository. 396 | * @param data {@code UserEntity} 397 | */ 398 | async save(data: AddUserParams): Promise { 399 | // Send the data to the repository. 400 | return this.userRepository.save({ ...data }) 401 | } 402 | } 403 | ``` 404 | 405 | 5. Create mongoose adapter and additionally you must include the url of the connection in the `.env` file 406 | 407 | ```shell 408 | scaffold create:adapter-orm --name=user --orm=mongo --manager=mongoose 409 | ``` 410 | 411 | ```typescript 412 | // src/infrastructure/driven-adapters/adapters/orm/mongoose/models/user.ts 413 | import { UserEntity } from '@/domain/entities/user' 414 | import { model, Schema } from 'mongoose' 415 | 416 | const schema = new Schema( 417 | { 418 | id: { 419 | type: String 420 | }, 421 | name: { 422 | type: String 423 | }, 424 | email: { 425 | type: String 426 | } 427 | }, 428 | { strict: false } 429 | ) 430 | 431 | export const UserModelSchema = model('users', schema) 432 | ``` 433 | 434 | ```typescript 435 | // src/infrastructure/driven-adapters/adapters/orm/mongoose/user-mongoose-repository-adapter.ts 436 | import { AddUserParams, UserEntity } from '@/domain/entities/user' 437 | import { IUserRepository } from '@/domain/models/contracts/user-repository' 438 | import { UserModelSchema as Schema } from '@/infrastructure/driven-adapters/adapters/orm/mongoose/models/user' 439 | 440 | export class UserMongooseRepositoryAdapter implements IUserRepository { 441 | async save(data: AddUserParams): Promise { 442 | return Schema.create(data) 443 | } 444 | } 445 | ``` 446 | 447 | 5.1 The `scaffold` automatically generates the connection instance 448 | 449 | ```typescript 450 | // src/application/config/mongoose-instance.ts 451 | import { connect, set } from 'mongoose' 452 | import { Logger } from '@tsclean/core' 453 | import { MONGODB_URI } from '@/application/config/environment' 454 | 455 | export class MongoConfiguration { 456 | private logger: Logger 457 | private static instance: MongoConfiguration 458 | 459 | private constructor() { 460 | this.logger = new Logger(MongoConfiguration.name) 461 | } 462 | 463 | public static getInstance(): MongoConfiguration { 464 | if (!this.instance) { 465 | this.instance = new MongoConfiguration() 466 | } 467 | return this.instance 468 | } 469 | 470 | public async managerConnectionMongo(): Promise { 471 | set('strictQuery', true) 472 | 473 | try { 474 | await connect(MONGODB_URI) 475 | this.logger.log( 476 | `Connection successfully to database of Mongo: ${MONGODB_URI}` 477 | ) 478 | } catch (error) { 479 | this.logger.error('Failed to connect to MongoDB', error) 480 | } 481 | } 482 | } 483 | ``` 484 | 485 | 5.2 The `scaffold` also includes the function in the array of singleton instances. 486 | 487 | ```typescript 488 | // src/application/singleton.ts 489 | import { MongoConfiguration } from '@/application/config/mongoose-instance' 490 | 491 | /** This array has all the singleton instances of the application */ 492 | export const singletonInitializers: Array<() => Promise> = [ 493 | async () => { 494 | const pgConfig = PgConfiguration.getInstance() 495 | await pgConfig.managerConnectionPg() 496 | } 497 | ] 498 | ``` 499 | 500 | 5.3 These instances are called in the `index.ts`. 501 | 502 | ```typescript 503 | import 'module-alias/register' 504 | 505 | import helmet from 'helmet' 506 | import { StartProjectInit } from '@tsclean/core' 507 | 508 | import { AppContainer } from '@/application/app' 509 | import { PORT } from '@/application/config/environment' 510 | import { singletonInitializers } from '@/application/singleton' 511 | 512 | async function init(): Promise { 513 | /** Iterate the singleton functions */ 514 | for (const initFn of singletonInitializers) { 515 | await initFn() 516 | } 517 | 518 | const app = await StartProjectInit.create(AppContainer) 519 | app.use(helmet()) 520 | await app.listen(PORT, () => console.log(`Running on port: ${PORT}`)) 521 | } 522 | 523 | void init().catch() 524 | ``` 525 | 526 | 6. Pass the key:value to do the dependency injections 527 | 528 | ```typescript 529 | // src/infrastructure/driven-adapters/providers/index.ts 530 | import { USER_SERVICE } from '@/domain/use-cases/user-service' 531 | import { USER_REPOSITORY } from '@/domain/models/contracts/user-repository' 532 | import { UserServiceImpl } from '@/domain/use-cases/impl/user-service-impl' 533 | import { UserMongooseRepositoryAdapter } from '@/infrastructure/driven-adapters/adapters/orm/mongoose/user-mongoose-repository-adapter' 534 | 535 | export const adapters = [ 536 | { 537 | // Constant referring to the interface 538 | provide: USER_REPOSITORY, 539 | // Class that implements the interface 540 | useClass: UserMongooseRepositoryAdapter 541 | } 542 | ] 543 | 544 | export const services = [ 545 | { 546 | // Constant referring to the interface 547 | provide: USER_SERVICE, 548 | // Class that implements the interface 549 | useClass: UserServiceImpl 550 | } 551 | ] 552 | ``` 553 | 554 | 7. Create controller user 555 | 556 | ```shell 557 | scaffold create:controller --name=user 558 | ``` 559 | 560 | ```typescript 561 | // src/infrastructure/entry-points/api/user-controller.ts 562 | import { Mapping, Post, Body } from '@tsclean/core' 563 | 564 | import { AddUserParams, ModelUser } from '@/domain/models/user' 565 | import { IUserService, USER_SERVICE } from '@/domain/use-cases/user-service' 566 | 567 | @Mapping('api/v1/users') 568 | export class UserController { 569 | constructor( 570 | // Decorator to keep the reference of the Interface, by means of the constant. 571 | @Adapter(USER_SERVICE) 572 | private readonly userService: IUserService 573 | ) {} 574 | 575 | @Post() 576 | async saveUserController( 577 | @Body() data: AddUserParams 578 | ): Promise { 579 | // Send the data to the service through the interface. 580 | const user = await this.userService.save(data) 581 | 582 | return { 583 | message: 'User created successfully', 584 | user 585 | } 586 | } 587 | } 588 | ``` 589 | 590 | 8. Finally you can test this endpoint `http://localhost:9000/api/v1/users`, method `POST` in the rest client of your choice and send the corresponding data. 591 | 592 | --- 593 | -------------------------------------------------------------------------------- /bin/scaffold.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import('../dist/index.js') 3 | -------------------------------------------------------------------------------- /lib/commands/CommandCreateAdapter.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import yargs from "yargs"; 3 | 4 | import { PATHS } from "../utils/paths"; 5 | import { EMOJIS } from "../utils/emojis"; 6 | import { MESSAGES } from "../utils/messages"; 7 | import { CommandUtils } from "./CommandUtils"; 8 | import { CONSTANTS } from "../utils/constants"; 9 | import { errorMessage, executeCommand } from "../utils/helpers"; 10 | import { DatabaseTemplate } from "../templates/DatabaseTemplate"; 11 | import { AdaptersTemplate } from "../templates/AdaptersTemplate"; 12 | import { ModelsTemplate } from "../templates/ModelsTemplate"; 13 | import { SingletonGenerateTemplate } from "../templates/SingletonGenerateTemplate"; 14 | import { SingletonTypes } from "../types/SingletonTypes"; 15 | 16 | export class AdapterCreateCommand implements yargs.CommandModule { 17 | command = "create:adapter-orm"; 18 | describe = "Generate a new adapter for ORM"; 19 | 20 | builder(args: yargs.Argv) { 21 | return args 22 | .option("name", { 23 | alias: "n", 24 | describe: "Name the adapter", 25 | demandOption: true 26 | }) 27 | .option("orm", { 28 | alias: "orm", 29 | describe: "Orm", 30 | demandOption: true 31 | }) 32 | .option("manager", { 33 | alias: "mn", 34 | describe: "Database manager", 35 | demandOption: true 36 | }); 37 | } 38 | 39 | async handler(args: yargs.Arguments) { 40 | let spinner; 41 | 42 | try { 43 | setTimeout( 44 | async () => (spinner = ora(CONSTANTS.INSTALLING).start()), 45 | 1000 46 | ); 47 | 48 | const basePath = PATHS.BASE_PATH_ADAPTER(args.orm as string); 49 | const filename = PATHS.FILE_NAME_ADAPTER( 50 | args.name as string, 51 | args.manager as string, 52 | args.orm as string 53 | ); 54 | 55 | // The path for the validation of the file input is made up. 56 | const path = `${basePath}${filename}`; 57 | 58 | const base = process.cwd(); 59 | 60 | // Validate that the entity exists for importing into the ORM adapter. 61 | CommandUtils.readModelFiles( 62 | PATHS.PATH_MODELS_ENTITY(), 63 | args.name as string 64 | ); 65 | 66 | // Validate that another manager is not implemented. 67 | CommandUtils.readManagerFiles( 68 | PATHS.PATH_MODELS_ORM(base, args.orm as string), 69 | args.manager as string 70 | ); 71 | 72 | if (args.orm === CONSTANTS.MONGO || args.orm === CONSTANTS.SEQUELIZE) { 73 | // We validate if the file exists, to throw the exception. 74 | const fileExists = await CommandUtils.fileExists(path); 75 | 76 | // Throw message exception 77 | if (fileExists) throw MESSAGES.FILE_EXISTS(path); 78 | 79 | const filePath = PATHS.PATH_SINGLETON(base); 80 | 81 | const paramsTemplate: SingletonTypes = { 82 | filepath: filePath, 83 | manager: args.manager as string, 84 | instance: args.orm as string 85 | }; 86 | 87 | // Singleton 88 | SingletonGenerateTemplate.generate(paramsTemplate); 89 | 90 | // Adapter 91 | await CommandUtils.createFile( 92 | PATHS.PATH_ADAPTER( 93 | base, 94 | args.orm, 95 | args.name as string, 96 | args.manager as string 97 | ), 98 | AdaptersTemplate.getRepositoryAdapter( 99 | args.name as string, 100 | args.orm as string, 101 | args.manager as string 102 | ) 103 | ); 104 | 105 | // Model 106 | await CommandUtils.createFile( 107 | PATHS.PATH_MODEL( 108 | base, 109 | args.orm, 110 | args.name as string, 111 | args.manager as string 112 | ), 113 | ModelsTemplate.getModels( 114 | args.name as string, 115 | args.orm as string, 116 | args.manager as string 117 | ) 118 | ); 119 | if (args.orm === CONSTANTS.SEQUELIZE) { 120 | // Singletons for mysql, pg 121 | await CommandUtils.createFile( 122 | PATHS.PATH_SINGLETON_INSTANCES(base, args.manager as string), 123 | DatabaseTemplate.getMysqlAndPostgresSingleton( 124 | args.name as string, 125 | args.manager as string 126 | ) 127 | ); 128 | } 129 | 130 | if (args.orm === CONSTANTS.MONGO) { 131 | // Singletons for mongoose 132 | await CommandUtils.createFile( 133 | PATHS.PATH_SINGLETON_INSTANCES( 134 | base, 135 | args.manager as string, 136 | args.orm as string 137 | ), 138 | DatabaseTemplate.getMongooseSingleton(args.orm as string) 139 | ); 140 | } 141 | 142 | // Dependencies 143 | const packageJsonContents = await CommandUtils.readFile( 144 | base + "/package.json" 145 | ); 146 | await CommandUtils.createFile( 147 | base + "/package.json", 148 | AdapterCreateCommand.getPackageJson( 149 | packageJsonContents, 150 | args.orm as string, 151 | args.manager as string 152 | ) 153 | ); 154 | await executeCommand(CONSTANTS.NPM_INSTALL); 155 | 156 | // This message is only displayed in sequelize 157 | const env = args.manager 158 | ? `${EMOJIS.ROCKET} ${MESSAGES.CONFIG_ENV()}` 159 | : ""; 160 | 161 | setTimeout(() => { 162 | spinner.succeed(CONSTANTS.INSTALLATION_COMPLETED); 163 | spinner.stopAndPersist({ 164 | symbol: EMOJIS.ROCKET, 165 | text: `${MESSAGES.FILE_SUCCESS(CONSTANTS.ADAPTER, path)} 166 | ${env}` 167 | }); 168 | }, 1000 * 5); 169 | } else { 170 | throw MESSAGES.ERROR_ORM(args.orm); 171 | } 172 | } catch (error) { 173 | errorMessage(error, CONSTANTS.ADAPTER); 174 | } 175 | } 176 | 177 | /** 178 | * 179 | * @protected 180 | */ 181 | protected static generateProvider() { 182 | return ` 183 | export const adapters = []; 184 | 185 | export const services = []; 186 | `; 187 | } 188 | 189 | /** 190 | * 191 | * @param dependencies 192 | * @param orm 193 | * @param manager 194 | * @private 195 | */ 196 | private static getPackageJson( 197 | dependencies: string, 198 | orm: string, 199 | manager: string 200 | ) { 201 | const updatePackages = JSON.parse(dependencies); 202 | 203 | switch (orm) { 204 | case CONSTANTS.MONGO: 205 | updatePackages.dependencies["mongoose"] = "^8.0.0"; 206 | break; 207 | case CONSTANTS.SEQUELIZE: 208 | updatePackages.dependencies["sequelize"] = "^6.37.5"; 209 | updatePackages.dependencies["sequelize-typescript"] = "^2.1.6"; 210 | updatePackages.devDependencies["@types/sequelize"] = "^4.28.20"; 211 | switch (manager) { 212 | case CONSTANTS.MYSQL: 213 | updatePackages.dependencies["mysql2"] = "^3.11.3"; 214 | break; 215 | case CONSTANTS.POSTGRES: 216 | updatePackages.dependencies["pg"] = "^8.11.3"; 217 | updatePackages.dependencies["pg-hstore"] = "^2.3.4"; 218 | break; 219 | default: 220 | break; 221 | } 222 | } 223 | 224 | return JSON.stringify(updatePackages, undefined, 3); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /lib/commands/CommandCreateAdapterSimple.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import yargs from "yargs"; 3 | 4 | import {PATHS} from "../utils/paths"; 5 | import {MESSAGES} from "../utils/messages"; 6 | import {CommandUtils} from "./CommandUtils"; 7 | import {CONSTANTS} from "../utils/constants"; 8 | import {banner, errorMessage} from "../utils/helpers"; 9 | import {EMOJIS} from "../utils/emojis"; 10 | 11 | export class CommandCreateAdapterSimple implements yargs.CommandModule { 12 | command = "create:adapter"; 13 | describe = "Generate a new adapter"; 14 | 15 | builder(args: yargs.Argv) { 16 | return args 17 | .option("name", { 18 | alias: "n", 19 | describe: "Name the adapter", 20 | demandOption: true 21 | }) 22 | } 23 | 24 | async handler(args: yargs.Arguments) { 25 | 26 | let spinner; 27 | 28 | try { 29 | 30 | setTimeout(async () => spinner = ora(CONSTANTS.INSTALLING).start(), 1000) 31 | 32 | const basePath = PATHS.BASE_PATH_ADAPTER_SIMPLE(); 33 | const filename = PATHS.FILE_NAME_ADAPTER_SIMPLE(args.name as string); 34 | 35 | // The path for the validation of the file input is made up. 36 | const path = `${basePath}${filename}`; 37 | 38 | // We validate if the file exists, to throw the exception. 39 | const fileExists = await CommandUtils.fileExists(path); 40 | 41 | // Throw message exception 42 | if (fileExists) throw MESSAGES.FILE_EXISTS(path); 43 | 44 | // Adapter 45 | await CommandUtils.createFile(PATHS.PATH_ADAPTER_SIMPLE(args.name as string), CommandCreateAdapterSimple.getRepositoryAdapter(args.name as string)) 46 | 47 | setTimeout(() => { 48 | spinner.succeed(CONSTANTS.INSTALLATION_COMPLETED) 49 | spinner.stopAndPersist({ 50 | symbol: EMOJIS.ROCKET, 51 | text: `${MESSAGES.FILE_SUCCESS(CONSTANTS.ADAPTER, PATHS.PATH_ADAPTER_SIMPLE(args.name as string))}` 52 | }); 53 | }, 2000); 54 | 55 | } catch (error) { 56 | errorMessage(error, CONSTANTS.ADAPTER) 57 | } 58 | } 59 | 60 | private static getRepositoryAdapter(name: string) { 61 | const _param = CommandUtils.capitalizeString(name); 62 | return `export class ${_param}Adapter { 63 | // Implementation 64 | }` 65 | } 66 | } -------------------------------------------------------------------------------- /lib/commands/CommandCreateController.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import yargs from "yargs"; 3 | 4 | import {EMOJIS} from "../utils/emojis"; 5 | import {MESSAGES} from "../utils/messages"; 6 | import {CommandUtils} from "./CommandUtils"; 7 | import {banner, errorMessage} from "../utils/helpers"; 8 | 9 | export class ControllerCreateCommand implements yargs.CommandModule { 10 | command = "create:controller" 11 | describe = "Generates a new controller." 12 | 13 | builder(args: yargs.Argv) { 14 | return args 15 | .option('n', { 16 | alias: "name", 17 | describe: "Name the Controller class", 18 | demandOption: true 19 | }) 20 | } 21 | 22 | async handler(args: yargs.Arguments) { 23 | let spinner 24 | 25 | try { 26 | 27 | const directoryImplementation = `${process.cwd()}/src/domain/use-cases/impl`; 28 | const files = await CommandUtils.injectServiceAdapter(directoryImplementation); 29 | 30 | const fileContent = ControllerCreateCommand.getTemplateController(args.name as any, files) 31 | const basePath = `${process.cwd()}/src/infrastructure/entry-points/api/` 32 | const filename = `${args.name}-controller.ts` 33 | const path = `${basePath}${filename}` 34 | const fileExists = await CommandUtils.fileExists(path) 35 | 36 | setTimeout(() => (spinner = ora('Installing...').start()), 1000) 37 | 38 | if (fileExists) throw MESSAGES.FILE_EXISTS(path) 39 | 40 | await CommandUtils.createFile(path, fileContent) 41 | 42 | setTimeout(() => { 43 | spinner.succeed("Installation completed") 44 | spinner.stopAndPersist({ 45 | symbol: EMOJIS.ROCKET, 46 | text: MESSAGES.FILE_SUCCESS('Controller', path) 47 | }); 48 | }, 1000 * 5); 49 | 50 | } catch (error) { 51 | setTimeout(() => (spinner.fail("Installation fail"), errorMessage(error, 'controller')), 2000) 52 | } 53 | } 54 | 55 | /** 56 | * Get content controllers files 57 | * @param param 58 | * @param files 59 | * @protected 60 | */ 61 | protected static getTemplateController(param: string, files: string[]) { 62 | let nameService: string = ""; 63 | let nameCapitalize: string = CommandUtils.capitalizeString(param); 64 | 65 | // This loop when the name of the controller matches the service, to then be injected through the constructor. 66 | for (const file of files) { 67 | let name = file.slice(0, -16); 68 | if(name === param) { 69 | nameService = name; 70 | const nameCapitalizeService = CommandUtils.capitalizeString(nameService); 71 | 72 | return `import {Mapping, Get} from "@tsclean/core"; 73 | 74 | @Mapping('api/v1/${nameService}') 75 | export class ${nameCapitalizeService}Controller { 76 | 77 | constructor() { 78 | } 79 | 80 | // Example function 81 | @Get() 82 | async getWelcome(): Promise { 83 | return 'Welcome to the world of clean architecture' 84 | } 85 | } 86 | ` 87 | }; 88 | } 89 | 90 | return `import {Mapping} from "@tsclean/core"; 91 | 92 | @Mapping('') 93 | export class ${nameCapitalize}Controller { 94 | constructor() { 95 | } 96 | } 97 | ` 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/commands/CommandCreateEntity.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import * as yargs from 'yargs' 3 | import {CommandUtils} from './CommandUtils' 4 | import {banner, errorMessage} from "../utils/helpers"; 5 | import {MESSAGES} from "../utils/messages"; 6 | import {EMOJIS} from "../utils/emojis"; 7 | 8 | 9 | export class EntityCreateCommand implements yargs.CommandModule { 10 | command = "create:entity"; 11 | describe = "Generates a new entity."; 12 | 13 | builder(args: yargs.Argv) { 14 | return args 15 | .option("n", { 16 | alias: "name", 17 | describe: "Name of the entity type", 18 | demand: true 19 | }) 20 | } 21 | 22 | async handler(args: yargs.Arguments) { 23 | let spinner 24 | try { 25 | 26 | const fileContent = EntityCreateCommand.getTemplate(args.name as any) 27 | const basePath = `${process.cwd()}/src/domain/entities/` 28 | const filename = `${args.name}.ts` 29 | const path = basePath + filename 30 | const fileExists = await CommandUtils.fileExists(path) 31 | 32 | setTimeout(() => (spinner = ora('Installing...').start()), 1000) 33 | 34 | if (fileExists) throw MESSAGES.FILE_EXISTS(path) 35 | 36 | await CommandUtils.createFile(path, fileContent) 37 | 38 | setTimeout(() => { 39 | spinner.succeed("Installation completed") 40 | spinner.stopAndPersist({ 41 | symbol: EMOJIS.ROCKET, 42 | text: MESSAGES.FILE_SUCCESS('Entity', path) 43 | }); 44 | }, 1000 * 5); 45 | } catch (error) { 46 | setTimeout(() => (spinner.fail("Installation fail"), errorMessage(error, 'entity')), 2000) 47 | } 48 | } 49 | 50 | /** 51 | * Gets content of the entity file 52 | * 53 | * @param param 54 | * @returns 55 | */ 56 | protected static getTemplate(param: string): string { 57 | const name = CommandUtils.capitalizeString(param) 58 | return `export type ${name}Entity = { 59 | // Attributes 60 | } 61 | 62 | export type Add${name}Params = Omit<${name}Entity, 'id'> 63 | ` 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/commands/CommandCreateInterface.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import yargs from "yargs"; 3 | 4 | import { EMOJIS } from "../utils/emojis"; 5 | import { MESSAGES } from "../utils/messages"; 6 | import { CommandUtils } from "./CommandUtils"; 7 | import { banner, errorMessage } from "../utils/helpers"; 8 | 9 | export class InterfaceCreateCommand implements yargs.CommandModule { 10 | command = "create:interface"; 11 | describe = "Generate a new interface" 12 | 13 | builder(args: yargs.Argv) { 14 | return args 15 | .option("n", { 16 | alias: "name", 17 | describe: "Name the Interface", 18 | demandOption: true 19 | }) 20 | .option("p", { 21 | alias: "path", 22 | describe: "File location", 23 | demandOption: true 24 | }) 25 | }; 26 | 27 | async handler(args: yargs.Arguments) { 28 | let spinner 29 | let basePath: string 30 | let fileName: string 31 | let path: string 32 | let fileExists: boolean 33 | 34 | try { 35 | const fileContent = InterfaceCreateCommand.getTemplateInterface(args.name as any, args.path as any) 36 | 37 | setTimeout(() => (spinner = ora('Installing...').start()), 1000); 38 | 39 | if (args.path as string === "entities" || args.path as string === "service" || args.path as string === "infra") { 40 | switch (args.path) { 41 | case "entities": 42 | basePath = `${process.cwd()}/src/domain/entities/contracts` 43 | fileName = `${args.name}-repository.ts` 44 | break 45 | case "service": 46 | basePath = `${process.cwd()}/src/domain/use-cases` 47 | fileName = `${args.name}-service.ts` 48 | break 49 | case "infra": 50 | basePath = `${process.cwd()}/src/infrastructure/entry-points/contracts` 51 | fileName = `${args.name}.ts` 52 | break 53 | } 54 | 55 | path = `${basePath}/${fileName}` 56 | fileExists = await CommandUtils.fileExists(path) 57 | if (fileExists) throw MESSAGES.FILE_EXISTS(path) 58 | 59 | await CommandUtils.createFile(path, fileContent) 60 | setTimeout(() => { 61 | spinner.succeed("Installation completed") 62 | spinner.stopAndPersist({ 63 | symbol: EMOJIS.ROCKET, 64 | text: MESSAGES.FILE_SUCCESS('Interface', path) 65 | }); 66 | }, 1000 * 5); 67 | } else { 68 | throw MESSAGES.ERROR_INTERFACE(args.path); 69 | } 70 | } catch (error) { 71 | setTimeout(() => (spinner.fail("Installation fail"), errorMessage(error, 'interface')), 2000) 72 | } 73 | } 74 | 75 | /** 76 | * Get contents interface files 77 | * @param param 78 | * @param path 79 | * @protected 80 | */ 81 | protected static getTemplateInterface(param: string, path: string) { 82 | 83 | const nameRef = param.toUpperCase().replace(/-/g, "_"); 84 | 85 | const string = param 86 | .split("-") 87 | .map((p) => p.charAt(0).toUpperCase() + p.slice(1)) 88 | .join(""); 89 | 90 | const repositoryConst = `export const ${nameRef}_REPOSITORY = '${nameRef}_REPOSITORY';`; 91 | 92 | switch (path) { 93 | case 'entities': 94 | return `${repositoryConst} 95 | 96 | export interface I${string}Repository { 97 | 98 | }` 99 | case 'service': 100 | return `${repositoryConst} 101 | 102 | export interface I${string}Service { 103 | 104 | }` 105 | case 'infra': 106 | return `${repositoryConst} 107 | 108 | export interface I${string} { 109 | 110 | }` 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/commands/CommandCreateInterfaceResource.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import yargs from "yargs"; 3 | 4 | import { PATHS } from "../utils/paths"; 5 | import { EMOJIS } from "../utils/emojis"; 6 | import { MESSAGES } from "../utils/messages"; 7 | import { CommandUtils } from "./CommandUtils"; 8 | import { errorMessage } from "../utils/helpers"; 9 | 10 | export class CommandCreateInterfaceResource implements yargs.CommandModule { 11 | command = "create:interface-resource"; 12 | describe = "Generate a new interface resource"; 13 | 14 | builder(args: yargs.Argv) { 15 | return args 16 | .option("n", { 17 | alias: "name", 18 | describe: "Name the Interface", 19 | demandOption: true 20 | }) 21 | .option("r", { 22 | alias: "resource", 23 | describe: "Interface resource", 24 | demandOption: true 25 | }); 26 | } 27 | 28 | async handler(args: yargs.Arguments) { 29 | let spinner; 30 | let basePath: string; 31 | let fileName: string; 32 | let path: string; 33 | let fileExists: boolean; 34 | 35 | try { 36 | const fileContent = CommandCreateInterfaceResource.getTemplateResourceInterface( 37 | args.name as any 38 | ); 39 | 40 | setTimeout(() => (spinner = ora("Installing...").start()), 1000); 41 | 42 | CommandUtils.readModelFiles( 43 | PATHS.PATH_MODELS_ENTITY(), 44 | args.name as string 45 | ); 46 | 47 | basePath = `${process.cwd()}/src/domain/entities/contracts`; 48 | fileName = `${args.name}-resource-repository.ts`; 49 | 50 | path = `${basePath}/${fileName}`; 51 | fileExists = await CommandUtils.fileExists(path); 52 | if (fileExists) throw MESSAGES.FILE_EXISTS(path); 53 | 54 | await CommandUtils.createFile(path, fileContent); 55 | 56 | setTimeout(() => { 57 | spinner.succeed("Installation completed"); 58 | spinner.stopAndPersist({ 59 | symbol: EMOJIS.ROCKET, 60 | text: MESSAGES.FILE_SUCCESS("Interface", path) 61 | }); 62 | }, 1000 * 5); 63 | } catch (error) { 64 | setTimeout( 65 | () => ( 66 | spinner.fail("Installation fail"), errorMessage(error, "interface") 67 | ), 68 | 2000 69 | ); 70 | } 71 | } 72 | 73 | /** 74 | * Get contents for resource interface 75 | * @param param nombre del recurso, puede contener guiones 76 | * @protected 77 | */ 78 | protected static getTemplateResourceInterface(param: string): string { 79 | const namePascal = CommandUtils.capitalizeString( 80 | param 81 | .split("-") 82 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 83 | .join("") 84 | ); 85 | 86 | const nameSnakeUpper = param.replace(/-/g, "_").toUpperCase(); 87 | 88 | return `import { Add${namePascal}Params, ${namePascal}Entity } from "@/domain/entities/${param}"; 89 | 90 | export const ${nameSnakeUpper}_RESOURCE_REPOSITORY = "${nameSnakeUpper}_RESOURCE_REPOSITORY"; 91 | 92 | export interface I${namePascal}ResourceRepository { 93 | findAll: () => Promise<${namePascal}Entity[]>; 94 | save: (data: Add${namePascal}Params) => Promise<${namePascal}Entity>; 95 | findById: (id: number) => Promise<${namePascal}Entity>; 96 | update: (id: number, data: any) => Promise; 97 | } 98 | `; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /lib/commands/CommandCreateService.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import yargs from "yargs"; 3 | 4 | import { EMOJIS } from "../utils/emojis"; 5 | import { MESSAGES } from "../utils/messages"; 6 | import { CommandUtils } from "./CommandUtils"; 7 | import { banner, errorMessage } from "../utils/helpers"; 8 | 9 | export class ServiceCreateCommand implements yargs.CommandModule { 10 | command = "create:service"; 11 | describe = "Generates a new service."; 12 | 13 | builder(args: yargs.Argv) { 14 | return args.option("n", { 15 | alias: "name", 16 | describe: "Name the Service class", 17 | demandOption: true 18 | }); 19 | } 20 | 21 | async handler(args: yargs.Arguments) { 22 | let spinner; 23 | 24 | try { 25 | const fileContent = ServiceCreateCommand.getTemplateService( 26 | args.name as any 27 | ); 28 | const fileContentRepository = ServiceCreateCommand.getTemplateIServices( 29 | args.name as any 30 | ); 31 | 32 | const basePath = `${process.cwd()}/src/domain/use-cases/`; 33 | const filename = `${args.name}-service-impl.ts`; 34 | const path = `${basePath}impl/${filename}`; 35 | const pathRepository = `${basePath + args.name}-service.ts`; 36 | 37 | const fileExists = await CommandUtils.fileExists(path); 38 | 39 | setTimeout(() => (spinner = ora("Installing...").start()), 1000); 40 | 41 | if (fileExists) throw MESSAGES.FILE_EXISTS(path); 42 | 43 | await CommandUtils.createFile(pathRepository, fileContentRepository); 44 | await CommandUtils.createFile(path, fileContent); 45 | 46 | setTimeout(() => { 47 | spinner.succeed("Installation completed"); 48 | spinner.stopAndPersist({ 49 | symbol: EMOJIS.ROCKET, 50 | prefixText: MESSAGES.REPOSITORY_SUCCESS(pathRepository), 51 | text: MESSAGES.FILE_SUCCESS("Services", path) 52 | }); 53 | }, 1000 * 5); 54 | } catch (error) { 55 | setTimeout( 56 | () => ( 57 | spinner.fail("Installation fail"), errorMessage(error, "service") 58 | ), 59 | 2000 60 | ); 61 | } 62 | } 63 | 64 | /** 65 | * Get contents services files 66 | * @param param 67 | * @protected 68 | */ 69 | protected static getTemplateService(param: any) { 70 | const name = CommandUtils.capitalizeString(param); 71 | return `import {Service} from "@tsclean/core"; 72 | import {I${name}Service} from "@/domain/use-cases/${param}-service"; 73 | 74 | @Service() 75 | export class ${name}ServiceImpl implements I${name}Service { 76 | constructor() { 77 | } 78 | }`; 79 | } 80 | 81 | /** 82 | * Get contents interface files 83 | * @param param 84 | * @protected 85 | */ 86 | protected static getTemplateIServices(param: string) { 87 | const name = CommandUtils.capitalizeString(param); 88 | const nameTransform = param.replace(/-/g, "_"); 89 | const nameRef = nameTransform.toUpperCase(); 90 | 91 | return `export const ${nameRef}_SERVICES = '${nameRef}_SERVICES'; 92 | 93 | export interface I${name}Service { 94 | 95 | }`; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/commands/CommandCreateServiceResource.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import * as yargs from "yargs"; 3 | 4 | import { PATHS } from "../utils/paths"; 5 | import { EMOJIS } from "../utils/emojis"; 6 | import { MESSAGES } from "../utils/messages"; 7 | import { CommandUtils } from "./CommandUtils"; 8 | import { CONSTANTS } from "../utils/constants"; 9 | import { errorMessage } from "../utils/helpers"; 10 | 11 | export class CommandCreateServiceResource implements yargs.CommandModule { 12 | command = "create:service-resource"; 13 | describe = "Generate a new service resource"; 14 | 15 | builder(args: yargs.Argv) { 16 | return args 17 | .option("name", { 18 | alias: "n", 19 | describe: "Name the service", 20 | demandOption: true 21 | }) 22 | .option("resource", { 23 | alias: "r", 24 | describe: "Service resource", 25 | demandOption: true 26 | }); 27 | } 28 | 29 | async handler(args: yargs.Arguments) { 30 | let spinner; 31 | 32 | setTimeout(async () => (spinner = ora(CONSTANTS.INSTALLING).start()), 1000); 33 | 34 | const basePath = PATHS.PATH_SERVICE_RESOURCE(); 35 | const fileService = `${basePath}/${args.name}-service-resource-impl.ts`; 36 | const fileContract = `${process.cwd()}/src/domain/use-cases/${ 37 | args.name 38 | }-service-resource.ts`; 39 | 40 | const fileServiceExists = await CommandUtils.fileExists(fileService); 41 | 42 | if (fileServiceExists) throw MESSAGES.FILE_EXISTS(fileService); 43 | 44 | try { 45 | await CommandUtils.createFile( 46 | `${fileContract}`, 47 | CommandCreateServiceResource.getServiceResourceInterface( 48 | args.name as string 49 | ) 50 | ); 51 | await CommandUtils.createFile( 52 | `${fileService}`, 53 | CommandCreateServiceResource.getServiceResource(args.name as string) 54 | ); 55 | 56 | setTimeout(() => { 57 | spinner.succeed("Installation completed"); 58 | spinner.stopAndPersist({ 59 | symbol: EMOJIS.ROCKET, 60 | prefixText: MESSAGES.REPOSITORY_SUCCESS(fileContract), 61 | text: MESSAGES.FILE_SUCCESS("Services resource", fileService) 62 | }); 63 | }, 1000 * 5); 64 | } catch (error) { 65 | setTimeout( 66 | () => ( 67 | spinner.fail("Installation fail"), errorMessage(error, "service") 68 | ), 69 | 2000 70 | ); 71 | } 72 | } 73 | 74 | static getServiceResourceInterface(param: string): string { 75 | const name = CommandUtils.capitalizeString(param); 76 | const nameTransform = param.replace(/-/g, "_"); 77 | const nameRef = nameTransform.toUpperCase(); 78 | 79 | return `export const ${nameRef}_RESOURCE_SERVICE = "${nameRef}_RESOURCE_SERVICE"; 80 | 81 | export interface I${name}ResourceService { 82 | findAll: () => Promise; 83 | save: (data: any) => Promise; 84 | findById: (id: number) => Promise; 85 | update: (id: number, data: any) => Promise 86 | } 87 | `; 88 | } 89 | 90 | static getServiceResource(param: any): string { 91 | const name = CommandUtils.capitalizeString(param); 92 | return `import {Service} from "@tsclean/core"; 93 | import {I${name}ResourceService} from "@/domain/use-cases/${param}-service-resource"; 94 | 95 | @Service() 96 | export class ${name}ServiceImpl implements I${name}ResourceService { 97 | constructor() { 98 | } 99 | 100 | // Se debe de tipar la devolución de la promesa 101 | async findAll(): Promise { 102 | return Promise.resolve([]); 103 | } 104 | 105 | // Se debe de tipar la devolución de la promesa 106 | async findById(id: number): Promise { 107 | return Promise.resolve(undefined); 108 | } 109 | 110 | // Se debe de tipar la devolución de la promesa y el input de la data 111 | async save(data: any): Promise { 112 | return Promise.resolve(undefined); 113 | } 114 | 115 | // Se debe de tipar el input de la data 116 | async update(id: number, data: any): Promise { 117 | return Promise.resolve(undefined); 118 | } 119 | }`; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/commands/CommandInit.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import * as path from "path"; 3 | import * as yargs from "yargs"; 4 | import { exec } from "child_process"; 5 | 6 | import { EMOJIS } from "../utils/emojis"; 7 | import { MESSAGES } from "../utils/messages"; 8 | import { CommandUtils } from "./CommandUtils"; 9 | import { errorMessage, structureInitialProject } from "../utils/helpers"; 10 | import { ProjectInitTemplate } from "../templates/ProjectInitTemplate"; 11 | import { PATHS } from "../utils/paths"; 12 | 13 | export class InitCommand implements yargs.CommandModule { 14 | command = "create:project"; 15 | describe = "Generate initial Clean Architecture project structure."; 16 | 17 | builder(args: yargs.Argv) { 18 | return args.option("n", { 19 | alias: "name", 20 | describe: "Name of project directory", 21 | demandOption: true 22 | }); 23 | } 24 | 25 | async handler(args: yargs.Arguments) { 26 | let spinner; 27 | 28 | try { 29 | const basePath = process.cwd() + (args.name ? "/" + args.name : ""); 30 | const projectName = args.name 31 | ? path.basename(args.name as any) 32 | : undefined; 33 | 34 | const fileExists = await CommandUtils.fileExists(basePath); 35 | if (!fileExists) structureInitialProject(args.name as string); 36 | 37 | setTimeout(async () => { 38 | spinner = ora(`Installing... ${EMOJIS.COFFEE}`).start(); 39 | }, 2000); 40 | 41 | if (fileExists) throw MESSAGES.PROJECT_EXISTS(basePath); 42 | 43 | await CommandUtils.createFile( 44 | basePath + "/.dockerignore", 45 | ProjectInitTemplate.getDockerIgnore() 46 | ); 47 | await CommandUtils.createFile( 48 | basePath + "/.env", 49 | ProjectInitTemplate.getEnvExampleTemplate() 50 | ); 51 | await CommandUtils.createFile( 52 | basePath + "/.env.example", 53 | ProjectInitTemplate.getEnvExampleTemplate() 54 | ); 55 | await CommandUtils.createFile( 56 | basePath + "/.gitignore", 57 | ProjectInitTemplate.getGitIgnoreFile() 58 | ); 59 | await CommandUtils.createFile( 60 | basePath + "/docker-compose.yml", 61 | ProjectInitTemplate.getDockerCompose() 62 | ); 63 | await CommandUtils.createFile( 64 | basePath + "/package.json", 65 | ProjectInitTemplate.getPackageJsonTemplate(projectName), 66 | false 67 | ); 68 | await CommandUtils.createFile( 69 | basePath + "/README.md", 70 | ProjectInitTemplate.getReadmeTemplate() 71 | ); 72 | await CommandUtils.createFile( 73 | basePath + "/tsconfig.json", 74 | ProjectInitTemplate.getTsConfigTemplate() 75 | ); 76 | await CommandUtils.createFile( 77 | basePath + "/tsconfig-build.json", 78 | ProjectInitTemplate.getTsConfigBuildTemplate() 79 | ); 80 | 81 | await CommandUtils.createFile( 82 | basePath + "/src/application/config/environment.ts", 83 | ProjectInitTemplate.getEnvironmentTemplate() 84 | ); 85 | await CommandUtils.createFile( 86 | basePath + "/src/application/app.ts", 87 | ProjectInitTemplate.getAppTemplate() 88 | ); 89 | await CommandUtils.createFile( 90 | PATHS.PATH_SINGLETON(basePath), 91 | ProjectInitTemplate.getSingleton() 92 | ); 93 | await CommandUtils.createFile( 94 | basePath + "/src/index.ts", 95 | ProjectInitTemplate.getIndexTemplate() 96 | ); 97 | 98 | await CommandUtils.createFile( 99 | basePath + "/src/deployment/Dockerfile", 100 | ProjectInitTemplate.getDockerfileTemplate() 101 | ); 102 | 103 | await CommandUtils.createDirectories(basePath + "/src/domain/entities"); 104 | await CommandUtils.createDirectories( 105 | basePath + "/src/domain/use-cases/impl" 106 | ); 107 | await CommandUtils.createDirectories( 108 | basePath + "/src/infrastructure/driven-adapters/adapters" 109 | ); 110 | await CommandUtils.createFile( 111 | basePath + "/src/infrastructure/driven-adapters/index.ts", 112 | ProjectInitTemplate.getDrivenAdaptersIndex() 113 | ); 114 | await CommandUtils.createFile( 115 | basePath + "/src/infrastructure/driven-adapters/adapters/index.ts", 116 | ProjectInitTemplate.getAdaptersIndex() 117 | ); 118 | await CommandUtils.createDirectories( 119 | basePath + "/src/infrastructure/driven-adapters/providers" 120 | ); 121 | await CommandUtils.createFile( 122 | basePath + "/src/infrastructure/driven-adapters/providers/index.ts", 123 | ProjectInitTemplate.getProvidersTemplate() 124 | ); 125 | 126 | await CommandUtils.createDirectories( 127 | basePath + "/src/infrastructure/entry-points/api" 128 | ); 129 | await CommandUtils.createDirectories( 130 | basePath + "/src/infrastructure/entry-points/helpers" 131 | ); 132 | await CommandUtils.createFile( 133 | basePath + "/src/infrastructure/entry-points/api/index.ts", 134 | ProjectInitTemplate.getIndexApiTemplate() 135 | ); 136 | await CommandUtils.createFile( 137 | basePath + "/src/infrastructure/entry-points/index.ts", 138 | ProjectInitTemplate.getEntryPointsTemplate() 139 | ); 140 | 141 | await CommandUtils.createDirectories(basePath + "/tests/domain"); 142 | await CommandUtils.createDirectories(basePath + "/tests/infrastructure"); 143 | 144 | const packageJsonContents = await CommandUtils.readFile( 145 | basePath + "/package.json" 146 | ); 147 | await CommandUtils.createFile( 148 | basePath + "/package.json", 149 | ProjectInitTemplate.appendPackageJson(packageJsonContents) 150 | ); 151 | 152 | if (args.name) { 153 | setTimeout(() => { 154 | spinner.stopAndPersist({ 155 | symbol: EMOJIS.ROCKET, 156 | text: MESSAGES.PROJECT_SUCCESS(basePath) 157 | }); 158 | console.log(""); 159 | console.log("👉 Get started with the following commands:"); 160 | console.log(""); 161 | console.log(`$ cd ${args.name}`); 162 | console.log(`$ npm run watch`); 163 | console.log(""); 164 | spinner.text = `We continue to work for you... ${EMOJIS.COFFEE}`; 165 | spinner.start(); 166 | const installProcess = exec("npm install", { cwd: basePath }); 167 | installProcess.on("exit", (code) => { 168 | if (code === 0) { 169 | spinner.succeed("Installation completed"); 170 | } else { 171 | spinner.fail("Installation failed"); 172 | } 173 | }); 174 | }, 5000); 175 | } 176 | 177 | await InitCommand.executeCommand("npm install", basePath); 178 | } catch (error) { 179 | setTimeout( 180 | () => ( 181 | spinner.fail("Installation fail"), errorMessage(error, "project") 182 | ), 183 | 3000 184 | ); 185 | } 186 | } 187 | 188 | protected static executeCommand(command: string, basePath: string) { 189 | return new Promise((resolve, reject) => { 190 | exec( 191 | `cd ${basePath} && ${command}`, 192 | (error: any, stdout: any, stderr: any) => { 193 | if (stdout) return resolve(stdout); 194 | if (stderr) return reject(stderr); 195 | if (error) return reject(error); 196 | resolve(""); 197 | } 198 | ); 199 | }); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lib/commands/CommandUtils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | import {mkdirp} from 'mkdirp' 4 | import {MESSAGES} from "../utils/messages"; 5 | import {CONSTANTS} from "../utils/constants"; 6 | 7 | export class CommandUtils { 8 | 9 | /** 10 | * Creates directories recursively 11 | * 12 | * @param directory 13 | * @returns 14 | */ 15 | static createDirectories(directory: string) { 16 | return mkdirp(directory, 0o777) 17 | } 18 | 19 | /** 20 | * Creates a file with the given content in the given path 21 | * 22 | * @param filePath 23 | * @param content 24 | * @param override 25 | * @returns 26 | */ 27 | static async createFile(filePath: string, content: string, override: boolean = true): Promise { 28 | await CommandUtils.createDirectories(path.dirname(filePath)) 29 | return new Promise((resolve, reject) => { 30 | if (override === false && fs.existsSync(filePath)) 31 | return resolve() 32 | 33 | fs.writeFile(filePath, content, err => err ? reject(err) : resolve()) 34 | }) 35 | } 36 | 37 | /** 38 | * Reads everything from a given file and returns its content as a string 39 | * 40 | * @param filePath 41 | * @returns 42 | */ 43 | static async readFile(filePath: string): Promise { 44 | return new Promise((resolve, reject) => { 45 | fs.readFile(filePath, (err, data) => err ? reject(err) : resolve(data.toString())) 46 | }) 47 | } 48 | 49 | /** 50 | * @param filePath 51 | * @returns 52 | */ 53 | static async fileExists(filePath: string) { 54 | return fs.existsSync(filePath) 55 | } 56 | 57 | /** 58 | * @param filePath 59 | */ 60 | static async deleteFile(filePath: string) { 61 | return fs.unlink(filePath, (err) => err) 62 | } 63 | 64 | /** 65 | * Capitalize string 66 | * @param param 67 | */ 68 | static capitalizeString(param: string) { 69 | let capitalize = param.replace(/(\b\w)/g, (str) => str.toUpperCase()); 70 | return capitalize.replace(/-/g, "") 71 | } 72 | 73 | /** 74 | * @param param 75 | */ 76 | static transformStringToUpperCase(param: string) { 77 | return param.toUpperCase(); 78 | } 79 | 80 | /** 81 | * Transforms the initial letter into lowercase 82 | * @param param 83 | */ 84 | static transformInitialString(param: string) { 85 | let stringInitial = param.replace(/(\b\w)/g, (str) => str.toUpperCase()); 86 | let lowerCaseString = stringInitial[0].toLowerCase() + stringInitial.substring(1); 87 | return lowerCaseString.replace(/-/g, "") 88 | } 89 | 90 | /** 91 | * Reads the files in the service implementation directory 92 | * @param directory 93 | */ 94 | static injectServiceAdapter(directory: string): Promise { 95 | return new Promise((resolve, reject) => { 96 | fs.readdir(directory, function (error, files: string[]) { 97 | if (error) return error; 98 | resolve(files); 99 | }); 100 | }) 101 | } 102 | 103 | /** 104 | * 105 | * @param directory 106 | * @param name 107 | */ 108 | static readModelFiles(directory: string, name: string) { 109 | fs.readdir(directory, function (error, files: string[]) { 110 | let fileExist: boolean; 111 | const result = files.find(item => item.slice(0, -3) === name); 112 | 113 | fileExist = result?.slice(0, -3) === name 114 | 115 | if (!fileExist) { 116 | console.log(MESSAGES.ERROR_MODEL(name)); 117 | process.exit(1); 118 | } 119 | }); 120 | } 121 | 122 | /** 123 | * 124 | * @param directory 125 | * @param manager 126 | */ 127 | static readManagerFiles(directory: string, manager: string) { 128 | if (manager === CONSTANTS.MYSQL || manager === CONSTANTS.POSTGRES) { 129 | fs.readdir(directory, function (error, files) { 130 | if (!files === undefined) { 131 | let flag: boolean; 132 | const searchManager = files.slice(0)[0].split("-")[1]; 133 | 134 | flag = searchManager === manager 135 | if (!flag) { 136 | console.log(MESSAGES.ERROR_MANAGER(manager, searchManager)); 137 | process.exit(1); 138 | } 139 | } 140 | return; 141 | }) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/contracts/SingletonInterace.ts: -------------------------------------------------------------------------------- 1 | import { SingletonTypes } from "types/SingletonTypes"; 2 | 3 | export interface SingletonInterface { 4 | generate(params: SingletonTypes): void; 5 | } 6 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs' 2 | import {InitCommand} from "./commands/CommandInit" 3 | import {EntityCreateCommand} from './commands/CommandCreateEntity' 4 | import {ServiceCreateCommand} from "./commands/CommandCreateService"; 5 | import {ControllerCreateCommand} from "./commands/CommandCreateController"; 6 | import {InterfaceCreateCommand} from "./commands/CommandCreateInterface"; 7 | import {AdapterCreateCommand} from "./commands/CommandCreateAdapter"; 8 | import {CommandCreateAdapterSimple} from "./commands/CommandCreateAdapterSimple"; 9 | import {CommandCreateServiceResource} from "./commands/CommandCreateServiceResource"; 10 | import {CommandCreateInterfaceResource} from './commands/CommandCreateInterfaceResource'; 11 | 12 | yargs.scriptName("scaffold").usage("Usage: $0 [options]") 13 | .command(new InitCommand()) 14 | .command(new AdapterCreateCommand()) 15 | .command(new CommandCreateAdapterSimple()) 16 | .command(new EntityCreateCommand()) 17 | .command(new InterfaceCreateCommand()) 18 | .command(new CommandCreateInterfaceResource()) 19 | .command(new ServiceCreateCommand()) 20 | .command(new CommandCreateServiceResource()) 21 | .command(new ControllerCreateCommand()) 22 | .recommendCommands() 23 | .demandCommand(1) 24 | .strict() 25 | .alias("v", "version") 26 | .help("h") 27 | .alias("h", "help") 28 | .argv 29 | 30 | require("yargonaut") 31 | .style("blue") 32 | .style("yellow", "required") 33 | .helpStyle("green") 34 | .errorsStyle("red"); 35 | -------------------------------------------------------------------------------- /lib/templates/AdaptersTemplate.ts: -------------------------------------------------------------------------------- 1 | import { CommandUtils } from "../commands/CommandUtils"; 2 | import { CONSTANTS } from "../utils/constants"; 3 | import { MESSAGES } from "../utils/messages"; 4 | 5 | export class AdaptersTemplate { 6 | /** 7 | * Metodo para crear el Adaptador de acuerdo al ORM que recibe como parametro 8 | * 9 | * @param param Nombre de la entidad 10 | * @param orm Nombre del orm (sequelize, mongo) 11 | * @param manager Nombre del gestor de base de datos (mysql, pg, mongoose) 12 | * @return 13 | * ```typescript 14 | * // Adapter 15 | * import {UserEntity} from "@/domain/entities/user"; 16 | * import {UserModelSchema} from "@/infrastructure/driven-adapters/adapters/orm/mongo/models/user-mongoose"; 17 | * 18 | * export class UserMongoRepositoryAdapter {} 19 | * ``` 20 | */ 21 | public static getRepositoryAdapter( 22 | param: string, 23 | orm: string, 24 | manager?: string 25 | ): string { 26 | const _param = CommandUtils.capitalizeString(param); 27 | const _orm = CommandUtils.capitalizeString(orm); 28 | 29 | switch (orm) { 30 | case CONSTANTS.MONGO: 31 | return `import {${_param}Entity} from "@/domain/entities/${param}"; 32 | import {${_param}ModelSchema} from "@/infrastructure/driven-adapters/adapters/orm/${orm}/models/${param}-${manager}"; 33 | 34 | export class ${_param}${_orm}RepositoryAdapter { 35 | // Implementation 36 | } 37 | `; 38 | case CONSTANTS.SEQUELIZE: 39 | if (manager === CONSTANTS.MYSQL || manager === CONSTANTS.POSTGRES) { 40 | const _manager = CommandUtils.capitalizeString(manager); 41 | return `import {${_param}Entity} from "@/domain/entities/${param}"; 42 | import {${_param}Model${_manager}}from "@/infrastructure/driven-adapters/adapters/orm/${orm}/models/${param}-${manager}"; 43 | 44 | export class ${_param}${_manager}RepositoryAdapter { 45 | // Implementation 46 | } 47 | `; 48 | } else { 49 | throw MESSAGES.ERROR_DATABASE(manager); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/templates/DatabaseTemplate.ts: -------------------------------------------------------------------------------- 1 | import { CommandUtils } from "../commands/CommandUtils"; 2 | 3 | /** 4 | * Clase para generar instancias de conexión a bases de datos con el patrón singletón 5 | * 6 | * @class DatabaseTemplate 7 | * @author John Piedrahita 8 | * @access public 9 | * @version 1.0.0 10 | */ 11 | export class DatabaseTemplate { 12 | 13 | /** 14 | * Método para generar una instancia de conexión para Mongo con el patrón Singletón 15 | * 16 | * @param orm string mongo 17 | * @returns 18 | * ```typescript 19 | * import { connect, set } from "mongoose"; 20 | * import { Logger } from "@tsclean/core"; 21 | * import { MONGODB_URI } from "@/application/config/environment"; 22 | * 23 | * export class MongoConfiguration { 24 | * private logger: Logger; 25 | * private static instance: MongoConfiguration; 26 | * 27 | * private constructor() { 28 | * this.logger = new Logger(MongoConfiguration.name); 29 | * } 30 | * 31 | * public static getInstance(): MongoConfiguration { 32 | * if (!this.instance) { 33 | * this.instance = new MongoConfiguration(); 34 | * } 35 | * return this.instance; 36 | * } 37 | * 38 | * public async managerConnectionMongo(): Promise { 39 | * set("strictQuery", true); 40 | * 41 | * try { 42 | * await connect(MONGODB_URI); 43 | * this.logger.log(`Connection successfully to database of Mongo: ${MONGODB_URI}`); 44 | * } catch (error) { 45 | * this.logger.error("Failed to connect to MongoDB", error); 46 | * } 47 | * } 48 | * } 49 | * ``` 50 | */ 51 | static getMongooseSingleton(orm: string): string { 52 | const nameCapitalize = CommandUtils.capitalizeString(orm); 53 | return `import { connect, set } from "mongoose"; 54 | import { Logger } from "@tsclean/core"; 55 | import { MONGODB_URI } from "@/application/config/environment"; 56 | 57 | export class ${nameCapitalize}Configuration { 58 | private logger: Logger; 59 | private static instance: ${nameCapitalize}Configuration; 60 | 61 | private constructor() { 62 | this.logger = new Logger(${nameCapitalize}Configuration.name); 63 | } 64 | 65 | public static getInstance(): ${nameCapitalize}Configuration { 66 | if (!this.instance) { 67 | this.instance = new ${nameCapitalize}Configuration(); 68 | } 69 | return this.instance; 70 | } 71 | 72 | public async managerConnection${nameCapitalize}(): Promise { 73 | set("strictQuery", true); 74 | 75 | try { 76 | await connect(MONGODB_URI); 77 | this.logger.log(\`Connection successfully to database of Mongo: \${MONGODB_URI}\`); 78 | } catch (error) { 79 | this.logger.error("Failed to connect to MongoDB", error); 80 | } 81 | } 82 | } 83 | `; 84 | } 85 | 86 | /** 87 | * Metodo que generar una instancia con el patrón Singleto para las instancias de bases de datos relacionales 88 | * 89 | * @param name Nombre del modelo 90 | * @param manager Gestor de base de datos (mysql, pg, mongoose) 91 | * @returns 92 | * ```typescript 93 | * import { Sequelize } from "sequelize-typescript"; 94 | 95 | import { Logger } from "@tsclean/core"; 96 | import { CONFIG_MYSQL } from "@/application/config/environment"; 97 | import { UserModelMysql } from "@/infrastructure/driven-adapters/adapters/orm/sequelize/models/user-mysql"; 98 | 99 | export class MysqlConfiguration { 100 | private logger: Logger; 101 | private static instance: MysqlConfiguration; 102 | 103 | public sequelize: Sequelize; 104 | 105 | private constructor() { 106 | this.logger = new Logger(MysqlConfiguration.name); 107 | this.sequelize = new Sequelize( 108 | CONFIG_MYSQL.database, 109 | CONFIG_MYSQL.user, 110 | CONFIG_MYSQL.password, 111 | { 112 | host: CONFIG_MYSQL.host, 113 | dialect: "mysql", 114 | // This array contains all the system models that are used for Mysql. 115 | models: [ 116 | UserModelMysql 117 | ] 118 | } 119 | ); 120 | } 121 | 122 | public static getInstance(): MysqlConfiguration { 123 | if (!MysqlConfiguration.instance) { 124 | MysqlConfiguration.instance = new MysqlConfiguration(); 125 | } 126 | return MysqlConfiguration.instance; 127 | } 128 | 129 | public async managerConnectionMysql(): Promise { 130 | try { 131 | await this.sequelize.authenticate(); 132 | this.logger.log( 133 | `Connection successfully to database of Mysql: ${CONFIG_MYSQL.database}` 134 | ); 135 | } catch (error) { 136 | this.logger.error("Failed to connect to Mysql", error); 137 | } 138 | } 139 | } 140 | * ``` 141 | */ 142 | static getMysqlAndPostgresSingleton(name: string, manager: string): string { 143 | const configEnv = `CONFIG_${manager.toUpperCase()}`; 144 | const nameCapitalize = CommandUtils.capitalizeString(name); 145 | const managerCapitalize = CommandUtils.capitalizeString(manager); 146 | const dialect = manager === "pg" ? "postgres" : "mysql"; 147 | 148 | return `import { Sequelize } from "sequelize-typescript"; 149 | 150 | import { Logger } from "@tsclean/core"; 151 | import { ${configEnv} } from "@/application/config/environment"; 152 | import { ${nameCapitalize}Model${managerCapitalize} } from "@/infrastructure/driven-adapters/adapters/orm/sequelize/models/${name}-${manager}"; 153 | 154 | /** 155 | * Class that generates a connection instance for ${managerCapitalize} using the Singleton pattern 156 | */ 157 | export class ${managerCapitalize}Configuration { 158 | private logger: Logger; 159 | private static instance: ${managerCapitalize}Configuration; 160 | 161 | public sequelize: Sequelize; 162 | 163 | private constructor() { 164 | this.logger = new Logger(${managerCapitalize}Configuration.name); 165 | this.sequelize = new Sequelize( 166 | ${configEnv}.database, 167 | ${configEnv}.user, 168 | ${configEnv}.password, 169 | { 170 | host: ${configEnv}.host, 171 | dialect: "${dialect}", 172 | // This array contains all the system models that are used for ${managerCapitalize}. 173 | models: [ 174 | ${nameCapitalize}Model${managerCapitalize} 175 | ] 176 | } 177 | ); 178 | } 179 | 180 | public static getInstance(): ${managerCapitalize}Configuration { 181 | if (!${managerCapitalize}Configuration.instance) { 182 | ${managerCapitalize}Configuration.instance = new ${managerCapitalize}Configuration(); 183 | } 184 | return ${managerCapitalize}Configuration.instance; 185 | } 186 | 187 | public async managerConnection${managerCapitalize}(): Promise { 188 | try { 189 | await this.sequelize.authenticate(); 190 | this.logger.log( 191 | \`Connection successfully to database of ${managerCapitalize}: \${${configEnv}.database}\` 192 | ); 193 | } catch (error) { 194 | this.logger.error("Failed to connect to ${managerCapitalize}", error); 195 | } 196 | } 197 | } 198 | `; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /lib/templates/ModelsTemplate.ts: -------------------------------------------------------------------------------- 1 | import { CommandUtils } from "../commands/CommandUtils"; 2 | import { CONSTANTS } from "../utils/constants"; 3 | 4 | export class ModelsTemplate { 5 | /** 6 | * Metodo para crear el modelo del adaptador de acuero al ORM 7 | * 8 | * @param param Nombre de la entidad 9 | * @param orm Nombre del orm (sequelize, mongo) 10 | * @param manager Nombre del gestor de base de datos (mysql, pg, mongoose) 11 | * ```typescript 12 | * // Model 13 | * import { model, Schema } from "mongoose"; 14 | * import { UserEntity } from '@/domain/entities/user'; 15 | * 16 | * const schema = new Schema({}); 17 | * 18 | * export const UserModelSchema = model('users', schema); 19 | * ``` 20 | */ 21 | public static getModels(param: string, orm: string, manager?: string) { 22 | const _name = CommandUtils.capitalizeString(param); 23 | 24 | switch (orm) { 25 | case CONSTANTS.MONGO: 26 | return `import { model, Schema } from "mongoose"; 27 | import { ${_name}Entity } from '@/domain/entities/${param}'; 28 | 29 | const schema = new Schema<${_name}Entity>({ 30 | // Implementation 31 | }); 32 | 33 | export const ${_name}ModelSchema = model<${_name}Entity>('${param}s', schema); 34 | `; 35 | case CONSTANTS.SEQUELIZE: 36 | const _manager = CommandUtils.capitalizeString(manager); 37 | return `import { Table, Column, Model, Sequelize } from 'sequelize-typescript' 38 | import { ${_name}Entity } from "@/domain/entities/${param}"; 39 | 40 | @Table({ tableName: '${param}s' }) 41 | export class ${_name}Model${_manager} extends Model<${_name}Entity> { 42 | // Implementation 43 | }`; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/templates/ProjectInitTemplate.ts: -------------------------------------------------------------------------------- 1 | export class ProjectInitTemplate { 2 | /** 3 | * Get contents of environment.ts file 4 | * @returns 5 | */ 6 | static getEnvironmentTemplate(): string { 7 | return `import dotenv from "dotenv"; 8 | 9 | dotenv.config({ path: ".env" }) 10 | 11 | 12 | /** 13 | |----------------------------------------------------------------------------------------| 14 | App Configuration 15 | |----------------------------------------------------------------------------------------| 16 | */ 17 | export const ENVIRONMENT = process.env.NODE_ENV; 18 | const PROD = ENVIRONMENT === "production" 19 | export const PORT = process.env.PORT 20 | 21 | 22 | /** 23 | |----------------------------------------------------------------------------------------| 24 | Authentication Configuration 25 | |----------------------------------------------------------------------------------------| 26 | */ 27 | 28 | export const SESSION_SECRET = process.env.JWT_SECRET || "" 29 | 30 | /** 31 | * Use only if you include jwt 32 | */ 33 | // if (!SESSION_SECRET) process.exit(1) 34 | 35 | 36 | /** 37 | |----------------------------------------------------------------------------------------| 38 | Databases Configuration 39 | |----------------------------------------------------------------------------------------| 40 | */ 41 | 42 | /** 43 | * MySQL 44 | */ 45 | export const CONFIG_MYSQL = { 46 | host : process.env.DB_HOST_MYSQL, 47 | user : process.env.DB_USER_MYSQL, 48 | password : process.env.DB_PASSWORD_MYSQL, 49 | database : process.env.DATABASE_MYSQL, 50 | port : process.env.DB_PORT_MYSQL 51 | } 52 | 53 | /** 54 | * Mongo DB 55 | */ 56 | export const MONGODB_URI = PROD 57 | ? process.env.MONGO_PRODUCTION 58 | : process.env.MONGO_DEVELOPMENT 59 | 60 | /** 61 | * Postgres 62 | */ 63 | export const CONFIG_PG = { 64 | host : process.env.DB_HOST_POSTGRES, 65 | user : process.env.DB_USER_POSTGRES, 66 | database : process.env.DATABASE_POSTGRES, 67 | password : process.env.DB_PASSWORD_POSTGRES, 68 | port : process.env.DB_PORT_POSTGRES, 69 | } 70 | `; 71 | } 72 | 73 | /** 74 | * Gets content of the app.ts file 75 | * @returns 76 | */ 77 | static getAppTemplate(): string { 78 | return `import {Container} from "@tsclean/core"; 79 | import {controllers} from "@/infrastructure/entry-points/api"; 80 | import {services, adapters} from "@/infrastructure/driven-adapters/providers"; 81 | 82 | @Container({ 83 | providers: [...services, ...adapters], 84 | controllers: [...controllers] 85 | }) 86 | 87 | export class AppContainer {} 88 | `; 89 | } 90 | 91 | /** 92 | * Get contents of tsconfig.json file 93 | * @returns 94 | */ 95 | static getTsConfigTemplate(): string { 96 | return JSON.stringify( 97 | { 98 | compilerOptions: { 99 | experimentalDecorators: true, 100 | emitDecoratorMetadata: true, 101 | outDir: "./dist", 102 | module: "commonjs", 103 | target: "es2019", 104 | esModuleInterop: true, 105 | sourceMap: true, 106 | rootDirs: ["src", "tests"], 107 | baseUrl: "src", 108 | paths: { 109 | "@/tests/*": ["../tests/*"], 110 | "@/*": ["*"] 111 | } 112 | }, 113 | include: ["src", "tests"], 114 | exclude: [] 115 | }, 116 | undefined, 117 | 3 118 | ); 119 | } 120 | 121 | /** 122 | * Gets contents of the new readme.md file. 123 | * @returns 124 | */ 125 | static getReadmeTemplate(): string { 126 | return `## Awesome Project Build with Clean Architecture 127 | 128 | Steps to run this project: 129 | 130 | 1. Run \`npm watch\` command 131 | 132 | `; 133 | } 134 | 135 | /** 136 | * 137 | * @returns 138 | */ 139 | static getGitIgnoreFile(): string { 140 | return `.idea/ 141 | .vscode/ 142 | node_modules/ 143 | build/ 144 | .env 145 | package-lock.json 146 | dist 147 | `; 148 | } 149 | 150 | static getDockerCompose() { 151 | return `version: '3.1' 152 | services: 153 | # Api Service 154 | api: 155 | build: 156 | context: . 157 | dockerfile: ./src/deployment/Dockerfile 158 | volumes: 159 | - ./:/app 160 | - /app/node_modules 161 | ports: 162 | - "9000:9000" 163 | environment: 164 | - NODE_ENV=development 165 | - PORT=9000 166 | command: sh -c 'npm install && npm run watch' 167 | networks: 168 | - api_network 169 | 170 | # # MySQL Service 171 | # mysql: 172 | # image: mysql:8.0 173 | # container_name: mysql 174 | # command: --default-authentication-plugin=mysql_native_password 175 | # restart: always 176 | # ports: 177 | # - \${DB_PORT_MYSQL}:3306 178 | # environment: 179 | # MYSQL_DATABASE: \${DATABASE_MYSQL} 180 | # MYSQL_ROOT_PASSWORD: \${DB_PASSWORD_MYSQL} 181 | # MYSQL_PASSWORD: \${DB_PASSWORD_MYSQL} 182 | # MYSQL_USER: \${DB_USER_MYSQL} 183 | # SERVICE_TAGS: dev 184 | # SERVICE_NAME: mysql 185 | # volumes: 186 | # - mysql_data:/var/lib/mysql 187 | # networks: 188 | # - api_network 189 | 190 | # # Mongo Service 191 | # mongo: 192 | # image: mongo 193 | # restart: always 194 | # container_name: mongo 195 | # ports: 196 | # - "27017:27017" 197 | # volumes: 198 | # - mongo_db:/data/db 199 | # - mongo_config:/data/config 200 | # networks: 201 | # - api_network 202 | 203 | # # Pg Service 204 | # postgres: 205 | # image: postgres:latest 206 | # container_name: api_postgres 207 | # environment: 208 | # POSTGRES_USER: \${DB_USER_POSTGRES} 209 | # POSTGRES_PASSWORD: \${DB_PASSWORD_POSTGRES} 210 | # POSTGRES_DB: \${DATABASE_POSTGRES} 211 | # ports: 212 | # - \${DB_PORT_POSTGRES}:5432 213 | # volumes: 214 | # - postgres_db:/var/lib/postgresql/data 215 | # networks: 216 | # - api_network 217 | 218 | # # pgAdmin Service 219 | # pgadmin: 220 | # image: dpage/pgadmin4:latest 221 | # environment: 222 | # PGADMIN_DEFAULT_EMAIL: admin@example.com # Change to the email you want to use to login to pgAdmin 223 | # PGADMIN_DEFAULT_PASSWORD: admin # Change to the password you want to use to login to pgAdmin 224 | # ports: 225 | # - "5050:80" 226 | # depends_on: 227 | # - postgres 228 | # networks: 229 | # - api_network 230 | 231 | # # phpAdmin Service 232 | # phpmyadmin: 233 | # image: phpmyadmin 234 | # depends_on: 235 | # - mysql 236 | # environment: 237 | # - UPLOAD_LIMIT=200M 238 | # - POST_MAX_SIZE=200M 239 | # - PHP_UPLOAD_MAX_FILESIZE=200M 240 | # ports: 241 | # - "8080:80" 242 | # networks: 243 | # - api_network 244 | 245 | # # mongoexpress Service 246 | # mongoexpress: 247 | # image: mongo-express 248 | # ports: 249 | # - "8081:8081" 250 | # depends_on: 251 | # - mongo 252 | # environment: 253 | # ME_CONFIG_MONGODB_URL: mongodb://mongo:27017/ 254 | # networks: 255 | # - api_network 256 | 257 | # volumes: 258 | # mysql_data: 259 | # mongo_db: 260 | # mongo_config: 261 | # postgres_db: 262 | 263 | networks: 264 | api_network: 265 | driver: bridge 266 | `; 267 | } 268 | 269 | /** 270 | * Gets contents of the package.json file. 271 | * @param projectName 272 | * @returns 273 | */ 274 | static getPackageJsonTemplate(projectName?: string): string { 275 | return JSON.stringify( 276 | { 277 | name: projectName || "clean-architecture", 278 | version: "1.0.0", 279 | description: "Awesome project developed with Clean Architecture", 280 | scripts: {}, 281 | dependencies: {}, 282 | devDependencies: {}, 283 | _moduleAliases: {}, 284 | engines: {} 285 | }, 286 | undefined, 287 | 3 288 | ); 289 | } 290 | 291 | /** 292 | * Appends to a given package.json template everything needed. 293 | * @param packageJson 294 | * @returns 295 | */ 296 | static appendPackageJson(packageJson: string): string { 297 | const packageJsonContent = JSON.parse(packageJson); 298 | 299 | if (!packageJsonContent.devDependencies) 300 | packageJsonContent.devDependencies = {}; 301 | Object.assign(packageJsonContent.devDependencies, { 302 | "@types/node": "^20.8.4", 303 | "@types/jest": "^29.5.14", 304 | jest: "^29.7.0", 305 | nodemon: "^3.0.7", 306 | rimraf: "^6.0.1", 307 | "ts-jest": "^29.2.5", 308 | "ts-node": "^10.9.2", 309 | typescript: "^5.6.3" 310 | }); 311 | 312 | packageJsonContent.dependencies["@tsclean/core"] = "^1.13.0"; 313 | packageJsonContent.dependencies["dotenv"] = "^16.4.5"; 314 | packageJsonContent.dependencies["helmet"] = "^8.0.0"; 315 | packageJsonContent.dependencies["module-alias"] = "^2.2.3"; 316 | 317 | packageJsonContent.scripts["start"] = "node ./dist/index.js"; 318 | packageJsonContent.scripts["build"] = 319 | "rimraf dist && tsc -p tsconfig-build.json"; 320 | packageJsonContent.scripts["watch"] = 321 | 'nodemon --exec "npm run build && npm run start" --watch src --ext ts'; 322 | 323 | packageJsonContent._moduleAliases["@"] = "dist"; 324 | packageJsonContent.engines["node"] = ">=20.15.1"; 325 | 326 | return JSON.stringify(packageJsonContent, undefined, 3); 327 | } 328 | 329 | static getEnvExampleTemplate() { 330 | return `# Mongo configuration 331 | # If you run the project with the local configuration [docker-compose.yml], 332 | # the host will be the Mongo container name 333 | MONGO_DEVELOPMENT= 334 | MONGO_PRODUCTION= 335 | 336 | # Mysql configuration 337 | DB_USER_MYSQL= 338 | DB_PASSWORD_MYSQL= 339 | DATABASE_MYSQL= 340 | DB_PORT_MYSQL= 341 | # If you run the project with the local configuration [docker-compose.yml], 342 | # the host will be the MySQL container name 343 | DB_HOST_MYSQL= 344 | 345 | # Postgres configuration 346 | DB_USER_POSTGRES= 347 | DATABASE_POSTGRES= 348 | DB_PASSWORD_POSTGRES= 349 | DB_PORT_POSTGRES= 350 | # If you run the project with the local configuration [docker-compose.yml], 351 | # the host will be the postgres container name 352 | DB_HOST_POSTGRES= 353 | 354 | JWT_SECRET= 355 | NODE_ENV=development 356 | HOST=127.0.0.1 357 | PORT=9000`; 358 | } 359 | 360 | static getTsConfigBuildTemplate() { 361 | return `{ 362 | "extends": "./tsconfig.json", 363 | "exclude": [ 364 | "coverage", 365 | "jest.config.js", 366 | "**/*.spec.ts", 367 | "**/*.test.ts", 368 | "**/tests" 369 | ] 370 | }`; 371 | } 372 | 373 | static getDockerIgnore() { 374 | return `Dockerfile 375 | node_modules`; 376 | } 377 | 378 | static getIndexTemplate() { 379 | return `import "module-alias/register"; 380 | 381 | import helmet from 'helmet'; 382 | import { StartProjectInit } from "@tsclean/core"; 383 | 384 | import { AppContainer } from "@/application/app"; 385 | import { PORT } from "@/application/config/environment"; 386 | import { singletonInitializers } from "@/application/singleton"; 387 | 388 | async function init(): Promise { 389 | /** Iterate the singleton functions */ 390 | for (const initFn of singletonInitializers) { 391 | await initFn(); 392 | } 393 | 394 | const app = await StartProjectInit.create(AppContainer) 395 | app.use(helmet()); 396 | await app.listen(PORT, () => console.log(\`Running on port: \${PORT}\`)) 397 | } 398 | 399 | void init().catch();`; 400 | } 401 | 402 | static getIndexApiTemplate() { 403 | return `export const controllers = [];`; 404 | } 405 | 406 | static getDockerfileTemplate(): string { 407 | return `FROM node:19.9.0 408 | 409 | WORKDIR /app 410 | 411 | COPY package*.json ./ 412 | 413 | RUN npm install 414 | 415 | RUN npm install nodemon -g 416 | 417 | COPY . . 418 | 419 | EXPOSE 9000 420 | 421 | CMD ["nodemon", "/dist/index.js"] 422 | `; 423 | } 424 | 425 | static getProvidersTemplate(): string { 426 | return `export const adapters = []; 427 | 428 | export const services = [];`; 429 | } 430 | 431 | /** 432 | * Metodo para crear un array donde se alojarán los singletons de la aplicación 433 | * 434 | * @returns 435 | * ```typescript 436 | * export const singletonInitializers: Array<() => Promise> = []; 437 | * ``` 438 | */ 439 | public static getSingleton(): string { 440 | return `/** 441 | * This array has all the singleton instances of the application 442 | */ 443 | export const singletonInitializers: Array<() => Promise> = []; 444 | `; 445 | } 446 | 447 | static getAdaptersIndex() { 448 | return ``; 449 | } 450 | 451 | static getDrivenAdaptersIndex() { 452 | return ``; 453 | } 454 | 455 | static getEntryPointsTemplate() { 456 | return ``; 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /lib/templates/SingletonGenerateTemplate.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { 3 | ts, 4 | Project, 5 | SourceFile, 6 | VariableStatement, 7 | VariableDeclaration, 8 | VariableDeclarationKind, 9 | ArrayLiteralExpression, 10 | ArrowFunction, 11 | SyntaxKind 12 | } from "ts-morph"; 13 | import { SingletonTypes } from "../types/SingletonTypes"; 14 | import { CommandUtils } from "../commands/CommandUtils"; 15 | 16 | export class SingletonGenerateTemplate { 17 | private static project: Project = new Project(); 18 | 19 | static generate(params: SingletonTypes): void { 20 | const { filepath, manager, instance } = params; 21 | let sourceFile: SourceFile; 22 | let variableStatement: VariableStatement; 23 | let variableDeclaration: VariableDeclaration; 24 | let arrayLiteralExpression: ArrayLiteralExpression; 25 | 26 | if (existsSync(filepath)) { 27 | sourceFile = SingletonGenerateTemplate.project.addSourceFileAtPathIfExists(filepath); 28 | } else { 29 | sourceFile = SingletonGenerateTemplate.project.createSourceFile(filepath, "", { overwrite: true }); 30 | } 31 | 32 | if (sourceFile.getVariableStatement("singletonInitializers")) { 33 | variableStatement = sourceFile.getVariableStatement("singletonInitializers")!; 34 | variableDeclaration = variableStatement.getDeclarations()[0]; 35 | arrayLiteralExpression = variableDeclaration.getInitializerIfKindOrThrow(ts.SyntaxKind.ArrayLiteralExpression); 36 | } else { 37 | variableStatement = sourceFile.addVariableStatement({ 38 | declarationKind: VariableDeclarationKind.Const, 39 | declarations: [ 40 | { 41 | name: "singletonInitializers", 42 | type: "Array<() => Promise>", 43 | initializer: "[]" 44 | } 45 | ], 46 | isExported: true 47 | }); 48 | variableDeclaration = variableStatement.getDeclarations()[0]; 49 | arrayLiteralExpression = variableDeclaration.getInitializerIfKindOrThrow(ts.SyntaxKind.ArrayLiteralExpression); 50 | } 51 | 52 | SingletonGenerateTemplate.getCodeBlockSingleton(arrayLiteralExpression, manager, instance); 53 | SingletonGenerateTemplate.getCodeBlockImports(sourceFile, manager, instance); 54 | 55 | sourceFile.formatText({ placeOpenBraceOnNewLineForFunctions: true }); 56 | sourceFile.saveSync(); 57 | } 58 | 59 | private static getCodeBlockSingleton( 60 | arrayLiteralExpression: ArrayLiteralExpression, 61 | manager: string, 62 | orm: string 63 | ) { 64 | const nameCapitalize = CommandUtils.capitalizeString(manager); 65 | const ormCapitalize = CommandUtils.capitalizeString(orm); 66 | const instance = manager === "mongoose" ? ormCapitalize : nameCapitalize; 67 | const varName = `${manager}Config`; 68 | const expectedSnippet = `const ${varName} = ${instance}Configuration.getInstance();`; 69 | 70 | const alreadyExists = arrayLiteralExpression.getElements().some((el) => { 71 | const arrowFn = el.asKind(SyntaxKind.ArrowFunction); 72 | if (!arrowFn) return false; 73 | 74 | const bodyText = arrowFn.getBody().getText(); 75 | return bodyText.includes(expectedSnippet); 76 | }); 77 | 78 | if (!alreadyExists) { 79 | arrayLiteralExpression.addElement(`async () => { 80 | const ${varName} = ${instance}Configuration.getInstance(); 81 | await ${varName}.managerConnection${instance}(); 82 | }`); 83 | } 84 | } 85 | 86 | private static getCodeBlockImports( 87 | sourceFile: SourceFile, 88 | manager: string, 89 | orm: string 90 | ) { 91 | const nameCapitalize = CommandUtils.capitalizeString(manager); 92 | const ormCapitalize = CommandUtils.capitalizeString(orm); 93 | const instance = manager === "mongoose" ? ormCapitalize : nameCapitalize; 94 | 95 | const alreadyImported = sourceFile.getImportDeclarations().some((decl) => { 96 | return decl.getNamedImports().some((imp) => imp.getName() === `${instance}Configuration`); 97 | }); 98 | 99 | if (!alreadyImported) { 100 | sourceFile.addImportDeclaration({ 101 | moduleSpecifier: `@/application/config/${manager}-instance`, 102 | namedImports: [`${instance}Configuration`] 103 | }); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/types/SingletonTypes.ts: -------------------------------------------------------------------------------- 1 | export type SingletonTypes = { 2 | filepath: string; // Archivo que se va a editar 3 | manager: string; // Gestor de base de datos 4 | instance: string; // Orm (mongoose, sequelize) 5 | }; 6 | -------------------------------------------------------------------------------- /lib/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const CONSTANTS = { 2 | MONGO: "mongo", 3 | MONGOOSE: "mongoose", 4 | SEQUELIZE: "sequelize", 5 | MYSQL: "mysql", 6 | POSTGRES: "pg", 7 | INSTALLING: 'Installing...', 8 | NPM_INSTALL: "npm install", 9 | ADAPTER: "Adapter", 10 | INSTALLATION_COMPLETED: "Installation completed", 11 | } -------------------------------------------------------------------------------- /lib/utils/emojis.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'node-emoji'; 2 | 3 | export const EMOJIS = { 4 | HEART: get('heart'), 5 | COFFEE: get('coffee'), 6 | BEER: get('beer'), 7 | BROKEN_HEART: get('broken_heart'), 8 | CRYING: get('crying_cat_face'), 9 | HEART_EYES: get('heart_eyes_cat'), 10 | JOY: get('joy_cat'), 11 | KISSING: get('kissing_cat'), 12 | SCREAM: get('scream_cat'), 13 | ROCKET: get('rocket'), 14 | SMIRK: get('smirk_cat'), 15 | RAISED_HANDS: get('raised_hands'), 16 | POINT_RIGHT: get('point_right'), 17 | ZAP: get('zap'), 18 | BOOM: get('boom'), 19 | PRAY: get('pray'), 20 | WINE: get('wine_glass'), 21 | NO_ENTRY: get('no_entry_sign'), 22 | APPLICATION: get('🛠️'), 23 | DEPLOYMENT: get('✈️'), 24 | ENTITIES: get('📶'), 25 | CASES: get('🧳'), 26 | INFRA: get('🏢'), 27 | }; 28 | -------------------------------------------------------------------------------- /lib/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import figlet from 'figlet' 2 | import {MESSAGES} from "./messages"; 3 | import {exec} from "child_process"; 4 | import chalk from "chalk"; 5 | import {EMOJIS} from "./emojis"; 6 | 7 | export const banner = () => { 8 | console.log(""); 9 | figlet.text('Clean Scaffold.', {font: 'ANSI Shadow', whitespaceBreak: true}, (err, data) => { 10 | if (err) return; 11 | console.log(data) 12 | }); 13 | } 14 | 15 | export const errorMessage = (error, type) => { 16 | console.log(MESSAGES.ERROR_HANDLER(`Error during ${type} creation.`)) 17 | console.error(error) 18 | process.exit(1) 19 | } 20 | 21 | export const executeCommand = (command: string) => { 22 | return new Promise((resolve, reject) => { 23 | exec(command, (error: any, stdout: any, stderr: any) => { 24 | if (stdout) return resolve(stdout) 25 | if (stderr) return reject(stderr) 26 | if (error) return reject(error) 27 | resolve("") 28 | }) 29 | }) 30 | } 31 | 32 | export function structureInitialProject(name: string) { 33 | console.log(` 34 | ${EMOJIS.APPLICATION} ${chalk.blue("Layer APPLICATION")} 35 | 36 | ${chalk.green("CREATE")} ${name}/src/application/app.ts 37 | ${chalk.green("CREATE")} ${name}/src/application/config/environment.ts 38 | 39 | ${EMOJIS.ROCKET} ${chalk.cyan("Layer DEPLOYMENT")} 40 | 41 | ${chalk.green("CREATE")} ${name}/src/deployment/Dockerfile 42 | 43 | ${EMOJIS.ENTITIES} ${chalk.yellow("Layer ENTITIES - DOMAIN")} 44 | 45 | ${chalk.green("CREATE")} ${name}/src/domain/entities 46 | 47 | ${EMOJIS.CASES} ${chalk.red("Layer USE CASES - DOMAIN")} 48 | 49 | ${chalk.green("CREATE")} ${name}/src/domain/use-cases/impl 50 | 51 | ${EMOJIS.INFRA} ${chalk.greenBright("Layer INFRASTRUCTURE")} 52 | 53 | ${chalk.green("CREATE")} ${name}/src/infrastructure/driven-adapters/index.ts 54 | ${chalk.green("CREATE")} ${name}/src/infrastructure/driven-adapters/adapters/index.ts 55 | ${chalk.green("CREATE")} ${name}/src/infrastructure/driven-adapters/providers/index.ts 56 | ${chalk.green("CREATE")} ${name}/src/infrastructure/entry-points/index.ts 57 | ${chalk.green("CREATE")} ${name}/src/infrastructure/entry-points/api/index.ts 58 | 59 | ${chalk.green("CREATE")} ${name}/.dockerignore 60 | ${chalk.green("CREATE")} ${name}/.env 61 | ${chalk.green("CREATE")} ${name}/.env.example 62 | ${chalk.green("CREATE")} ${name}/.gitignore 63 | ${chalk.green("CREATE")} ${name}/README.md 64 | ${chalk.green("CREATE")} ${name}/docker-compose.yml 65 | ${chalk.green("CREATE")} ${name}/package.json 66 | ${chalk.green("CREATE")} ${name}/tsconfig.build.json 67 | ${chalk.green("CREATE")} ${name}/tsconfig.json 68 | ${chalk.green("CREATE")} ${name}/src/index.ts 69 | ${chalk.green("CREATE")} ${name}/tests/domain 70 | ${chalk.green("CREATE")} ${name}/tests/infrastructure 71 | `); 72 | } 73 | -------------------------------------------------------------------------------- /lib/utils/messages.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { EMOJIS } from "./emojis"; 3 | 4 | export const MESSAGES = { 5 | PROJECT_SUCCESS: (basePath) => 6 | chalk.green( 7 | "Project created inside " + chalk.blue(basePath) + " directory." 8 | ), 9 | PROJECT_EXISTS: (basePath) => 10 | `Project ${chalk.blue(basePath)} already exists`, 11 | FILE_SUCCESS: (type, path) => 12 | chalk.green(`${type} ${chalk.blue(path)} has been created successfully`), 13 | REPOSITORY_SUCCESS: (pathRepository) => 14 | " " + 15 | EMOJIS.ROCKET + 16 | " " + 17 | chalk.green( 18 | `Repository ${chalk.blue( 19 | pathRepository 20 | )} has been created successfully.\n` 21 | ), 22 | PROVIDER_SUCCESS: (pathAdapter) => 23 | " " + 24 | EMOJIS.ROCKET + 25 | " " + 26 | chalk.green( 27 | `Provider ${chalk.blue(pathAdapter)} has been created successfully.\n` 28 | ), 29 | UPDATE_FILE_SUCCESS: (base) => 30 | EMOJIS.ROCKET + 31 | " " + 32 | chalk.blue( 33 | `File ${chalk.green( 34 | base + "/src/application/server.ts" 35 | )} has been updated successfully.` 36 | ), 37 | FILE_EXISTS: (path) => 38 | `${EMOJIS.NO_ENTRY} File ${chalk.blue(path)} already exists`, 39 | CONFIG_ENV: () => 40 | `${chalk.green( 41 | `Continue setting the environment variables in the ${chalk.blue( 42 | ".env" 43 | )} file` 44 | )}`, 45 | ERROR_HANDLER: (message) => `${chalk.black.bgRedBright(message)}`, 46 | ERROR_MODEL: (name) => 47 | `${EMOJIS.NO_ENTRY} First you must create the entity ${chalk.red( 48 | name 49 | )} in order to be imported into the file.`, 50 | ERROR_MANAGER: (manager, args) => 51 | `${EMOJIS.NO_ENTRY} The manager ${chalk.red( 52 | manager 53 | )} can not be implemented because it already has the implementation of ${chalk.blue( 54 | args 55 | )}`, 56 | ERROR_INTERFACE: (path) => 57 | `${EMOJIS.NO_ENTRY} Path ${chalk.blue( 58 | path 59 | )} does not correspond to models, service the infra.`, 60 | ERROR_ORM: (orm) => 61 | `${EMOJIS.NO_ENTRY} ORM ${chalk.blue( 62 | orm 63 | )} does not correspond to mongoose the sequelize.`, 64 | ERROR_DATABASE: (manager) => 65 | `${EMOJIS.NO_ENTRY} Database manager ${chalk.blue( 66 | manager 67 | )} does not correspond to mysql the postgres.`, 68 | MESSAGE_ERROR_HANDLER: `${chalk.white( 69 | "The interface to connect the domain layer with the infrastructure layer has not been created." 70 | )}`, 71 | ERROR_UPDATE_INDEX: (filePath: string) => 72 | `${EMOJIS.NO_ENTRY} SingletonInitializers array not found in ${chalk.red( 73 | filePath 74 | )} this configuration must exist in order to create the Adapter. \n 75 | Example: export const singletonInitializers: Array<() => Promise> = []; \n 76 | if it does not exist, please include it in the file ${chalk.blue( 77 | filePath 78 | )}`, 79 | UPDATE_INDEX_SUCCESSFULLY: (filePath: string) => 80 | `${chalk.green( 81 | `The file ${chalk.blue(`${filePath}`)} was updated correctly` 82 | )}` 83 | }; 84 | -------------------------------------------------------------------------------- /lib/utils/paths.ts: -------------------------------------------------------------------------------- 1 | import { CONSTANTS } from "./constants"; 2 | 3 | export const PATHS = { 4 | PATH_INDEX: (base: string) => `${base}/src/index.ts`, 5 | PATH_MODELS_ENTITY: () => `${process.cwd()}/src/domain/entities/`, 6 | PATH_MODELS_ORM: (base: string, args: string) => 7 | `${base}/src/infrastructure/driven-adapters/adapters/orm/${args}`, 8 | BASE_PATH_ADAPTER: (orm: string) => 9 | `${process.cwd()}/src/infrastructure/driven-adapters/adapters/orm/${orm}/`, 10 | BASE_PATH_ADAPTER_SIMPLE: () => 11 | `${process.cwd()}/src/infrastructure/driven-adapters/adapters/`, 12 | FILE_NAME_ADAPTER: (name: string, manager: string, orm: string) => 13 | manager 14 | ? `${name}-${manager}-repository-adapter.ts` 15 | : `${name}-${orm}-repository-adapter.ts`, 16 | FILE_NAME_ADAPTER_SIMPLE: (name: string) => `${name}-adapter.ts`, 17 | PATH_ADAPTER: (base: string, orm: string, name: string, manager: string) => 18 | `${base}/src/infrastructure/driven-adapters/adapters/orm/${orm}/${name}-${manager}-repository-adapter.ts`, 19 | PATH_ADAPTER_SIMPLE: (name: string) => 20 | `${process.cwd()}/src/infrastructure/driven-adapters/adapters/${name}-adapter.ts`, 21 | PATH_PROVIDER_SEQUELIZE: ( 22 | base: string, 23 | orm: string, 24 | name: string, 25 | manager: string 26 | ) => 27 | `${base}/src/infrastructure/driven-adapters/providers/orm/${orm}/${name}-${manager}-providers.ts`, 28 | PATH_PROVIDER_MONGOOSE: (base: string, orm: string, name: string) => 29 | `${base}/src/infrastructure/driven-adapters/providers/orm/${orm}/${name}-${orm}-providers.ts`, 30 | PATH_SINGLETON: (base: string) => `${base}/src/application/singleton.ts`, 31 | PATH_SINGLETON_INSTANCES: (base: string, manager: string, orm?: string) => 32 | orm === "mongoose" 33 | ? `${base}/src/application/config/${orm}-instance.ts` 34 | : `${base}/src/application/config/${manager}-instance.ts`, 35 | // PATH_PROVIDER: (base, orm, name, manager) => manager ? PATHS.PATH_PROVIDER_SEQUELIZE(base, orm, name, manager) : PATHS.PATH_PROVIDER_MONGOOSE(base, orm, name), 36 | PATH_MODEL: (base: string, orm: string, name: string, manager: string) => 37 | orm === CONSTANTS.MONGOOSE 38 | ? `${base}/src/infrastructure/driven-adapters/adapters/orm/${orm}/models/${name}.ts` 39 | : `${base}/src/infrastructure/driven-adapters/adapters/orm/${orm}/models/${name}-${manager}.ts`, 40 | PATH_PROVIDER: (base: string) => 41 | `${base}/src/infrastructure/driven-adapters/providers/index.ts`, 42 | PATH_SERVICE_RESOURCE: () => `${process.cwd()}/src/domain/use-cases/impl` 43 | }; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tsclean/scaffold", 3 | "version": "2.6.1", 4 | "description": "This CLI creates an initial structure of a project based on clean architecture.", 5 | "main": "index.js", 6 | "bin": { 7 | "scaffold": "bin/scaffold.js" 8 | }, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "start": "node ./dist/index.js", 14 | "build": "tsc -p tsconfig.json", 15 | "prepublish": "npm run build" 16 | }, 17 | "engines": { 18 | "node": ">= 20.15.1", 19 | "npm": ">= 10.7.0" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/tsclean/scaffold.git" 24 | }, 25 | "keywords": [ 26 | "plugin", 27 | "scaffold", 28 | "clean", 29 | "architecture", 30 | "typescript" 31 | ], 32 | "author": "John Piedrahita", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/tsclean/scaffold/issues" 36 | }, 37 | "homepage": "https://github.com/tsclean/scaffold#readme", 38 | "devDependencies": { 39 | "@types/app-root-path": "^1.2.8", 40 | "@types/node": "^22.8.4", 41 | "@types/yargs": "^17.0.33", 42 | "chalk": "^5.3.0", 43 | "typescript": "^5.6.3" 44 | }, 45 | "dependencies": { 46 | "app-root-path": "^3.1.0", 47 | "figlet": "^1.8.0", 48 | "mkdirp": "^3.0.1", 49 | "node-emoji": "^2.1.3", 50 | "ora": "^8.1.0", 51 | "ts-morph": "^24.0.0", 52 | "yargonaut": "^1.1.4", 53 | "yargs": "^17.7.2" 54 | }, 55 | "files": [ 56 | "dist", 57 | "package.json", 58 | "README.md" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "module": "commonjs", 5 | "target": "es2019", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "rootDirs": ["lib"], 9 | "baseUrl": "lib" 10 | }, 11 | "include": ["lib"], 12 | "exclude": [] 13 | } 14 | --------------------------------------------------------------------------------