├── .artillery └── config.yaml ├── .docker ├── collector │ └── collector-config.yaml ├── grafana │ └── promtail-config.yml ├── mongo │ ├── rs-init.sh │ └── start-replicaset.sh ├── postgres │ └── create-database.sql └── prometheus │ ├── alert.rules.yml │ ├── alertmanager.yml │ └── config.yml ├── .dockerignore ├── .env ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .lintstagedrc.json ├── .nvmrc ├── .prettierrc ├── .releaserc.json ├── .swcrc ├── .vscode ├── launch.json ├── settings.json ├── usecase-test.code-snippets └── usecase.code-snippets ├── CHANGELOG.md ├── Dockerfile ├── OnionGraph.jpg ├── README.md ├── TRACING.md ├── commitlint.config.js ├── diagram.png ├── docker-compose-infra.yml ├── docker-compose.yml ├── docs ├── .gitignore ├── README.md ├── docker-compose.yml ├── package-lock.json ├── package.json ├── src │ ├── main.tsp │ ├── modules │ │ ├── cat │ │ │ ├── controller.tsp │ │ │ ├── exception.tsp │ │ │ └── model.tsp │ │ ├── health │ │ │ ├── controller.tsp │ │ │ └── model.tsp │ │ ├── login │ │ │ ├── controller.tsp │ │ │ ├── exception.tsp │ │ │ └── model.tsp │ │ ├── logout │ │ │ ├── controller.tsp │ │ │ ├── exception.tsp │ │ │ └── model.tsp │ │ ├── permission │ │ │ ├── controller.tsp │ │ │ ├── exception.tsp │ │ │ └── model.tsp │ │ ├── reset-password │ │ │ ├── controller.tsp │ │ │ ├── exception.tsp │ │ │ └── model.tsp │ │ ├── role │ │ │ ├── controller.tsp │ │ │ ├── exception.tsp │ │ │ └── model.tsp │ │ └── user │ │ │ ├── controller.tsp │ │ │ ├── exception.tsp │ │ │ └── model.tsp │ └── utils │ │ ├── exceptions.tsp │ │ ├── model.tsp │ │ └── versioning.tsp ├── tsp-output │ └── @typespec │ │ └── openapi3 │ │ ├── openapi.api.1.0.yaml │ │ ├── openapi.api.Cat.1.0.yaml │ │ ├── openapi.api.Health.1.0.yaml │ │ ├── openapi.api.Login.1.0.yaml │ │ ├── openapi.api.Logout.1.0.yaml │ │ ├── openapi.api.Permission.1.0.yaml │ │ ├── openapi.api.ResetPassword.1.0.yaml │ │ ├── openapi.api.Role.1.0.yaml │ │ └── openapi.api.User.1.0.yaml ├── tspconfig.yaml └── yarn.lock ├── ecosystem.config.js ├── eslint.config.mjs ├── jest-coverage.config.ts ├── jest.config.ts ├── nest-cli.json ├── ohmy.gif ├── package-lock.json ├── package.json ├── scripts └── npm-audit.sh ├── src ├── app.module.ts ├── core │ ├── cat │ │ ├── entity │ │ │ └── cat.ts │ │ ├── repository │ │ │ └── cat.ts │ │ └── use-cases │ │ │ ├── __tests__ │ │ │ ├── cat-create.spec.ts │ │ │ ├── cat-delete.spec.ts │ │ │ ├── cat-get-by-id.spec.ts │ │ │ ├── cat-list.spec.ts │ │ │ └── cat-update.spec.ts │ │ │ ├── cat-create.ts │ │ │ ├── cat-delete.ts │ │ │ ├── cat-get-by-id.ts │ │ │ ├── cat-list.ts │ │ │ └── cat-update.ts │ ├── permission │ │ ├── entity │ │ │ └── permission.ts │ │ ├── repository │ │ │ └── permission.ts │ │ └── use-cases │ │ │ ├── __tests__ │ │ │ ├── permission-create.spec.ts │ │ │ ├── permission-delete.spec.ts │ │ │ ├── permission-get-by-id.spec.ts │ │ │ ├── permission-list.spec.ts │ │ │ └── permission-update.spec.ts │ │ │ ├── permission-create.ts │ │ │ ├── permission-delete.ts │ │ │ ├── permission-get-by-id.ts │ │ │ ├── permission-list.ts │ │ │ └── permission-update.ts │ ├── reset-password │ │ ├── entity │ │ │ └── reset-password.ts │ │ ├── repository │ │ │ └── reset-password.ts │ │ └── use-cases │ │ │ ├── __tests__ │ │ │ ├── reset-password-confirm.spec.ts │ │ │ └── reset-password-send-email.spec.ts │ │ │ ├── reset-password-confirm.ts │ │ │ └── reset-password-send-email.ts │ ├── role │ │ ├── entity │ │ │ └── role.ts │ │ ├── repository │ │ │ └── role.ts │ │ └── use-cases │ │ │ ├── __tests__ │ │ │ ├── role-add-permission.spec.ts │ │ │ ├── role-create.spec.ts │ │ │ ├── role-delete-permission.spec.ts │ │ │ ├── role-delete.spec.ts │ │ │ ├── role-get-by-id.spec.ts │ │ │ ├── role-list.spec.ts │ │ │ └── role-update.spec.ts │ │ │ ├── role-add-permission.ts │ │ │ ├── role-create.ts │ │ │ ├── role-delete-permission.ts │ │ │ ├── role-delete.ts │ │ │ ├── role-get-by-id.ts │ │ │ ├── role-list.ts │ │ │ └── role-update.ts │ └── user │ │ ├── entity │ │ ├── user-password.ts │ │ └── user.ts │ │ ├── repository │ │ └── user.ts │ │ └── use-cases │ │ ├── __tests__ │ │ ├── user-change-password.spec.ts │ │ ├── user-create.spec.ts │ │ ├── user-delete.spec.ts │ │ ├── user-get-by-id.spec.ts │ │ ├── user-list.spec.ts │ │ ├── user-login.spec.ts │ │ ├── user-logout.spec.ts │ │ ├── user-refresh-token.spec.ts │ │ └── user-update.spec.ts │ │ ├── user-change-password.ts │ │ ├── user-create.ts │ │ ├── user-delete.ts │ │ ├── user-get-by-id.ts │ │ ├── user-list.ts │ │ ├── user-login.ts │ │ ├── user-logout.ts │ │ ├── user-refresh-token.ts │ │ └── user-update.ts ├── infra │ ├── cache │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── memory │ │ │ ├── index.ts │ │ │ ├── module.ts │ │ │ ├── service.ts │ │ │ └── types.ts │ │ ├── redis │ │ │ ├── index.ts │ │ │ ├── module.ts │ │ │ ├── service.ts │ │ │ └── types.ts │ │ └── types.ts │ ├── database │ │ ├── adapter.ts │ │ ├── enum.ts │ │ ├── index.ts │ │ ├── mongo │ │ │ ├── config.ts │ │ │ ├── index.ts │ │ │ ├── migrations │ │ │ │ └── 1709943706267_createCatsCollection.ts │ │ │ ├── module.ts │ │ │ ├── schemas │ │ │ │ └── cat.ts │ │ │ └── service.ts │ │ ├── postgres │ │ │ ├── config.ts │ │ │ ├── index.ts │ │ │ ├── migrations │ │ │ │ ├── 1727653462661-createPermissionTable.ts │ │ │ │ ├── 1727653565690-createRoleTable.ts │ │ │ │ ├── 1727653630438-createUserPasswordTable.ts │ │ │ │ ├── 1727653714156-createUserTable.ts │ │ │ │ ├── 1727653808424-createResetPassword.ts │ │ │ │ ├── 1727653954337-createPermissionRoleTable.ts │ │ │ │ ├── 1727654008041-createUserRoleTable.ts │ │ │ │ ├── 1727654289658-createTableRelationship.ts │ │ │ │ ├── 1727654555722-insertPermissions.ts │ │ │ │ ├── 1727654843890-insertRoles.ts │ │ │ │ ├── 1727655177319-insertUser.ts │ │ │ │ └── 1727657387427-addUnaccentExtension.ts │ │ │ ├── module.ts │ │ │ ├── schemas │ │ │ │ ├── permission.ts │ │ │ │ ├── reset-password.ts │ │ │ │ ├── role.ts │ │ │ │ ├── user-password.ts │ │ │ │ └── user.ts │ │ │ └── service.ts │ │ └── types.ts │ ├── email │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── module.ts │ │ ├── service.ts │ │ └── templates │ │ │ ├── reque-reset-password.handlebars │ │ │ ├── reset-password.handlebars │ │ │ └── welcome.handlebars │ ├── http │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── module.ts │ │ └── service.ts │ ├── logger │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── module.ts │ │ ├── service.ts │ │ └── types.ts │ ├── module.ts │ ├── repository │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── mongo │ │ │ └── repository.ts │ │ ├── postgres │ │ │ └── repository.ts │ │ ├── types.ts │ │ └── util.ts │ └── secrets │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── module.ts │ │ ├── service.ts │ │ └── types.ts ├── libs │ ├── event │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── module.ts │ │ ├── service.ts │ │ └── types.ts │ ├── i18n │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── languages │ │ │ ├── en │ │ │ │ └── info.json │ │ │ └── pt │ │ │ │ └── info.json │ │ ├── module.ts │ │ ├── service.ts │ │ └── types.ts │ ├── metrics │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── module.ts │ │ ├── service.ts │ │ └── types.ts │ ├── module.ts │ └── token │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── module.ts │ │ └── service.ts ├── main.ts ├── middlewares │ ├── filters │ │ ├── exception-handler.filter.ts │ │ └── index.ts │ ├── guards │ │ ├── authorization.guard.ts │ │ └── index.ts │ ├── interceptors │ │ ├── exception-handler.interceptor.ts │ │ ├── http-logger.interceptor.ts │ │ ├── index.ts │ │ ├── metrics.interceptor.ts │ │ ├── request-timeout.interceptor.ts │ │ └── tracing.interceptor.ts │ └── middlewares │ │ ├── authentication.middleware.ts │ │ └── index.ts ├── modules │ ├── alert │ │ ├── controller.ts │ │ └── module.ts │ ├── cat │ │ ├── __tests__ │ │ │ └── controller.e2e.spec.ts │ │ ├── adapter.ts │ │ ├── controller.ts │ │ ├── module.ts │ │ └── repository.ts │ ├── health │ │ ├── adapter.ts │ │ ├── controller.ts │ │ ├── module.ts │ │ ├── service.ts │ │ └── types.ts │ ├── login │ │ ├── adapter.ts │ │ ├── controller.ts │ │ └── module.ts │ ├── logout │ │ ├── adapter.ts │ │ ├── controller.ts │ │ └── module.ts │ ├── permission │ │ ├── adapter.ts │ │ ├── controller.ts │ │ ├── module.ts │ │ └── repository.ts │ ├── reset-password │ │ ├── adapter.ts │ │ ├── controller.ts │ │ ├── module.ts │ │ └── repository.ts │ ├── role │ │ ├── adapter.ts │ │ ├── controller.ts │ │ ├── module.ts │ │ └── repository.ts │ └── user │ │ ├── __tests__ │ │ └── controller.spec.ts │ │ ├── adapter.ts │ │ ├── controller.ts │ │ ├── module.ts │ │ └── repository.ts └── utils │ ├── axios.ts │ ├── collection.ts │ ├── crypto.ts │ ├── date.ts │ ├── decorators │ ├── circuit-breaker.decorator.ts │ ├── database │ │ ├── mongo │ │ │ ├── convert-mongoose-filter.decorator.ts │ │ │ └── validate-mongoose-filter.decorator.ts │ │ ├── postgres │ │ │ └── validate-typeorm-filter.decorator.ts │ │ ├── utils.ts │ │ └── validate-database-sort-allowed.decorator.ts │ ├── index.ts │ ├── log-execution-time.decorator.ts │ ├── process │ │ ├── process.decorator.ts │ │ └── process.ts │ ├── request-timeout.decorator.ts │ ├── role.decorator.ts │ ├── types.ts │ ├── validate-schema.decorator.ts │ └── workers │ │ ├── thread.decorator.ts │ │ └── thread.ts │ ├── entity.ts │ ├── excel.ts │ ├── exception.ts │ ├── http-status.ts │ ├── mongoose.ts │ ├── pagination.ts │ ├── request.ts │ ├── search.ts │ ├── sort.ts │ ├── text.ts │ ├── tracing.ts │ ├── types.ts │ ├── usecase.ts │ ├── uuid.ts │ └── validator.ts ├── test ├── containers.ts ├── initialization.ts └── mock.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.artillery/config.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | target: 'http://localhost:4000' 3 | phases: 4 | - duration: 60 5 | arrivalRate: 100 6 | - duration: 60 7 | arrivalRate: 140 8 | 9 | scenarios: 10 | - flow: 11 | - post: 12 | url: '/api/v1/login' 13 | json: 14 | email: 'admin@admin.com' 15 | password: 'admin' 16 | headers: 17 | accept: 'application/json' 18 | Content-Type: 'application/json' 19 | -------------------------------------------------------------------------------- /.docker/collector/collector-config.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | endpoint: '0.0.0.0:4317' # Endpoint para recepção de traces via gRPC 6 | http: 7 | endpoint: '0.0.0.0:4318' # Endpoint para recepção de traces via HTTP 8 | cors: 9 | allowed_origins: 10 | - http://* # Permite requisições de qualquer origem via HTTP 11 | - https://* # Permite requisições de qualquer origem via HTTPS 12 | 13 | exporters: 14 | zipkin: 15 | endpoint: 'http://zipkin-all-in-one:9411/api/v2/spans' # Endpoint para exportação de traces para o Zipkin 16 | prometheus: 17 | endpoint: '0.0.0.0:9464' # Endpoint para exportação de métricas para o Prometheus 18 | loki: 19 | endpoint: 'http://loki:3100/loki/api/v1/push' # Endpoint para exportação de logs para o Loki 20 | 21 | processors: 22 | batch: 23 | timeout: 5s # Define o tempo máximo para agrupar logs antes de enviá-los 24 | send_batch_size: 1024 # Define o número máximo de logs por lote 25 | 26 | attributes: 27 | actions: 28 | - key: service.name 29 | value: 'nestjs-boilerplate-microservice-api' # Define o nome do serviço nos logs 30 | action: insert # Ação para inserir o atributo nos logs 31 | 32 | service: 33 | telemetry: 34 | logs: 35 | level: 'debug' # Define o nível de log para o Collector 36 | pipelines: 37 | logs: 38 | receivers: [otlp] # Recebe logs via OTLP 39 | processors: [batch, attributes] # Processa logs com os processadores definidos 40 | exporters: [loki] # Exporta logs para o Loki 41 | traces: 42 | receivers: [otlp] # Recebe traces via OTLP 43 | exporters: [zipkin] # Exporta traces para o Zipkin 44 | processors: [batch] # Processa traces com o processador de batch 45 | metrics: 46 | receivers: [otlp] # Recebe métricas via OTLP 47 | exporters: [prometheus] # Exporta métricas para o Prometheus 48 | processors: [batch] # Processa métricas com o processador de batch 49 | -------------------------------------------------------------------------------- /.docker/grafana/promtail-config.yml: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_port: 3100 3 | grpc_listen_port: 9095 4 | 5 | positions: 6 | filename: /tmp/positions.yaml 7 | 8 | clients: 9 | - url: http://loki:3100/loki/api/v1/push 10 | 11 | scrape_configs: 12 | - job_name: nestjs-boilerplate-microservice-api 13 | static_configs: 14 | - targets: 15 | - localhost 16 | labels: 17 | job: varlogs 18 | __path__: /var/log/*.log 19 | -------------------------------------------------------------------------------- /.docker/mongo/rs-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mongo <= 5 6 | for: 0m 7 | labels: 8 | severity: critical 9 | annotations: 10 | summary: 'HTTP 500 Errors in 1 Minute' 11 | description: 'There was at least 1 HTTP 500 error in the last 60 seconds. Please check the affected service.' 12 | -------------------------------------------------------------------------------- /.docker/prometheus/alertmanager.yml: -------------------------------------------------------------------------------- 1 | global: 2 | resolve_timeout: 1m 3 | 4 | route: 5 | receiver: 'webhook-nodejs' 6 | group_wait: 10s 7 | group_interval: 5s 8 | repeat_interval: 1s 9 | routes: 10 | - matchers: 11 | - alertname="AlwaysFiring" 12 | receiver: 'webhook-nodejs' 13 | 14 | receivers: 15 | - name: 'webhook-nodejs' 16 | webhook_configs: 17 | - url: 'http://172.17.0.1:4000/alert' 18 | send_resolved: true 19 | -------------------------------------------------------------------------------- /.docker/prometheus/config.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s # Default is every 1 minute. 3 | 4 | rule_files: 5 | - '/etc/prometheus/alert.rules.yml' 6 | 7 | alerting: 8 | alertmanagers: 9 | - static_configs: 10 | - targets: ['app-alertmanager:9093'] 11 | 12 | scrape_configs: 13 | - job_name: 'collector' 14 | static_configs: 15 | - targets: ['collector:9464'] 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | scripts 5 | ^.* 6 | __test__ -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT=4000 2 | 3 | NODE_ENV='local' 4 | HOST=http://localhost:4000 5 | WEBHOOK_HOST=201.46.20.98#YOUR IP 6 | 7 | TZ='America/Sao_Paulo' 8 | 9 | LOG_LEVEL=trace 10 | 11 | # MONGO 12 | MONGO_URL=mongodb://localhost:27081,localhost:27082,localhost:27083/nestjs-microservice?replicaSet=app&serverSelectionTimeoutMS=5000&connectTimeoutMS=8000&socketTimeoutMS=8000&minPoolSize=20 13 | MONGO_DATABASE=nestjs-microservice 14 | 15 | # SYSTEM 16 | UV_THREADPOOL_SIZE=8 17 | 18 | #MONGO-EXPRESS 19 | ME_CONFIG_MONGODB_ENABLE_ADMIN=true 20 | ME_CONFIG_MONGODB_PORT=27017 21 | ME_CONFIG_MONGODB_SERVER=10.5.0.5,10.5.0.6,10.5.0.7 22 | ME_CONFIG_OPTIONS_EDITORTHEME=ambiance 23 | MONGO_EXPRESS_URL=http://localhost:8081 24 | 25 | # POSTGRES 26 | POSTGRES_HOST=localhost 27 | POSTGRES_PORT=5432 28 | POSTGRES_USER=admin 29 | POSTGRES_PASSWORD=admin 30 | POSTGRES_DATABASE=nestjs-microservice 31 | 32 | # PGADMIN 33 | PGADMIN_URL=http://localhost:16543 34 | PGADMIN_DEFAULT_EMAIL="pgadmin@gmail.com" 35 | PGADMIN_DEFAULT_PASSWORD="PgAdmin2019!" 36 | 37 | # REDIS 38 | REDIS_URL=redis://localhost:6379 39 | 40 | TOKEN_EXPIRATION=1h 41 | REFRESH_TOKEN_EXPIRATION=1d 42 | 43 | JWT_SECRET_KEY=MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgFokYswri4Dekw/Q50IhqEQ4X2BJyYZYmHVLwnI4gna6FyBEShlsvSNUxTYx41rseIN8nosrvptSoRVpiEHZDOcURn57Yh+/VVF5MNtHpR3Yf0E7TeMTOgU4mPABR08dHcZ6EakoADU8qDyJxZgmFMPbPmuCPSxmNFLW8jHkLh53AgMBAAE= 44 | 45 | DATE_FORMAT="dd/MM/yyyy HH:mm:ss" 46 | 47 | 48 | # OPENTELEMETRY 49 | ZIPKIN_URL=http://localhost:9411 50 | PROMETHUES_URL=http://localhost:9090 51 | 52 | COLLECTOR_OTLP_ENABLED=true 53 | 54 | # EMAIL 55 | EMAIL_HOST=smtp-mail.outlook.com 56 | EMAIL_FROM=admin@admin.com 57 | EMAIL_PORT=587 58 | EMAIL_USER=admin@admin.com 59 | EMAIL_PASS=**** 60 | 61 | # GOOGLE AUTH 62 | GOOGLE_CLIENT_ID=**** 63 | GOOGLE_CLIENT_SECRET=**** 64 | GOOGLE_REDIRECT_URI=http://localhost:4000/api/v1/login/google/callback 65 | 66 | #Grafana 67 | GRAFANA_URL=http://localhost:3000 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | id-token: write 13 | pull-requests: write 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: '22.14.0' 22 | 23 | - name: Install dependencies 24 | run: npm install 25 | 26 | - name: Run semantic-release 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: npx semantic-release 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | node_modules 3 | **/node_modules 4 | dist 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | 3 | echo '\nI Know What You Did Last Commit\n' -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run test 2 | npx npm run build 3 | npm run make-badges 4 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,jsx}": [ 3 | "yarn lint --fix", 4 | "yarn prettier", 5 | "yarn test --verbose --findRelatedTests --passWithNoTests --forceExit" 6 | ] 7 | } -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.14.0 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "printWidth": 120, 5 | "tabWidth": 2 6 | } -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master" 4 | ], 5 | "tagFormat": "v${version}-production", 6 | "plugins": [ 7 | "@semantic-release/commit-analyzer", 8 | "@semantic-release/release-notes-generator", 9 | "@semantic-release/changelog", 10 | [ 11 | "@semantic-release/npm", 12 | { 13 | "npmPublish": false 14 | } 15 | ], 16 | [ 17 | "@semantic-release/git", 18 | { 19 | "assets": [ 20 | "CHANGELOG.md", 21 | "package.json", 22 | "package-lock.json" 23 | ], 24 | "message": "chore(release): ${nextRelease.version} [skip ci]" 25 | } 26 | ], 27 | "@semantic-release/github" 28 | ] 29 | } -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "sourceMaps": true, 4 | "module": { 5 | "type": "commonjs", 6 | "strict": true 7 | }, 8 | "jsc": { 9 | "target": "es2021", 10 | "parser": { 11 | "syntax": "typescript", 12 | "decorators": true 13 | }, 14 | "transform": { 15 | "legacyDecorator": true, 16 | "decoratorMetadata": true 17 | }, 18 | "keepClassNames": true, 19 | "baseUrl": "./" 20 | }, 21 | "minify": false 22 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug: App", 6 | "type": "node", 7 | "request": "launch", 8 | "restart": true, 9 | "skipFiles": [ 10 | "/**" 11 | ], 12 | "console": "integratedTerminal", 13 | "runtimeExecutable": "npm", 14 | "runtimeArgs": [ 15 | "run-script", 16 | "start:debug" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll": "always", 5 | "source.organizeImports": "always" 6 | }, 7 | "[typescript]": { 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": "always", 10 | "source.organizeImports": "always" 11 | }, 12 | "editor.formatOnSave": true, 13 | }, 14 | "[javascript]": { 15 | "editor.codeActionsOnSave": { 16 | "source.fixAll": "always", 17 | "source.organizeImports": "always" 18 | }, 19 | "editor.formatOnSave": true, 20 | }, 21 | "editor.formatOnSave": true, 22 | "editor.detectIndentation": true, 23 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 24 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 25 | "[javascript][typescript]": { 26 | "editor.codeActionsOnSave": { 27 | "source.fixAll": "always", 28 | "source.organizeImports": "always" 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine as build 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN npm i -g @nestjs/cli 8 | RUN npm ci --omit=dev --ignore-scripts 9 | RUN npm run build 10 | 11 | RUN ls dist/src -al 12 | 13 | CMD ["node", "dist/src/main.js"] 14 | -------------------------------------------------------------------------------- /OnionGraph.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikemajesty/nestjs-microservice-boilerplate-api/2a0d31df6ed52d2cb7210e33a898f9e321d61ecb/OnionGraph.jpg -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | const { readdirSync } = require('fs'); 2 | 3 | const getDirectories = (source) => 4 | readdirSync(source, { withFileTypes: true }) 5 | .filter((dirent) => dirent.isDirectory()) 6 | .map((dirent) => dirent.name); 7 | 8 | const scopes = []; 9 | for (const path of getDirectories('./src').map((p) => `./src/${p}`)) { 10 | const files = readdirSync(path, { withFileTypes: true }); 11 | scopes.push(...files.filter((item) => item.isDirectory()).map((item) => item.name)); 12 | } 13 | 14 | scopes.push( 15 | 'remove', 16 | 'revert', 17 | 'conflict', 18 | 'config', 19 | 'entity', 20 | 'utils', 21 | 'deps', 22 | 'modules', 23 | 'test', 24 | 'migration', 25 | 'core', 26 | 'swagger' 27 | ); 28 | 29 | module.exports = { 30 | extends: ['@commitlint/config-conventional'], 31 | ignores: [(message) => message.includes('release')], 32 | rules: { 33 | 'scope-empty': [2, 'never'], 34 | 'scope-enum': [2, 'always', scopes] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikemajesty/nestjs-microservice-boilerplate-api/2a0d31df6ed52d2cb7210e33a898f9e321d61ecb/diagram.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | microservice-api: 4 | container_name: microservice-api 5 | env_file: 6 | - .env 7 | build: 8 | context: . 9 | dockerfile: ./Dockerfile 10 | ports: 11 | - "3000:3000" 12 | volumes: 13 | - ./src:/app/src 14 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS 2 | .DS_Store 3 | 4 | # Default TypeSpec output 5 | dist/ 6 | 7 | # Dependency directories 8 | node_modules/ -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ### Building and Running the documentaion 4 | 5 | Documentation: 6 | [documentation](https://typespec.io/) 7 | 8 | --- 9 | 10 | - install dependencies 11 | ``` 12 | $ yarn doc:install 13 | ``` 14 | - running infra 15 | ``` 16 | $ yarn infra 17 | ``` 18 | - starting doc 19 | ``` 20 | $ yarn start 21 | ``` 22 | 23 | --- 24 | 25 | The following is a list of all the people that have contributed Nestjs monorepo boilerplate. Thanks for your contributions! 26 | 27 | [mikemajesty](https://github.com/mikemajesty) 28 | 29 | ## License 30 | 31 | It is available under the MIT license. 32 | [License](https://opensource.org/licenses/mit-license.php) 33 | -------------------------------------------------------------------------------- /docs/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | swagger-editor: 4 | image: swaggerapi/swagger-editor 5 | container_name: microservice-api-swagger-editor 6 | environment: 7 | - SWAGGER_FILE=/tmp/tsp-output/@typespec/openapi3/openapi.api.1.0.yaml 8 | ports: 9 | - "8080:8080" 10 | volumes: 11 | - .:/tmp 12 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.1.0", 4 | "type": "commonjs", 5 | "scripts": { 6 | "doc:install": "npx tsp install", 7 | "doc:init": "npx tsp init", 8 | "doc:compiler": "npx tsp compile ./src --watch", 9 | "doc:start": "browser-sync start --proxy 'localhost:8080' --files './tsp-output/@typespec/openapi3/openapi.api.1.0.yaml' ", 10 | "start": "concurrently \"yarn doc:start\" \"yarn doc:compiler\"", 11 | "infra": "docker-compose down --volumes && docker-compose up --build --remove-orphans" 12 | }, 13 | "peerDependencies": { 14 | "@typespec/compiler": "latest", 15 | "@typespec/http": "latest", 16 | "@typespec/openapi3": "latest", 17 | "@typespec/rest": "latest", 18 | "@typespec/versioning": "latest" 19 | }, 20 | "devDependencies": { 21 | "@typespec/compiler": "latest", 22 | "@typespec/http": "latest", 23 | "@typespec/openapi3": "latest", 24 | "@typespec/rest": "latest", 25 | "browser-sync": "^3.0.3", 26 | "concurrently": "^9.1.2" 27 | }, 28 | "private": true, 29 | "exports": { 30 | ".": { 31 | "typespec": "./src/main.tsp" 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /docs/src/main.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "@typespec/rest"; 3 | import "@typespec/openapi3"; 4 | import "@typespec/versioning"; 5 | import "./modules/login/controller.tsp"; 6 | import "./modules/user/controller.tsp"; 7 | import "./modules/role/controller.tsp"; 8 | import "./modules/reset-password/controller.tsp"; 9 | import "./modules/permission/controller.tsp"; 10 | import "./modules/logout/controller.tsp"; 11 | import "./modules/health/controller.tsp"; 12 | import "./modules/cat/controller.tsp"; 13 | 14 | using TypeSpec.Http; 15 | using Utils.Versioning; 16 | using TypeSpec.Versioning; 17 | 18 | @service({ 19 | title: "Microservice API", 20 | contact: { 21 | name: "API Support", 22 | email: "mike.rodrigues.lima@gmail.com", 23 | }, 24 | license: { 25 | name: "MIT", 26 | url: "https://opensource.org/license/mit", 27 | }, 28 | }) 29 | @server("http://localhost:4000/") 30 | @versioned(DocVersionVersion) 31 | namespace api; 32 | -------------------------------------------------------------------------------- /docs/src/modules/cat/controller.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "@typespec/rest"; 3 | import "@typespec/openapi3"; 4 | import "@typespec/versioning"; 5 | import "../../utils/model.tsp"; 6 | import "./model.tsp"; 7 | import "./exception.tsp"; 8 | 9 | using TypeSpec.Http; 10 | using TypeSpec.Versioning; 11 | using OpenAPI; 12 | using Utils.Model; 13 | using Utils.Versioning; 14 | 15 | @service({ 16 | title: "Cat", 17 | }) 18 | namespace api.Cat; 19 | 20 | @tag("Cat") 21 | @route("api/{version}/cats") 22 | @useAuth(BearerAuth) 23 | interface CatController { 24 | @post 25 | @doc("create cat") 26 | @returnsDoc("cat created successfully") 27 | create(...VersionParams, @body body: CreateInput): CreateOutput | CreateValidationException; 28 | 29 | @put 30 | @doc("create update") 31 | @returnsDoc("cat updated successfully") 32 | update( 33 | ...VersionParams, 34 | ...UpdateParamInput, 35 | @body body: UpdateInput, 36 | ): UpdateOutput | UpdateValidationException | UpdateNotFoundException; 37 | 38 | @get 39 | @doc("get cat by id") 40 | @returnsDoc("get cat by id successfully") 41 | getById( 42 | ...VersionParams, 43 | ...GetByIdParamInput, 44 | ): GetByIdOutput | GetByIdValidationException | GetByIdNotFoundException; 45 | 46 | @get 47 | @doc("list cat") 48 | @returnsDoc("list cat successfully") 49 | list(...VersionParams, ...ListQueryInput): ListOutput; 50 | 51 | @delete 52 | @doc("delete cat") 53 | @returnsDoc("cat deleted successfully") 54 | delete(...VersionParams, ...DeleteParamInput): DeleteOutput | DeleteValidationException | DeleteNotFoundException; 55 | } 56 | -------------------------------------------------------------------------------- /docs/src/modules/cat/exception.tsp: -------------------------------------------------------------------------------- 1 | import "../../utils/model.tsp"; 2 | import "../../utils/exceptions.tsp"; 3 | import "@typespec/http"; 4 | import "@typespec/versioning"; 5 | 6 | using TypeSpec.Http; 7 | using TypeSpec.Versioning; 8 | using Utils.Model; 9 | using Utils.Exception; 10 | using Utils.Versioning; 11 | 12 | namespace api.Cat; 13 | 14 | // ## CREATE ## // 15 | @doc("When input is invalid") 16 | @error 17 | model CreateValidationError extends ApiBadRequestException<"name: Required, breed: Required, age: Required"> { 18 | @statusCode statusCode: 400; 19 | } 20 | 21 | alias CreateValidationException = CreateValidationError; 22 | 23 | // ## UPDATE ## // 24 | @doc("When input is invalid") 25 | @error 26 | model UpdateValidationError extends ApiBadRequestException<"id: Required"> { 27 | @statusCode statusCode: 400; 28 | } 29 | 30 | alias UpdateValidationException = UpdateValidationError; 31 | 32 | @doc("When cat not found") 33 | @error 34 | model UpdateCatNotFoundError extends ApiNotFoundException<"catNotFound"> { 35 | @statusCode statusCode: 404; 36 | } 37 | 38 | alias UpdateNotFoundException = UpdateCatNotFoundError; 39 | 40 | // ## GET BY ID ## // 41 | @doc("When input is invalid") 42 | @error 43 | model GetByIdValidationError extends ApiBadRequestException<"id: Required"> { 44 | @statusCode statusCode: 400; 45 | } 46 | 47 | alias GetByIdValidationException = GetByIdValidationError; 48 | 49 | @doc("When cat not found") 50 | @error 51 | model GetByIdCatNotFoundError extends ApiNotFoundException<"catNotFound"> { 52 | @statusCode statusCode: 404; 53 | } 54 | 55 | alias GetByIdNotFoundException = GetByIdCatNotFoundError; 56 | 57 | // ## DELETE ## // 58 | @doc("When input is invalid") 59 | @error 60 | model DeleteValidationError extends ApiBadRequestException<"id: Required"> { 61 | @statusCode statusCode: 400; 62 | } 63 | 64 | alias DeleteValidationException = DeleteValidationError; 65 | 66 | @doc("When cat not found") 67 | @error 68 | model DeleteCatNotFoundError extends ApiNotFoundException<"catNotFound"> { 69 | @statusCode statusCode: 404; 70 | } 71 | 72 | alias DeleteNotFoundException = DeleteCatNotFoundError; 73 | -------------------------------------------------------------------------------- /docs/src/modules/cat/model.tsp: -------------------------------------------------------------------------------- 1 | import "../../utils/model.tsp"; 2 | import "@typespec/http"; 3 | import "@typespec/versioning"; 4 | 5 | using TypeSpec.Http; 6 | using TypeSpec.Versioning; 7 | using Utils.Model; 8 | using Utils.Versioning; 9 | 10 | namespace api.Cat; 11 | 12 | @doc("cat base entity") 13 | model CatEntity { 14 | name: string; 15 | breed: string; 16 | age: int16; 17 | createdAt: string; 18 | updatedAt: string; 19 | deletedAt: string | null = null; 20 | } 21 | 22 | // ## CREATE ## // 23 | @doc("cat create input") 24 | model CreateInput extends PickProperties {} 25 | @doc("cat create output") 26 | model CreateOutput extends CatEntity {} 27 | 28 | // ## UPDATE ## // 29 | model UpdateParamInput { 30 | @doc("cat id") 31 | @path 32 | id: string; 33 | } 34 | @doc("cat update input") 35 | model UpdateInput extends PickProperties {} 36 | @doc("cat update output") 37 | model UpdateOutput extends CatEntity {} 38 | 39 | // ## GET BY ID ## // 40 | model GetByIdParamInput { 41 | @doc("cat id") 42 | @path 43 | id: string; 44 | } 45 | @doc("cat get by id input") 46 | model GetByIdOutput extends CatEntity {} 47 | 48 | // ## LIST ## // 49 | model ListQueryInput extends ApiPaginationInput {} 50 | @doc("cat list output") 51 | model ListOutput extends ApiPaginationOutput {} 52 | 53 | // ## DELETE ## // 54 | model DeleteParamInput { 55 | @doc("cat id") 56 | @path 57 | id: string; 58 | } 59 | @doc("cat delete output") 60 | model DeleteOutput extends OmitProperties { 61 | deletedAt: utcDateTime; 62 | } 63 | -------------------------------------------------------------------------------- /docs/src/modules/health/controller.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "@typespec/rest"; 3 | import "@typespec/openapi3"; 4 | import "@typespec/versioning"; 5 | import "../../utils/model.tsp"; 6 | import "./model.tsp"; 7 | 8 | using TypeSpec.Http; 9 | using TypeSpec.Versioning; 10 | using OpenAPI; 11 | using Utils.Model; 12 | 13 | @service({ 14 | title: "Health", 15 | }) 16 | namespace api.Health; 17 | 18 | @tag("Health") 19 | @route("/") 20 | interface HealthController { 21 | @get 22 | @route("/health") 23 | @doc("app health") 24 | @returnsDoc("app health successfully") 25 | health(): HealthOuput; 26 | 27 | @get 28 | @route("/") 29 | @doc("app health") 30 | @returnsDoc("app health successfully") 31 | health1(): HealthOuput; 32 | } 33 | -------------------------------------------------------------------------------- /docs/src/modules/login/controller.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "@typespec/rest"; 3 | import "@typespec/openapi3"; 4 | import "@typespec/versioning"; 5 | import "../../utils/versioning.tsp"; 6 | import "../../utils/exceptions.tsp"; 7 | import "./model.tsp"; 8 | import "./exception.tsp"; 9 | 10 | using TypeSpec.Http; 11 | using TypeSpec.Versioning; 12 | using OpenAPI; 13 | using Utils.Versioning; 14 | using Utils.Exception; 15 | using Utils.Model; 16 | 17 | @service({ 18 | title: "Login", 19 | }) 20 | namespace api.Login; 21 | 22 | @tag("Login") 23 | interface LoginController { 24 | @post 25 | @route("/api/{version}/login") 26 | @doc("user login") 27 | @returnsDoc("login successfully") 28 | login(...VersionParams, @body login: LoginInput): DefaultSuccessResponse< 29 | LoginOutput, 30 | 200 31 | > | LoginNotFoundException | LoginInputException; 32 | 33 | @post 34 | @route("/api/{version}/refresh") 35 | @doc("get user refresh token") 36 | @returnsDoc("login refresh token successfully") 37 | refresh(...VersionParams, @body login: RefreshTokenInput): RefreshTokenOutput; 38 | 39 | @get 40 | @route("/api/{version}/login/google") 41 | loginGoogle(...VersionParams): void; 42 | 43 | @get 44 | @route("/api/{version}/login/google/callback") 45 | loginGoogleCallback(...VersionParams): void; 46 | } 47 | -------------------------------------------------------------------------------- /docs/src/modules/login/exception.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "../../utils/exceptions.tsp"; 3 | 4 | using TypeSpec.Http; 5 | using api.Utils.Exception; 6 | 7 | namespace api.Login; 8 | 9 | // ## LOGIN ## // 10 | @doc("When user not found") 11 | @error 12 | model LoginUserNotFoundError extends ApiNotFoundException<"userNotFound"> { 13 | @statusCode statusCode: 404; 14 | } 15 | 16 | @doc("When user role not found") 17 | @error 18 | model LoginRoleNotFoundError extends ApiNotFoundException<"roleNotFound"> { 19 | @statusCode statusCode: 404; 20 | } 21 | 22 | alias LoginNotFoundException = LoginUserNotFoundError | LoginRoleNotFoundError; 23 | 24 | @doc("When login input is invalid") 25 | @error 26 | model LoginValidationError extends ApiBadRequestException<"email: Required" | "password: Required"> { 27 | @statusCode statusCode: 400; 28 | } 29 | 30 | @doc("When user password is wrong") 31 | @error 32 | model LoginWrongPasswordError extends ApiBadRequestException<"incorrectPassword"> { 33 | @statusCode statusCode: 400; 34 | } 35 | 36 | alias LoginInputException = LoginValidationError | LoginWrongPasswordError; 37 | -------------------------------------------------------------------------------- /docs/src/modules/login/model.tsp: -------------------------------------------------------------------------------- 1 | namespace api.Login; 2 | 3 | // ## LOGIN ## // 4 | @doc("login input") 5 | model LoginInput { 6 | email: string; 7 | password: string; 8 | } 9 | 10 | @doc("login ouput") 11 | model LoginOutput { 12 | accessToken: string; 13 | refreshToken: string; 14 | } 15 | 16 | // ## REFRESH TOKEN ## // 17 | @doc("login refresh token input") 18 | model RefreshTokenInput { 19 | refreshToken: string; 20 | } 21 | 22 | @doc("login refresh token output") 23 | model RefreshTokenOutput { 24 | accessToken: string; 25 | refreshToken: string; 26 | } 27 | -------------------------------------------------------------------------------- /docs/src/modules/logout/controller.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "@typespec/rest"; 3 | import "@typespec/openapi3"; 4 | import "@typespec/versioning"; 5 | import "../../utils/exceptions.tsp"; 6 | import "../../utils/versioning.tsp"; 7 | import "../../utils/model.tsp"; 8 | import "./model.tsp"; 9 | import "./exception.tsp"; 10 | 11 | using TypeSpec.Http; 12 | using TypeSpec.Versioning; 13 | using OpenAPI; 14 | using Utils.Versioning; 15 | using Utils.Exception; 16 | using Utils.Model; 17 | 18 | @service({ 19 | title: "Logout", 20 | }) 21 | namespace api.Logout; 22 | 23 | @tag("Logout") 24 | @route("/api/{version}/logout") 25 | @useAuth(BearerAuth) 26 | interface LogoutController { 27 | @doc("user logout") 28 | @returnsDoc("user logout successfully") 29 | logout(...VersionParams, @body body: LogoutInput): DefaultSuccessResponse | LogoutValidationException; 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/modules/logout/exception.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "../../utils/exceptions.tsp"; 3 | 4 | using TypeSpec.Http; 5 | using api.Utils.Exception; 6 | 7 | namespace api.Logout; 8 | 9 | // ## LOGOUT ## // 10 | @doc("When input is invalid") 11 | @error 12 | model LogoutValidationError extends ApiBadRequestException<"token: Required"> { 13 | @statusCode statusCode: 400; 14 | } 15 | 16 | alias LogoutValidationException = LogoutValidationError; 17 | -------------------------------------------------------------------------------- /docs/src/modules/logout/model.tsp: -------------------------------------------------------------------------------- 1 | import "../../utils/model.tsp"; 2 | 3 | using api.Utils.Model; 4 | 5 | namespace api.Logout; 6 | 7 | // ## LOGOUT ## // 8 | @doc("logout input") 9 | model LogoutInput { 10 | token: string; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/modules/permission/controller.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "@typespec/rest"; 3 | import "@typespec/openapi3"; 4 | import "@typespec/versioning"; 5 | import "../../utils/exceptions.tsp"; 6 | import "../../utils/versioning.tsp"; 7 | import "../../utils/model.tsp"; 8 | import "./model.tsp"; 9 | import "./exception.tsp"; 10 | 11 | using Utils.Model; 12 | using Utils.Versioning; 13 | using TypeSpec.Http; 14 | using TypeSpec.Versioning; 15 | using OpenAPI; 16 | 17 | @service({ 18 | title: "Permission", 19 | }) 20 | namespace api.Permission; 21 | 22 | @tag("Permission") 23 | @route("/api/{version}/permissions") 24 | @useAuth(BearerAuth) 25 | interface PermissionController { 26 | @post 27 | @doc("create permission") 28 | @returnsDoc("permission created successfully") 29 | create(...VersionParams, @body body: CreateInput): CreateOutput | CreateValidationException | CreateConflictException; 30 | 31 | @put 32 | @doc("update permission") 33 | @returnsDoc("permission updated successfully") 34 | update(...VersionParams, ...UpdateParamsInput, @body body: UpdateInput): 35 | | UpdateOutput 36 | | UpdateValidationException 37 | | UpdateNotFoundException 38 | | UpdateConflictException; 39 | 40 | @get 41 | @doc("get permission") 42 | @returnsDoc("get permission successfully") 43 | getById( 44 | ...VersionParams, 45 | ...GetByIdParamsInput, 46 | ): GetByIdOutput | GetByIdValidationException | GeyByIdNotFoundException; 47 | 48 | @get 49 | @doc("list permission") 50 | @returnsDoc("list permission successfully") 51 | list(...VersionParams, ...ListQueryInput): ListOutput; 52 | 53 | @delete 54 | @doc("delete permission") 55 | @returnsDoc("delete permission successfully") 56 | delete(...VersionParams, ...DeleteParamsInput): DeleteOutput | DeleteValidationException | DeleteNotFoundException; 57 | } 58 | -------------------------------------------------------------------------------- /docs/src/modules/permission/model.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "../../utils/exceptions.tsp"; 3 | import "../../utils/model.tsp"; 4 | 5 | using TypeSpec.Http; 6 | using api.Utils.Exception; 7 | using api.Utils.Model; 8 | 9 | namespace api.Permission; 10 | 11 | @doc("permission base entity") 12 | model PermisisonEntity { 13 | id: string; 14 | name: string; 15 | createdAt: string; 16 | updatedAt: string; 17 | deletedAt: string | null = null; 18 | } 19 | 20 | // ## CREATE ## // 21 | @doc("permission create input") 22 | model CreateInput extends PickProperties {} 23 | @doc("permission create output") 24 | model CreateOutput extends PermisisonEntity {} 25 | 26 | // ## UPDATE ## // 27 | model UpdateParamsInput { 28 | @doc("permission id") 29 | @path 30 | id: string; 31 | } 32 | @doc("permission update input") 33 | model UpdateInput extends PickProperties {} 34 | @doc("permission update output") 35 | model UpdateOutput extends PermisisonEntity {} 36 | 37 | // ## LIST ## // 38 | @doc("permission list input") 39 | model ListQueryInput extends ApiPaginationInput {} 40 | @doc("permission list output") 41 | model ListOutput extends ApiPaginationOutput {} 42 | 43 | // ## DELETE ## // 44 | model DeleteParamsInput { 45 | @doc("permission id") 46 | @path 47 | id: string; 48 | } 49 | @doc("permission depete output") 50 | model DeleteOutput extends OmitProperties { 51 | deletedAt: utcDateTime; 52 | } 53 | 54 | // ## GET BY ID ## // 55 | model GetByIdParamsInput { 56 | @doc("permission id") 57 | @path 58 | id: string; 59 | } 60 | @doc("permission get by id output") 61 | model GetByIdOutput extends OmitProperties { 62 | deletedAt: utcDateTime; 63 | } 64 | -------------------------------------------------------------------------------- /docs/src/modules/reset-password/controller.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "@typespec/rest"; 3 | import "@typespec/openapi3"; 4 | import "@typespec/versioning"; 5 | import "../../utils/exceptions.tsp"; 6 | import "../../utils/versioning.tsp"; 7 | import "../../utils/model.tsp"; 8 | import "./model.tsp"; 9 | import "./exception.tsp"; 10 | 11 | using api.Utils.Model; 12 | using TypeSpec.Http; 13 | using TypeSpec.Versioning; 14 | using OpenAPI; 15 | using Utils.Versioning; 16 | using Utils.Exception; 17 | 18 | @service({ 19 | title: "Reset password", 20 | }) 21 | namespace api.ResetPassword; 22 | 23 | @tag("Reset password") 24 | @route("/api/{version}/reset-password") 25 | @useAuth(BearerAuth) 26 | interface ResetPasswordController { 27 | @post 28 | @route("/send-email") 29 | @doc("send email") 30 | @returnsDoc("email sended successfully") 31 | sendEmail(...VersionParams, @body body: SendEmailInput): DefaultSuccessResponse< 32 | void, 33 | 200 34 | > | SendEmailNotFoundException; 35 | 36 | @put 37 | @doc("reset password") 38 | @returnsDoc("password changed successfully") 39 | confirmResetPassword(...VersionParams, ...ConfirmResetPasswordParamsInput, @body body: ConfirmResetPasswordInput): 40 | | DefaultSuccessResponse 41 | | ConfirmResetPasswordBadRequestException 42 | | ConfirmResetPasswordNotFoundException 43 | | ConfirmResetPasswordUnauthorizedException; 44 | } 45 | -------------------------------------------------------------------------------- /docs/src/modules/reset-password/exception.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "../../utils/exceptions.tsp"; 3 | 4 | using TypeSpec.Http; 5 | using api.Utils.Exception; 6 | using api.Utils.Model; 7 | 8 | namespace api.ResetPassword; 9 | 10 | // ## Confirm Reset Password ## // 11 | @doc("When password are differents") 12 | @error 13 | model ConfirmResetPasswordPasswordAreDifferentError extends ApiBadRequestException<"passwords are different"> { 14 | @statusCode statusCode: 400; 15 | } 16 | 17 | alias ConfirmResetPasswordBadRequestException = ConfirmResetPasswordPasswordAreDifferentError; 18 | 19 | @doc("When user not found") 20 | @error 21 | model ConfirmResetPasswordUserNotFoundError extends ApiNotFoundException<"user not found"> { 22 | @statusCode statusCode: 404; 23 | } 24 | 25 | alias ConfirmResetPasswordNotFoundException = ConfirmResetPasswordUserNotFoundError; 26 | 27 | @doc("When password are differents") 28 | @error 29 | model ConfirmResetPasswordTokenWasExpiredError extends ApiUnauthorizedException<"token was expired"> { 30 | @statusCode statusCode: 401; 31 | } 32 | 33 | alias ConfirmResetPasswordUnauthorizedException = ConfirmResetPasswordTokenWasExpiredError; 34 | 35 | // ## SEND EMAIL ## // 36 | @doc("When user not found") 37 | @error 38 | model SendEmailUserNotFoundError extends ApiNotFoundException<"user not found"> { 39 | @statusCode statusCode: 404; 40 | } 41 | 42 | alias SendEmailNotFoundException = SendEmailUserNotFoundError; 43 | -------------------------------------------------------------------------------- /docs/src/modules/reset-password/model.tsp: -------------------------------------------------------------------------------- 1 | import "../../utils/model.tsp"; 2 | import "@typespec/http"; 3 | 4 | using Utils.Model; 5 | using TypeSpec.Http; 6 | 7 | namespace api.ResetPassword; 8 | 9 | @doc("reset password entity base") 10 | model ResetPasswordEntity { 11 | token: string; 12 | } 13 | 14 | // ## SEND EMAIL ## // 15 | @doc("reset password send email input") 16 | model SendEmailInput { 17 | email: string; 18 | } 19 | 20 | // ## Confirm Reset Password ## // 21 | @doc("confirm reset password input") 22 | model ConfirmResetPasswordInput { 23 | password: string; 24 | token: string; 25 | confirmPassword: string; 26 | } 27 | 28 | @doc("confirm reset password params input") 29 | model ConfirmResetPasswordParamsInput { 30 | @path 31 | @doc("user token") 32 | token: string; 33 | } 34 | -------------------------------------------------------------------------------- /docs/src/utils/exceptions.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "@typespec/rest"; 3 | import "@typespec/openapi3"; 4 | 5 | namespace api.Utils.Exception; 6 | 7 | @doc("default error model") 8 | model DefaultException { 9 | @doc("status code") 10 | code: Status; 11 | 12 | @doc("request traceid") 13 | traceid: string; 14 | 15 | @doc("class that error occur") 16 | context: string; 17 | 18 | @doc("error message") 19 | message: Message[]; 20 | 21 | @doc("timestamp that error occur") 22 | timestamp: string; 23 | 24 | @doc("path error") 25 | path: string; 26 | } 27 | 28 | @doc("default exception reponse") 29 | model ApiErrorType { 30 | error: DefaultException; 31 | } 32 | 33 | @doc("When resource not found.") 34 | @error 35 | model ApiNotFoundException { 36 | ...ApiErrorType<404, Message>; 37 | } 38 | 39 | @doc("When input is invalid.") 40 | @error 41 | model ApiBadRequestException { 42 | ...ApiErrorType<400, Message>; 43 | } 44 | 45 | @doc("When conflict occour.") 46 | @error 47 | model ApiConflictException { 48 | ...ApiErrorType<409, Message>; 49 | } 50 | 51 | @doc("When unauthorized occour.") 52 | @error 53 | model ApiUnauthorizedException { 54 | ...ApiErrorType<401, Message>; 55 | } 56 | -------------------------------------------------------------------------------- /docs/src/utils/model.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | import "@typespec/versioning"; 3 | 4 | using TypeSpec.Versioning; 5 | using Utils.Versioning; 6 | 7 | namespace api.Utils.Model; 8 | 9 | using TypeSpec.Http; 10 | 11 | model DefaultSuccessResponse { 12 | @statusCode status: Status; 13 | @body body: Body; 14 | } 15 | 16 | @doc("document created successfully") 17 | model ApiCreatedOutput { 18 | @doc("document id") 19 | id: string; 20 | 21 | @doc("conditional if document was created") 22 | created: boolean; 23 | } 24 | 25 | @doc("pagination default response") 26 | model ApiPaginationOutput { 27 | @doc("documents") 28 | docs: T[]; 29 | 30 | @doc("current page") 31 | page: int32 = 1; 32 | 33 | @doc("limit per page") 34 | limit: int32 = 10; 35 | 36 | @doc("total items") 37 | total: int64 = 1; 38 | 39 | @doc("total pages") 40 | totalPages?: int32 = 1; 41 | } 42 | 43 | model ApiPaginationInput { 44 | @doc("pagination current page") 45 | @query 46 | page: int32 = 1; 47 | 48 | @doc("pagination limit per page") 49 | @query 50 | limit: int32 = 10; 51 | 52 | @doc("sort by property **property1:desc,property2:asc**") 53 | @query 54 | sort?: string = "createdAt:desc"; 55 | 56 | @doc("search by property **property1:value1|value2**") 57 | @query 58 | search?: string; 59 | } 60 | -------------------------------------------------------------------------------- /docs/src/utils/versioning.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/http"; 2 | 3 | using TypeSpec.Http; 4 | 5 | namespace api.Utils.Versioning; 6 | 7 | enum ApiVersion { 8 | v1, 9 | } 10 | 11 | enum DocVersionVersion { 12 | v1: "1.0", 13 | } 14 | 15 | model VersionParams { 16 | @doc("route version") 17 | @path 18 | version: ApiVersion; 19 | } 20 | -------------------------------------------------------------------------------- /docs/tspconfig.yaml: -------------------------------------------------------------------------------- 1 | emit: 2 | - "@typespec/openapi3" 3 | options: 4 | compiler: 5 | include: ["./**/*.tsp"] 6 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | const pack = require('./package.json'); 2 | 3 | module.exports = { 4 | apps: [ 5 | { 6 | name: pack.name, 7 | script: './dist/main.js', 8 | env_production: { 9 | NODE_ENV: 'prod' 10 | } 11 | } 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /jest-coverage.config.ts: -------------------------------------------------------------------------------- 1 | import { pathsToModuleNameMapper } from 'ts-jest'; 2 | 3 | import { compilerOptions } from './tsconfig.json'; 4 | 5 | export default { 6 | moduleFileExtensions: ['js', 'json', 'ts'], 7 | rootDir: 'src/core', 8 | testRegex: '.*\\.spec\\.ts$', 9 | transform: { 10 | '^.+\\.(t|j)s$': 'ts-jest' 11 | }, 12 | setupFilesAfterEnv: ['../../test/initialization.ts'], 13 | testEnvironment: 'node', 14 | collectCoverage: true, 15 | coverageThreshold: { 16 | global: { 17 | branches: 100, 18 | functions: 100, 19 | lines: 100, 20 | statements: 100 21 | } 22 | }, 23 | collectCoverageFrom: ['**/*.ts'], 24 | coverageDirectory: '../../coverage', 25 | coverageReporters: ['json-summary', 'lcov'], 26 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/../../' }) 27 | }; 28 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { pathsToModuleNameMapper } from 'ts-jest'; 2 | 3 | import { compilerOptions } from './tsconfig.json'; 4 | 5 | export default { 6 | moduleFileExtensions: ['js', 'json', 'ts'], 7 | roots: ['src/core', 'src/modules'], 8 | testRegex: '.*\\.spec\\.ts$', 9 | transform: { 10 | '^.+\\.(t|j)s$': [ 11 | '@swc/jest', 12 | { 13 | jsc: { 14 | target: 'es2021' 15 | }, 16 | sourceMaps: 'inline' 17 | } 18 | ] 19 | }, 20 | setupFilesAfterEnv: ['./test/initialization.ts'], 21 | testEnvironment: 'node', 22 | collectCoverageFrom: ['**/*.ts'], 23 | coverageDirectory: './coverage', 24 | coverageReporters: ['json-summary', 'lcov'], 25 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }) 26 | }; 27 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "typeCheck": true, 8 | "builder": { 9 | "type": "swc", 10 | "options": { 11 | "copyFiles": true, 12 | "quiet": true, 13 | "sync": true, 14 | "outDir": "dist", 15 | "watch": true, 16 | "swcrcPath": ".swcrc" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ohmy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikemajesty/nestjs-microservice-boilerplate-api/2a0d31df6ed52d2cb7210e33a898f9e321d61ecb/ohmy.gif -------------------------------------------------------------------------------- /scripts/npm-audit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | AUDIT=$(npm audit --omit=dev) 3 | 4 | high='high' 5 | 6 | case $AUDIT in 7 | *"$high"*) 8 | echo "" 9 | echo "\033[0;31mnpm high audit" 10 | echo "" 11 | echo "run npm audit --omit=dev and fixit" 12 | echo "" 13 | echo $AUDIT 14 | exit 1 15 | ;; 16 | esac 17 | 18 | critical='critical' 19 | 20 | case $AUDIT in 21 | *"$critical"*) 22 | echo "" 23 | echo "\033[0;31mnpm critical audit" 24 | echo "" 25 | echo "run npm audit --omit=dev and fixit" 26 | echo "" 27 | echo $AUDIT 28 | exit 1 29 | ;; 30 | esac -------------------------------------------------------------------------------- /src/core/cat/entity/cat.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity } from '@/utils/entity'; 2 | import { Infer, InputValidator } from '@/utils/validator'; 3 | 4 | const ID = InputValidator.string().uuid(); 5 | const Name = InputValidator.string().trim().min(1).max(200); 6 | const Breed = InputValidator.string().trim().min(1).max(200); 7 | const Age = InputValidator.number().min(0).max(30); 8 | const CreatedAt = InputValidator.date().nullish(); 9 | const UpdatedAt = InputValidator.date().nullish(); 10 | const DeletedAt = InputValidator.date().nullish(); 11 | 12 | export const CatEntitySchema = InputValidator.object({ 13 | id: ID, 14 | name: Name, 15 | breed: Breed, 16 | age: Age, 17 | createdAt: CreatedAt, 18 | updatedAt: UpdatedAt, 19 | deletedAt: DeletedAt 20 | }); 21 | 22 | type Cat = Infer; 23 | 24 | export class CatEntity extends BaseEntity() { 25 | name!: string; 26 | 27 | breed!: string; 28 | 29 | age!: number; 30 | 31 | constructor(entity: Cat) { 32 | super(CatEntitySchema); 33 | Object.assign(this, this.validate(entity)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/core/cat/repository/cat.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '@/infra/repository'; 2 | 3 | import { CatEntity } from '../entity/cat'; 4 | import { CatListInput, CatListOutput } from '../use-cases/cat-list'; 5 | 6 | export abstract class ICatRepository extends IRepository { 7 | abstract paginate(input: CatListInput): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/core/cat/use-cases/cat-create.ts: -------------------------------------------------------------------------------- 1 | import { CreatedModel } from '@/infra/repository'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiTrancingInput } from '@/utils/request'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { UUIDUtils } from '@/utils/uuid'; 6 | import { Infer } from '@/utils/validator'; 7 | 8 | import { CatEntity, CatEntitySchema } from '../entity/cat'; 9 | import { ICatRepository } from '../repository/cat'; 10 | 11 | export const CatCreateSchema = CatEntitySchema.pick({ 12 | name: true, 13 | breed: true, 14 | age: true 15 | }); 16 | 17 | export class CatCreateUsecase implements IUsecase { 18 | constructor(private readonly catRepository: ICatRepository) {} 19 | 20 | @ValidateSchema(CatCreateSchema) 21 | async execute(input: CatCreateInput, { tracing, user }: ApiTrancingInput): Promise { 22 | const entity = new CatEntity({ id: UUIDUtils.create(), ...input }); 23 | 24 | const created = await this.catRepository.create(entity); 25 | 26 | tracing.logEvent('cat-created', `cat created by: ${user.email}`); 27 | 28 | return created; 29 | } 30 | } 31 | 32 | export type CatCreateInput = Infer; 33 | export type CatCreateOutput = CreatedModel; 34 | -------------------------------------------------------------------------------- /src/core/cat/use-cases/cat-delete.ts: -------------------------------------------------------------------------------- 1 | import { ICatRepository } from '@/core/cat/repository/cat'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiNotFoundException } from '@/utils/exception'; 4 | import { ApiTrancingInput } from '@/utils/request'; 5 | import { IUsecase } from '@/utils/usecase'; 6 | import { Infer } from '@/utils/validator'; 7 | 8 | import { CatEntity, CatEntitySchema } from '../entity/cat'; 9 | 10 | export const CatDeleteSchema = CatEntitySchema.pick({ 11 | id: true 12 | }); 13 | 14 | export class CatDeleteUsecase implements IUsecase { 15 | constructor(private readonly catRepository: ICatRepository) {} 16 | 17 | @ValidateSchema(CatDeleteSchema) 18 | async execute({ id }: CatDeleteInput, { tracing, user }: ApiTrancingInput): Promise { 19 | const cat = await this.catRepository.findById(id); 20 | 21 | if (!cat) { 22 | throw new ApiNotFoundException(); 23 | } 24 | 25 | const entity = new CatEntity(cat); 26 | 27 | entity.deactivated(); 28 | 29 | await this.catRepository.updateOne({ id: entity.id }, entity); 30 | tracing.logEvent('cat-deleted', `cat deleted by: ${user.email}`); 31 | 32 | return entity; 33 | } 34 | } 35 | 36 | export type CatDeleteInput = Infer; 37 | export type CatDeleteOutput = CatEntity; 38 | -------------------------------------------------------------------------------- /src/core/cat/use-cases/cat-get-by-id.ts: -------------------------------------------------------------------------------- 1 | import { CatEntitySchema } from '@/core/cat/entity/cat'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiNotFoundException } from '@/utils/exception'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { Infer } from '@/utils/validator'; 6 | 7 | import { CatEntity } from '../entity/cat'; 8 | import { ICatRepository } from '../repository/cat'; 9 | 10 | export const CatGetByIdSchema = CatEntitySchema.pick({ 11 | id: true 12 | }); 13 | 14 | export class CatGetByIdUsecase implements IUsecase { 15 | constructor(private readonly catRepository: ICatRepository) {} 16 | 17 | @ValidateSchema(CatGetByIdSchema) 18 | async execute({ id }: CatGetByIdInput): Promise { 19 | const cat = await this.catRepository.findById(id); 20 | 21 | if (!cat) { 22 | throw new ApiNotFoundException(); 23 | } 24 | 25 | return new CatEntity(cat); 26 | } 27 | } 28 | 29 | export type CatGetByIdInput = Infer; 30 | export type CatGetByIdOutput = CatEntity; 31 | -------------------------------------------------------------------------------- /src/core/cat/use-cases/cat-list.ts: -------------------------------------------------------------------------------- 1 | import { CatEntity } from '@/core/cat/entity/cat'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { PaginationInput, PaginationOutput, PaginationSchema } from '@/utils/pagination'; 4 | import { SearchSchema } from '@/utils/search'; 5 | import { SortSchema } from '@/utils/sort'; 6 | import { IUsecase } from '@/utils/usecase'; 7 | import { InputValidator } from '@/utils/validator'; 8 | 9 | import { ICatRepository } from '../repository/cat'; 10 | 11 | export const CatListSchema = InputValidator.intersection(PaginationSchema, SortSchema.merge(SearchSchema)); 12 | export class CatListUsecase implements IUsecase { 13 | constructor(private readonly catRepository: ICatRepository) {} 14 | 15 | @ValidateSchema(CatListSchema) 16 | async execute(input: CatListInput): Promise { 17 | return await this.catRepository.paginate(input); 18 | } 19 | } 20 | 21 | export type CatListInput = PaginationInput; 22 | export type CatListOutput = PaginationOutput; 23 | -------------------------------------------------------------------------------- /src/core/cat/use-cases/cat-update.ts: -------------------------------------------------------------------------------- 1 | import { ICatRepository } from '@/core/cat/repository/cat'; 2 | import { ILoggerAdapter } from '@/infra/logger'; 3 | import { ValidateSchema } from '@/utils/decorators'; 4 | import { ApiNotFoundException } from '@/utils/exception'; 5 | import { ApiTrancingInput } from '@/utils/request'; 6 | import { IUsecase } from '@/utils/usecase'; 7 | import { Infer } from '@/utils/validator'; 8 | 9 | import { CatEntity, CatEntitySchema } from '../entity/cat'; 10 | 11 | export const CatUpdateSchema = CatEntitySchema.pick({ 12 | id: true 13 | }).merge(CatEntitySchema.omit({ id: true }).partial()); 14 | 15 | export class CatUpdateUsecase implements IUsecase { 16 | constructor( 17 | private readonly catRepository: ICatRepository, 18 | private readonly loggerService: ILoggerAdapter 19 | ) {} 20 | 21 | @ValidateSchema(CatUpdateSchema) 22 | async execute(input: CatUpdateInput, { tracing, user }: ApiTrancingInput): Promise { 23 | const cat = await this.catRepository.findById(input.id); 24 | 25 | if (!cat) { 26 | throw new ApiNotFoundException(); 27 | } 28 | 29 | const entity = new CatEntity({ ...cat, ...input }); 30 | 31 | await this.catRepository.updateOne({ id: entity.id }, entity); 32 | 33 | this.loggerService.info({ message: 'cat updated.', obj: { cat: input } }); 34 | 35 | const updated = await this.catRepository.findById(entity.id); 36 | 37 | tracing.logEvent('cat-updated', `cat updated by: ${user.email}`); 38 | 39 | return new CatEntity(updated as CatEntity); 40 | } 41 | } 42 | 43 | export type CatUpdateInput = Infer; 44 | export type CatUpdateOutput = CatEntity; 45 | -------------------------------------------------------------------------------- /src/core/permission/entity/permission.ts: -------------------------------------------------------------------------------- 1 | import { RoleEntity } from '@/core/role/entity/role'; 2 | import { BaseEntity } from '@/utils/entity'; 3 | import { Infer, InputValidator } from '@/utils/validator'; 4 | 5 | const ID = InputValidator.string().uuid(); 6 | const Name = InputValidator.string() 7 | .transform((value) => value.trim().replace(/ /g, '_').toLowerCase()) 8 | .refine((val) => val.includes(':'), { 9 | message: "permission must contains ':'" 10 | }); 11 | const CreatedAt = InputValidator.date().nullish().optional(); 12 | const UpdatedAt = InputValidator.date().nullish().optional(); 13 | const DeletedAt = InputValidator.date().nullish().optional(); 14 | const Roles = InputValidator.array(InputValidator.unknown()).optional(); 15 | 16 | export const PermissionEntitySchema = InputValidator.object({ 17 | id: ID, 18 | name: Name, 19 | roles: Roles, 20 | createdAt: CreatedAt, 21 | updatedAt: UpdatedAt, 22 | deletedAt: DeletedAt 23 | }); 24 | 25 | type Permission = Infer; 26 | 27 | export class PermissionEntity extends BaseEntity() { 28 | name!: string; 29 | 30 | roles?: RoleEntity[]; 31 | 32 | constructor(entity: Permission) { 33 | super(PermissionEntitySchema); 34 | Object.assign(this, this.validate(entity)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/core/permission/repository/permission.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '@/infra/repository'; 2 | 3 | import { PermissionEntity } from '../entity/permission'; 4 | import { PermissionListInput, PermissionListOutput } from '../use-cases/permission-list'; 5 | 6 | export type ExistsOnUpdateInput = { 7 | idNotEquals: string; 8 | nameEquals: string; 9 | }; 10 | 11 | export abstract class IPermissionRepository extends IRepository { 12 | abstract existsOnUpdate(input: ExistsOnUpdateInput): Promise; 13 | abstract paginate(input: PermissionListInput): Promise; 14 | abstract findOneWithRelation( 15 | filter: Partial, 16 | relations: { [key in keyof Partial]: true | false } 17 | ): Promise; 18 | } 19 | -------------------------------------------------------------------------------- /src/core/permission/use-cases/permission-create.ts: -------------------------------------------------------------------------------- 1 | import { ILoggerAdapter } from '@/infra/logger'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiConflictException } from '@/utils/exception'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { UUIDUtils } from '@/utils/uuid'; 6 | import { Infer } from '@/utils/validator'; 7 | 8 | import { IPermissionRepository } from '../repository/permission'; 9 | import { PermissionEntity, PermissionEntitySchema } from './../entity/permission'; 10 | 11 | export const PermissionCreateSchema = PermissionEntitySchema.pick({ 12 | name: true 13 | }); 14 | 15 | export class PermissionCreateUsecase implements IUsecase { 16 | constructor( 17 | private readonly permissionRepository: IPermissionRepository, 18 | private readonly loggerService: ILoggerAdapter 19 | ) {} 20 | 21 | @ValidateSchema(PermissionCreateSchema) 22 | async execute(input: PermissionCreateInput): Promise { 23 | const permission = await this.permissionRepository.findOne({ name: input.name }); 24 | 25 | if (permission) { 26 | throw new ApiConflictException('permissionExists'); 27 | } 28 | 29 | const entity = new PermissionEntity({ id: UUIDUtils.create(), ...input }); 30 | 31 | await this.permissionRepository.create(entity); 32 | 33 | this.loggerService.info({ message: 'permission created.', obj: { permission } }); 34 | 35 | return entity; 36 | } 37 | } 38 | 39 | export type PermissionCreateInput = Infer; 40 | export type PermissionCreateOutput = PermissionEntity; 41 | -------------------------------------------------------------------------------- /src/core/permission/use-cases/permission-delete.ts: -------------------------------------------------------------------------------- 1 | import { IPermissionRepository } from '@/core/permission/repository/permission'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiConflictException, ApiNotFoundException } from '@/utils/exception'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { Infer } from '@/utils/validator'; 6 | 7 | import { PermissionEntity, PermissionEntitySchema } from '../entity/permission'; 8 | 9 | export const PermissionDeleteSchema = PermissionEntitySchema.pick({ 10 | id: true 11 | }); 12 | 13 | export class PermissionDeleteUsecase implements IUsecase { 14 | constructor(private readonly permissionRepository: IPermissionRepository) {} 15 | 16 | @ValidateSchema(PermissionDeleteSchema) 17 | async execute({ id }: PermissionDeleteInput): Promise { 18 | const permission = await this.permissionRepository.findOneWithRelation({ id }, { roles: true }); 19 | 20 | if (!permission) { 21 | throw new ApiNotFoundException('permissionNotFound'); 22 | } 23 | 24 | if (permission.roles?.length) { 25 | throw new ApiConflictException( 26 | `permissionHasAssociationWithRole: ${permission.roles.map((r) => r.name).join(', ')}` 27 | ); 28 | } 29 | 30 | const entity = new PermissionEntity(permission); 31 | 32 | entity.deactivated(); 33 | 34 | await this.permissionRepository.create(entity); 35 | 36 | return entity; 37 | } 38 | } 39 | 40 | export type PermissionDeleteInput = Infer; 41 | export type PermissionDeleteOutput = PermissionEntity; 42 | -------------------------------------------------------------------------------- /src/core/permission/use-cases/permission-get-by-id.ts: -------------------------------------------------------------------------------- 1 | import { PermissionEntitySchema } from '@/core/permission/entity/permission'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiNotFoundException } from '@/utils/exception'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { Infer } from '@/utils/validator'; 6 | 7 | import { PermissionEntity } from '../entity/permission'; 8 | import { IPermissionRepository } from '../repository/permission'; 9 | 10 | export const PermissionGetByIdSchema = PermissionEntitySchema.pick({ 11 | id: true 12 | }); 13 | 14 | export class PermissionGetByIdUsecase implements IUsecase { 15 | constructor(private readonly permissionRepository: IPermissionRepository) {} 16 | 17 | @ValidateSchema(PermissionGetByIdSchema) 18 | async execute({ id }: PermissionGetByIdInput): Promise { 19 | const permission = await this.permissionRepository.findById(id); 20 | 21 | if (!permission) { 22 | throw new ApiNotFoundException('permissionNotFound'); 23 | } 24 | 25 | return new PermissionEntity(permission); 26 | } 27 | } 28 | 29 | export type PermissionGetByIdInput = Infer; 30 | export type PermissionGetByIdOutput = PermissionEntity; 31 | -------------------------------------------------------------------------------- /src/core/permission/use-cases/permission-list.ts: -------------------------------------------------------------------------------- 1 | import { PermissionEntity } from '@/core/permission/entity/permission'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { PaginationInput, PaginationOutput, PaginationSchema } from '@/utils/pagination'; 4 | import { SearchSchema } from '@/utils/search'; 5 | import { SortSchema } from '@/utils/sort'; 6 | import { IUsecase } from '@/utils/usecase'; 7 | import { InputValidator } from '@/utils/validator'; 8 | 9 | import { IPermissionRepository } from '../repository/permission'; 10 | 11 | export const PermissionListSchema = InputValidator.intersection(PaginationSchema, SortSchema.merge(SearchSchema)); 12 | 13 | export class PermissionListUsecase implements IUsecase { 14 | constructor(private readonly permissionRepository: IPermissionRepository) {} 15 | 16 | @ValidateSchema(PermissionListSchema) 17 | async execute(input: PermissionListInput): Promise { 18 | return await this.permissionRepository.paginate(input); 19 | } 20 | } 21 | 22 | export type PermissionListInput = PaginationInput; 23 | export type PermissionListOutput = PaginationOutput; 24 | -------------------------------------------------------------------------------- /src/core/permission/use-cases/permission-update.ts: -------------------------------------------------------------------------------- 1 | import { IPermissionRepository } from '@/core/permission/repository/permission'; 2 | import { ILoggerAdapter } from '@/infra/logger'; 3 | import { ValidateSchema } from '@/utils/decorators'; 4 | import { ApiConflictException, ApiNotFoundException } from '@/utils/exception'; 5 | import { IUsecase } from '@/utils/usecase'; 6 | import { Infer } from '@/utils/validator'; 7 | 8 | import { PermissionEntity, PermissionEntitySchema } from './../entity/permission'; 9 | 10 | export const PermissionUpdateSchema = PermissionEntitySchema.pick({ 11 | id: true 12 | }).merge(PermissionEntitySchema.omit({ id: true }).partial()); 13 | 14 | export class PermissionUpdateUsecase implements IUsecase { 15 | constructor( 16 | private readonly permissionRepository: IPermissionRepository, 17 | private readonly loggerService: ILoggerAdapter 18 | ) {} 19 | 20 | @ValidateSchema(PermissionUpdateSchema) 21 | async execute(input: PermissionUpdateInput): Promise { 22 | const permission = await this.permissionRepository.findById(input.id); 23 | 24 | if (!permission) { 25 | throw new ApiNotFoundException('permissionNotFound'); 26 | } 27 | 28 | if (input.name) { 29 | const permissionExists = await this.permissionRepository.existsOnUpdate({ 30 | idNotEquals: input.id, 31 | nameEquals: input.name 32 | }); 33 | 34 | if (permissionExists) { 35 | throw new ApiConflictException('permissionExists'); 36 | } 37 | } 38 | 39 | const entity = new PermissionEntity({ ...permission, ...input }); 40 | 41 | await this.permissionRepository.updateOne({ id: entity.id }, entity); 42 | 43 | this.loggerService.info({ message: 'permission updated.', obj: { permission: input } }); 44 | 45 | return entity; 46 | } 47 | } 48 | 49 | export type PermissionUpdateInput = Infer; 50 | export type PermissionUpdateOutput = PermissionEntity; 51 | -------------------------------------------------------------------------------- /src/core/reset-password/entity/reset-password.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity, UserEntitySchema } from '@/core/user/entity/user'; 2 | import { BaseEntity } from '@/utils/entity'; 3 | import { Infer, InputValidator } from '@/utils/validator'; 4 | 5 | const ID = InputValidator.string().uuid(); 6 | const Token = InputValidator.string().min(1).trim(); 7 | const User = UserEntitySchema; 8 | const CreatedAt = InputValidator.date().nullish(); 9 | const UpdatedAt = InputValidator.date().nullish(); 10 | const DeletedAt = InputValidator.date().nullish(); 11 | 12 | export const ResetPasswordEntitySchema = InputValidator.object({ 13 | id: ID, 14 | token: Token, 15 | user: User.optional(), 16 | createdAt: CreatedAt, 17 | updatedAt: UpdatedAt, 18 | deletedAt: DeletedAt 19 | }); 20 | 21 | type ResetPassword = Infer; 22 | 23 | export class ResetPasswordEntity extends BaseEntity() { 24 | token!: string; 25 | 26 | user!: UserEntity; 27 | 28 | constructor(entity: ResetPassword) { 29 | super(ResetPasswordEntitySchema); 30 | Object.assign(this, this.validate(entity)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/core/reset-password/repository/reset-password.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '@/infra/repository'; 2 | 3 | import { ResetPasswordEntity } from '../entity/reset-password'; 4 | 5 | export abstract class IResetPasswordRepository extends IRepository { 6 | abstract findByIdUserId(id: string): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/core/role/entity/role.ts: -------------------------------------------------------------------------------- 1 | import { PermissionEntity, PermissionEntitySchema } from '@/core/permission/entity/permission'; 2 | import { BaseEntity } from '@/utils/entity'; 3 | import { Infer, InputValidator } from '@/utils/validator'; 4 | 5 | export enum RoleEnum { 6 | USER = 'USER', 7 | BACKOFFICE = 'BACKOFFICE' 8 | } 9 | 10 | const ID = InputValidator.string().uuid(); 11 | const Name = InputValidator.string().transform((value) => value.trim().replace(/ /g, '_').toUpperCase()); 12 | const Permissions = InputValidator.array(PermissionEntitySchema).optional(); 13 | const CreatedAt = InputValidator.date().nullish(); 14 | const UpdatedAt = InputValidator.date().nullish(); 15 | const DeletedAt = InputValidator.date().optional().nullish(); 16 | 17 | export const RoleEntitySchema = InputValidator.object({ 18 | id: ID, 19 | name: Name, 20 | permissions: Permissions, 21 | createdAt: CreatedAt, 22 | updatedAt: UpdatedAt, 23 | deletedAt: DeletedAt 24 | }); 25 | 26 | type Role = Infer; 27 | 28 | export class RoleEntity extends BaseEntity() { 29 | name!: RoleEnum; 30 | 31 | permissions!: PermissionEntity[]; 32 | 33 | constructor(entity: Role) { 34 | super(RoleEntitySchema); 35 | Object.assign(this, this.validate(entity)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/core/role/repository/role.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '@/infra/repository'; 2 | 3 | import { RoleEntity } from '../entity/role'; 4 | import { RoleListInput, RoleListOutput } from '../use-cases/role-list'; 5 | 6 | export abstract class IRoleRepository extends IRepository { 7 | abstract paginate(input: RoleListInput): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/core/role/use-cases/__tests__/role-create.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { TestMock } from 'test/mock'; 3 | 4 | import { ILoggerAdapter } from '@/infra/logger'; 5 | import { IRoleCreateAdapter } from '@/modules/role/adapter'; 6 | import { ZodExceptionIssue } from '@/utils/validator'; 7 | 8 | import { RoleEnum } from '../../entity/role'; 9 | import { IRoleRepository } from '../../repository/role'; 10 | import { RoleCreateInput, RoleCreateOutput, RoleCreateUsecase } from '../role-create'; 11 | 12 | describe(RoleCreateUsecase.name, () => { 13 | let usecase: IRoleCreateAdapter; 14 | let repository: IRoleRepository; 15 | 16 | beforeEach(async () => { 17 | const app = await Test.createTestingModule({ 18 | providers: [ 19 | { 20 | provide: IRoleRepository, 21 | useValue: {} 22 | }, 23 | { 24 | provide: ILoggerAdapter, 25 | useValue: { 26 | info: TestMock.mockReturnValue() 27 | } 28 | }, 29 | { 30 | provide: IRoleCreateAdapter, 31 | useFactory: (roleRepository: IRoleRepository, logger: ILoggerAdapter) => { 32 | return new RoleCreateUsecase(roleRepository, logger); 33 | }, 34 | inject: [IRoleRepository, ILoggerAdapter] 35 | } 36 | ] 37 | }).compile(); 38 | 39 | usecase = app.get(IRoleCreateAdapter); 40 | repository = app.get(IRoleRepository); 41 | }); 42 | 43 | test('when no input is specified, should expect an error', async () => { 44 | await TestMock.expectZodError( 45 | () => usecase.execute({} as RoleCreateInput), 46 | (issues: ZodExceptionIssue[]) => { 47 | expect(issues).toEqual([{ message: 'Required', path: TestMock.nameOf('name') }]); 48 | } 49 | ); 50 | }); 51 | 52 | const input: RoleCreateInput = { 53 | name: RoleEnum.USER 54 | }; 55 | 56 | test('when role created successfully, should expect a role created', async () => { 57 | const output: RoleCreateOutput = { created: true, id: TestMock.getMockUUID() }; 58 | repository.create = TestMock.mockResolvedValue(output); 59 | 60 | await expect(usecase.execute(input)).resolves.toEqual(output); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/core/role/use-cases/role-create.ts: -------------------------------------------------------------------------------- 1 | import { ILoggerAdapter } from '@/infra/logger'; 2 | import { CreatedModel } from '@/infra/repository'; 3 | import { ValidateSchema } from '@/utils/decorators'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { UUIDUtils } from '@/utils/uuid'; 6 | import { Infer } from '@/utils/validator'; 7 | 8 | import { IRoleRepository } from '../repository/role'; 9 | import { RoleEntity, RoleEntitySchema } from './../entity/role'; 10 | 11 | export const RoleCreateSchema = RoleEntitySchema.pick({ 12 | name: true 13 | }).strict(); 14 | 15 | export class RoleCreateUsecase implements IUsecase { 16 | constructor( 17 | private readonly roleRepository: IRoleRepository, 18 | private readonly loggerService: ILoggerAdapter 19 | ) {} 20 | 21 | @ValidateSchema(RoleCreateSchema) 22 | async execute(input: RoleCreateInput): Promise { 23 | const entity = new RoleEntity({ id: UUIDUtils.create(), ...input }); 24 | 25 | const role = await this.roleRepository.create(entity); 26 | 27 | this.loggerService.info({ message: 'role created.', obj: { role } }); 28 | 29 | return role; 30 | } 31 | } 32 | 33 | export type RoleCreateInput = Infer; 34 | export type RoleCreateOutput = CreatedModel; 35 | -------------------------------------------------------------------------------- /src/core/role/use-cases/role-delete-permission.ts: -------------------------------------------------------------------------------- 1 | import { IPermissionRepository } from '@/core/permission/repository/permission'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiNotFoundException } from '@/utils/exception'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { Infer, InputValidator } from '@/utils/validator'; 6 | 7 | import { RoleEntity, RoleEntitySchema } from '../entity/role'; 8 | import { IRoleRepository } from '../repository/role'; 9 | 10 | export const RoleDeletePermissionSchema = RoleEntitySchema.pick({ 11 | id: true 12 | }).merge(InputValidator.object({ permissions: InputValidator.array(InputValidator.string()) })); 13 | 14 | export class RoleDeletePermissionUsecase implements IUsecase { 15 | constructor( 16 | private readonly roleRepository: IRoleRepository, 17 | private readonly permissionRepository: IPermissionRepository 18 | ) {} 19 | 20 | @ValidateSchema(RoleDeletePermissionSchema) 21 | async execute(input: RoleDeletePermissionInput): Promise { 22 | const role = await this.roleRepository.findOne({ id: input.id }); 23 | 24 | if (!role) { 25 | throw new ApiNotFoundException('roleNotFound'); 26 | } 27 | 28 | const entity = new RoleEntity(role); 29 | 30 | const permissions = await this.permissionRepository.findIn({ name: input.permissions }); 31 | 32 | for (const permission of input.permissions) { 33 | const permissionExists = permissions.find((p) => p.name === permission); 34 | 35 | if (!permissionExists) { 36 | continue; 37 | } 38 | 39 | const permissionAssociated = entity.permissions.find((p) => p.name === permission); 40 | 41 | if (permissionAssociated) { 42 | entity.permissions = entity.permissions.filter((p) => p.name !== permissionAssociated.name); 43 | } 44 | } 45 | 46 | await this.roleRepository.create(entity); 47 | } 48 | } 49 | 50 | export type RoleDeletePermissionInput = Infer; 51 | export type RoleDeletePermissionOutput = void; 52 | -------------------------------------------------------------------------------- /src/core/role/use-cases/role-delete.ts: -------------------------------------------------------------------------------- 1 | import { IRoleRepository } from '@/core/role/repository/role'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiConflictException, ApiNotFoundException } from '@/utils/exception'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { Infer } from '@/utils/validator'; 6 | 7 | import { RoleEntity, RoleEntitySchema } from '../entity/role'; 8 | 9 | export const RoleDeleteSchema = RoleEntitySchema.pick({ 10 | id: true 11 | }); 12 | 13 | export class RoleDeleteUsecase implements IUsecase { 14 | constructor(private readonly roleRepository: IRoleRepository) {} 15 | 16 | @ValidateSchema(RoleDeleteSchema) 17 | async execute({ id }: RoleDeleteInput): Promise { 18 | const role = await this.roleRepository.findById(id); 19 | 20 | if (!role) { 21 | throw new ApiNotFoundException('roleNotFound'); 22 | } 23 | 24 | if (role.permissions?.length) { 25 | throw new ApiConflictException(`roleHasAssociationWithPermission: ${role.permissions.map((p) => p.name)}`); 26 | } 27 | 28 | const entity = new RoleEntity(role); 29 | 30 | entity.deactivated(); 31 | 32 | await this.roleRepository.create(entity); 33 | 34 | return entity; 35 | } 36 | } 37 | 38 | export type RoleDeleteInput = Infer; 39 | export type RoleDeleteOutput = RoleEntity; 40 | -------------------------------------------------------------------------------- /src/core/role/use-cases/role-get-by-id.ts: -------------------------------------------------------------------------------- 1 | import { RoleEntitySchema } from '@/core/role/entity/role'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiNotFoundException } from '@/utils/exception'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { Infer } from '@/utils/validator'; 6 | 7 | import { RoleEntity } from '../entity/role'; 8 | import { IRoleRepository } from '../repository/role'; 9 | 10 | export const RoleGetByIdSchema = RoleEntitySchema.pick({ 11 | id: true 12 | }); 13 | 14 | export class RoleGetByIdUsecase implements IUsecase { 15 | constructor(private readonly roleRepository: IRoleRepository) {} 16 | 17 | @ValidateSchema(RoleGetByIdSchema) 18 | async execute({ id }: RoleGetByIdInput): Promise { 19 | const role = await this.roleRepository.findById(id); 20 | 21 | if (!role) { 22 | throw new ApiNotFoundException('roleNotFound'); 23 | } 24 | 25 | return new RoleEntity(role); 26 | } 27 | } 28 | 29 | export type RoleGetByIdInput = Infer; 30 | export type RoleGetByIdOutput = RoleEntity; 31 | -------------------------------------------------------------------------------- /src/core/role/use-cases/role-list.ts: -------------------------------------------------------------------------------- 1 | import { RoleEntity } from '@/core/role/entity/role'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { PaginationInput, PaginationOutput, PaginationSchema } from '@/utils/pagination'; 4 | import { SearchSchema } from '@/utils/search'; 5 | import { SortSchema } from '@/utils/sort'; 6 | import { IUsecase } from '@/utils/usecase'; 7 | import { InputValidator } from '@/utils/validator'; 8 | 9 | import { IRoleRepository } from '../repository/role'; 10 | 11 | export const RoleListSchema = InputValidator.intersection(PaginationSchema, SortSchema.merge(SearchSchema)); 12 | 13 | export class RoleListUsecase implements IUsecase { 14 | constructor(private readonly roleRepository: IRoleRepository) {} 15 | 16 | @ValidateSchema(RoleListSchema) 17 | async execute(input: RoleListInput): Promise { 18 | return await this.roleRepository.paginate(input); 19 | } 20 | } 21 | 22 | export type RoleListInput = PaginationInput; 23 | export type RoleListOutput = PaginationOutput; 24 | -------------------------------------------------------------------------------- /src/core/role/use-cases/role-update.ts: -------------------------------------------------------------------------------- 1 | import { IRoleRepository } from '@/core/role/repository/role'; 2 | import { ILoggerAdapter } from '@/infra/logger'; 3 | import { ValidateSchema } from '@/utils/decorators'; 4 | import { ApiNotFoundException } from '@/utils/exception'; 5 | import { IUsecase } from '@/utils/usecase'; 6 | import { Infer } from '@/utils/validator'; 7 | 8 | import { RoleEntity, RoleEntitySchema } from './../entity/role'; 9 | 10 | export const RoleUpdateSchema = RoleEntitySchema.pick({ 11 | id: true 12 | }) 13 | .merge(RoleEntitySchema.pick({ name: true }).partial()) 14 | .strict(); 15 | 16 | export class RoleUpdateUsecase implements IUsecase { 17 | constructor( 18 | private readonly roleRepository: IRoleRepository, 19 | private readonly loggerService: ILoggerAdapter 20 | ) {} 21 | 22 | @ValidateSchema(RoleUpdateSchema) 23 | async execute(input: RoleUpdateInput): Promise { 24 | const role = await this.roleRepository.findById(input.id); 25 | 26 | if (!role) { 27 | throw new ApiNotFoundException('roleNotFound'); 28 | } 29 | 30 | const entity = new RoleEntity({ ...role, ...input }); 31 | 32 | await this.roleRepository.create(entity); 33 | 34 | this.loggerService.info({ message: 'role updated.', obj: { roles: input } }); 35 | 36 | const updated = await this.roleRepository.findById(entity.id); 37 | 38 | return new RoleEntity(updated as RoleEntity); 39 | } 40 | } 41 | 42 | export type RoleUpdateInput = Infer; 43 | export type RoleUpdateOutput = RoleEntity; 44 | -------------------------------------------------------------------------------- /src/core/user/entity/user-password.ts: -------------------------------------------------------------------------------- 1 | import { CryptoUtils } from '@/utils/crypto'; 2 | import { BaseEntity } from '@/utils/entity'; 3 | import { ApiBadRequestException } from '@/utils/exception'; 4 | import { Infer, InputValidator } from '@/utils/validator'; 5 | 6 | const ID = InputValidator.string().uuid(); 7 | const Password = InputValidator.string(); 8 | const CreatedAt = InputValidator.date().nullish(); 9 | const UpdatedAt = InputValidator.date().nullish(); 10 | const DeletedAt = InputValidator.date().nullish(); 11 | 12 | export const UserPasswordEntitySchema = InputValidator.object({ 13 | id: ID, 14 | password: Password, 15 | createdAt: CreatedAt, 16 | updatedAt: UpdatedAt, 17 | deletedAt: DeletedAt 18 | }); 19 | 20 | type UserPassword = Infer; 21 | 22 | export class UserPasswordEntity extends BaseEntity() { 23 | password!: string; 24 | 25 | constructor(entity: UserPassword) { 26 | super(UserPasswordEntitySchema); 27 | Object.assign(this, this.validate(entity)); 28 | } 29 | 30 | createPassword() { 31 | this.password = CryptoUtils.createHash(this.password); 32 | return this.password; 33 | } 34 | 35 | verifyPassword(password: string) { 36 | if (this.password !== password) { 37 | throw new ApiBadRequestException('incorrectPassword'); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/core/user/entity/user.ts: -------------------------------------------------------------------------------- 1 | import { RoleEntity, RoleEntitySchema } from '@/core/role/entity/role'; 2 | import { BaseEntity } from '@/utils/entity'; 3 | import { Infer, InputValidator } from '@/utils/validator'; 4 | 5 | import { UserPasswordEntity, UserPasswordEntitySchema } from './user-password'; 6 | 7 | const ID = InputValidator.string().uuid(); 8 | const Email = InputValidator.string().email(); 9 | const Name = InputValidator.string(); 10 | const Password = UserPasswordEntitySchema; 11 | const Role = RoleEntitySchema; 12 | const CreatedAt = InputValidator.date().nullish(); 13 | const UpdatedAt = InputValidator.date().nullish(); 14 | const DeletedAt = InputValidator.date().nullish(); 15 | 16 | export const UserEntitySchema = InputValidator.object({ 17 | id: ID, 18 | name: Name, 19 | email: Email, 20 | roles: InputValidator.array(Role.optional()).min(1), 21 | password: Password.optional(), 22 | createdAt: CreatedAt, 23 | updatedAt: UpdatedAt, 24 | deletedAt: DeletedAt 25 | }); 26 | 27 | type User = Infer; 28 | 29 | export class UserEntity extends BaseEntity() { 30 | name!: string; 31 | 32 | email!: string; 33 | 34 | roles!: RoleEntity[]; 35 | 36 | password!: UserPasswordEntity; 37 | 38 | constructor(entity: User) { 39 | super(UserEntitySchema); 40 | Object.assign(this, this.validate(entity)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/core/user/repository/user.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from '@/infra/repository'; 2 | 3 | import { UserEntity } from '../entity/user'; 4 | import { UserListInput, UserListOutput } from '../use-cases/user-list'; 5 | 6 | export abstract class IUserRepository extends IRepository { 7 | abstract existsOnUpdate( 8 | equalFilter: Pick, 9 | notEqualFilter: Pick 10 | ): Promise; 11 | abstract paginate(input: UserListInput): Promise; 12 | abstract softRemove(entity: Partial): Promise; 13 | abstract findOneWithRelation( 14 | filter: Partial, 15 | relations: { [key in keyof Partial]: true | false } 16 | ): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /src/core/user/use-cases/__tests__/user-logout.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { TestMock } from 'test/mock'; 3 | 4 | import { ICacheAdapter } from '@/infra/cache'; 5 | import { ISecretsAdapter, SecretsModule } from '@/infra/secrets'; 6 | import { TokenLibModule } from '@/libs/token'; 7 | import { ILogoutAdapter } from '@/modules/logout/adapter'; 8 | import { ZodExceptionIssue } from '@/utils/validator'; 9 | 10 | import { LogoutInput, LogoutUsecase } from '../user-logout'; 11 | 12 | describe(LogoutUsecase.name, () => { 13 | let usecase: ILogoutAdapter; 14 | let cache: ICacheAdapter; 15 | 16 | beforeEach(async () => { 17 | const app = await Test.createTestingModule({ 18 | imports: [TokenLibModule, SecretsModule], 19 | providers: [ 20 | { 21 | provide: ICacheAdapter, 22 | useValue: { 23 | set: TestMock.mockResolvedValue() 24 | } 25 | }, 26 | { 27 | provide: ILogoutAdapter, 28 | useFactory: (cache: ICacheAdapter, secrets: ISecretsAdapter) => { 29 | return new LogoutUsecase(cache, secrets); 30 | }, 31 | inject: [ICacheAdapter, ISecretsAdapter] 32 | } 33 | ] 34 | }).compile(); 35 | 36 | usecase = app.get(ILogoutAdapter); 37 | cache = app.get(ICacheAdapter); 38 | }); 39 | 40 | test('when no input is specified, should expect an error', async () => { 41 | await TestMock.expectZodError( 42 | () => usecase.execute({} as LogoutInput, TestMock.getMockTracing()), 43 | (issues: ZodExceptionIssue[]) => { 44 | expect(issues).toEqual([{ message: 'Required', path: TestMock.nameOf('token') }]); 45 | } 46 | ); 47 | }); 48 | 49 | test('when user logout, should expect set token to blacklist', async () => { 50 | cache.set = TestMock.mockResolvedValue(); 51 | 52 | await expect(usecase.execute({ token: '12345678910' }, TestMock.getMockTracing())).resolves.toBeUndefined(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/core/user/use-cases/user-change-password.ts: -------------------------------------------------------------------------------- 1 | import { CryptoUtils } from '@/utils/crypto'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiBadRequestException, ApiNotFoundException } from '@/utils/exception'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { Infer, InputValidator } from '@/utils/validator'; 6 | 7 | import { UserEntitySchema } from '../entity/user'; 8 | import { UserPasswordEntity } from '../entity/user-password'; 9 | import { IUserRepository } from '../repository/user'; 10 | 11 | export const UserChangePasswordSchema = UserEntitySchema.pick({ 12 | id: true 13 | }).merge( 14 | InputValidator.object({ 15 | password: InputValidator.string(), 16 | newPassword: InputValidator.string(), 17 | confirmPassword: InputValidator.string() 18 | }) 19 | ); 20 | 21 | export class UserChangePasswordUsecase implements IUsecase { 22 | constructor(private readonly repository: IUserRepository) {} 23 | 24 | @ValidateSchema(UserChangePasswordSchema) 25 | async execute(input: UserChangePasswordInput): Promise { 26 | const user = await this.repository.findOneWithRelation({ id: input.id }, { password: true }); 27 | 28 | if (!user) { 29 | throw new ApiNotFoundException('userNotFound'); 30 | } 31 | 32 | const entityPassword = new UserPasswordEntity(user.password); 33 | 34 | const password = CryptoUtils.createHash(input.password); 35 | 36 | entityPassword.verifyPassword(password); 37 | 38 | if (input.newPassword !== input.confirmPassword) { 39 | throw new ApiBadRequestException('passwordIsDifferent'); 40 | } 41 | 42 | entityPassword.password = input.newPassword; 43 | 44 | entityPassword.createPassword(); 45 | 46 | user.password = entityPassword; 47 | 48 | await this.repository.create(user); 49 | } 50 | } 51 | 52 | export type UserChangePasswordInput = Infer; 53 | export type UserChangePasswordOutput = void; 54 | -------------------------------------------------------------------------------- /src/core/user/use-cases/user-delete.ts: -------------------------------------------------------------------------------- 1 | import { ValidateSchema } from '@/utils/decorators'; 2 | import { ApiNotFoundException } from '@/utils/exception'; 3 | import { ApiTrancingInput } from '@/utils/request'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | import { Infer } from '@/utils/validator'; 6 | 7 | import { UserEntity, UserEntitySchema } from '../entity/user'; 8 | import { IUserRepository } from '../repository/user'; 9 | 10 | export const UserDeleteSchema = UserEntitySchema.pick({ 11 | id: true 12 | }); 13 | 14 | export class UserDeleteUsecase implements IUsecase { 15 | constructor(private readonly userRepository: IUserRepository) {} 16 | 17 | @ValidateSchema(UserDeleteSchema) 18 | async execute({ id }: UserDeleteInput, { tracing, user: userData }: ApiTrancingInput): Promise { 19 | const user = await this.userRepository.findOneWithRelation({ id }, { password: true }); 20 | 21 | if (!user) { 22 | throw new ApiNotFoundException('userNotFound'); 23 | } 24 | 25 | const entity = new UserEntity(user); 26 | 27 | await this.userRepository.softRemove(entity); 28 | 29 | tracing.logEvent('user-deleted', `user: ${user.email} deleted by: ${userData.email}`); 30 | 31 | return entity; 32 | } 33 | } 34 | 35 | export type UserDeleteInput = Infer; 36 | export type UserDeleteOutput = UserEntity; 37 | -------------------------------------------------------------------------------- /src/core/user/use-cases/user-get-by-id.ts: -------------------------------------------------------------------------------- 1 | import { ValidateSchema } from '@/utils/decorators'; 2 | import { ApiNotFoundException } from '@/utils/exception'; 3 | import { IUsecase } from '@/utils/usecase'; 4 | import { Infer } from '@/utils/validator'; 5 | 6 | import { UserEntity, UserEntitySchema } from '../entity/user'; 7 | import { IUserRepository } from '../repository/user'; 8 | 9 | export const UserGetByIdSchema = UserEntitySchema.pick({ 10 | id: true 11 | }); 12 | 13 | export class UserGetByIdUsecase implements IUsecase { 14 | constructor(private readonly userRepository: IUserRepository) {} 15 | 16 | @ValidateSchema(UserGetByIdSchema) 17 | async execute({ id }: UserGetByIdInput): Promise { 18 | const user = await this.userRepository.findOne({ id }); 19 | 20 | if (!user) { 21 | throw new ApiNotFoundException('userNotFound'); 22 | } 23 | 24 | const entity = new UserEntity(user); 25 | 26 | return entity; 27 | } 28 | } 29 | 30 | export type UserGetByIdInput = Infer; 31 | export type UserGetByIdOutput = UserEntity; 32 | -------------------------------------------------------------------------------- /src/core/user/use-cases/user-list.ts: -------------------------------------------------------------------------------- 1 | import { ValidateSchema } from '@/utils/decorators'; 2 | import { PaginationInput, PaginationOutput, PaginationSchema } from '@/utils/pagination'; 3 | import { SearchSchema } from '@/utils/search'; 4 | import { SortSchema } from '@/utils/sort'; 5 | import { IUsecase } from '@/utils/usecase'; 6 | import { InputValidator } from '@/utils/validator'; 7 | 8 | import { UserEntity } from '../entity/user'; 9 | import { IUserRepository } from '../repository/user'; 10 | 11 | export const UserListSchema = InputValidator.intersection(PaginationSchema, SortSchema.merge(SearchSchema)); 12 | 13 | export class UserListUsecase implements IUsecase { 14 | constructor(private readonly userRepository: IUserRepository) {} 15 | 16 | @ValidateSchema(UserListSchema) 17 | async execute(input: UserListInput): Promise { 18 | const users = await this.userRepository.paginate(input); 19 | 20 | return { 21 | docs: users.docs.map((user) => { 22 | const entity = new UserEntity(user); 23 | return entity; 24 | }), 25 | limit: users.limit, 26 | page: users.page, 27 | total: users.total 28 | }; 29 | } 30 | } 31 | 32 | export type UserListInput = PaginationInput; 33 | export type UserListOutput = PaginationOutput; 34 | -------------------------------------------------------------------------------- /src/core/user/use-cases/user-login.ts: -------------------------------------------------------------------------------- 1 | import { ITokenAdapter } from '@/libs/token'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiNotFoundException } from '@/utils/exception'; 4 | import { ApiTrancingInput, UserRequest } from '@/utils/request'; 5 | import { IUsecase } from '@/utils/usecase'; 6 | import { UUIDUtils } from '@/utils/uuid'; 7 | import { Infer } from '@/utils/validator'; 8 | 9 | import { UserEntitySchema } from '../entity/user'; 10 | import { UserPasswordEntity, UserPasswordEntitySchema } from '../entity/user-password'; 11 | import { IUserRepository } from '../repository/user'; 12 | 13 | export const LoginSchema = UserEntitySchema.pick({ 14 | email: true 15 | }).merge(UserPasswordEntitySchema.pick({ password: true })); 16 | 17 | export class LoginUsecase implements IUsecase { 18 | constructor( 19 | private readonly userRepository: IUserRepository, 20 | private readonly tokenService: ITokenAdapter 21 | ) {} 22 | 23 | @ValidateSchema(LoginSchema) 24 | async execute(input: LoginInput, { tracing }: ApiTrancingInput): Promise { 25 | const user = await this.userRepository.findOneWithRelation( 26 | { 27 | email: input.email 28 | }, 29 | { password: true } 30 | ); 31 | 32 | if (!user) { 33 | throw new ApiNotFoundException('userNotFound'); 34 | } 35 | 36 | if (!user.roles.length) { 37 | throw new ApiNotFoundException('roleNotFound'); 38 | } 39 | 40 | const passwordEntity = new UserPasswordEntity({ id: UUIDUtils.create(), password: input.password }); 41 | 42 | passwordEntity.createPassword(); 43 | 44 | passwordEntity.verifyPassword(user.password.password); 45 | 46 | tracing.logEvent('user-login', `${user}`); 47 | 48 | const { token } = this.tokenService.sign({ 49 | email: user.email, 50 | name: user.name, 51 | id: user.id 52 | } as UserRequest); 53 | 54 | const { token: refreshToken } = this.tokenService.sign({ userId: user.id }); 55 | 56 | return { accessToken: token, refreshToken }; 57 | } 58 | } 59 | 60 | export type LoginInput = Infer; 61 | export type LoginOutput = { accessToken: string; refreshToken: string }; 62 | -------------------------------------------------------------------------------- /src/core/user/use-cases/user-logout.ts: -------------------------------------------------------------------------------- 1 | import { ICacheAdapter } from '@/infra/cache'; 2 | import { ISecretsAdapter } from '@/infra/secrets'; 3 | import { ValidateSchema } from '@/utils/decorators'; 4 | import { ApiTrancingInput } from '@/utils/request'; 5 | import { IUsecase } from '@/utils/usecase'; 6 | import { Infer, InputValidator } from '@/utils/validator'; 7 | 8 | export const LogoutSchema = InputValidator.object({ token: InputValidator.string().trim().min(10) }); 9 | 10 | export class LogoutUsecase implements IUsecase { 11 | constructor( 12 | private readonly redis: ICacheAdapter, 13 | private readonly secretes: ISecretsAdapter 14 | ) {} 15 | 16 | @ValidateSchema(LogoutSchema) 17 | async execute(input: LogoutInput, { tracing, user }: ApiTrancingInput): LogoutOutput { 18 | await this.redis.set(input.token, input.token, { PX: this.secretes.TOKEN_EXPIRATION }); 19 | 20 | tracing.logEvent('user-logout', `${user.email}`); 21 | } 22 | } 23 | 24 | export type LogoutInput = Infer; 25 | export type LogoutOutput = Promise; 26 | -------------------------------------------------------------------------------- /src/core/user/use-cases/user-refresh-token.ts: -------------------------------------------------------------------------------- 1 | import { ITokenAdapter } from '@/libs/token'; 2 | import { ValidateSchema } from '@/utils/decorators'; 3 | import { ApiBadRequestException, ApiNotFoundException } from '@/utils/exception'; 4 | import { UserRequest } from '@/utils/request'; 5 | import { IUsecase } from '@/utils/usecase'; 6 | import { Infer, InputValidator } from '@/utils/validator'; 7 | 8 | import { IUserRepository } from '../repository/user'; 9 | 10 | export const RefreshTokenSchema = InputValidator.object({ refreshToken: InputValidator.string().trim().min(1) }); 11 | 12 | export class RefreshTokenUsecase implements IUsecase { 13 | constructor( 14 | private readonly userRepository: IUserRepository, 15 | private readonly tokenService: ITokenAdapter 16 | ) {} 17 | 18 | @ValidateSchema(RefreshTokenSchema) 19 | async execute(input: RefreshTokenInput): Promise { 20 | const userToken = await this.tokenService.verify(input.refreshToken); 21 | 22 | if (!userToken.userId) { 23 | throw new ApiBadRequestException('incorrectToken'); 24 | } 25 | 26 | const user = await this.userRepository.findOne({ 27 | id: userToken.userId 28 | }); 29 | 30 | if (!user) { 31 | throw new ApiNotFoundException('userNotFound'); 32 | } 33 | 34 | if (!user.roles.length) { 35 | throw new ApiNotFoundException('roleNotFound'); 36 | } 37 | 38 | const { token } = this.tokenService.sign({ 39 | email: user.email, 40 | name: user.name, 41 | id: user.id 42 | } as UserRequest); 43 | 44 | const { token: refreshToken } = this.tokenService.sign({ userId: user.id }); 45 | 46 | return { accessToken: token, refreshToken }; 47 | } 48 | } 49 | 50 | export type RefreshTokenInput = Infer; 51 | export type RefreshTokenOutput = { accessToken: string; refreshToken: string }; 52 | 53 | export type UserRefreshTokenVerifyInput = { 54 | userId: string | null; 55 | }; 56 | -------------------------------------------------------------------------------- /src/infra/cache/adapter.ts: -------------------------------------------------------------------------------- 1 | import { MemoryCacheSetType } from './memory/types'; 2 | import { RedisCacheKeyArgument, RedisCacheKeyValue, RedisCacheValueArgument } from './redis/types'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export abstract class ICacheAdapter { 6 | client!: T; 7 | 8 | abstract ping(): Promise; 9 | 10 | abstract connect(): Promise | T; 11 | 12 | abstract set< 13 | TKey extends RedisCacheKeyArgument = RedisCacheKeyArgument, 14 | TValue extends RedisCacheValueArgument = RedisCacheValueArgument, 15 | TConf extends object = object 16 | >(key: TKey, value: TValue, config?: TConf): Promise | void; 17 | 18 | abstract del(key: TKey): Promise | boolean; 19 | 20 | abstract get(key: TKey): Promise | string; 21 | 22 | abstract setMulti(redisList?: RedisCacheKeyValue[]): Promise; 23 | 24 | abstract pExpire( 25 | key: PCache, 26 | milliseconds: number 27 | ): Promise | boolean; 28 | 29 | abstract hGet< 30 | TKey extends RedisCacheKeyArgument = RedisCacheKeyArgument, 31 | TArs extends RedisCacheKeyArgument = RedisCacheKeyArgument 32 | >(key?: TKey, field?: TArs): Promise | void; 33 | 34 | abstract hSet< 35 | TKey extends RedisCacheKeyArgument = RedisCacheKeyArgument, 36 | TArgs extends RedisCacheKeyArgument = RedisCacheKeyArgument, 37 | TValue extends RedisCacheValueArgument = RedisCacheValueArgument 38 | >(key?: TKey, field?: TArgs, value?: TValue): Promise | void; 39 | 40 | abstract hGetAll( 41 | key: TKey 42 | ): Promise | void; 43 | 44 | abstract mSet(model?: TSet[]): boolean; 45 | 46 | abstract mGet(key?: string[]): unknown | null; 47 | 48 | abstract has(key?: string | number): boolean; 49 | } 50 | -------------------------------------------------------------------------------- /src/infra/cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/infra/cache/memory/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './service'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/infra/cache/memory/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ILoggerAdapter, LoggerModule } from '@/infra/logger'; 4 | 5 | import { ICacheAdapter } from '../adapter'; 6 | import { MemoryCacheService } from './service'; 7 | 8 | @Module({ 9 | imports: [LoggerModule], 10 | providers: [ 11 | { 12 | provide: ICacheAdapter, 13 | useFactory: async (logger: ILoggerAdapter) => { 14 | const cacheService = new MemoryCacheService(logger); 15 | cacheService.connect(); 16 | return cacheService; 17 | }, 18 | inject: [ILoggerAdapter] 19 | } 20 | ], 21 | exports: [ICacheAdapter] 22 | }) 23 | export class MemoryCacheModule {} 24 | -------------------------------------------------------------------------------- /src/infra/cache/memory/service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import NodeCache from 'node-cache'; 3 | 4 | import { ILoggerAdapter } from '@/infra/logger'; 5 | 6 | import { ICacheAdapter } from '../adapter'; 7 | import { MemoryCacheKeyArgument, MemoryCacheSetType, MemoryCacheTTL, MemoryCacheValueArgument } from './types'; 8 | 9 | @Injectable() 10 | export class MemoryCacheService implements Partial> { 11 | client!: NodeCache; 12 | 13 | constructor(private readonly logger: ILoggerAdapter) {} 14 | 15 | connect(config?: NodeCache.Options): NodeCache { 16 | this.client = new NodeCache(config || { stdTTL: 3600, checkperiod: 3600 }); 17 | this.logger.log('🎯 cacheMemory connected!'); 18 | return this.client; 19 | } 20 | 21 | mSet(model: TSet[]): boolean { 22 | return this.client.mset(model); 23 | } 24 | 25 | mGet(key: string[]): unknown { 26 | return this.client.mget(key); 27 | } 28 | 29 | has(key: string | number): boolean { 30 | return this.client.has(key); 31 | } 32 | 33 | set( 34 | key: TKey, 35 | value: TValue, 36 | config?: TConf 37 | ): void { 38 | this.client.set(key as MemoryCacheKeyArgument, value, config as MemoryCacheTTL); 39 | } 40 | 41 | del(key: TKey): boolean { 42 | return !!this.client.del(key as MemoryCacheKeyArgument); 43 | } 44 | 45 | get(key: TKey): string { 46 | return this.client.get(key as MemoryCacheKeyArgument) as string; 47 | } 48 | 49 | pExpire(key: TCache, ttl: number): boolean { 50 | return this.client.ttl(key as MemoryCacheKeyArgument, ttl); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/infra/cache/memory/types.ts: -------------------------------------------------------------------------------- 1 | export type MemoryCacheKeyArgument = string | number; 2 | export type MemoryCacheValueArgument = number | string | Buffer; 3 | export type MemoryCacheTTL = number | string; 4 | 5 | export type MemoryCacheSetType = { 6 | key: string; 7 | val: unknown; 8 | ttl?: number; 9 | }; 10 | -------------------------------------------------------------------------------- /src/infra/cache/redis/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './service'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/infra/cache/redis/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { createClient, RedisClientType } from 'redis'; 3 | 4 | import { ILoggerAdapter, LoggerModule } from '@/infra/logger'; 5 | import { ISecretsAdapter, SecretsModule } from '@/infra/secrets'; 6 | 7 | import { ICacheAdapter } from '../adapter'; 8 | import { RedisService } from './service'; 9 | 10 | @Module({ 11 | imports: [LoggerModule, SecretsModule], 12 | providers: [ 13 | { 14 | provide: ICacheAdapter, 15 | useFactory: async ({ REDIS_URL }: ISecretsAdapter, logger: ILoggerAdapter) => { 16 | const client = createClient({ url: REDIS_URL }) as RedisClientType; 17 | const cacheService = new RedisService(logger, client); 18 | await cacheService.connect(); 19 | return cacheService; 20 | }, 21 | inject: [ISecretsAdapter, ILoggerAdapter] 22 | } 23 | ], 24 | exports: [ICacheAdapter] 25 | }) 26 | export class RedisCacheModule {} 27 | -------------------------------------------------------------------------------- /src/infra/cache/redis/types.ts: -------------------------------------------------------------------------------- 1 | export type RedisCacheKeyArgument = string | Buffer; 2 | export type RedisCacheValueArgument = string | Buffer; 3 | 4 | export type RedisCacheKeyValue = { 5 | key: RedisCacheKeyArgument; 6 | value: RedisCacheValueArgument | RedisCacheValueArgument[]; 7 | }; 8 | -------------------------------------------------------------------------------- /src/infra/cache/types.ts: -------------------------------------------------------------------------------- 1 | export type CacheKeyArgument = string | Buffer; 2 | export type CacheValueArgument = string | Buffer; 3 | 4 | export type CacheKeyValue = { 5 | key: CacheKeyArgument; 6 | value: CacheValueArgument | CacheValueArgument[]; 7 | }; 8 | -------------------------------------------------------------------------------- /src/infra/database/adapter.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionType } from './types'; 2 | 3 | export abstract class IDataBaseAdapter { 4 | abstract getConnection(model: ConnectionType): TConnection; 5 | abstract getDatabase(): TInstance; 6 | } 7 | -------------------------------------------------------------------------------- /src/infra/database/enum.ts: -------------------------------------------------------------------------------- 1 | export enum ConnectionName { 2 | CATS = `CATS_CONNECTION` 3 | } 4 | -------------------------------------------------------------------------------- /src/infra/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/infra/database/mongo/config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import { mongoMigrateCli } from 'mongo-migrate-ts'; 4 | import path from 'path'; 5 | 6 | import { LoggerService } from '@/infra/logger'; 7 | 8 | LoggerService.log(`ENV: ${process.env['NODE_ENV']} mongo migration running.\n`); 9 | 10 | mongoMigrateCli({ 11 | uri: process.env['MONGO_URL'], 12 | database: process.env['MONGO_DATABASE'], 13 | migrationsDir: path.join(__dirname, './migrations'), 14 | migrationsCollection: 'migrations' 15 | }); 16 | -------------------------------------------------------------------------------- /src/infra/database/mongo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './service'; 3 | -------------------------------------------------------------------------------- /src/infra/database/mongo/migrations/1709943706267_createCatsCollection.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface } from 'mongo-migrate-ts'; 2 | import { Db } from 'mongodb'; 3 | 4 | export class CreateUserCollection1709943706267 implements MigrationInterface { 5 | async up(db: Db): Promise { 6 | await db.createCollection('cats'); 7 | } 8 | 9 | async down(db: Db): Promise { 10 | await db.dropCollection('cats'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/infra/database/mongo/schemas/cat.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document } from 'mongoose'; 3 | import paginate from 'mongoose-paginate-v2'; 4 | 5 | import { CatEntity } from '@/core/cat/entity/cat'; 6 | 7 | export type CatDocument = Document & CatEntity; 8 | 9 | @Schema({ 10 | collection: 'cats', 11 | autoIndex: true, 12 | timestamps: true 13 | }) 14 | export class Cat { 15 | @Prop({ type: String }) 16 | _id!: string; 17 | 18 | @Prop({ min: 0, max: 200, required: true, type: String }) 19 | name!: string; 20 | 21 | @Prop({ min: 0, max: 200, required: true, type: String }) 22 | breed!: string; 23 | 24 | @Prop({ min: 0, max: 200, required: true, type: Number }) 25 | age!: string; 26 | 27 | @Prop({ type: Date, default: null }) 28 | deletedAt!: Date; 29 | } 30 | 31 | const CatSchema = SchemaFactory.createForClass(Cat); 32 | 33 | CatSchema.index({ name: 1 }, { partialFilterExpression: { deletedAt: { $eq: null } } }); 34 | 35 | CatSchema.plugin(paginate); 36 | 37 | CatSchema.virtual('id').get(function () { 38 | return this._id; 39 | }); 40 | 41 | export { CatSchema }; 42 | -------------------------------------------------------------------------------- /src/infra/database/mongo/service.ts: -------------------------------------------------------------------------------- 1 | import { MongooseModuleOptions } from '@nestjs/mongoose'; 2 | 3 | import { IDataBaseAdapter } from '../adapter'; 4 | import { ConnectionType } from '../types'; 5 | 6 | export class MongoService implements Partial { 7 | getConnection({ URI }: ConnectionType): TOpt { 8 | return { 9 | uri: URI 10 | } as TOpt; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/infra/database/postgres/config.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import path from 'path'; 3 | import { DataSource } from 'typeorm'; 4 | import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; 5 | 6 | config(); 7 | 8 | const dataSource = new DataSource({ 9 | type: 'postgres', 10 | host: process.env.POSTGRES_HOST, 11 | port: Number(process.env.POSTGRES_PORT), 12 | username: process.env.POSTGRES_USER, 13 | password: process.env.POSTGRES_PASSWORD, 14 | namingStrategy: new SnakeNamingStrategy(), 15 | logging: true, 16 | database: process.env.POSTGRES_DATABASE, 17 | migrationsTableName: 'migrations', 18 | migrations: [path.join(__dirname, '/migrations/*.{ts,js}')], 19 | entities: [path.join(__dirname, '/schemas/*.{ts,js}')] 20 | }); 21 | 22 | export default dataSource; 23 | -------------------------------------------------------------------------------- /src/infra/database/postgres/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './service'; 3 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727653462661-createPermissionTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class createPermissionTable1727653462661 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `CREATE TABLE IF NOT EXISTS "permissions" ("id" uuid NOT NULL, "name" text NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, CONSTRAINT "UQ_48ce552495d14eae9b187bb6716" UNIQUE ("name"), CONSTRAINT "PK_920331560282b8bd21bb02290df" PRIMARY KEY ("id"))` 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.dropTable('permissions', true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727653565690-createRoleTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class createRoleTable1727653565690 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `CREATE TABLE IF NOT EXISTS "roles" ("id" uuid NOT NULL, "name" text NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, CONSTRAINT "UQ_648e3f5447f725579d7d4ffdfb7" UNIQUE ("name"), CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY ("id"))` 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.dropTable('roles', true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727653630438-createUserPasswordTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class createUserPasswordTable1727653630438 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `CREATE TABLE IF NOT EXISTS "users_password" ("id" uuid NOT NULL, "password" text NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, CONSTRAINT "PK_4175c832d328c98ebafbc82aa95" PRIMARY KEY ("id"))` 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.dropTable('users_password', true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727653714156-createUserTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class createUserTable1727653714156 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `CREATE TABLE IF NOT EXISTS "users" ("id" uuid NOT NULL, "name" text NOT NULL, "email" text NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, "password_id" uuid, CONSTRAINT "REL_4175c832d328c98ebafbc82aa9" UNIQUE ("password_id"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))` 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.dropTable('users_password', true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727653808424-createResetPassword.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class createResetPassword1727653808424 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `CREATE TABLE IF NOT EXISTS "reset_password" ("id" uuid NOT NULL, "token" text NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, "user_id" uuid, CONSTRAINT "PK_82bffbeb85c5b426956d004a8f5" PRIMARY KEY ("id"))` 7 | ); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.dropTable('reset_password', true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727653954337-createPermissionRoleTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class createPermissionRoleTable1727653954337 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `CREATE TABLE IF NOT EXISTS "permissions_roles" ("roles_id" uuid NOT NULL, "permissions_id" uuid NOT NULL, CONSTRAINT "PK_4e9d6f04b532a4d1f8da11505f4" PRIMARY KEY ("roles_id", "permissions_id"))` 7 | ); 8 | await queryRunner.query(`CREATE INDEX "IDX_982f19c6e0628e9d5ba8132ec6" ON "permissions_roles" ("roles_id")`); 9 | await queryRunner.query(`CREATE INDEX "IDX_f1c8ff871433d51c817a17234d" ON "permissions_roles" ("permissions_id")`); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.dropIndex('permissions_roles', 'IDX_982f19c6e0628e9d5ba8132ec6'); 14 | await queryRunner.dropIndex('permissions_roles', 'IDX_f1c8ff871433d51c817a17234d'); 15 | await queryRunner.dropTable('permissions_roles', true); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727654008041-createUserRoleTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class createUserRoleTable1727654008041 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `CREATE TABLE IF NOT EXISTS "users_roles" ("users_id" uuid NOT NULL, "roles_id" uuid NOT NULL, CONSTRAINT "PK_366245cb19fcd0ca30321644748" PRIMARY KEY ("users_id", "roles_id"))` 7 | ); 8 | await queryRunner.query(`CREATE INDEX "IDX_259b5a2e24d0a5480262a774e4" ON "users_roles"("users_id") `); 9 | await queryRunner.query(`CREATE INDEX "IDX_ed1bb3304475cfa49b2964aaf2" ON "users_roles"("roles_id")`); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.dropIndex('users_roles', 'IDX_259b5a2e24d0a5480262a774e4'); 14 | await queryRunner.dropIndex('users_roles', 'IDX_ed1bb3304475cfa49b2964aaf2'); 15 | await queryRunner.dropTable('users_roles', true); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727654289658-createTableRelationship.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class createTableRelationship1727654289658 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `ALTER TABLE "users" ADD CONSTRAINT "FK_4175c832d328c98ebafbc82aa95" FOREIGN KEY ("password_id") REFERENCES "users_password"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` 7 | ); 8 | await queryRunner.query( 9 | `ALTER TABLE "reset_password" ADD CONSTRAINT "FK_de65040d842349a5e6428ff21e6" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` 10 | ); 11 | await queryRunner.query( 12 | `ALTER TABLE "permissions_roles" ADD CONSTRAINT "FK_982f19c6e0628e9d5ba8132ec67" FOREIGN KEY ("roles_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE` 13 | ); 14 | await queryRunner.query( 15 | `ALTER TABLE "permissions_roles" ADD CONSTRAINT "FK_f1c8ff871433d51c817a17234de" FOREIGN KEY ("permissions_id") REFERENCES "permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE` 16 | ); 17 | await queryRunner.query( 18 | `ALTER TABLE "users_roles" ADD CONSTRAINT "FK_259b5a2e24d0a5480262a774e46" FOREIGN KEY ("users_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE` 19 | ); 20 | await queryRunner.query( 21 | `ALTER TABLE "users_roles" ADD CONSTRAINT "FK_ed1bb3304475cfa49b2964aaf27" FOREIGN KEY ("roles_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE` 22 | ); 23 | } 24 | 25 | public async down(queryRunner: QueryRunner): Promise { 26 | await queryRunner.dropCheckConstraint('users', 'FK_4175c832d328c98ebafbc82aa95'); 27 | await queryRunner.dropCheckConstraint('reset_password', 'FK_de65040d842349a5e6428ff21e6'); 28 | await queryRunner.dropCheckConstraint('permissions_roles', 'FK_982f19c6e0628e9d5ba8132ec67'); 29 | await queryRunner.dropCheckConstraint('permissions_roles', 'FK_f1c8ff871433d51c817a17234de'); 30 | await queryRunner.dropCheckConstraint('users_roles', 'FK_259b5a2e24d0a5480262a774e46'); 31 | await queryRunner.dropCheckConstraint('users_roles', 'FK_ed1bb3304475cfa49b2964aaf27'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727654555722-insertPermissions.ts: -------------------------------------------------------------------------------- 1 | import { PermissionEntity } from '@/core/permission/entity/permission'; 2 | import { UUIDUtils } from '@/utils/uuid'; 3 | import { MigrationInterface, QueryRunner } from 'typeorm'; 4 | import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; 5 | import { PermissionSchema } from '../schemas/permission'; 6 | 7 | export const userPermissions = [ 8 | 'cat:create', 9 | 'cat:update', 10 | 'cat:getbyid', 11 | 'cat:list', 12 | 'cat:delete', 13 | 'user:logout', 14 | 'user:create', 15 | 'user:update', 16 | 'user:list', 17 | 'user:getbyid', 18 | 'user:changepassword', 19 | 'user:delete' 20 | ]; 21 | export const backofficePermissions = [ 22 | 'permission:create', 23 | 'permission:update', 24 | 'permission:getbyid', 25 | 'permission:list', 26 | 'permission:delete', 27 | 'role:create', 28 | 'role:update', 29 | 'role:getbyid', 30 | 'role:list', 31 | 'role:delete', 32 | 'role:addpermission', 33 | 'role:deletepermission' 34 | ]; 35 | 36 | export class insertPermissions1727654555722 implements MigrationInterface { 37 | public async up(queryRunner: QueryRunner): Promise { 38 | const permissionsPromises = []; 39 | for (const permission of userPermissions.concat(backofficePermissions)) { 40 | const entity = new PermissionEntity({ id: UUIDUtils.create(), name: permission }); 41 | permissionsPromises.push( 42 | queryRunner.manager.insert(PermissionSchema, entity as QueryDeepPartialEntity) 43 | ); 44 | } 45 | 46 | await Promise.all(permissionsPromises); 47 | } 48 | 49 | public async down(queryRunner: QueryRunner): Promise { 50 | await queryRunner.manager.remove(PermissionSchema); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727654843890-insertRoles.ts: -------------------------------------------------------------------------------- 1 | import { RoleEntity, RoleEnum } from '@/core/role/entity/role'; 2 | import { UUIDUtils } from '@/utils/uuid'; 3 | import { MigrationInterface, QueryRunner } from 'typeorm'; 4 | import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; 5 | import { RoleSchema } from '../schemas/role'; 6 | 7 | export class insertRoles1727654843890 implements MigrationInterface { 8 | entityBackOffice = new RoleEntity({ id: UUIDUtils.create(), name: RoleEnum.BACKOFFICE }); 9 | entityUser = new RoleEntity({ id: UUIDUtils.create(), name: RoleEnum.USER }); 10 | 11 | public async up(queryRunner: QueryRunner): Promise { 12 | await queryRunner.manager.insert(RoleSchema, this.entityBackOffice as QueryDeepPartialEntity); 13 | await queryRunner.manager.insert(RoleSchema, this.entityUser as QueryDeepPartialEntity); 14 | } 15 | 16 | public async down(queryRunner: QueryRunner): Promise { 17 | await queryRunner.manager.delete(RoleSchema, { id: this.entityBackOffice.id }); 18 | await queryRunner.manager.delete(RoleSchema, { id: this.entityUser.id }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/infra/database/postgres/migrations/1727657387427-addUnaccentExtension.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class addUnaccentExtension1727657387427 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS unaccent`); 6 | } 7 | 8 | public async down(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query(`DROP EXTENSION IF EXISTS unaccent`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/infra/database/postgres/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import path from 'path'; 4 | import { DataSource, DataSourceOptions } from 'typeorm'; 5 | import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; 6 | 7 | import { ISecretsAdapter, SecretsModule } from '@/infra/secrets'; 8 | 9 | import { name } from '../../../../package.json'; 10 | import { PostgresService } from './service'; 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forRootAsync({ 14 | useFactory: ({ POSTGRES: { POSTGRES_URL }, IS_LOCAL }: ISecretsAdapter) => { 15 | const conn = new PostgresService().getConnection({ URI: POSTGRES_URL }); 16 | return { 17 | ...conn, 18 | timeout: 5000, 19 | connectTimeout: 5000, 20 | logging: false, 21 | autoLoadEntities: true, 22 | namingStrategy: new SnakeNamingStrategy(), 23 | synchronize: IS_LOCAL, 24 | migrationsTableName: 'migrations', 25 | migrations: [path.join(__dirname, '/migrations/*.{ts,js}')], 26 | entities: [path.join(__dirname, '/schemas/*.{ts,js}')], 27 | applicationName: name, 28 | extra: { 29 | connectionTimeoutMillis: 10000, 30 | idleTimeoutMillis: 30000, 31 | max: 90, 32 | min: 10 33 | } 34 | }; 35 | }, 36 | async dataSourceFactory(options) { 37 | const dataSource = new DataSource(options as DataSourceOptions); 38 | return dataSource.initialize(); 39 | }, 40 | imports: [SecretsModule], 41 | inject: [ISecretsAdapter] 42 | }) 43 | ] 44 | }) 45 | export class PostgresDatabaseModule {} 46 | -------------------------------------------------------------------------------- /src/infra/database/postgres/schemas/permission.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | Entity, 7 | JoinTable, 8 | ManyToMany, 9 | Relation, 10 | UpdateDateColumn 11 | } from 'typeorm'; 12 | 13 | import { RoleSchema } from './role'; 14 | 15 | @Entity({ name: 'permissions' }) 16 | export class PermissionSchema extends BaseEntity { 17 | @Column({ type: 'uuid', primary: true }) 18 | id!: string; 19 | 20 | @Column('text', { unique: true }) 21 | name!: string; 22 | 23 | @ManyToMany(() => RoleSchema) 24 | @JoinTable({ name: 'permissions_roles' }) 25 | roles!: Relation; 26 | 27 | @CreateDateColumn() 28 | createdAt!: Date; 29 | 30 | @UpdateDateColumn() 31 | updatedAt!: Date; 32 | 33 | @DeleteDateColumn({ nullable: true }) 34 | deletedAt!: Date; 35 | } 36 | -------------------------------------------------------------------------------- /src/infra/database/postgres/schemas/reset-password.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | Entity, 7 | JoinColumn, 8 | ManyToOne, 9 | Relation, 10 | UpdateDateColumn 11 | } from 'typeorm'; 12 | 13 | import { UserSchema } from './user'; 14 | 15 | @Entity({ name: 'reset_password' }) 16 | export class ResetPasswordSchema extends BaseEntity { 17 | @Column({ type: 'uuid', primary: true }) 18 | id!: string; 19 | 20 | @Column('text') 21 | token!: string; 22 | 23 | @ManyToOne(() => UserSchema, { cascade: ['insert', 'remove', 'update', 'soft-remove'], eager: true }) 24 | @JoinColumn() 25 | user!: Relation; 26 | 27 | @CreateDateColumn() 28 | createdAt!: Date; 29 | 30 | @UpdateDateColumn() 31 | updatedAt!: Date; 32 | 33 | @DeleteDateColumn({ nullable: true }) 34 | deletedAt!: Date; 35 | } 36 | -------------------------------------------------------------------------------- /src/infra/database/postgres/schemas/role.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | Entity, 7 | JoinTable, 8 | ManyToMany, 9 | Relation, 10 | UpdateDateColumn 11 | } from 'typeorm'; 12 | 13 | import { RoleEnum } from '@/core/role/entity/role'; 14 | 15 | import { PermissionSchema } from './permission'; 16 | 17 | @Entity({ name: 'roles' }) 18 | export class RoleSchema extends BaseEntity { 19 | @Column({ type: 'uuid', primary: true }) 20 | id!: string; 21 | 22 | @Column('text', { unique: true }) 23 | name!: RoleEnum; 24 | 25 | @ManyToMany(() => PermissionSchema, { eager: true, cascade: ['insert', 'recover', 'update'] }) 26 | @JoinTable({ name: 'permissions_roles' }) 27 | permissions!: Relation; 28 | 29 | @CreateDateColumn() 30 | createdAt!: Date; 31 | 32 | @UpdateDateColumn() 33 | updatedAt!: Date; 34 | 35 | @DeleteDateColumn({ nullable: true }) 36 | deletedAt!: Date; 37 | } 38 | -------------------------------------------------------------------------------- /src/infra/database/postgres/schemas/user-password.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, CreateDateColumn, DeleteDateColumn, Entity, UpdateDateColumn } from 'typeorm'; 2 | 3 | @Entity({ name: 'users_password' }) 4 | export class UserPasswordSchema extends BaseEntity { 5 | @Column({ type: 'uuid', primary: true }) 6 | id!: string; 7 | 8 | @Column('text') 9 | password!: string; 10 | 11 | @CreateDateColumn() 12 | createdAt!: Date; 13 | 14 | @UpdateDateColumn() 15 | updatedAt!: Date; 16 | 17 | @DeleteDateColumn({ nullable: true }) 18 | deletedAt!: Date; 19 | } 20 | -------------------------------------------------------------------------------- /src/infra/database/postgres/schemas/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | Entity, 7 | JoinColumn, 8 | JoinTable, 9 | ManyToMany, 10 | OneToOne, 11 | Relation, 12 | UpdateDateColumn 13 | } from 'typeorm'; 14 | 15 | import { RoleSchema } from './role'; 16 | import { UserPasswordSchema } from './user-password'; 17 | 18 | @Entity({ name: 'users' }) 19 | export class UserSchema extends BaseEntity { 20 | @Column({ type: 'uuid', primary: true }) 21 | id!: string; 22 | 23 | @Column('text') 24 | name!: string; 25 | 26 | @Column('text') 27 | email!: string; 28 | 29 | @OneToOne(() => UserPasswordSchema, { cascade: ['insert', 'recover', 'update', 'remove', 'soft-remove'] }) 30 | @JoinColumn() 31 | password!: Relation; 32 | 33 | @ManyToMany(() => RoleSchema, { eager: true, cascade: ['recover'] }) 34 | @JoinTable({ name: 'users_roles' }) 35 | roles!: Relation; 36 | 37 | @CreateDateColumn() 38 | createdAt!: Date; 39 | 40 | @UpdateDateColumn() 41 | updatedAt!: Date; 42 | 43 | @DeleteDateColumn({ nullable: true }) 44 | deletedAt!: Date; 45 | } 46 | -------------------------------------------------------------------------------- /src/infra/database/postgres/service.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 2 | 3 | import { name } from '../../../../package.json'; 4 | import { IDataBaseAdapter } from '../adapter'; 5 | import { ConnectionType } from '../types'; 6 | 7 | export class PostgresService implements Partial { 8 | getConnection({ URI }: ConnectionType): TOpt { 9 | return { 10 | type: 'postgres', 11 | url: URI, 12 | database: name 13 | } as TOpt; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/infra/database/types.ts: -------------------------------------------------------------------------------- 1 | export type ConnectionType = { 2 | URI: string; 3 | }; 4 | -------------------------------------------------------------------------------- /src/infra/email/adapter.ts: -------------------------------------------------------------------------------- 1 | import { SendEmailInput, SendEmailOutput } from './service'; 2 | 3 | export abstract class IEmailAdapter { 4 | abstract send(input: SendEmailInput): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/infra/email/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './module'; 3 | export * from './service'; 4 | -------------------------------------------------------------------------------- /src/infra/email/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import nodemailer from 'nodemailer'; 3 | 4 | import { ILoggerAdapter, LoggerModule } from '../logger'; 5 | import { ISecretsAdapter, SecretsModule } from '../secrets'; 6 | import { IEmailAdapter } from './adapter'; 7 | import { EmailService } from './service'; 8 | 9 | @Module({ 10 | imports: [SecretsModule, LoggerModule], 11 | providers: [ 12 | { 13 | provide: IEmailAdapter, 14 | useFactory: (secret: ISecretsAdapter, logger: ILoggerAdapter) => { 15 | const transporter = nodemailer.createTransport({ 16 | host: secret.EMAIL.HOST, 17 | port: secret.EMAIL.PORT, 18 | auth: { 19 | user: secret.EMAIL.USER, 20 | pass: secret.EMAIL.PASS 21 | } 22 | }); 23 | return new EmailService(secret, logger, transporter); 24 | }, 25 | inject: [ISecretsAdapter, ILoggerAdapter] 26 | } 27 | ], 28 | exports: [IEmailAdapter] 29 | }) 30 | export class EmailModule {} 31 | -------------------------------------------------------------------------------- /src/infra/email/service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { OnEvent } from '@nestjs/event-emitter'; 3 | import fs from 'fs'; 4 | import handlebars from 'handlebars'; 5 | import { Transporter } from 'nodemailer'; 6 | import SMTPTransport from 'nodemailer/lib/smtp-transport'; 7 | import path from 'path'; 8 | 9 | import { EventNameEnum } from '@/libs/event/types'; 10 | 11 | import { ILoggerAdapter } from '../logger'; 12 | import { ISecretsAdapter } from '../secrets'; 13 | import { IEmailAdapter } from './adapter'; 14 | 15 | export type SendEmailInput = { 16 | subject: string; 17 | email: string; 18 | template: string; 19 | payload: object; 20 | }; 21 | 22 | export type SendEmailOutput = SMTPTransport.SentMessageInfo; 23 | 24 | @Injectable() 25 | export class EmailService implements IEmailAdapter { 26 | constructor( 27 | private readonly secrets: ISecretsAdapter, 28 | private readonly logger: ILoggerAdapter, 29 | private readonly transporter: Transporter 30 | ) {} 31 | 32 | async send(input: SendEmailInput): Promise { 33 | /* eslint-disable-next-line security/detect-non-literal-fs-filename */ 34 | const source = fs.readFileSync(path.join(__dirname, `/templates/${input.template}.handlebars`), 'utf8'); 35 | const compiledTemplate = handlebars.compile(source); 36 | const options = () => { 37 | return { 38 | from: this.secrets.EMAIL.FROM, 39 | to: input.email, 40 | subject: input.subject, 41 | html: compiledTemplate(input.payload) 42 | }; 43 | }; 44 | 45 | return new Promise((res, rej) => { 46 | this.transporter.sendMail(options(), (error, info) => { 47 | if (error) { 48 | return rej(error); 49 | } 50 | 51 | return res(info); 52 | }); 53 | }); 54 | } 55 | 56 | @OnEvent(EventNameEnum.SEND_EMAIL) 57 | async handleSendEmailEvent(payload: SendEmailInput) { 58 | await this.send(payload); 59 | this.logger.info({ message: 'email sended successfully.' }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/infra/email/templates/reque-reset-password.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 |

Hi {{name}},

9 |

You requested to reset your password.

10 |

Please, click the link below to reset your password

11 | Reset Password 12 | 13 | -------------------------------------------------------------------------------- /src/infra/email/templates/reset-password.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 |

Hi {{name}},

9 |

Your password has been changed successfully

10 | 11 | -------------------------------------------------------------------------------- /src/infra/email/templates/welcome.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 |

Hi {{name}},

9 |

Welcome to your new account

10 | 11 | -------------------------------------------------------------------------------- /src/infra/http/adapter.ts: -------------------------------------------------------------------------------- 1 | import { Axios, AxiosInstance } from 'axios'; 2 | 3 | import { TracingType } from '@/utils/request'; 4 | 5 | export abstract class IHttpAdapter { 6 | abstract instance(): T; 7 | abstract tracing?: TracingType; 8 | } 9 | -------------------------------------------------------------------------------- /src/infra/http/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './module'; 3 | export * from './service'; 4 | -------------------------------------------------------------------------------- /src/infra/http/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ILoggerAdapter, LoggerModule } from '@/infra/logger'; 4 | 5 | import { IHttpAdapter } from './adapter'; 6 | import { HttpService } from './service'; 7 | 8 | @Module({ 9 | imports: [LoggerModule], 10 | providers: [ 11 | { 12 | provide: IHttpAdapter, 13 | useFactory: (logger: ILoggerAdapter) => { 14 | return new HttpService(logger); 15 | }, 16 | inject: [ILoggerAdapter] 17 | } 18 | ], 19 | exports: [IHttpAdapter] 20 | }) 21 | export class HttpModule {} 22 | -------------------------------------------------------------------------------- /src/infra/http/service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import axios, { Axios, AxiosInstance } from 'axios'; 3 | import axiosBetterStacktrace from 'axios-better-stacktrace'; 4 | import https from 'https'; 5 | 6 | import { AxiosUtils } from '@/utils/axios'; 7 | import { TracingType } from '@/utils/request'; 8 | 9 | import { ILoggerAdapter } from '../logger'; 10 | import { IHttpAdapter } from './adapter'; 11 | 12 | @Injectable() 13 | export class HttpService implements IHttpAdapter { 14 | public tracing!: Exclude; 15 | 16 | private axios: AxiosInstance; 17 | 18 | constructor(private readonly loggerService: ILoggerAdapter) { 19 | const httpsAgent = new https.Agent({ 20 | keepAlive: true, 21 | rejectUnauthorized: false 22 | }); 23 | 24 | this.axios = axios.create({ proxy: false, httpsAgent }); 25 | AxiosUtils.requestRetry({ axios: this.axios, logger: this.loggerService }); 26 | 27 | axiosBetterStacktrace(this.axios); 28 | 29 | this.axios.interceptors.response.use( 30 | (response) => response, 31 | (error) => { 32 | AxiosUtils.interceptAxiosResponseError(error); 33 | return Promise.reject(error); 34 | } 35 | ); 36 | } 37 | 38 | instance(): AxiosInstance | Axios { 39 | return this.axios; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/infra/logger/adapter.ts: -------------------------------------------------------------------------------- 1 | import { HttpLogger } from 'pino-http'; 2 | 3 | import { ErrorType, LogLevelEnum, MessageType } from './types'; 4 | 5 | export abstract class ILoggerAdapter { 6 | abstract logger: T; 7 | abstract connect(logLevel?: TLevel): void; 8 | abstract setApplication(app: string): void; 9 | abstract log(message: string): void; 10 | abstract debug({ message, context, obj }: MessageType): void; 11 | abstract info({ message, context, obj }: MessageType): void; 12 | abstract warn({ message, context, obj }: MessageType): void; 13 | abstract error(error: ErrorType, message?: string | string[]): void; 14 | abstract fatal(error: ErrorType, message?: string | string[]): void; 15 | abstract setGlobalParameters(input: object): void; 16 | } 17 | -------------------------------------------------------------------------------- /src/infra/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './module'; 3 | export * from './service'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/infra/logger/module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | 3 | import { ISecretsAdapter, SecretsModule } from '@/infra/secrets'; 4 | 5 | import { ILoggerAdapter } from './adapter'; 6 | import { LoggerService } from './service'; 7 | import { LogLevelEnum } from './types'; 8 | 9 | @Module({ 10 | imports: [forwardRef(() => SecretsModule)], 11 | providers: [ 12 | { 13 | provide: ILoggerAdapter, 14 | useFactory: async ({ LOG_LEVEL }: ISecretsAdapter) => { 15 | const logger = new LoggerService(); 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | await logger.connect((LogLevelEnum as any)[`${LOG_LEVEL}`]); 18 | return logger; 19 | }, 20 | inject: [ISecretsAdapter] 21 | } 22 | ], 23 | exports: [ILoggerAdapter] 24 | }) 25 | export class LoggerModule {} 26 | -------------------------------------------------------------------------------- /src/infra/logger/types.ts: -------------------------------------------------------------------------------- 1 | import { BaseException } from '@/utils/exception'; 2 | 3 | export type MessageType = { 4 | message: string; 5 | context?: string; 6 | obj?: object; 7 | }; 8 | 9 | export type ErrorType = Error & BaseException; 10 | 11 | export enum LogLevelEnum { 12 | fatal = 'fatal', 13 | error = 'error', 14 | warn = 'warn', 15 | info = 'info', 16 | debug = 'debug', 17 | trace = 'trace', 18 | silent = 'silent' 19 | } 20 | -------------------------------------------------------------------------------- /src/infra/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { MemoryCacheModule } from './cache/memory'; 4 | import { RedisCacheModule } from './cache/redis'; 5 | import { MongoDatabaseModule } from './database/mongo'; 6 | import { PostgresDatabaseModule } from './database/postgres/module'; 7 | import { EmailModule } from './email'; 8 | import { HttpModule } from './http'; 9 | import { LoggerModule } from './logger'; 10 | import { SecretsModule } from './secrets'; 11 | 12 | @Module({ 13 | imports: [ 14 | SecretsModule, 15 | MongoDatabaseModule, 16 | PostgresDatabaseModule, 17 | LoggerModule, 18 | HttpModule, 19 | RedisCacheModule, 20 | MemoryCacheModule, 21 | EmailModule 22 | ] 23 | }) 24 | export class InfraModule {} 25 | -------------------------------------------------------------------------------- /src/infra/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './mongo/repository'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/infra/repository/types.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongoose'; 2 | 3 | export type UpdatedModel = { 4 | matchedCount: number; 5 | modifiedCount: number; 6 | acknowledged: boolean; 7 | upsertedId: unknown | ObjectId; 8 | upsertedCount: number; 9 | }; 10 | 11 | export type RemovedModel = { 12 | deletedCount: number; 13 | deleted: boolean; 14 | }; 15 | 16 | export type CreatedModel = { 17 | id: string; 18 | created: boolean; 19 | }; 20 | 21 | export type CreatedOrUpdateModel = { 22 | id: string; 23 | created: boolean; 24 | updated: boolean; 25 | }; 26 | 27 | export enum DatabaseOperationEnum { 28 | EQUAL = 'equal', 29 | NOT_EQUAL = 'not_equal', 30 | NOT_CONTAINS = 'not_contains', 31 | CONTAINS = 'contains' 32 | } 33 | 34 | export type DatabaseOperationCommand = { 35 | property: keyof T; 36 | value: unknown[]; 37 | command: DatabaseOperationEnum; 38 | }; 39 | -------------------------------------------------------------------------------- /src/infra/repository/util.ts: -------------------------------------------------------------------------------- 1 | import { CollectionUtil } from '@/utils/collection'; 2 | import { ApiBadRequestException } from '@/utils/exception'; 3 | 4 | import { DatabaseOperationCommand, DatabaseOperationEnum } from './types'; 5 | 6 | export const validateFindByCommandsFilter = (filterList: DatabaseOperationCommand[]) => { 7 | const groupList = CollectionUtil.groupBy>(filterList, 'property'); 8 | 9 | for (const key in groupList) { 10 | const commands = groupList[`${key}`].map((g) => g.command); 11 | const isLikeAndNotAllowedOperation = commands.filter( 12 | (g) => g === DatabaseOperationEnum.CONTAINS || g === DatabaseOperationEnum.NOT_CONTAINS 13 | ); 14 | 15 | const NOT_ALLOWED_COMBINATION = 2; 16 | 17 | if (isLikeAndNotAllowedOperation.length === NOT_ALLOWED_COMBINATION) { 18 | throw new ApiBadRequestException( 19 | `it is not possible to filter: '${key}' with the commands '${commands.join(', ')}'` 20 | ); 21 | } 22 | 23 | const isEqualNotAllowedOperation = commands.filter( 24 | (g) => g === DatabaseOperationEnum.EQUAL || g === DatabaseOperationEnum.NOT_EQUAL 25 | ); 26 | 27 | if (isEqualNotAllowedOperation.length === NOT_ALLOWED_COMBINATION) { 28 | throw new ApiBadRequestException( 29 | `it is not possible to filter: '${key}' with the commands '${commands.join(', ')}'` 30 | ); 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/infra/secrets/adapter.ts: -------------------------------------------------------------------------------- 1 | export abstract class ISecretsAdapter { 2 | ENV!: string; 3 | 4 | PORT!: number | string; 5 | 6 | HOST!: string; 7 | 8 | LOG_LEVEL!: string; 9 | 10 | DATE_FORMAT!: string; 11 | 12 | TZ!: string; 13 | 14 | MONGO!: { 15 | MONGO_URL: string; 16 | MONGO_DATABASE: string; 17 | MONGO_EXPRESS_URL: string; 18 | }; 19 | 20 | POSTGRES!: { 21 | POSTGRES_URL: string; 22 | POSTGRES_PGADMIN_URL: string; 23 | }; 24 | 25 | EMAIL!: { 26 | HOST: string; 27 | PORT: number; 28 | USER: string; 29 | PASS: string; 30 | FROM: string; 31 | }; 32 | 33 | REDIS_URL!: string; 34 | 35 | ZIPKIN_URL!: string; 36 | 37 | PROMETHUES_URL!: string; 38 | GRAFANA_URL!: string; 39 | 40 | TOKEN_EXPIRATION!: number | string; 41 | REFRESH_TOKEN_EXPIRATION!: number | string; 42 | 43 | JWT_SECRET_KEY!: string; 44 | 45 | IS_LOCAL!: boolean; 46 | 47 | IS_PRODUCTION!: boolean; 48 | 49 | AUTH!: { 50 | GOOGLE: { 51 | CLIENT_ID: string; 52 | CLIENT_SECRET: string; 53 | REDIRECT_URL: string; 54 | }; 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/infra/secrets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './module'; 3 | export * from './service'; 4 | -------------------------------------------------------------------------------- /src/infra/secrets/types.ts: -------------------------------------------------------------------------------- 1 | export enum EnvEnum { 2 | LOCAL = 'local', 3 | TEST = 'test', 4 | DEV = 'dev', 5 | HML = 'hml', 6 | PRD = 'prod' 7 | } 8 | -------------------------------------------------------------------------------- /src/libs/event/adapter.ts: -------------------------------------------------------------------------------- 1 | import { EmitEventOutput } from './service'; 2 | import { EventNameEnum } from './types'; 3 | 4 | export abstract class IEventAdapter { 5 | abstract emit(event: EventNameEnum, payload: T): EmitEventOutput; 6 | } 7 | -------------------------------------------------------------------------------- /src/libs/event/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './module'; 3 | export * from './service'; 4 | -------------------------------------------------------------------------------- /src/libs/event/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; 3 | 4 | import { IEventAdapter } from './adapter'; 5 | import { EventService } from './service'; 6 | 7 | @Module({ 8 | imports: [EventEmitterModule.forRoot()], 9 | providers: [ 10 | { 11 | provide: IEventAdapter, 12 | useFactory: (event: EventEmitter2) => new EventService(event), 13 | inject: [EventEmitter2] 14 | } 15 | ], 16 | exports: [IEventAdapter] 17 | }) 18 | export class EventLibModule {} 19 | -------------------------------------------------------------------------------- /src/libs/event/service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { EventEmitter2 } from '@nestjs/event-emitter'; 3 | 4 | import { IEventAdapter } from './adapter'; 5 | 6 | @Injectable() 7 | export class EventService implements IEventAdapter { 8 | constructor(private eventEmitter: EventEmitter2) {} 9 | 10 | emit(event: string, payload: T): EmitEventOutput { 11 | const eventEmitter = this.eventEmitter.emit(event, payload); 12 | 13 | return eventEmitter; 14 | } 15 | } 16 | 17 | export type EmitEventInput = T; 18 | export type EmitEventOutput = boolean; 19 | -------------------------------------------------------------------------------- /src/libs/event/types.ts: -------------------------------------------------------------------------------- 1 | export enum EventNameEnum { 2 | SEND_EMAIL = 'send-email' 3 | } 4 | -------------------------------------------------------------------------------- /src/libs/i18n/adapter.ts: -------------------------------------------------------------------------------- 1 | import { TranslateOptions } from './types'; 2 | 3 | export abstract class II18nAdapter { 4 | abstract translate(key: string, options?: TranslateOptions): unknown; 5 | } 6 | -------------------------------------------------------------------------------- /src/libs/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './module'; 3 | export * from './service'; 4 | -------------------------------------------------------------------------------- /src/libs/i18n/languages/en/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "HELLO": "Hello" 3 | } 4 | -------------------------------------------------------------------------------- /src/libs/i18n/languages/pt/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "HELLO": "Olá" 3 | } 4 | -------------------------------------------------------------------------------- /src/libs/i18n/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AcceptLanguageResolver, I18nModule, QueryResolver } from 'nestjs-i18n'; 3 | import path from 'path'; 4 | 5 | import { II18nAdapter } from './adapter'; 6 | import { I18nService } from './service'; 7 | 8 | @Module({ 9 | imports: [ 10 | I18nModule.forRoot({ 11 | fallbackLanguage: 'en', 12 | loaderOptions: { 13 | path: path.join(__dirname, '/languages'), 14 | watch: false 15 | }, 16 | resolvers: [{ use: QueryResolver, options: ['lang'] }, AcceptLanguageResolver] 17 | }) 18 | ], 19 | providers: [ 20 | { 21 | provide: II18nAdapter, 22 | useClass: I18nService 23 | } 24 | ], 25 | exports: [II18nAdapter] 26 | }) 27 | export class I18nLibModule {} 28 | -------------------------------------------------------------------------------- /src/libs/i18n/service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { I18nContext, I18nService as Service } from 'nestjs-i18n'; 3 | 4 | import { II18nAdapter } from './adapter'; 5 | import { TranslateOptions } from './types'; 6 | 7 | @Injectable() 8 | export class I18nService implements II18nAdapter { 9 | constructor(private readonly i18n: Service) {} 10 | 11 | translate(key: string, options: TranslateOptions = {}): T { 12 | Object.assign(options, { lang: I18nContext?.current()?.lang }); 13 | return this.i18n.translate(key, options) as T; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/libs/i18n/types.ts: -------------------------------------------------------------------------------- 1 | export type TranslateOptions = { 2 | args?: 3 | | ( 4 | | { 5 | [k: string]: unknown; 6 | } 7 | | string 8 | )[] 9 | | { 10 | [k: string]: unknown; 11 | }; 12 | defaultValue?: string; 13 | debug?: boolean; 14 | }; 15 | -------------------------------------------------------------------------------- /src/libs/metrics/adapter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Attributes, 3 | Counter, 4 | Histogram, 5 | ObservableCounter, 6 | ObservableUpDownCounter, 7 | UpDownCounter 8 | } from '@opentelemetry/api'; 9 | 10 | import { MetricOptionsInput } from './types'; 11 | 12 | export abstract class IMetricsAdapter { 13 | abstract getCounter(name: string, options?: MetricOptionsInput): Counter; 14 | abstract getUpDownCounter(name: string, options?: MetricOptionsInput): UpDownCounter; 15 | abstract getHistogram(name: string, options?: MetricOptionsInput): Histogram; 16 | abstract getObservableCounter(name: string, options?: MetricOptionsInput): ObservableCounter; 17 | abstract getObservableGauge(name: string, options?: MetricOptionsInput): ObservableCounter; 18 | abstract getObservableUpDownCounter(name: string, options?: MetricOptionsInput): ObservableUpDownCounter; 19 | } 20 | -------------------------------------------------------------------------------- /src/libs/metrics/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './module'; 3 | export * from './service'; 4 | -------------------------------------------------------------------------------- /src/libs/metrics/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MetricService, OpenTelemetryModule } from 'nestjs-otel'; 3 | 4 | import { IMetricsAdapter } from './adapter'; 5 | import { MetricsService } from './service'; 6 | 7 | const OpenTelemetryModuleConfig = OpenTelemetryModule.forRoot({ 8 | metrics: { 9 | hostMetrics: true, 10 | apiMetrics: { 11 | enable: true, 12 | ignoreRoutes: ['/favicon.ico'], 13 | ignoreUndefinedRoutes: false 14 | } 15 | } 16 | }); 17 | 18 | @Module({ 19 | imports: [OpenTelemetryModuleConfig], 20 | providers: [ 21 | { 22 | provide: IMetricsAdapter, 23 | useFactory: (metrics: MetricService) => new MetricsService(metrics), 24 | inject: [MetricService] 25 | } 26 | ], 27 | exports: [IMetricsAdapter] 28 | }) 29 | export class MetricsLibModule {} 30 | -------------------------------------------------------------------------------- /src/libs/metrics/service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | Attributes, 4 | Counter, 5 | Histogram, 6 | ObservableCounter, 7 | ObservableUpDownCounter, 8 | UpDownCounter 9 | } from '@opentelemetry/api'; 10 | import { MetricService } from 'nestjs-otel'; 11 | 12 | import { IMetricsAdapter } from './adapter'; 13 | import { MetricOptionsInput } from './types'; 14 | 15 | @Injectable() 16 | export class MetricsService implements IMetricsAdapter { 17 | constructor(private readonly metrics: MetricService) {} 18 | 19 | getCounter(name: string, options?: MetricOptionsInput): Counter { 20 | return this.metrics.getCounter(name, options); 21 | } 22 | 23 | getUpDownCounter(name: string, options?: MetricOptionsInput): UpDownCounter { 24 | return this.metrics.getUpDownCounter(name, options); 25 | } 26 | 27 | getHistogram(name: string, options?: MetricOptionsInput): Histogram { 28 | return this.metrics.getHistogram(name, options); 29 | } 30 | 31 | getObservableCounter(name: string, options?: MetricOptionsInput): ObservableCounter { 32 | return this.metrics.getObservableCounter(name, options); 33 | } 34 | 35 | getObservableGauge(name: string, options?: MetricOptionsInput): ObservableCounter { 36 | return this.metrics.getObservableGauge(name, options); 37 | } 38 | 39 | getObservableUpDownCounter(name: string, options?: MetricOptionsInput): ObservableUpDownCounter { 40 | return this.metrics.getObservableUpDownCounter(name, options); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/libs/metrics/types.ts: -------------------------------------------------------------------------------- 1 | export type MetricOptionsInput = { 2 | description?: string; 3 | unit?: string; 4 | valueType?: ValueType; 5 | advice?: { explicitBucketBoundaries?: number[] }; 6 | }; 7 | 8 | export enum ValueType { 9 | INT = 0, 10 | DOUBLE = 1 11 | } 12 | -------------------------------------------------------------------------------- /src/libs/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { EventLibModule } from './event'; 4 | import { I18nLibModule } from './i18n'; 5 | import { MetricsLibModule } from './metrics'; 6 | import { TokenLibModule } from './token'; 7 | 8 | @Module({ 9 | imports: [TokenLibModule, EventLibModule, I18nLibModule, MetricsLibModule] 10 | }) 11 | export class LibModule {} 12 | -------------------------------------------------------------------------------- /src/libs/token/adapter.ts: -------------------------------------------------------------------------------- 1 | import { SignOutput } from './service'; 2 | 3 | export abstract class ITokenAdapter { 4 | abstract sign(model: object, options?: T): SignOutput; 5 | abstract verify(token: string): Promise>; 6 | } 7 | -------------------------------------------------------------------------------- /src/libs/token/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './module'; 3 | export * from './service'; 4 | -------------------------------------------------------------------------------- /src/libs/token/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ISecretsAdapter, SecretsModule } from '@/infra/secrets'; 4 | 5 | import { ITokenAdapter } from './adapter'; 6 | import { TokenService } from './service'; 7 | 8 | @Module({ 9 | imports: [SecretsModule], 10 | providers: [ 11 | { 12 | provide: ITokenAdapter, 13 | useFactory: (secret: ISecretsAdapter) => new TokenService(secret), 14 | inject: [ISecretsAdapter] 15 | } 16 | ], 17 | exports: [ITokenAdapter] 18 | }) 19 | export class TokenLibModule {} 20 | -------------------------------------------------------------------------------- /src/libs/token/service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import jwt from 'jsonwebtoken'; 3 | 4 | import { UserEntitySchema } from '@/core/user/entity/user'; 5 | import { ISecretsAdapter } from '@/infra/secrets'; 6 | import { ApiUnauthorizedException } from '@/utils/exception'; 7 | import { Infer, InputValidator } from '@/utils/validator'; 8 | 9 | import { ITokenAdapter } from './adapter'; 10 | 11 | export const TokenGetSchema = UserEntitySchema.pick({ 12 | email: true, 13 | roles: true 14 | }).merge(InputValidator.object({ password: InputValidator.string() })); 15 | 16 | @Injectable() 17 | export class TokenService implements ITokenAdapter { 18 | constructor(private readonly secret: ISecretsAdapter) {} 19 | 20 | sign(model: SignInput, options?: TOpt): SignOutput { 21 | const token = jwt.sign( 22 | model, 23 | this.secret.JWT_SECRET_KEY, 24 | options || { 25 | expiresIn: this.secret.TOKEN_EXPIRATION as jwt.SignOptions['expiresIn'] 26 | } 27 | ); 28 | 29 | return { token }; 30 | } 31 | 32 | async verify(token: string): Promise { 33 | return new Promise((res, rej) => { 34 | jwt.verify(token, this.secret.JWT_SECRET_KEY, (error, decoded) => { 35 | if (error) rej(new ApiUnauthorizedException(error.message)); 36 | 37 | res(decoded as T); 38 | }); 39 | }); 40 | } 41 | } 42 | 43 | export type SignInput = Infer; 44 | 45 | export type SignOutput = { 46 | token: string; 47 | }; 48 | -------------------------------------------------------------------------------- /src/middlewares/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './exception-handler.filter'; 2 | -------------------------------------------------------------------------------- /src/middlewares/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authorization.guard'; 2 | -------------------------------------------------------------------------------- /src/middlewares/interceptors/http-logger.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { ILoggerAdapter } from '@/infra/logger'; 5 | import { UUIDUtils } from '@/utils/uuid'; 6 | 7 | @Injectable() 8 | export class HttpLoggerInterceptor implements NestInterceptor { 9 | constructor(private readonly logger: ILoggerAdapter) {} 10 | 11 | intercept(executionContext: ExecutionContext, next: CallHandler): Observable { 12 | const context = `${executionContext.getClass().name}/${executionContext.getHandler().name}`; 13 | 14 | const request = executionContext.switchToHttp().getRequest(); 15 | 16 | request['context'] = context; 17 | 18 | if (!request.headers?.traceid) { 19 | request.headers.traceid = UUIDUtils.create(); 20 | request.id = request.headers.traceid; 21 | } 22 | 23 | this.logger.setGlobalParameters({ traceid: request.id }); 24 | 25 | return next.handle(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/middlewares/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './exception-handler.interceptor'; 2 | export * from './http-logger.interceptor'; 3 | export * from './metrics.interceptor'; 4 | export * from './request-timeout.interceptor'; 5 | export * from './tracing.interceptor'; 6 | -------------------------------------------------------------------------------- /src/middlewares/interceptors/metrics.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; 2 | import { Counter, Histogram, Meter, metrics } from '@opentelemetry/api'; 3 | import { catchError, Observable, tap, throwError } from 'rxjs'; 4 | 5 | import { name, version } from '../../../package.json'; 6 | 7 | @Injectable() 8 | export class MetricsInterceptor implements NestInterceptor { 9 | private meter: Meter; 10 | private counter: Counter; 11 | private histogram: Histogram; 12 | 13 | constructor() { 14 | this.meter = metrics.getMeter(name, version); 15 | this.counter = this.meter.createCounter('http_server_requests_count', { 16 | description: 'Total HTTP requests', 17 | unit: 'requests' 18 | }); 19 | 20 | this.histogram = this.meter.createHistogram('http_server_requests_duration', { 21 | description: 'Duration of HTTP requests', 22 | unit: 'ms' 23 | }); 24 | } 25 | 26 | intercept(context: ExecutionContext, next: CallHandler): Observable { 27 | const httpContext = context.switchToHttp(); 28 | const request = httpContext.getRequest(); 29 | const response = httpContext.getResponse(); 30 | 31 | const startTime = process.hrtime.bigint(); 32 | 33 | const recordMetrics = (statusCode: number) => { 34 | const duration = Number(process.hrtime.bigint() - startTime) / 1_000_000; 35 | 36 | const labels = { 37 | 'http.method': request.method, 38 | 'http.url': request.url, 39 | 'http.route': request.route?.path || 'unknown', 40 | 'http.status_code': statusCode, 41 | 'http.status_class': `${Math.floor(statusCode / 100)}xx` 42 | }; 43 | 44 | this.counter.add(1, labels); 45 | this.histogram.record(duration, labels); 46 | }; 47 | 48 | return next.handle().pipe( 49 | tap(() => { 50 | recordMetrics(response.statusCode); 51 | }), 52 | catchError((err) => { 53 | const statusCode = err?.status || err?.statusCode || 500; 54 | recordMetrics(statusCode); 55 | return throwError(() => err); 56 | }) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/middlewares/interceptors/request-timeout.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { Observable, throwError, TimeoutError } from 'rxjs'; 4 | import { catchError, timeout } from 'rxjs/operators'; 5 | 6 | import { ILoggerAdapter } from '@/infra/logger'; 7 | import { ApiTimeoutException } from '@/utils/exception'; 8 | 9 | @Injectable() 10 | export class RequestTimeoutInterceptor implements NestInterceptor { 11 | constructor( 12 | private readonly reflector: Reflector, 13 | private readonly logger: ILoggerAdapter 14 | ) {} 15 | 16 | intercept(context: ExecutionContext, next: CallHandler): Observable { 17 | const requestTimeout = this.reflector.getAllAndOverride('request-timeout', [ 18 | context.getHandler(), 19 | context.getClass() 20 | ]); 21 | 22 | const request = context.switchToHttp().getRequest(); 23 | const response = context.switchToHttp().getResponse(); 24 | 25 | const ONE_MINUTE = 1 * 60 * 1000; 26 | 27 | return next.handle().pipe( 28 | timeout(requestTimeout ?? ONE_MINUTE), 29 | catchError((err) => { 30 | this.logger.logger(request, response); 31 | if (err instanceof TimeoutError) { 32 | return throwError(() => new ApiTimeoutException(err.message)); 33 | } 34 | return throwError(() => err); 35 | }) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/middlewares/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication.middleware'; 2 | -------------------------------------------------------------------------------- /src/modules/alert/controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | 3 | import { ILoggerAdapter } from '@/infra/logger'; 4 | 5 | @Controller('alert') 6 | export class AlertController { 7 | constructor(private readonly logger: ILoggerAdapter) {} 8 | 9 | @Post() 10 | handleAlert(@Body() body: unknown) { 11 | this.logger.warn({ message: '🔔 Alerta received:\n' + JSON.stringify(body, null, 2) }); 12 | return { status: 'ok' }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/alert/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { LoggerModule } from '@/infra/logger'; 4 | 5 | import { AlertController } from './controller'; 6 | 7 | @Module({ 8 | imports: [LoggerModule], 9 | controllers: [AlertController], 10 | providers: [], 11 | exports: [] 12 | }) 13 | export class AlertModule {} 14 | -------------------------------------------------------------------------------- /src/modules/cat/adapter.ts: -------------------------------------------------------------------------------- 1 | import { CatCreateInput, CatCreateOutput } from '@/core/cat/use-cases/cat-create'; 2 | import { CatDeleteInput, CatDeleteOutput } from '@/core/cat/use-cases/cat-delete'; 3 | import { CatGetByIdInput, CatGetByIdOutput } from '@/core/cat/use-cases/cat-get-by-id'; 4 | import { CatListInput, CatListOutput } from '@/core/cat/use-cases/cat-list'; 5 | import { CatUpdateInput, CatUpdateOutput } from '@/core/cat/use-cases/cat-update'; 6 | import { ApiTrancingInput } from '@/utils/request'; 7 | import { IUsecase } from '@/utils/usecase'; 8 | 9 | export abstract class ICatCreateAdapter implements IUsecase { 10 | abstract execute(input: CatCreateInput, trace: ApiTrancingInput): Promise; 11 | } 12 | 13 | export abstract class ICatUpdateAdapter implements IUsecase { 14 | abstract execute(input: CatUpdateInput, trace: ApiTrancingInput): Promise; 15 | } 16 | 17 | export abstract class ICatGetByIdAdapter implements IUsecase { 18 | abstract execute(input: CatGetByIdInput): Promise; 19 | } 20 | 21 | export abstract class ICatListAdapter implements IUsecase { 22 | abstract execute(input: CatListInput): Promise; 23 | } 24 | 25 | export abstract class ICatDeleteAdapter implements IUsecase { 26 | abstract execute(input: CatDeleteInput, trace: ApiTrancingInput): Promise; 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/cat/repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { FilterQuery, PaginateModel } from 'mongoose'; 4 | 5 | import { CatEntity } from '@/core/cat/entity/cat'; 6 | import { ICatRepository } from '@/core/cat/repository/cat'; 7 | import { CatListInput, CatListOutput } from '@/core/cat/use-cases/cat-list'; 8 | import { Cat, CatDocument } from '@/infra/database/mongo/schemas/cat'; 9 | import { MongoRepository } from '@/infra/repository'; 10 | import { ConvertMongooseFilter, SearchTypeEnum, ValidateDatabaseSortAllowed } from '@/utils/decorators'; 11 | import { IEntity } from '@/utils/entity'; 12 | import { MongoRepositoryModelSessionType } from '@/utils/mongoose'; 13 | 14 | @Injectable() 15 | export class CatRepository extends MongoRepository implements ICatRepository { 16 | constructor(@InjectModel(Cat.name) readonly entity: MongoRepositoryModelSessionType>) { 17 | super(entity); 18 | } 19 | 20 | @ValidateDatabaseSortAllowed({ name: 'createdAt' }, { name: 'breed' }) 21 | @ConvertMongooseFilter([ 22 | { name: 'name', type: SearchTypeEnum.like }, 23 | { name: 'breed', type: SearchTypeEnum.like }, 24 | { name: 'age', type: SearchTypeEnum.equal, format: 'Number' } 25 | ]) 26 | async paginate({ limit, page, search, sort }: CatListInput): Promise { 27 | const cats = await this.entity.paginate(search as FilterQuery, { 28 | page, 29 | limit, 30 | sort: sort as object 31 | }); 32 | 33 | return { 34 | docs: cats.docs.map((u) => new CatEntity(u.toObject({ virtuals: true }))), 35 | limit, 36 | page, 37 | total: cats.totalDocs 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/health/adapter.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'mongoose'; 2 | import { RedisClientType } from 'redis'; 3 | import { DataSource } from 'typeorm'; 4 | 5 | import { ICacheAdapter } from '@/infra/cache'; 6 | 7 | import { DatabaseConnectionOutput, DatabaseMemoryOutput, HealthStatus, Load, MemotyOutput } from './types'; 8 | 9 | export abstract class IHealthAdapter { 10 | abstract mongo: Connection; 11 | abstract postgres: DataSource; 12 | abstract redis: ICacheAdapter; 13 | abstract getMongoStatus(): HealthStatus; 14 | abstract getRedisStatus(): Promise; 15 | abstract getPostgresStatus(): Promise; 16 | abstract getMemoryUsageInMB(): MemotyOutput; 17 | abstract getLoadAvarage(time: number, numCpus: number): Load; 18 | abstract getActiveConnections(): Promise; 19 | abstract getLatency(host?: string): Promise; 20 | abstract getMongoConnections(): Promise; 21 | abstract getPostgresConnections(): Promise; 22 | abstract getPostgresMemory(): Promise; 23 | abstract getMongoMemory(): Promise; 24 | abstract getCPUCore(): Promise<{ cpus: { load: number }[] }>; 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/health/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { getConnectionToken } from '@nestjs/mongoose'; 3 | import { getDataSourceToken } from '@nestjs/typeorm'; 4 | import { Connection } from 'mongoose'; 5 | import { RedisClientType } from 'redis'; 6 | import { DataSource } from 'typeorm'; 7 | 8 | import { ICacheAdapter } from '@/infra/cache'; 9 | import { RedisCacheModule } from '@/infra/cache/redis'; 10 | import { ConnectionName } from '@/infra/database/enum'; 11 | import { PostgresDatabaseModule } from '@/infra/database/postgres'; 12 | import { ILoggerAdapter, LoggerModule } from '@/infra/logger'; 13 | 14 | import { IHealthAdapter } from './adapter'; 15 | import { HealthController } from './controller'; 16 | import { HealthService } from './service'; 17 | 18 | @Module({ 19 | imports: [LoggerModule, PostgresDatabaseModule, RedisCacheModule], 20 | controllers: [HealthController], 21 | providers: [ 22 | { 23 | provide: IHealthAdapter, 24 | useFactory: async ( 25 | connection: Connection, 26 | dataSource: DataSource, 27 | cache: ICacheAdapter, 28 | logger: ILoggerAdapter 29 | ) => { 30 | const service = new HealthService(logger); 31 | service.postgres = dataSource; 32 | service.mongo = connection; 33 | service.redis = cache; 34 | return service; 35 | }, 36 | inject: [ 37 | getConnectionToken(ConnectionName.CATS), 38 | getDataSourceToken(), 39 | ICacheAdapter, 40 | ILoggerAdapter 41 | ] 42 | } 43 | ], 44 | exports: [IHealthAdapter] 45 | }) 46 | export class HealthModule {} 47 | -------------------------------------------------------------------------------- /src/modules/health/types.ts: -------------------------------------------------------------------------------- 1 | export enum HealthStatus { 2 | UP = `UP 🟢`, 3 | DOWN = `DOWN 🔴` 4 | } 5 | 6 | export type MemotyOutput = { 7 | process: { 8 | usedRam: string; 9 | heapTotal: string; 10 | heapUsed: string; 11 | external: string; 12 | }; 13 | v8: { 14 | totalHeapSize: string; 15 | usedHeapSize: string; 16 | executableHeapSize: string; 17 | heapSizeLimit: string; 18 | }; 19 | }; 20 | 21 | export type HealthOutput = { 22 | server: string; 23 | version: string; 24 | mongo: { 25 | status: string; 26 | connection: DatabaseConnectionOutput; 27 | memory: DatabaseMemoryOutput; 28 | }; 29 | postgres: { 30 | status: string; 31 | connection: DatabaseConnectionOutput; 32 | memory: DatabaseMemoryOutput; 33 | }; 34 | redisState: string; 35 | network: { 36 | latency: string; 37 | connections: number; 38 | }; 39 | memory: MemotyOutput; 40 | cpu: { 41 | cpus: number; 42 | globalAvarage: { 43 | lastMinute: Load; 44 | lastFiveMinutes: Load; 45 | lastFifteenMinutes: Load; 46 | }; 47 | cores: { core: number; load: string }[]; 48 | }; 49 | }; 50 | 51 | export type Load = { 52 | load: number; 53 | status: string; 54 | }; 55 | 56 | export type DatabaseConnectionOutput = { 57 | active: number; 58 | available: number; 59 | current: number; 60 | totalCreated: number; 61 | }; 62 | 63 | export type DatabaseMemoryOutput = { 64 | ramUsed: number | string; 65 | reservedMemory: number | string; 66 | }; 67 | -------------------------------------------------------------------------------- /src/modules/login/adapter.ts: -------------------------------------------------------------------------------- 1 | import { LoginInput, LoginOutput } from '@/core/user/use-cases/user-login'; 2 | import { RefreshTokenInput, RefreshTokenOutput } from '@/core/user/use-cases/user-refresh-token'; 3 | import { ApiTrancingInput } from '@/utils/request'; 4 | import { IUsecase } from '@/utils/usecase'; 5 | 6 | export abstract class ILoginAdapter implements IUsecase { 7 | abstract execute(input: LoginInput, trace: ApiTrancingInput): Promise; 8 | } 9 | 10 | export abstract class IRefreshTokenAdapter implements IUsecase { 11 | abstract execute(input: RefreshTokenInput): Promise; 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/login/module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { IUserRepository } from '@/core/user/repository/user'; 4 | import { LoginUsecase } from '@/core/user/use-cases/user-login'; 5 | import { RefreshTokenUsecase } from '@/core/user/use-cases/user-refresh-token'; 6 | import { HttpModule } from '@/infra/http'; 7 | import { SecretsModule } from '@/infra/secrets'; 8 | import { ITokenAdapter, TokenLibModule } from '@/libs/token'; 9 | 10 | import { UserModule } from '../user/module'; 11 | import { ILoginAdapter, IRefreshTokenAdapter } from './adapter'; 12 | import { LoginController } from './controller'; 13 | 14 | @Module({ 15 | imports: [TokenLibModule, UserModule, SecretsModule, HttpModule, UserModule], 16 | controllers: [LoginController], 17 | providers: [ 18 | { 19 | provide: ILoginAdapter, 20 | useFactory: (repository: IUserRepository, tokenService: ITokenAdapter) => { 21 | return new LoginUsecase(repository, tokenService); 22 | }, 23 | inject: [IUserRepository, ITokenAdapter] 24 | }, 25 | { 26 | provide: IRefreshTokenAdapter, 27 | useFactory: (repository: IUserRepository, tokenService: ITokenAdapter) => { 28 | return new RefreshTokenUsecase(repository, tokenService); 29 | }, 30 | inject: [IUserRepository, ITokenAdapter] 31 | } 32 | ] 33 | }) 34 | export class LoginModule {} 35 | -------------------------------------------------------------------------------- /src/modules/logout/adapter.ts: -------------------------------------------------------------------------------- 1 | import { LogoutInput, LogoutOutput } from '@/core/user/use-cases/user-logout'; 2 | import { ApiTrancingInput } from '@/utils/request'; 3 | import { IUsecase } from '@/utils/usecase'; 4 | 5 | export abstract class ILogoutAdapter implements IUsecase { 6 | abstract execute(input: LogoutInput, trace: ApiTrancingInput): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/logout/controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpCode, Post, Req, Version } from '@nestjs/common'; 2 | 3 | import { LogoutInput, LogoutOutput } from '@/core/user/use-cases/user-logout'; 4 | import { Permission } from '@/utils/decorators'; 5 | import { ApiRequest } from '@/utils/request'; 6 | 7 | import { ILogoutAdapter } from './adapter'; 8 | 9 | @Controller() 10 | export class LogoutController { 11 | constructor(private readonly logoutUsecase: ILogoutAdapter) {} 12 | 13 | @Post('/logout') 14 | @HttpCode(401) 15 | @Version('1') 16 | @Permission('user:logout') 17 | async logout(@Req() { body, user, tracing }: ApiRequest): LogoutOutput { 18 | return this.logoutUsecase.execute(body as LogoutInput, { user, tracing }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/logout/module.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; 2 | 3 | import { LogoutUsecase } from '@/core/user/use-cases/user-logout'; 4 | import { ICacheAdapter } from '@/infra/cache'; 5 | import { RedisCacheModule } from '@/infra/cache/redis'; 6 | import { LoggerModule } from '@/infra/logger'; 7 | import { ISecretsAdapter, SecretsModule } from '@/infra/secrets'; 8 | import { AuthenticationMiddleware } from '@/middlewares/middlewares'; 9 | 10 | import { TokenLibModule } from '../../libs/token/module'; 11 | import { ILogoutAdapter } from './adapter'; 12 | import { LogoutController } from './controller'; 13 | 14 | @Module({ 15 | imports: [RedisCacheModule, SecretsModule, RedisCacheModule, TokenLibModule, LoggerModule], 16 | controllers: [LogoutController], 17 | providers: [ 18 | { 19 | provide: ILogoutAdapter, 20 | useFactory: (cache: ICacheAdapter, secrets: ISecretsAdapter) => { 21 | return new LogoutUsecase(cache, secrets); 22 | }, 23 | inject: [ICacheAdapter, ISecretsAdapter] 24 | } 25 | ], 26 | exports: [ILogoutAdapter] 27 | }) 28 | export class LogoutModule implements NestModule { 29 | configure(consumer: MiddlewareConsumer) { 30 | consumer.apply(AuthenticationMiddleware).forRoutes(LogoutController); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/permission/adapter.ts: -------------------------------------------------------------------------------- 1 | import { PermissionCreateInput, PermissionCreateOutput } from '@/core/permission/use-cases/permission-create'; 2 | import { PermissionDeleteInput, PermissionDeleteOutput } from '@/core/permission/use-cases/permission-delete'; 3 | import { PermissionGetByIdInput, PermissionGetByIdOutput } from '@/core/permission/use-cases/permission-get-by-id'; 4 | import { PermissionListInput, PermissionListOutput } from '@/core/permission/use-cases/permission-list'; 5 | import { PermissionUpdateInput, PermissionUpdateOutput } from '@/core/permission/use-cases/permission-update'; 6 | import { IUsecase } from '@/utils/usecase'; 7 | 8 | export abstract class IPermissionCreateAdapter implements IUsecase { 9 | abstract execute(input: PermissionCreateInput): Promise; 10 | } 11 | 12 | export abstract class IPermissionUpdateAdapter implements IUsecase { 13 | abstract execute(input: PermissionUpdateInput): Promise; 14 | } 15 | 16 | export abstract class IPermissionGetByIdAdapter implements IUsecase { 17 | abstract execute(input: PermissionGetByIdInput): Promise; 18 | } 19 | 20 | export abstract class IPermissionListAdapter implements IUsecase { 21 | abstract execute(input: PermissionListInput): Promise; 22 | } 23 | 24 | export abstract class IPermissionDeleteAdapter implements IUsecase { 25 | abstract execute(input: PermissionDeleteInput): Promise; 26 | } 27 | 28 | export abstract class IPermissionCreateCloneCascaDeBalaAdapter implements IUsecase { 29 | abstract execute(input: PermissionDeleteInput): Promise; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/reset-password/adapter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResetPasswordConfirmInput, 3 | ResetPasswordConfirmOutput 4 | } from '@/core/reset-password/use-cases/reset-password-confirm'; 5 | import { 6 | ResetPasswordSendEmailInput, 7 | ResetPasswordSendEmailOutput 8 | } from '@/core/reset-password/use-cases/reset-password-send-email'; 9 | import { IUsecase } from '@/utils/usecase'; 10 | 11 | export abstract class ISendEmailResetPasswordAdapter implements IUsecase { 12 | abstract execute(input: ResetPasswordSendEmailInput): Promise; 13 | } 14 | 15 | export abstract class IConfirmResetPasswordAdapter implements IUsecase { 16 | abstract execute(input: ResetPasswordConfirmInput): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/reset-password/controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpCode, Post, Put, Req, Version } from '@nestjs/common'; 2 | 3 | import { 4 | ResetPasswordConfirmInput, 5 | ResetPasswordConfirmOutput 6 | } from '@/core/reset-password/use-cases/reset-password-confirm'; 7 | import { 8 | ResetPasswordSendEmailInput, 9 | ResetPasswordSendEmailOutput 10 | } from '@/core/reset-password/use-cases/reset-password-send-email'; 11 | import { ApiRequest } from '@/utils/request'; 12 | 13 | import { IConfirmResetPasswordAdapter, ISendEmailResetPasswordAdapter } from './adapter'; 14 | 15 | @Controller('/reset-password') 16 | export class ResetPasswordController { 17 | constructor( 18 | private readonly sendEmailUsecase: ISendEmailResetPasswordAdapter, 19 | private readonly confirmResetPasswordUsecase: IConfirmResetPasswordAdapter 20 | ) {} 21 | 22 | @Post('send-email') 23 | @Version('1') 24 | async sendEmail(@Req() { body }: ApiRequest): Promise { 25 | return await this.sendEmailUsecase.execute(body as ResetPasswordSendEmailInput); 26 | } 27 | 28 | @Put(':token') 29 | @Version('1') 30 | @HttpCode(200) 31 | async confirmResetPassword(@Req() { params, body }: ApiRequest): Promise { 32 | return await this.confirmResetPasswordUsecase.execute({ 33 | token: params.token, 34 | ...body 35 | } as ResetPasswordConfirmInput); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/reset-password/repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { FindOptionsWhere, MoreThan, Repository } from 'typeorm'; 3 | 4 | import { ResetPasswordEntity } from '@/core/reset-password/entity/reset-password'; 5 | import { IResetPasswordRepository } from '@/core/reset-password/repository/reset-password'; 6 | import { TypeORMRepository } from '@/infra/repository/postgres/repository'; 7 | import { DateUtils } from '@/utils/date'; 8 | 9 | import { ResetPasswordSchema } from '../../infra/database/postgres/schemas/reset-password'; 10 | 11 | @Injectable() 12 | export class ResetPasswordRepository extends TypeORMRepository implements IResetPasswordRepository { 13 | constructor(readonly repository: Repository) { 14 | super(repository); 15 | } 16 | 17 | async findByIdUserId(id: string): Promise { 18 | const date = DateUtils.getDate().minus(1800000).toJSDate(); 19 | return (await this.repository.findOne({ 20 | where: { user: { id }, createdAt: MoreThan(date) } as FindOptionsWhere 21 | })) as Model; 22 | } 23 | } 24 | 25 | type Model = ResetPasswordSchema & ResetPasswordEntity; 26 | -------------------------------------------------------------------------------- /src/modules/role/adapter.ts: -------------------------------------------------------------------------------- 1 | import { RoleAddPermissionInput, RoleAddPermissionOutput } from '@/core/role/use-cases/role-add-permission'; 2 | import { RoleCreateInput, RoleCreateOutput } from '@/core/role/use-cases/role-create'; 3 | import { RoleDeleteInput, RoleDeleteOutput } from '@/core/role/use-cases/role-delete'; 4 | import { RoleDeletePermissionInput, RoleDeletePermissionOutput } from '@/core/role/use-cases/role-delete-permission'; 5 | import { RoleGetByIdInput, RoleGetByIdOutput } from '@/core/role/use-cases/role-get-by-id'; 6 | import { RoleListInput, RoleListOutput } from '@/core/role/use-cases/role-list'; 7 | import { RoleUpdateInput, RoleUpdateOutput } from '@/core/role/use-cases/role-update'; 8 | import { IUsecase } from '@/utils/usecase'; 9 | 10 | export abstract class IRoleCreateAdapter implements IUsecase { 11 | abstract execute(input: RoleCreateInput): Promise; 12 | } 13 | 14 | export abstract class IRoleUpdateAdapter implements IUsecase { 15 | abstract execute(input: RoleUpdateInput): Promise; 16 | } 17 | 18 | export abstract class IRoleGetByIdAdapter implements IUsecase { 19 | abstract execute(input: RoleGetByIdInput): Promise; 20 | } 21 | 22 | export abstract class IRoleListAdapter implements IUsecase { 23 | abstract execute(input: RoleListInput): Promise; 24 | } 25 | 26 | export abstract class IRoleDeleteAdapter implements IUsecase { 27 | abstract execute(input: RoleDeleteInput): Promise; 28 | } 29 | 30 | export abstract class IRoleAddPermissionAdapter implements IUsecase { 31 | abstract execute(input: RoleAddPermissionInput): Promise; 32 | } 33 | 34 | export abstract class IRoleDeletePermissionAdapter implements IUsecase { 35 | abstract execute(input: RoleDeletePermissionInput): Promise; 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/role/repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { FindOptionsOrder, FindOptionsWhere, Repository } from 'typeorm'; 3 | 4 | import { RoleEntity } from '@/core/role/entity/role'; 5 | import { IRoleRepository } from '@/core/role/repository/role'; 6 | import { RoleListInput, RoleListOutput } from '@/core/role/use-cases/role-list'; 7 | import { RoleSchema } from '@/infra/database/postgres/schemas/role'; 8 | import { TypeORMRepository } from '@/infra/repository/postgres/repository'; 9 | import { ConvertTypeOrmFilter, SearchTypeEnum, ValidateDatabaseSortAllowed } from '@/utils/decorators'; 10 | import { IEntity } from '@/utils/entity'; 11 | import { PaginationUtils } from '@/utils/pagination'; 12 | 13 | @Injectable() 14 | export class RoleRepository extends TypeORMRepository implements IRoleRepository { 15 | constructor(readonly repository: Repository) { 16 | super(repository); 17 | } 18 | 19 | @ConvertTypeOrmFilter([{ name: 'name', type: SearchTypeEnum.like }]) 20 | @ValidateDatabaseSortAllowed({ name: 'name' }, { name: 'createdAt' }) 21 | async paginate(input: RoleListInput): Promise { 22 | const skip = PaginationUtils.calculateSkip(input); 23 | 24 | const [docs, total] = await this.repository.findAndCount({ 25 | take: input.limit, 26 | skip, 27 | order: input.sort as FindOptionsOrder, 28 | where: input.search as FindOptionsWhere 29 | }); 30 | 31 | return { docs, total, page: input.page, limit: input.limit }; 32 | } 33 | } 34 | 35 | type Model = RoleSchema & RoleEntity; 36 | -------------------------------------------------------------------------------- /src/modules/user/adapter.ts: -------------------------------------------------------------------------------- 1 | import { UserChangePasswordInput, UserChangePasswordOutput } from '@/core/user/use-cases/user-change-password'; 2 | import { UserCreateInput, UserCreateOutput } from '@/core/user/use-cases/user-create'; 3 | import { UserDeleteInput, UserDeleteOutput } from '@/core/user/use-cases/user-delete'; 4 | import { UserGetByIdInput, UserGetByIdOutput } from '@/core/user/use-cases/user-get-by-id'; 5 | import { UserListInput, UserListOutput } from '@/core/user/use-cases/user-list'; 6 | import { UserUpdateInput, UserUpdateOutput } from '@/core/user/use-cases/user-update'; 7 | import { ApiTrancingInput } from '@/utils/request'; 8 | import { IUsecase } from '@/utils/usecase'; 9 | 10 | export abstract class IUserCreateAdapter implements IUsecase { 11 | abstract execute(input: UserCreateInput, trace: ApiTrancingInput): Promise; 12 | } 13 | 14 | export abstract class IUserUpdateAdapter implements IUsecase { 15 | abstract execute(input: UserUpdateInput, trace: ApiTrancingInput): Promise; 16 | } 17 | 18 | export abstract class IUserListAdapter implements IUsecase { 19 | abstract execute(input: UserListInput): Promise; 20 | } 21 | 22 | export abstract class IUserDeleteAdapter implements IUsecase { 23 | abstract execute(input: UserDeleteInput, trace: ApiTrancingInput): Promise; 24 | } 25 | 26 | export abstract class IUserGetByIdAdapter implements IUsecase { 27 | abstract execute(input: UserGetByIdInput): Promise; 28 | } 29 | 30 | export abstract class IUserChangePasswordAdapter implements IUsecase { 31 | abstract execute(input: UserChangePasswordInput): Promise; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export class CryptoUtils { 4 | static createHash(input: string): string { 5 | return crypto.createHash('sha256').update(input).digest('hex'); 6 | } 7 | 8 | static generateRandomBase64(): string { 9 | return crypto.randomBytes(16).toString('base64'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | export class DateUtils { 4 | static getDateStringWithFormat(input: Partial = {}): string { 5 | if (!input?.date) { 6 | Object.assign(input, { date: DateUtils.getJSDate() }); 7 | } 8 | 9 | if (!input?.format) { 10 | Object.assign(input, { format: process.env.DATE_FORMAT }); 11 | } 12 | 13 | return DateTime.fromJSDate(input.date as Date, { zone: 'utc' }) 14 | .setZone(process.env.TZ) 15 | .toFormat(input?.format as string); 16 | } 17 | 18 | static getISODateString(): string { 19 | return DateTime.fromJSDate(DateUtils.getJSDate(), { zone: 'utc' }).setZone(process.env.TZ).toJSON() as string; 20 | } 21 | 22 | static getJSDate(): Date { 23 | return DateTime.fromJSDate(DateTime.now().toJSDate(), { zone: 'utc' }).setZone(process.env.TZ).toJSDate(); 24 | } 25 | 26 | static createJSDate({ date, utc = true }: CreateDateInput): Date { 27 | if (utc) { 28 | return DateTime.fromISO(date, { zone: 'utc' }).setZone(process.env.TZ).toJSDate(); 29 | } 30 | return DateTime.fromISO(date).setZone(process.env.TZ).toJSDate(); 31 | } 32 | 33 | static createISODate({ date, utc = true }: CreateDateInput): string { 34 | if (utc) { 35 | return DateTime.fromISO(date, { zone: 'utc' }).setZone(process.env.TZ).toISO() as string; 36 | } 37 | return DateTime.fromISO(date).setZone(process.env.TZ).toISO() as string; 38 | } 39 | 40 | static getDate(): DateTime { 41 | return DateTime.fromJSDate(DateUtils.getJSDate(), { zone: 'utc' }).setZone(process.env.TZ); 42 | } 43 | } 44 | 45 | type GetDateWithFormatFormatInput = { 46 | date?: Date; 47 | format?: string; 48 | }; 49 | 50 | type CreateDateInput = { 51 | date: string; 52 | utc: boolean; 53 | }; 54 | -------------------------------------------------------------------------------- /src/utils/decorators/database/mongo/convert-mongoose-filter.decorator.ts: -------------------------------------------------------------------------------- 1 | import { FilterQuery, RootFilterQuery } from 'mongoose'; 2 | 3 | import { IEntity } from '@/utils/entity'; 4 | 5 | export function ConvertMongoFilterToBaseRepository() { 6 | return (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => { 7 | const originalMethod = descriptor.value; 8 | descriptor.value = function (...args: { id?: string }[]) { 9 | const input: RootFilterQuery = args[0]; 10 | 11 | if (!input) { 12 | const result = originalMethod.apply(this, args); 13 | return result; 14 | } 15 | 16 | input.deletedAt = null; 17 | 18 | if (input.id) { 19 | input._id = input.id; 20 | delete input.id; 21 | } 22 | 23 | args[0] = convertObjectFilterToMongoFilter(input); 24 | const result = originalMethod.apply(this, args); 25 | return result; 26 | }; 27 | }; 28 | } 29 | 30 | const convertObjectFilterToMongoFilter = (input: FilterQuery, recursiveInput: FilterQuery = {}) => { 31 | const filterFormated: FilterQuery = recursiveInput; 32 | 33 | for (const key in input) { 34 | if (input[`${key}`] && typeof input[`${key}`] === 'object') { 35 | for (const subKey of Object.keys(input[`${key}`])) { 36 | convertObjectFilterToMongoFilter({ [`${key}.${subKey}`]: input[`${key}`][`${subKey}`] }, filterFormated); 37 | continue; 38 | } 39 | continue; 40 | } 41 | filterFormated[`${key}`] = input[`${key}`]; 42 | } 43 | 44 | return filterFormated; 45 | }; 46 | -------------------------------------------------------------------------------- /src/utils/decorators/database/utils.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | 3 | import { DateUtils } from '@/utils/date'; 4 | import { ApiBadRequestException } from '@/utils/exception'; 5 | import { MongoUtils } from '@/utils/mongoose'; 6 | 7 | import { AllowedFilter } from '../types'; 8 | 9 | export const convertFilterValue = (input: Pick, 'format'> & { value: unknown }) => { 10 | if (input.format === 'String') { 11 | return `${input.value}`; 12 | } 13 | 14 | if (input.format === 'Date') { 15 | return DateUtils.createJSDate({ date: `${input.value}`, utc: false }); 16 | } 17 | 18 | if (input.format === 'DateIso') { 19 | return DateUtils.createISODate({ date: `${input.value}`, utc: false }); 20 | } 21 | 22 | if (input.format === 'Boolean') { 23 | if (input.value === 'true') { 24 | return true; 25 | } 26 | 27 | if (input.value === 'false') { 28 | return false; 29 | } 30 | throw new ApiBadRequestException('invalid boolean filter'); 31 | } 32 | 33 | if (input.format === 'Number') { 34 | const notNumber = Number.isNaN(input.value); 35 | if (notNumber) { 36 | throw new ApiBadRequestException('invalid number filter'); 37 | } 38 | return Number(input.value); 39 | } 40 | 41 | if (input.format === 'ObjectId') { 42 | const isObjectId = MongoUtils.isObjectId(`${input.value}`); 43 | 44 | if (!isObjectId) { 45 | throw new ApiBadRequestException('invalid objectId filter'); 46 | } 47 | return new Types.ObjectId(`${input.value} `); 48 | } 49 | 50 | return input.value; 51 | }; 52 | -------------------------------------------------------------------------------- /src/utils/decorators/database/validate-database-sort-allowed.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ApiBadRequestException } from '@/utils/exception'; 2 | import { PaginationSchema } from '@/utils/pagination'; 3 | import { SearchSchema } from '@/utils/search'; 4 | import { SortEnum, SortSchema } from '@/utils/sort'; 5 | import { Infer, InputValidator } from '@/utils/validator'; 6 | 7 | export const ListSchema = InputValidator.intersection(PaginationSchema, SortSchema.merge(SearchSchema)); 8 | 9 | type AllowedSort = { name: keyof T; map?: string }; 10 | 11 | export function ValidateDatabaseSortAllowed(...allowedSortList: AllowedSort[]) { 12 | return (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => { 13 | const originalMethod = descriptor.value; 14 | descriptor.value = function (...args: Infer[]) { 15 | const input = args[0]; 16 | 17 | const sort: { [key: string]: SortEnum } = {}; 18 | 19 | const sortList = (allowedSortList || []) as AllowedSort[]; 20 | 21 | Object.keys(input.sort || {}).forEach((key) => { 22 | const allowed = sortList.find((s) => s.name === key); 23 | 24 | if (!allowed) throw new ApiBadRequestException(`sort ${key} not allowed, allowed list: ${sortList.join(', ')}`); 25 | }); 26 | 27 | for (const allowedFilter of sortList) { 28 | if (!input.sort) continue; 29 | const filter = input.sort[`${allowedFilter.name.toString()}`]; 30 | if (filter) { 31 | sort[`${allowedFilter?.map ?? allowedFilter.name.toString()}`] = filter; 32 | } 33 | } 34 | 35 | args[0].sort = sort; 36 | const result = originalMethod.apply(this, args); 37 | return result; 38 | }; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './circuit-breaker.decorator'; 2 | export * from './database/mongo/convert-mongoose-filter.decorator'; 3 | export * from './database/mongo/validate-mongoose-filter.decorator'; 4 | export * from './database/postgres/validate-typeorm-filter.decorator'; 5 | export * from './database/validate-database-sort-allowed.decorator'; 6 | export * from './process/process.decorator'; 7 | export * from './request-timeout.decorator'; 8 | export * from './role.decorator'; 9 | export * from './types'; 10 | export * from './validate-schema.decorator'; 11 | export * from './workers/thread.decorator'; 12 | -------------------------------------------------------------------------------- /src/utils/decorators/log-execution-time.decorator.ts: -------------------------------------------------------------------------------- 1 | import { performance } from 'node:perf_hooks'; 2 | 3 | import { yellow } from 'colorette'; 4 | 5 | import { LoggerService } from '@/infra/logger'; 6 | 7 | export function LogExecutionTime(target: object, propertyKey: string, descriptor: PropertyDescriptor): void { 8 | const originalMethod = descriptor.value; 9 | 10 | descriptor.value = async function (...args: unknown[]) { 11 | const start = performance.now(); 12 | const result = await originalMethod.apply(this, args); 13 | const className = target.constructor.name; 14 | const methodName = propertyKey; 15 | const end = performance.now(); 16 | LoggerService.log(yellow(`Function ${className}/${methodName} took ${(end - start).toFixed(3)}ms to execute.`)); 17 | return result; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/decorators/process/process.ts: -------------------------------------------------------------------------------- 1 | process.on('message', async ({ functionCode, args }) => { 2 | try { 3 | const removeAsync = (functionCode as string).replace('async', '').trim(); 4 | 5 | const fn = new Function(`return async function ${removeAsync}`)(); 6 | 7 | const result = (await fn(...args)) || []; 8 | if (process?.send) { 9 | process?.send({ success: result }); 10 | } 11 | } catch (error) { 12 | if (process?.send) { 13 | const newError = { message: (error as Error)?.message, stack: (error as Error).stack }; 14 | process?.send({ error: newError }); 15 | } 16 | } finally { 17 | process.exit(); 18 | } 19 | }); 20 | 21 | process.on('uncaughtException', (error) => { 22 | if (process.send) { 23 | process.send({ error: { message: error.message, stack: error.stack } }); 24 | } 25 | process.exit(1); 26 | }); 27 | 28 | process.on('unhandledRejection', (reason) => { 29 | let errorMessage = String(reason); 30 | let errorStack = ''; 31 | 32 | if (reason instanceof Error) { 33 | errorMessage = reason.message; 34 | errorStack = reason.stack || ''; 35 | } 36 | 37 | if (process.send) { 38 | process.send({ error: { message: errorMessage, stack: errorStack } }); 39 | } 40 | 41 | process.exit(1); 42 | }); 43 | -------------------------------------------------------------------------------- /src/utils/decorators/request-timeout.decorator.ts: -------------------------------------------------------------------------------- 1 | import { CustomDecorator, SetMetadata } from '@nestjs/common'; 2 | 3 | export const RequestTimeout = (milliseconds: number): CustomDecorator => { 4 | return SetMetadata('request-timeout', milliseconds); 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/decorators/role.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | import { ApiInternalServerException } from '../exception'; 4 | 5 | export const PERMISSION_GUARD = 'permissions'; 6 | export const Permission = (permission: string) => { 7 | const permissionSanitize = permission.toLowerCase(); 8 | 9 | if (!permissionSanitize.includes(':')) { 10 | throw new ApiInternalServerException(`permission: ${permission} must contain ":", example: user:create`); 11 | } 12 | 13 | return SetMetadata(PERMISSION_GUARD, permissionSanitize); 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/decorators/types.ts: -------------------------------------------------------------------------------- 1 | export enum SearchTypeEnum { 2 | 'like', 3 | 'equal' 4 | } 5 | 6 | export type AllowedFilter = { 7 | type: SearchTypeEnum; 8 | map?: string; 9 | format?: 'String' | 'Number' | 'Date' | 'DateIso' | 'Boolean' | 'ObjectId'; 10 | name: keyof T; 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/decorators/validate-schema.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Schema, ZodError, ZodIssue } from 'zod'; 2 | 3 | export function ValidateSchema(...schema: Schema[]) { 4 | return (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => { 5 | const originalMethod = descriptor.value; 6 | descriptor.value = function (...args: unknown[]) { 7 | const validatorError: { error?: ZodError | null; issues: ZodIssue[] } = { 8 | issues: [], 9 | error: null 10 | }; 11 | 12 | for (const [index, value] of schema.entries()) { 13 | try { 14 | const model = value.parse(args[`${index}`]); 15 | 16 | for (const key in model) { 17 | if (model[`${key}`] === undefined) { 18 | delete model[`${key}`]; 19 | } 20 | } 21 | 22 | args[`${index}`] = model; 23 | } catch (error) { 24 | Object.assign(validatorError, { error }); 25 | validatorError.issues.push(...(validatorError.error as ZodError).issues); 26 | } 27 | } 28 | 29 | if (validatorError.error) { 30 | const error = validatorError.error; 31 | error.issues = validatorError.issues; 32 | throw error; 33 | } 34 | 35 | const result = originalMethod.apply(this, args); 36 | return result; 37 | }; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/decorators/workers/thread.decorator.ts: -------------------------------------------------------------------------------- 1 | import { red } from 'colorette'; 2 | import { Worker } from 'worker_threads'; 3 | 4 | import { ApiTimeoutException } from '@/utils/exception'; 5 | 6 | export function RunInNewThread(timeout?: number) { 7 | return function (target: object, key: string, descriptor: PropertyDescriptor): PropertyDescriptor { 8 | const originalMethod = descriptor.value; 9 | 10 | descriptor.value = function (...args: unknown[]) { 11 | const fnCode = originalMethod.toString(); 12 | 13 | const worker = new Worker(`${__dirname}/thread.js`); 14 | 15 | let timeoutId: NodeJS.Timeout | null = null; 16 | 17 | worker.postMessage([fnCode, args]); 18 | 19 | if (timeout) { 20 | timeoutId = setTimeout(() => { 21 | const className = target.constructor.name; 22 | const methodName = key; 23 | const error = new ApiTimeoutException('worker execution timed out.', { timeout }); 24 | Object.assign(error, { context: `${className}/${methodName}` }); 25 | 26 | console.error(error); 27 | worker.terminate(); 28 | }, timeout); 29 | } 30 | 31 | worker.on('message', (value: { error: Error; success: unknown }) => { 32 | if (timeoutId) clearTimeout(timeoutId); 33 | if (value.error) { 34 | return Promise.reject(value.error); 35 | } 36 | 37 | if (value.success) { 38 | return Promise.resolve(value.success); 39 | } 40 | }); 41 | 42 | worker.on('error', (err: Error) => { 43 | if (timeoutId) clearTimeout(timeoutId); 44 | if (err.name === 'ReferenceError') { 45 | console.error(red('worker error '), err); 46 | return; 47 | } 48 | console.error(red('worker error '), err?.message ?? err); 49 | }); 50 | 51 | worker.on('exit', (code: number) => { 52 | if (timeoutId) clearTimeout(timeoutId); 53 | if (code !== 0) { 54 | console.error(red(`worker stopped with exit code ${code}`)); 55 | } 56 | }); 57 | }; 58 | 59 | return descriptor; 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/decorators/workers/thread.ts: -------------------------------------------------------------------------------- 1 | import { parentPort } from 'worker_threads'; 2 | 3 | parentPort?.on('message', async (data) => { 4 | try { 5 | const [fnCode, args] = data; 6 | 7 | const removeAsync = (fnCode as string).replace('async', ''); 8 | 9 | const fn = new Function(`return async function ${removeAsync}`)(); 10 | const result = await fn(...args); 11 | 12 | parentPort?.postMessage({ success: result }); 13 | } catch (error) { 14 | parentPort?.postMessage({ error }); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/utils/entity.ts: -------------------------------------------------------------------------------- 1 | import { ZodSchema } from 'zod'; 2 | 3 | import { DateUtils } from './date'; 4 | import { UUIDUtils } from './uuid'; 5 | 6 | export const withID = (entity: { _id?: string; id?: string }) => { 7 | Object.assign(entity, { id: [entity?.id, entity?._id, UUIDUtils.create()].find(Boolean) }); 8 | return entity; 9 | }; 10 | 11 | export interface IEntity { 12 | id: string; 13 | 14 | createdAt?: Date | null | undefined; 15 | 16 | updatedAt?: Date | null | undefined; 17 | 18 | deletedAt?: Date | null | undefined; 19 | } 20 | 21 | let schema: ZodSchema; 22 | 23 | export const BaseEntity = () => { 24 | abstract class Entity implements IEntity { 25 | constructor(zodSchema: ZodSchema) { 26 | schema = zodSchema; 27 | } 28 | 29 | readonly id!: string; 30 | 31 | readonly createdAt?: Date | null | undefined; 32 | 33 | readonly updatedAt?: Date | null | undefined; 34 | 35 | deletedAt?: Date | null | undefined; 36 | 37 | static nameOf = (name: keyof T) => name as D; 38 | 39 | deactivated() { 40 | this.deletedAt = DateUtils.getJSDate(); 41 | } 42 | 43 | activated() { 44 | Object.assign(this, { deletedAt: null }); 45 | } 46 | 47 | validate(entity: T): T { 48 | Object.assign(entity as IEntity, withID(entity as IEntity)); 49 | Object.assign(this, { id: (entity as Pick).id }); 50 | return schema.parse(entity) as T; 51 | } 52 | } 53 | 54 | return Entity; 55 | }; 56 | -------------------------------------------------------------------------------- /src/utils/excel.ts: -------------------------------------------------------------------------------- 1 | import { Cell, Row } from 'write-excel-file'; 2 | import writeXlsxFile from 'write-excel-file/node'; 3 | 4 | import { ApiBadRequestException } from './exception'; 5 | 6 | type GenerateExcelV2ConfigInput = { 7 | property: string; 8 | header: string; 9 | type: StringConstructor | DateConstructor | NumberConstructor | BooleanConstructor; 10 | config?: Cell; 11 | }; 12 | 13 | type GenerateExcelV2DataInput = { data: { [key: string]: string | number | Date | boolean }[]; sheetName?: string }; 14 | 15 | export class ExcelUtils { 16 | static getExcelBuffer = async ( 17 | { data, sheetName }: GenerateExcelV2DataInput, 18 | configList: GenerateExcelV2ConfigInput[] 19 | ): Promise => { 20 | const MAX_SYNC_LIMIT = 10000; 21 | if (data.length > MAX_SYNC_LIMIT) { 22 | throw new ApiBadRequestException(`limit: ${MAX_SYNC_LIMIT} was reached`); 23 | } 24 | 25 | const HEADER_ROW: Row = configList.map((l) => ({ value: l.header })); 26 | 27 | const DATA_ROWS: Row[] = []; 28 | 29 | for (const line of data) { 30 | const DATA_ROW = []; 31 | 32 | for (const key in line) { 33 | const value = line[`${key}`]; 34 | const row = configList.find((c) => c.property === key); 35 | if (!row) { 36 | throw new ApiBadRequestException(`property: ${key} not found`); 37 | } 38 | DATA_ROW.push({ value, format: row.config?.format, type: row.type }); 39 | } 40 | 41 | DATA_ROWS.push(DATA_ROW as Row); 42 | } 43 | 44 | const excelData = [HEADER_ROW, ...DATA_ROWS]; 45 | 46 | return await writeXlsxFile(excelData, { buffer: true, sheet: sheetName ?? 'default' }); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/http-status.ts: -------------------------------------------------------------------------------- 1 | export const DefaultErrorMessage: { [key: string]: string } = { 2 | ECONNREFUSED: 'Connection Refused', 3 | 403: 'You Shall Not Pass', 4 | 405: 'Method Not Allowed', 5 | 406: 'Not Acceptable', 6 | 408: 'Request Timeout', 7 | 413: 'Payload Too Large', 8 | 414: 'URI Too Long', 9 | 422: 'Unprocessable Entity', 10 | 428: 'Precondition Required', 11 | 429: 'Too Many Requests', 12 | 500: 'Internal Server Error.', 13 | 501: 'Not Implemented', 14 | 502: 'Bad Gateway', 15 | 503: 'Service Unavailable', 16 | 504: 'Gateway Timeout', 17 | 507: 'Insufficient Storage', 18 | 508: 'Loop Detected' 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/mongoose.ts: -------------------------------------------------------------------------------- 1 | import { Connection, Types } from 'mongoose'; 2 | 3 | export class MongoUtils { 4 | static createObjectId = (id?: string): Types.ObjectId => { 5 | return new Types.ObjectId(id); 6 | }; 7 | 8 | static isObjectId = (id?: string) => { 9 | if (id) { 10 | return Types.ObjectId.isValid(id); 11 | } 12 | return false; 13 | }; 14 | 15 | static skipParentheses = (filter: string | string[]): string | string[] => { 16 | if (typeof filter === 'string') { 17 | return filter?.replace('(', '\\(')?.replace(')', '\\)'); 18 | } 19 | if (typeof filter === 'object') { 20 | return filter.map((f) => f.replace('(', '\\(')?.replace(')', '\\)')); 21 | } 22 | 23 | return filter; 24 | }; 25 | 26 | static calculateSkip = (page: number, limit: number): number => { 27 | return ((page || 1) - 1) * Number(limit || 10); 28 | }; 29 | 30 | static diacriticSensitiveRegex = (filter: string | string[]): string | string[] => { 31 | const handlerFilter = (innerFilter: string) => { 32 | return innerFilter 33 | .replace(/a/g, '[a,á,à,ä,â,ã]') 34 | .replace(/e/g, '[e,é,ë,è,ê]') 35 | .replace(/i/g, '[i,í,ï,ì,î]') 36 | .replace(/o/g, '[o,ó,ö,ò,ô]') 37 | .replace(/c/g, '[c,ç]') 38 | .replace(/u/g, '[u,ü,ú,ù]'); 39 | }; 40 | 41 | if (typeof filter === 'string') { 42 | return handlerFilter(filter); 43 | } 44 | if (typeof filter === 'object') { 45 | const regexText = filter.map((f) => { 46 | return handlerFilter(f); 47 | }); 48 | 49 | return regexText; 50 | } 51 | 52 | return filter; 53 | }; 54 | 55 | static createRegexFilterText = (text: string | string[]): string | string[] => { 56 | return this.diacriticSensitiveRegex(this.skipParentheses(text)); 57 | }; 58 | } 59 | 60 | export type MongoRepositoryModelSessionType = T & { connection?: Connection }; 61 | 62 | export type MongoRepositorySession = { 63 | abortTransaction: () => Promise<{ [key: string]: unknown }>; 64 | commitTransaction: () => Promise<{ [key: string]: unknown }>; 65 | }; 66 | -------------------------------------------------------------------------------- /src/utils/pagination.ts: -------------------------------------------------------------------------------- 1 | import { SearchInput } from './search'; 2 | import { SortInput } from './sort'; 3 | import { Infer, InputValidator } from './validator'; 4 | 5 | const maxLimit = (limit: number) => (limit > 100 ? 100 : limit); 6 | 7 | export const PaginationSchema = InputValidator.object({ 8 | page: InputValidator.number().or(InputValidator.string()).or(InputValidator.nan()).default(1), 9 | limit: InputValidator.number().or(InputValidator.string()).or(InputValidator.nan()).default(10) 10 | }) 11 | .transform((pagination) => { 12 | let limit = Number(pagination.limit); 13 | let page = Number(pagination.page); 14 | 15 | if (isNaN(limit)) { 16 | limit = 10; 17 | } 18 | 19 | if (isNaN(page)) { 20 | page = 1; 21 | } 22 | 23 | return { 24 | page: page > 0 ? page : 1, 25 | limit: limit > 0 ? maxLimit(limit) : 10 26 | }; 27 | }) 28 | .refine((pagination) => Number.isInteger(pagination.page), { 29 | path: ['page'], 30 | message: 'invalidInteger' 31 | }) 32 | .refine((pagination) => Number.isInteger(pagination.limit), { 33 | path: ['limit'], 34 | message: 'invalidInteger' 35 | }); 36 | 37 | export class PaginationUtils { 38 | static calculateSkip = (input: Infer) => { 39 | return (input.page - 1) * input.limit; 40 | }; 41 | 42 | static calculateTotalPages = ({ limit, total }: { limit: number; total: number }) => { 43 | return Number(Math.ceil(total / limit)); 44 | }; 45 | } 46 | 47 | export type PaginationInput = Infer & SortInput & SearchInput>; 48 | export type PaginationOutput = Infer & { total: number; docs: T[]; totalPages?: number }; 49 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { AttributeValue, Context, Span, SpanStatus, TimeInput, Tracer } from '@opentelemetry/api'; 2 | import { AxiosInstance, AxiosRequestConfig } from 'axios'; 3 | 4 | import { UserEntity } from '@/core/user/entity/user'; 5 | 6 | export type TracingType = { 7 | span: Span; 8 | tracer: Tracer; 9 | tracerId: string; 10 | axios: (config?: AxiosRequestConfig) => AxiosInstance; 11 | setStatus: (status: SpanStatus) => void; 12 | logEvent: (name: string, attributesOrStartTime?: AttributeValue | TimeInput) => void; 13 | addAttribute: (key: string, value: AttributeValue) => void; 14 | createSpan: (name: string, parent?: Context) => Span; 15 | finish: () => void; 16 | }; 17 | 18 | export type UserRequest = Pick; 19 | 20 | export interface ApiRequest { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | readonly body: any; 23 | readonly tracing: TracingType; 24 | readonly user: UserRequest; 25 | readonly params: { [key: string]: string | number }; 26 | readonly query: { [key: string]: string | number }; 27 | readonly headers: Headers & { authorization: string }; 28 | readonly url: string; 29 | readonly files: { 30 | buffer: Buffer; 31 | encoding: string; 32 | fieldname: string; 33 | mimetype: string; 34 | originalname: string; 35 | size: number; 36 | }[]; 37 | } 38 | 39 | export type ApiTrancingInput = Pick; 40 | 41 | export const getPathWithoutUUID = (path: string) => 42 | path.replace(/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/, 'uuid'); 43 | 44 | export class ApiOkResponse { 45 | static STATUS: 200; 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/search.ts: -------------------------------------------------------------------------------- 1 | import { Infer, InputValidator } from './validator'; 2 | 3 | export type SearchInput = { search: T | null }; 4 | 5 | export const SearchHttpSchema = InputValidator.string() 6 | .optional() 7 | .refine( 8 | (check) => { 9 | if (!check) return true; 10 | return [!check.startsWith(':'), check.includes(':')].every(Boolean); 11 | }, 12 | { 13 | message: 'invalidSearchFormat' 14 | } 15 | ) 16 | .refine( 17 | (search) => { 18 | if (!search) return true; 19 | 20 | return String(search) 21 | .split(',') 22 | .every((s) => { 23 | const [value] = s.split(':').reverse(); 24 | 25 | if (!value) return false; 26 | return true; 27 | }); 28 | }, 29 | { 30 | message: 'searchMustBe: value' 31 | } 32 | ) 33 | .transform((searchString) => { 34 | if (!searchString) return null; 35 | const search: { [key: string]: unknown } = {}; 36 | 37 | searchString.split(',').forEach((s) => { 38 | const [field, value] = s.split(':'); 39 | const finalValue = value.split('|').map((v) => v.trim()); 40 | if (finalValue.length > 1) { 41 | search[`${field}`] = value.split('|').map((v) => v.trim()); 42 | return; 43 | } 44 | search[`${field}`] = value.trim(); 45 | }); 46 | 47 | return search; 48 | }); 49 | 50 | export type SearchHttpSchemaInput = Infer; 51 | 52 | export const SearchSchema = InputValidator.object({ 53 | search: InputValidator.record( 54 | InputValidator.string().trim(), 55 | InputValidator.number().or(InputValidator.string()).or(InputValidator.array(InputValidator.any())) 56 | ) 57 | .nullable() 58 | .default({}) 59 | }); 60 | -------------------------------------------------------------------------------- /src/utils/sort.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { Infer, InputValidator } from './validator'; 4 | 5 | export enum SortEnum { 6 | asc = 1, 7 | desc = -1 8 | } 9 | 10 | export const SortHttpSchema = z 11 | .string() 12 | .optional() 13 | .refine( 14 | (check) => { 15 | if (!check) return true; 16 | return [!check.startsWith(':'), check.includes(':')].every(Boolean); 17 | }, 18 | { 19 | message: 'invalidSortFormat' 20 | } 21 | ) 22 | .refine( 23 | (sort) => { 24 | if (!sort) return true; 25 | 26 | return String(sort) 27 | .split(',') 28 | .every((s) => { 29 | const [order] = s.split(':').reverse(); 30 | return ['asc', 'desc'].includes(order.trim().toLowerCase() || 'asc'); 31 | }); 32 | }, 33 | { 34 | message: 'invalidSortOrderMustBe: asc, desc' 35 | } 36 | ) 37 | .transform((sort) => { 38 | const sortDefault = sort || 'createdAt:desc'; 39 | 40 | const order = Object.fromEntries( 41 | String(sort && !sort.includes('createdAt') ? sort : sortDefault) 42 | .split(',') 43 | .map((s) => { 44 | const [field, order] = s.split(':'); 45 | const sorted = [field.trim(), SortEnum[(order.trim().toLowerCase() || 'asc') as keyof typeof SortEnum]]; 46 | return sorted; 47 | }) 48 | ); 49 | 50 | return order; 51 | }); 52 | 53 | export const SortSchema = InputValidator.object({ 54 | sort: InputValidator.record(InputValidator.string().trim().min(1), InputValidator.nativeEnum(SortEnum)) 55 | .nullable() 56 | .default({}) 57 | }); 58 | 59 | export type SortInput = Infer; 60 | -------------------------------------------------------------------------------- /src/utils/text.ts: -------------------------------------------------------------------------------- 1 | export class TextUtils { 2 | static snakeCase = (text: string) => { 3 | return text 4 | .replace(/([a-z0-9])([A-Z])/g, '$1_$2') 5 | .replace(/\W+/g, ' ') 6 | .split(/ |\B(?=[A-Z])/) 7 | .map((word) => word.toLowerCase()) 8 | .join('_'); 9 | }; 10 | 11 | static capitalizeFirstLetter = (text: string = '') => { 12 | return text.charAt(0).toUpperCase() + text.slice(1); 13 | }; 14 | 15 | static removeAccentsFromString = (text: string = ''): string => { 16 | return text 17 | ? text 18 | .normalize('NFD') 19 | .replace(/[\u0300-\u036f]/g, '') 20 | .trim() 21 | : text; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { ZodOptional, ZodPipeline, ZodType } from 'zod'; 2 | 3 | type Equals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false; 4 | 5 | type NonUndefined = Exclude; 6 | 7 | export type ZodInferSchema = { 8 | [Key in keyof T]-?: Equals> extends false 9 | ? ZodOptional>> | ZodPipeline>, ZodType> 10 | : ZodType | ZodPipeline, ZodType>; 11 | }; 12 | 13 | export type MakePartial = { 14 | [P in keyof T]: T[P] extends object ? MakePartial : T[P]; 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/usecase.ts: -------------------------------------------------------------------------------- 1 | export interface IUsecase { 2 | execute(...input: unknown[]): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | export class UUIDUtils { 4 | static create() { 5 | return uuidv4(); 6 | } 7 | 8 | static isUUID = (uuid: string) => { 9 | const regex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'; 10 | const isUUID = new RegExp(regex).exec(uuid) || []; 11 | 12 | return isUUID.length > 0; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /test/initialization.ts: -------------------------------------------------------------------------------- 1 | import { TestMock } from 'test/mock'; 2 | 3 | jest.setTimeout(30000); 4 | 5 | process.env.NODE_ENV = 'test'; 6 | process.env.TOKEN_TEST = 7 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImFkbWluQGFkbWluLmNvbSIsIm5hbWUiOiJBZG1pbiIsImlkIjoiZGYwMzJmMTktZmM0Yy00NDA5LTlhYTktMzMyNjRkMmM3YjcxIiwiaWF0IjoxNzM4MjU3NjI0LCJleHAiOjE3Njk3OTM2MjR9.j0pvqjU3Z_BICTo5wFLrkLo7jTvjW6SWbHcGbJ5T6mQ'; 8 | 9 | jest.mock('pino-http', () => ({ 10 | HttpLogger: {}, 11 | pinoHttp: () => ({ 12 | logger: { 13 | info: TestMock.mockReturnValue(), 14 | error: TestMock.mockReturnValue() 15 | } 16 | }) 17 | })); 18 | 19 | jest.mock('pino', () => jest.genMockFromModule('pino')); 20 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "node_modules" 5 | ] 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2017", 11 | "resolveJsonModule": true, 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "paths": { 15 | "@/*": [ 16 | "./src/*" 17 | ], 18 | "@/test/*": [ 19 | "./test/*" 20 | ] 21 | }, 22 | "baseUrl": "./", 23 | "incremental": true, 24 | "strict": true, 25 | "skipLibCheck": true, 26 | "strictNullChecks": true, 27 | "noImplicitAny": true, 28 | "strictBindCallApply": false, 29 | "forceConsistentCasingInFileNames": false, 30 | "noFallthroughCasesInSwitch": false 31 | } 32 | } --------------------------------------------------------------------------------