├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── Dockerfile ├── Makefile ├── README.md ├── db.sqlite ├── docker-compose-dev.yml ├── docker-compose-sq.yml ├── docker └── development.Dockerfile ├── imgs └── hexag-architecture.png ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.spec.ts │ ├── app.service.ts │ └── request.http ├── auth │ ├── application │ │ ├── create-user │ │ │ ├── create-user.service.spec.ts │ │ │ └── create-user.service.ts │ │ ├── encryption-facade │ │ │ ├── encryption.facade.service.spec.ts │ │ │ └── encryption.facade.service.ts │ │ ├── jwt-facade │ │ │ ├── jwt.facade.service.spec.ts │ │ │ └── jwt.facade.service.ts │ │ ├── refresh-token │ │ │ ├── refresh-token.service.spec.ts │ │ │ └── refresh-token.service.ts │ │ ├── signin │ │ │ ├── signin.service.spec.ts │ │ │ └── signin.service.ts │ │ ├── signup │ │ │ ├── signup.service.test.ts │ │ │ └── signup.service.ts │ │ └── validate-user │ │ │ ├── validate-user.service.ts │ │ │ └── validate.user.service.spec.ts │ ├── auth.module.ts │ ├── domain │ │ ├── entity │ │ │ └── user.ts │ │ └── ports │ │ │ ├── db │ │ │ └── user.repository.ts │ │ │ └── primary │ │ │ └── http │ │ │ ├── refresh-token.controller.interface.ts │ │ │ ├── signin.controller.interface.ts │ │ │ ├── signout.controller.interface.ts │ │ │ ├── signup.controller.interface.ts │ │ │ └── whoami.controller.interface.ts │ └── infrastructure │ │ ├── adapters │ │ ├── primary │ │ │ └── http │ │ │ │ ├── refresh-token │ │ │ │ ├── dto │ │ │ │ │ └── refresh-token.response.dto.ts │ │ │ │ ├── refresh-token.controller.spec.ts │ │ │ │ ├── refresh-token.controller.ts │ │ │ │ └── request.http │ │ │ │ ├── signout │ │ │ │ ├── request.http │ │ │ │ ├── signout.controller.spec.ts │ │ │ │ └── signout.controller.ts │ │ │ │ ├── signup │ │ │ │ ├── dto │ │ │ │ │ ├── signup.request.dto.ts │ │ │ │ │ └── user.response.dto.ts │ │ │ │ ├── request.http │ │ │ │ ├── signup.controller.ts │ │ │ │ └── singup.controller.spec.ts │ │ │ │ ├── singin │ │ │ │ ├── dto │ │ │ │ │ ├── signin.request.dto.ts │ │ │ │ │ └── signin.response.dto.ts │ │ │ │ ├── request.http │ │ │ │ ├── signin.controller.ts │ │ │ │ └── singin.controller.spec.ts │ │ │ │ └── whoami │ │ │ │ ├── dto │ │ │ │ └── user.response.dto.ts │ │ │ │ ├── request.http │ │ │ │ ├── whoami.controller.spec.ts │ │ │ │ └── whoami.controller.ts │ │ └── secondary │ │ │ └── db │ │ │ ├── dao │ │ │ └── user.dao.ts │ │ │ └── user.repository.ts │ │ ├── auth-strategies │ │ ├── jwt-strategy.ts │ │ ├── local-strategy.ts │ │ └── refresh-token.strategy.ts │ │ ├── decorators │ │ └── new-refresh-token.decorator.ts │ │ ├── guards │ │ ├── is-admin.guard.ts │ │ ├── jwt-auth.guard.ts │ │ ├── local-auth.guard.ts │ │ └── refresh-jwt-auth.guard.ts │ │ └── interceptors │ │ └── current-user.interceptor.ts ├── config │ ├── constants.ts │ ├── db │ │ ├── data-source.ts │ │ ├── database.config.ts │ │ ├── migration-new-way.ts │ │ └── migrations │ │ │ └── 1692199497672-NewMigration.ts │ └── environments │ │ ├── dev.env │ │ ├── local.env │ │ ├── prod.env │ │ ├── qa.env │ │ ├── staging.env │ │ └── test.env ├── main.ts ├── reports │ ├── application │ │ ├── approved-report │ │ │ ├── approved-report.service.spec.ts │ │ │ └── approved-report.service.ts │ │ ├── create-report │ │ │ ├── create-report.service.spec.ts │ │ │ └── create-report.service.ts │ │ └── get-estimate │ │ │ ├── get-estimate.service.spec.ts │ │ │ └── get-estimate.service.ts │ ├── domain │ │ ├── entity │ │ │ ├── report.ts │ │ │ └── user.ts │ │ ├── errors │ │ │ ├── mileage-error.ts │ │ │ ├── price-error.ts │ │ │ └── year-error.ts │ │ └── ports │ │ │ ├── primary │ │ │ └── http │ │ │ │ ├── approved-report.controller.interface.ts │ │ │ │ ├── create-report.controller.interface.ts │ │ │ │ └── get-estimate.controller.interface.ts │ │ │ └── secondary │ │ │ └── db │ │ │ └── user.repository.interface.ts │ ├── infrastructure │ │ └── adapters │ │ │ ├── primary │ │ │ └── http │ │ │ │ ├── approved-report │ │ │ │ ├── approved-report.controller.spec.ts │ │ │ │ ├── approved-report.controller.ts │ │ │ │ ├── dto │ │ │ │ │ └── approved.request.dto.ts │ │ │ │ └── request.http │ │ │ │ ├── create-report │ │ │ │ ├── create-report.controller.spec.ts │ │ │ │ ├── create-report.controller.ts │ │ │ │ ├── dto │ │ │ │ │ ├── report.request.dto.ts │ │ │ │ │ └── report.response.dto.ts │ │ │ │ └── request.http │ │ │ │ └── get-estimate │ │ │ │ ├── dto │ │ │ │ ├── get-estimate.request.dto.ts │ │ │ │ └── get-estimate.response.dto.ts │ │ │ │ ├── get-estimate.controller.spec.ts │ │ │ │ ├── get-estimate.controller.ts │ │ │ │ └── request.http │ │ │ └── secondary │ │ │ └── db │ │ │ ├── dao │ │ │ ├── report.dao.ts │ │ │ └── user.dao.ts │ │ │ └── report.repository.ts │ └── reports.module.ts ├── setup-app.ts ├── shared │ ├── domain │ │ ├── errors │ │ │ └── value-required-error.ts │ │ └── value-objects │ │ │ ├── email.value.object.ts │ │ │ ├── password.value.object.ts │ │ │ └── value-object-base.abstract.ts │ └── infrastructure │ │ ├── decorators │ │ ├── current-user.decorator.ts │ │ ├── is-admin.decorator.ts │ │ ├── public.decorator.ts │ │ └── serialize.decorator.ts │ │ ├── interceptors │ │ └── serialize.interceptor.ts │ │ ├── request-base.dto.abstract.ts │ │ └── response-base.dto.abstract.ts └── users │ ├── application │ ├── find-user-by-id │ │ ├── find-user-by-id.service.spec.ts │ │ └── find-user-by-id.service.ts │ ├── find-users │ │ ├── find-users.service.spec.ts │ │ └── find-users.service.ts │ ├── remove-user │ │ ├── remove-user.service.spec.ts │ │ └── remove-user.service.ts │ └── update-user │ │ ├── update-user.service.spec.ts │ │ └── update-user.service.ts │ ├── constants.ts │ ├── domain │ ├── entity │ │ └── user.ts │ └── ports │ │ ├── primary │ │ └── api │ │ │ ├── find-user-by-id.controller.interface.ts │ │ │ ├── find-users.controller.interface.ts │ │ │ ├── remove-user.controller.interface.ts │ │ │ └── update.controller.interface.ts │ │ └── secondary │ │ └── db │ │ └── user.repository.interface.ts │ ├── infrastructure │ └── adapters │ │ ├── primary │ │ └── http │ │ │ ├── find-user-by-id │ │ │ ├── dto │ │ │ │ └── user.response.dto.ts │ │ │ ├── find-user-by-id.controller.spec.ts │ │ │ ├── find-user-by-id.controller.ts │ │ │ └── request.http │ │ │ ├── find-users │ │ │ ├── dto │ │ │ │ ├── product.response.dto.ts │ │ │ │ └── user.response.dto.ts │ │ │ ├── find-users.controller.spec.ts │ │ │ ├── find-users.controller.ts │ │ │ └── request.http │ │ │ ├── remove-user │ │ │ ├── dto │ │ │ │ └── user.response.dto.ts │ │ │ ├── remove-user.controller.spec.ts │ │ │ ├── remove-user.controller.ts │ │ │ └── request.http │ │ │ └── update-user │ │ │ ├── dto │ │ │ ├── user.request.dto.ts │ │ │ └── user.response.dto.ts │ │ │ ├── request.http │ │ │ ├── update-user.controller.spec.ts │ │ │ └── update-user.controller.ts │ │ └── secondary │ │ └── db │ │ ├── dao │ │ ├── report.dao.ts │ │ └── user.dao.ts │ │ └── user.repository.ts │ └── users.module.ts ├── test ├── app.e2e-spec.ts ├── auth │ ├── signin.e2e-spec.ts │ ├── signout.e2e-spec.ts │ ├── signup.e2e-spec.ts │ └── whoami.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint', '@typescript-eslint/eslint-plugin'], 9 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], 10 | root: true, 11 | env: { 12 | node: true, 13 | jest: true, 14 | }, 15 | ignorePatterns: ['.eslintrc.js'], 16 | rules: { 17 | '@typescript-eslint/explicit-function-return-type': 'off', 18 | '@typescript-eslint/explicit-module-boundary-types': 'off', 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | 'no-console': 'error', 21 | '@typescript-eslint/no-var-requires': 'off', 22 | 'import/no-unresolved': 'off', 23 | 'import/extensions': 'off', 24 | 'class-methods-use-this': ['off', { exceptMethods: ['error'] }], 25 | 'import/no-extraneous-dependencies': ['off', { devDependencies: ['**/*.test.js', '**/*.spec.js'] }], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | # .vscode/* 32 | # !.vscode/settings.json 33 | # !.vscode/tasks.json 34 | # !.vscode/launch.json 35 | # !.vscode/extensions.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 120, 5 | "singleQuote": true, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Configurar variables de entorno para rest-client 3 | "rest-client.environmentVariables": { 4 | "$shared": { 5 | "version": "v1", 6 | "contentType": "application/json" 7 | }, 8 | "local": { 9 | "host": "localhost:3000", 10 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJ5YXNuaWVsQGdtYWlsLmNvbSIsImlzQWRtaW4iOmZhbHNlLCJpYXQiOjE2OTIwMTk2NzEsImV4cCI6MTY5MjYyNDQ3MX0.Sfg8CUqwV_RNAXJ7mMjavvoszwNOaePbHQjkD6kyXzk", 11 | "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJ5YXNuaWVsQGdtYWlsLmNvbSIsImlzQWRtaW4iOmZhbHNlLCJpYXQiOjE2OTIwMTk2NzEsImV4cCI6MTY5NDYxMTY3MX0.YAYaZHg3Z20Eg_zYDl-nMOMrHMbD42J_-SPEIMkhyrU" 12 | }, 13 | "production": { 14 | "host": "example.com", 15 | "token": "", 16 | "refreshToken": "", 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-buster-slim AS builder 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install --only=production 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | # --- 14 | 15 | FROM node:18-alpine 16 | 17 | RUN apk --update add curl ttf-freefont fontconfig && rm -rf /var/cache/apk/* 18 | 19 | WORKDIR /usr/src/app 20 | 21 | COPY --from=builder /usr/src/app /usr/src/app 22 | 23 | EXPOSE 3000 24 | 25 | CMD ["npm", "run", "start:prod"] 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prod: 2 | docker-compose up -d --force-recreate 3 | 4 | dev: 5 | docker compose -f docker-compose-dev.yml up --force-recreate 6 | 7 | sq: 8 | docker compose -f docker-compose-sq.yml up --force-recreate 9 | 10 | down-prod: 11 | docker compose down 12 | 13 | down-dev: 14 | docker compose -f docker-compose-dev.yml down 15 | 16 | down-sq: 17 | docker compose -f docker-compose-sq.yml down 18 | 19 | logs: 20 | docker-compose logs -f 21 | 22 | unrootify: 23 | sudo chown -R $$(id -u):$$(id -g) . 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hexagonal Architecture 2 | 3 | También conocida como Puertos y Adaptadores (Ports and Adapters), se basa en la separación del dominio de negocio de los detalles de implementación. Todas las entradas y salidas de la 4 | aplicación se exponen a través de puertos. 5 | 6 | 7 | 8 | ## Ports 9 | 10 | Los **puertos** son las interfaces que definen la interacción con el exterior y exponen únicamente datos de nuestro dominio, dejando que toda la lógica de transformación esté de puertas afuera y no se contamine el interior. 11 | 12 | ## Adapters 13 | 14 | Y los **adaptadores** son precisamente la forma de conectar el exterior con los puertos, implementando la comunicación y la conversión de datos entre el dominio y lo que se necesite fuera. Los adaptadores no pertenecen al core como tal y podrían implementarse cada uno completamente por separado si quisiésemos mientras dependan del puerto que usan/implementan. 15 | 16 | ## _Ventajas_ 17 | 18 | - Mas fácil de testear 19 | - Arquitectura más mantenible a largo plazo 20 | - Flexibilidad en cuanto a tecnologías externas 21 | - Nos permite retrasar al maximo la eleccion de la tecgnologías 22 | 23 | ## _Desventajas_ 24 | 25 | - Arquitectura muy pesada ya que se agregan muchas clases e interfaces 26 | - Coste de adaptación por parte de los desarrolladores nuevos que no son tan expertos en la arquitectura lo que impacta en los tiempos de las primeras actividades. 27 | - Puede ser confuso al aplicar con frameworks muy extrictos ya que se debe considerar al framework como algo externo también. 28 | 29 | ## _Cuando usar_ 30 | 31 | - Proyectos grandes. 32 | - Proyectos con tiempos de vida muy largos 33 | - Necesidad de una mayor flexibilidad en cuanto a tegnologias. 34 | - Equipos relativamente expertos, almenos deberia haber 1 o 2 expertos en la arquitectura. 35 | 36 | ## TypeORM Migration Comands 37 | 38 | Si una entidad dao cambia deberiamos generar un cambio en nuestras migracionesy de esta manera 39 | va quedando un registro de cambios en la base de datos. 40 | 41 | Si algo sale mal podemos ejecutar revert 42 | 43 | Si cambia algo en algun dao, se debe generara un nueva migracion y luego esto se correra en el servidor de integracion continua. 44 | 45 | 1- You can create a new migration using CLI: 46 | 47 | ``` 48 | typeorm migration:create migrations/migrationName 49 | 50 | ``` 51 | 52 | 2- Generate migration from entities: 53 | 54 | ``` 55 | npm run typeorm migration:generate -- src/config/db/migrations/NewMigration 56 | ``` 57 | 58 | 3- To execute all pending migrations use following command: 59 | 60 | ``` 61 | typeorm migration:run -- -d src/config/db/migrations 62 | ``` 63 | 64 | 4- Revertir cambios 65 | 66 | ``` 67 | typeorm migration:revert -- -d src/config/db/migrations 68 | ``` 69 | -------------------------------------------------------------------------------- /db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasniel1408/mycv-nestjs-backend-hexagonal-architecture/710a99271f509fc19b694f6f1d7fdb019b77f313/db.sqlite -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | api: 5 | container_name: api 6 | build: 7 | context: ./ 8 | dockerfile: ./docker/development.Dockerfile 9 | environment: 10 | EXAMPLE: example 11 | command: ['npm', 'run', 'typeorm migration:run -- -d src/config/db/migrations'] 12 | depends_on: 13 | - db 14 | ports: 15 | - '3000:3000' 16 | volumes: 17 | - ./:/usr/src 18 | - /usr/src/node_modules 19 | db: 20 | container_name: db 21 | image: arm64v8/mysql 22 | restart: always 23 | environment: 24 | MYSQL_ROOT_PASSWORD: root 25 | MYSQL_DATABASE: db 26 | MYSQL_USER: user 27 | MYSQL_PASSWORD: password 28 | ports: 29 | - '3306:3306' 30 | volumes: 31 | - my-datavolume:/var/lib/mysql 32 | volumes: 33 | my-datavolume: 34 | -------------------------------------------------------------------------------- /docker-compose-sq.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | sonarqube: 5 | container_name: sonarqube 6 | image: davealdon/sonarqube-with-docker-and-m1-macs 7 | depends_on: 8 | - sonarqube-db 9 | environment: 10 | SONAR_JDBC_URL: jdbc:postgresql://sonarqube-db:5432/sonar 11 | SONAR_JDBC_USERNAME: sonar 12 | SONAR_JDBC_PASSWORD: sonar 13 | restart: always 14 | volumes: 15 | - sonarqube_data:/opt/sonarqube/data 16 | - sonarqube_extensions:/opt/sonarqube/extensions 17 | - sonarqube_logs:/opt/sonarqube/logs 18 | ports: 19 | - '9000:9000' 20 | sonarqube-db: 21 | container_name: sonarqube-db 22 | image: postgres:12 23 | platform: linux/arm64/v8 24 | environment: 25 | POSTGRES_USER: sonar 26 | POSTGRES_PASSWORD: sonar 27 | POSTGRES_DB: sonar 28 | volumes: 29 | - postgresql:/var/lib/postgresql 30 | - postgresql_data:/var/lib/postgresql/data 31 | 32 | volumes: 33 | sonarqube_data: 34 | sonarqube_extensions: 35 | sonarqube_logs: 36 | postgresql: 37 | postgresql_data: 38 | -------------------------------------------------------------------------------- /docker/development.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.17.0-alpine 2 | 3 | WORKDIR /usr/src 4 | 5 | COPY ["./package.json", "./package-lock.json", "/usr/src/"] 6 | 7 | RUN npm install 8 | 9 | COPY ["./", "/usr/src/"] 10 | 11 | EXPOSE 3000 12 | 13 | RUN chmod a+rx /usr/src/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node 14 | 15 | RUN npm rebuild bcrypt --update-binary 16 | 17 | CMD ["npm", "run", "start:dev"] 18 | -------------------------------------------------------------------------------- /imgs/hexag-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasniel1408/mycv-nestjs-backend-hexagonal-architecture/710a99271f509fc19b694f6f1d7fdb019b77f313/imgs/hexag-architecture.png -------------------------------------------------------------------------------- /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 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mycv-nestjs-backend-hexagonal-architecture", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "NODE_ENV=dev nest start --watch", 13 | "start:local": "NODE_ENV=local nest start --watch", 14 | "start:staging": "NODE_ENV=staging nest start --watch", 15 | "start:qa": "NODE_ENV=qa nest start --watch", 16 | "start:debug": "NODE_ENV=test nest start --debug --watch", 17 | "start:prod": "NODE_ENV=prod node dist/main", 18 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 19 | "test": "jest", 20 | "test:watch": "jest --watch", 21 | "test:cov": "jest --coverage", 22 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 23 | "test:e2e": "NODE_ENV=test jest --config ./test/jest-e2e.json --MaxWorkers=1 --runInBand", 24 | "typeorm": "npm run build && NODE_ENV=dev typeorm-ts-node-commonjs -d dist/config/db/data-source.js", 25 | "migration:generate": "npm run typeorm -- migration:generate", 26 | "migration:run": "npm run typeorm -- migration:run", 27 | "migration:revert": "npm run typeorm -- migration:revert" 28 | }, 29 | "dependencies": { 30 | "@nestjs/common": "^10.0.0", 31 | "@nestjs/config": "^3.0.0", 32 | "@nestjs/core": "^10.0.0", 33 | "@nestjs/jwt": "^10.1.0", 34 | "@nestjs/passport": "^10.0.0", 35 | "@nestjs/platform-express": "^10.0.0", 36 | "@nestjs/typeorm": "^10.0.0", 37 | "@types/bcrypt": "^5.0.0", 38 | "bcrypt": "^5.1.0", 39 | "class-transformer": "^0.5.1", 40 | "class-validator": "^0.14.0", 41 | "dotenv": "^16.3.1", 42 | "moment": "^2.29.4", 43 | "mysql2": "^3.5.2", 44 | "passport": "^0.6.0", 45 | "passport-jwt": "^4.0.1", 46 | "passport-local": "^1.0.0", 47 | "reflect-metadata": "^0.1.13", 48 | "rxjs": "^7.8.1", 49 | "shallow-equal-object": "^1.1.1", 50 | "sqlite3": "^5.1.1", 51 | "typeorm": "^0.3.17" 52 | }, 53 | "devDependencies": { 54 | "@automock/jest": "^1.2.2", 55 | "@nestjs/cli": "^10.0.0", 56 | "@nestjs/schematics": "^10.0.0", 57 | "@nestjs/testing": "^10.0.0", 58 | "@types/express": "^4.17.17", 59 | "@types/jest": "^29.5.2", 60 | "@types/node": "^20.3.1", 61 | "@types/passport-jwt": "^3.0.9", 62 | "@types/passport-local": "^1.0.35", 63 | "@types/supertest": "^2.0.12", 64 | "@typescript-eslint/eslint-plugin": "^5.59.11", 65 | "@typescript-eslint/parser": "^5.59.11", 66 | "eslint": "^8.42.0", 67 | "eslint-config-prettier": "^8.8.0", 68 | "eslint-plugin-import": "^2.25.2", 69 | "eslint-plugin-prettier": "^4.2.1", 70 | "jest": "^29.5.0", 71 | "prettier": "^2.8.8", 72 | "source-map-support": "^0.5.21", 73 | "supertest": "^6.3.3", 74 | "ts-jest": "^29.1.0", 75 | "ts-loader": "^9.4.3", 76 | "ts-node": "^10.9.1", 77 | "tsconfig-paths": "^4.2.0", 78 | "typescript": "^5.1.3" 79 | }, 80 | "jest": { 81 | "moduleFileExtensions": [ 82 | "js", 83 | "json", 84 | "ts" 85 | ], 86 | "rootDir": "src", 87 | "testRegex": ".*\\.spec\\.ts$", 88 | "transform": { 89 | "^.+\\.(t|j)s$": "ts-jest" 90 | }, 91 | "collectCoverageFrom": [ 92 | "**/*.(t|j)s" 93 | ], 94 | "coverageDirectory": "../coverage", 95 | "coverageReporters": [ 96 | "json", 97 | "html", 98 | "lcov" 99 | ], 100 | "coverageThreshold": { 101 | "global": { 102 | "branches": 90, 103 | "lines": 90 104 | } 105 | }, 106 | "testEnvironment": "node", 107 | "moduleNameMapper": { 108 | "^@src(.*)$": "$1", 109 | "^@app(.*)$": "/app$1", 110 | "^@config(.*)$": "/config$1", 111 | "^@users(.*)$": "/users$1", 112 | "^@auth(.*)$": "/auth$1", 113 | "^@reports(.*)$": "/reports$1", 114 | "^@utils(.*)$": "/utils$1", 115 | "^@shared(.*)$": "/shared$1" 116 | } 117 | }, 118 | "_moduleAliases": { 119 | "@app": "dist/app", 120 | "@config": "dist/config", 121 | "@users": "dist/users", 122 | "@auth": "dist/auth", 123 | "@reports": "dist/reports", 124 | "@shared": "dist/shared", 125 | "@utils": "dist/utils" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/app/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | describe('AppController', () => { 7 | let controller: AppController; 8 | 9 | beforeEach(async () => { 10 | const app: TestingModule = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | // eslint-disable-next-line @typescript-eslint/no-empty-function 13 | providers: [{ provide: AppService, useValue: { getVersion: () => {} } }], 14 | }).compile(); 15 | 16 | controller = app.get(AppController); 17 | }); 18 | 19 | it('should be defined.', () => { 20 | expect(controller).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | import { Public } from '@shared/infrastructure/decorators/public.decorator'; 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Public() 9 | @Get() 10 | public healthCheck(): any { 11 | return this.appService.getAPIData(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ReportsModule } from '@reports/reports.module'; 3 | import { UsersModule } from '@users/users.module'; 4 | import { AppController } from './app.controller'; 5 | import { AppService } from './app.service'; 6 | import { AuthModule } from '@auth/auth.module'; 7 | import { ConfigModule, ConfigService } from '@nestjs/config'; 8 | import { TypeOrmModule } from '@nestjs/typeorm'; 9 | import * as dotenv from 'dotenv'; 10 | import { APP_GUARD } from '@nestjs/core'; 11 | import { JwtAuthGuard } from '@auth/infrastructure/guards/jwt-auth.guard'; 12 | import * as dbConfig from '@src/config/db/database.config'; 13 | dotenv.config(); 14 | 15 | @Module({ 16 | imports: [ 17 | // Environment 18 | ConfigModule.forRoot({ 19 | envFilePath: `./src/config/environments/${process.env.NODE_ENV}.env`, 20 | isGlobal: true, 21 | cache: true, 22 | }), 23 | // Database 24 | TypeOrmModule.forRootAsync({ 25 | useFactory: async (configService: ConfigService) => dbConfig.default(configService), 26 | inject: [ConfigService], 27 | }), 28 | UsersModule, 29 | ReportsModule, 30 | AuthModule, 31 | ReportsModule, 32 | ], 33 | controllers: [AppController], 34 | providers: [ 35 | AppService, 36 | { 37 | provide: APP_GUARD, // Proteger la app con JWT, por defautl debes estar autenticado para acceder a cualquier ruta 38 | useClass: JwtAuthGuard, 39 | }, 40 | ], 41 | exports: [], 42 | }) 43 | export class AppModule {} 44 | -------------------------------------------------------------------------------- /src/app/app.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppService } from './app.service'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | describe('AppController', () => { 6 | let service: AppService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [AppService, { provide: ConfigService, useValue: {} }], 11 | }).compile(); 12 | 13 | service = module.get(AppService); 14 | }); 15 | 16 | it('should be defined.', () => { 17 | expect(service).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | 4 | @Injectable() 5 | export class AppService { 6 | constructor(private readonly config: ConfigService) {} 7 | 8 | public async getAPIData(): Promise { 9 | return { 10 | env: this.config.getOrThrow('NODE_ENV'), 11 | version: this.config.getOrThrow('API_VERSION'), 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/request.http: -------------------------------------------------------------------------------- 1 | 2 | GET http://{{host}}/api/{{version}} HTTP/1.1 3 | Content-Type: {{contentType}} 4 | -------------------------------------------------------------------------------- /src/auth/application/create-user/create-user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { CreateUserService } from './create-user.service'; 3 | 4 | describe('CreateUserService', () => { 5 | let service: CreateUserService; 6 | 7 | beforeEach(async () => { 8 | const moduleRef = await Test.createTestingModule({ 9 | imports: [], // Add 10 | controllers: [], // Add 11 | providers: [CreateUserService], // Add 12 | }).compile(); 13 | 14 | service = moduleRef.get(CreateUserService); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(service).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/auth/application/create-user/create-user.service.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@auth/domain/entity/user'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { EmailValueObject } from '@shared/domain/value-objects/email.value.object'; 4 | import { PasswordValueObject } from '@shared/domain/value-objects/password.value.object'; 5 | 6 | @Injectable() 7 | export class CreateUserService { 8 | create(email: string, password: string) { 9 | const user: User = new User(new EmailValueObject(email), new PasswordValueObject(password)); 10 | 11 | return user.toJSON(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/auth/application/encryption-facade/encryption.facade.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { EncryptionFacadeService } from './encryption.facade.service'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | describe('EncryptionFacadeService', () => { 6 | let service: EncryptionFacadeService; 7 | 8 | beforeEach(async () => { 9 | const moduleRef = await Test.createTestingModule({ 10 | imports: [], // Add 11 | controllers: [], // Add 12 | providers: [EncryptionFacadeService, { provide: ConfigService, useValue: jest.mock }], // Add 13 | }).compile(); 14 | 15 | service = moduleRef.get(EncryptionFacadeService); 16 | }); 17 | 18 | it('should be defined', () => { 19 | expect(service).toBeDefined(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/auth/application/encryption-facade/encryption.facade.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { compare, genSalt, hash } from 'bcrypt'; 4 | 5 | @Injectable() 6 | export class EncryptionFacadeService { 7 | constructor(private readonly configService: ConfigService) {} 8 | 9 | async hash(plain: string): Promise { 10 | return hash(plain, await genSalt(Number(this.configService.getOrThrow('SALT_ROUNDS')))); 11 | } 12 | 13 | async compare(plain: string, encrypted: string): Promise { 14 | return compare(plain, encrypted); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/auth/application/jwt-facade/jwt.facade.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { JwtFacadeService } from './jwt.facade.service'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { JwtService } from '@nestjs/jwt'; 5 | import { AuthRepository } from '@auth/infrastructure/adapters/secondary/db/user.repository'; 6 | import { UserDao } from '@auth/infrastructure/adapters/secondary/db/dao/user.dao'; 7 | 8 | describe('JwtFacadeService', () => { 9 | let service: JwtFacadeService; 10 | 11 | const mockRepository = { 12 | findOneBy: jest.fn().mockImplementation((dao: UserDao) => { 13 | return Promise.resolve({ 14 | id: Math.ceil(Math.random() * 10), 15 | ...dao, 16 | }); 17 | }), 18 | findByEmail: jest.fn().mockImplementation((dao: UserDao) => { 19 | return Promise.resolve({ 20 | id: Math.ceil(Math.random() * 10), 21 | ...dao, 22 | }); 23 | }), 24 | }; 25 | 26 | beforeEach(async () => { 27 | const moduleRef = await Test.createTestingModule({ 28 | imports: [], // Add 29 | controllers: [], // Add 30 | providers: [ 31 | JwtFacadeService, 32 | JwtService, 33 | { provide: ConfigService, useValue: jest.mock }, 34 | { provide: AuthRepository, useValue: mockRepository }, 35 | ], // Add 36 | }).compile(); 37 | 38 | service = moduleRef.get(JwtFacadeService); 39 | }); 40 | 41 | it('should be defined', () => { 42 | expect(service).toBeDefined(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/auth/application/jwt-facade/jwt.facade.service.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { JwtService } from '@nestjs/jwt'; 4 | import { AuthRepository } from '@auth/infrastructure/adapters/secondary/db/user.repository'; 5 | 6 | @Injectable() 7 | export class JwtFacadeService { 8 | constructor( 9 | private jwtService: JwtService, 10 | private readonly configService: ConfigService, 11 | private userRepository: AuthRepository, 12 | ) {} 13 | 14 | async verifyToken(token: string) { 15 | try { 16 | await this.jwtService.verifyAsync(token, { secret: this.configService.getOrThrow('JWT_KEY') }); 17 | return true; 18 | } catch (error) { 19 | throw new ForbiddenException('Access Denied'); 20 | } 21 | } 22 | 23 | async createJwtAndRefreshToken(user) { 24 | const token = await this.createJwt(user, '7d'); 25 | 26 | const refreshToken = await this.createJwt(user, '30d'); 27 | 28 | return { token, refreshToken }; 29 | } 30 | 31 | async createJwt(user: any, expiresIn: string): Promise { 32 | const payload = { id: user.id, email: user.email, name: user.name, isAdmin: user.isAdmin }; 33 | 34 | return await this.jwtService.signAsync(payload, { 35 | secret: this.configService.getOrThrow('JWT_KEY'), 36 | expiresIn, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/auth/application/refresh-token/refresh-token.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { RefreshTokenService } from './refresh-token.service'; 3 | import { JwtFacadeService } from '../jwt-facade/jwt.facade.service'; 4 | import { UserDao } from '@auth/infrastructure/adapters/secondary/db/dao/user.dao'; 5 | import { getRepositoryToken } from '@nestjs/typeorm'; 6 | import { AuthRepository } from '@auth/infrastructure/adapters/secondary/db/user.repository'; 7 | import { EncryptionFacadeService } from '../encryption-facade/encryption.facade.service'; 8 | import { ValidateUserService } from '../validate-user/validate-user.service'; 9 | 10 | describe('RefreshTokenService', () => { 11 | let service: RefreshTokenService; 12 | 13 | const mockRepository = { 14 | findOneBy: jest.fn().mockImplementation((dao: UserDao) => { 15 | return Promise.resolve({ 16 | id: Math.ceil(Math.random() * 10), 17 | ...dao, 18 | }); 19 | }), 20 | save: jest.fn().mockImplementation((dao: UserDao) => { 21 | return Promise.resolve({ 22 | id: Math.ceil(Math.random() * 10), 23 | ...dao, 24 | }); 25 | }), 26 | }; 27 | 28 | beforeEach(async () => { 29 | const moduleRef = await Test.createTestingModule({ 30 | imports: [], // Add 31 | controllers: [], // Add 32 | providers: [ 33 | RefreshTokenService, 34 | { provide: AuthRepository, useValue: mockRepository }, 35 | { 36 | provide: EncryptionFacadeService, 37 | useValue: { compare: async () => true }, 38 | }, 39 | { provide: ValidateUserService, useValue: { validate: async () => ({ id: 1 }) } }, 40 | { provide: getRepositoryToken(UserDao), useValue: mockRepository }, 41 | { 42 | provide: JwtFacadeService, 43 | useValue: { createJwtAndRefreshToken: async () => ({ token: 'token', refreshToken: 'refreshToken' }) }, 44 | }, 45 | ], // Add 46 | }).compile(); 47 | 48 | service = moduleRef.get(RefreshTokenService); 49 | }); 50 | 51 | it('should be defined', () => { 52 | expect(service).toBeDefined(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/auth/application/refresh-token/refresh-token.service.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, Injectable } from '@nestjs/common'; 2 | import { JwtFacadeService } from '../jwt-facade/jwt.facade.service'; 3 | import { AuthRepository } from '@auth/infrastructure/adapters/secondary/db/user.repository'; 4 | 5 | @Injectable() 6 | export class RefreshTokenService { 7 | constructor(private jwtFacadeService: JwtFacadeService, private userRepository: AuthRepository) {} 8 | 9 | async refreshTokens(email: string, refreshToken: string) { 10 | const user = await this.userRepository.findByEmail(email); 11 | 12 | if (!user && !user.refreshToken) throw new ForbiddenException('Access Denied'); 13 | 14 | await this.jwtFacadeService.verifyToken(refreshToken); 15 | 16 | const refreshTokenMatches = refreshToken === user.refreshToken; 17 | if (!refreshTokenMatches) throw new ForbiddenException('Access Denied'); 18 | 19 | const { token, refreshToken: newRefreshToken } = await this.jwtFacadeService.createJwtAndRefreshToken(user); 20 | 21 | Object.assign(user, { refreshToken: newRefreshToken }); 22 | 23 | await this.userRepository.save(user); 24 | 25 | return { token, refreshToken: newRefreshToken }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/auth/application/signin/signin.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { SignInService } from './signin.service'; 3 | import { JwtFacadeService } from '../jwt-facade/jwt.facade.service'; 4 | import { UserDao } from '@auth/infrastructure/adapters/secondary/db/dao/user.dao'; 5 | import { getRepositoryToken } from '@nestjs/typeorm'; 6 | import { AuthRepository } from '@auth/infrastructure/adapters/secondary/db/user.repository'; 7 | import { EncryptionFacadeService } from '../encryption-facade/encryption.facade.service'; 8 | import { ValidateUserService } from '../validate-user/validate-user.service'; 9 | 10 | describe('SignInService', () => { 11 | let service: SignInService; 12 | 13 | const mockRepository = { 14 | findOneBy: jest.fn().mockImplementation((dao: UserDao) => { 15 | return Promise.resolve({ 16 | id: Math.ceil(Math.random() * 10), 17 | ...dao, 18 | }); 19 | }), 20 | save: jest.fn().mockImplementation((dao: UserDao) => { 21 | return Promise.resolve({ 22 | id: Math.ceil(Math.random() * 10), 23 | ...dao, 24 | }); 25 | }), 26 | }; 27 | 28 | beforeEach(async () => { 29 | const moduleRef = await Test.createTestingModule({ 30 | imports: [], // Add 31 | controllers: [], // Add 32 | providers: [ 33 | SignInService, 34 | { provide: AuthRepository, useValue: mockRepository }, 35 | { 36 | provide: EncryptionFacadeService, 37 | useValue: { compare: async () => true }, 38 | }, 39 | { provide: ValidateUserService, useValue: { validate: async () => ({ id: 1 }) } }, 40 | { provide: getRepositoryToken(UserDao), useValue: mockRepository }, 41 | { 42 | provide: JwtFacadeService, 43 | useValue: { createJwtAndRefreshToken: async () => ({ token: 'token', refreshToken: 'refreshToken' }) }, 44 | }, 45 | ], // Add 46 | }).compile(); 47 | 48 | service = moduleRef.get(SignInService); 49 | }); 50 | 51 | it('should be defined', () => { 52 | expect(service).toBeDefined(); 53 | }); 54 | 55 | it('should return token', async () => { 56 | const result = await service.signin('test@gmail.com', 'test'); 57 | expect(result).toStrictEqual({ token: 'token', refreshToken: 'refreshToken' }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/auth/application/signin/signin.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ValidateUserService } from '../validate-user/validate-user.service'; 3 | import { JwtFacadeService } from '../jwt-facade/jwt.facade.service'; 4 | import { AuthRepository } from '@auth/infrastructure/adapters/secondary/db/user.repository'; 5 | import { UserDao } from '@auth/infrastructure/adapters/secondary/db/dao/user.dao'; 6 | 7 | @Injectable() 8 | export class SignInService { 9 | constructor( 10 | private validateUserService: ValidateUserService, 11 | private jwtFacadeService: JwtFacadeService, 12 | private userRepository: AuthRepository, 13 | ) {} 14 | 15 | async signin(email: string, password: string): Promise<{ token: string; refreshToken: string }> { 16 | const user: UserDao = await this.validateUserService.validate(email, password); 17 | 18 | const { token, refreshToken } = await this.jwtFacadeService.createJwtAndRefreshToken(user); 19 | 20 | Object.assign(user, { refreshToken }); // le asignamos lo que esta en attrs a lo que esta en user 21 | 22 | await this.userRepository.save(user as UserDao); 23 | 24 | return { token, refreshToken }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/auth/application/signup/signup.service.test.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { SignUpService } from './signup.service'; 3 | 4 | describe('SignUpService', () => { 5 | let service: SignUpService; 6 | 7 | beforeEach(async () => { 8 | const moduleRef = await Test.createTestingModule({ 9 | imports: [], // Add 10 | controllers: [], // Add 11 | providers: [], // Add 12 | }).compile(); 13 | 14 | service = moduleRef.get(SignUpService); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(service).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/auth/application/signup/signup.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable } from '@nestjs/common'; 2 | import { EncryptionFacadeService } from '../encryption-facade/encryption.facade.service'; 3 | import { CreateUserService } from '@auth/application/create-user/create-user.service'; 4 | import { UserDao } from '@auth/infrastructure/adapters/secondary/db/dao/user.dao'; 5 | import { AuthRepository } from '@auth/infrastructure/adapters/secondary/db/user.repository'; 6 | 7 | @Injectable() 8 | export class SignUpService { 9 | constructor( 10 | private encryptionFacadeService: EncryptionFacadeService, 11 | private createUserService: CreateUserService, 12 | private userRepository: AuthRepository, 13 | ) {} 14 | 15 | async signup(email: string, password: string): Promise { 16 | // Validar que no exista un usuario con el email repetido 17 | const ifUserExist: UserDao = await this.userRepository.findByEmail(email); 18 | 19 | if (ifUserExist) { 20 | throw new BadRequestException('Email in use!'); 21 | } 22 | 23 | const user = this.createUserService.create(email, password); 24 | 25 | const encryptedPassword = await this.encryptionFacadeService.hash(user.password); 26 | 27 | const userDao: UserDao = this.userRepository.create({ ...user, password: encryptedPassword } as UserDao); 28 | 29 | return this.userRepository.save(userDao); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/auth/application/validate-user/validate-user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { EncryptionFacadeService } from '../encryption-facade/encryption.facade.service'; 3 | import { UserDao } from '@auth/infrastructure/adapters/secondary/db/dao/user.dao'; 4 | import { AuthRepository } from '@auth/infrastructure/adapters/secondary/db/user.repository'; 5 | 6 | @Injectable() 7 | export class ValidateUserService { 8 | constructor(private encryptionFacadeService: EncryptionFacadeService, private userRepository: AuthRepository) {} 9 | 10 | async validate(email: string, password: string): Promise { 11 | const user: UserDao = await this.userRepository.findByEmail(email); 12 | 13 | if (!user) { 14 | throw new UnauthorizedException('Invalid email or password'); 15 | } 16 | 17 | if (!(await this.encryptionFacadeService.compare(password, user.password))) { 18 | throw new UnauthorizedException('Invalid email or password'); 19 | } 20 | 21 | return user; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/auth/application/validate-user/validate.user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { ValidateUserService } from './validate-user.service'; 3 | import { UserDao } from '@auth/infrastructure/adapters/secondary/db/dao/user.dao'; 4 | import { EncryptionFacadeService } from '../encryption-facade/encryption.facade.service'; 5 | import { getRepositoryToken } from '@nestjs/typeorm'; 6 | import { AuthRepository } from '@auth/infrastructure/adapters/secondary/db/user.repository'; 7 | 8 | describe('ValidateUserService', () => { 9 | let service: ValidateUserService; 10 | const userMock = { id: 1, email: 'test@gmail.com', password: 'test' } as UserDao; 11 | 12 | const mockRepository = { 13 | findOneBy: jest.fn().mockImplementation(() => { 14 | return Promise.resolve(userMock); 15 | }), 16 | }; 17 | 18 | beforeEach(async () => { 19 | const moduleRef = await Test.createTestingModule({ 20 | imports: [], // Add 21 | controllers: [], // Add 22 | providers: [ 23 | ValidateUserService, 24 | AuthRepository, 25 | { provide: getRepositoryToken(UserDao), useValue: mockRepository }, 26 | { 27 | provide: EncryptionFacadeService, 28 | useValue: { compare: async () => true }, 29 | }, 30 | ], // Add 31 | }).compile(); 32 | 33 | service = moduleRef.get(ValidateUserService); 34 | }); 35 | 36 | it('should be defined', () => { 37 | expect(service).toBeDefined(); 38 | }); 39 | 40 | it('should validate user by email and password', async () => { 41 | const result = await service.validate('test@gmail.com', 'test'); 42 | expect(result.id).toBe(userMock.id); 43 | }); 44 | 45 | it('should throw error if not find user', async () => { 46 | try { 47 | await service.validate('test@gmail.com', 'test'); 48 | } catch (error) { 49 | expect(error.message).toBe('Invalid email or password'); 50 | } 51 | }); 52 | 53 | it('should throw error if not return true compare passwords', async () => { 54 | jest.spyOn(service['encryptionFacadeService'], 'compare').mockImplementation(async () => false); 55 | try { 56 | await service.validate('test@gmail.com', 'test'); 57 | } catch (error) { 58 | expect(error.message).toBe('Invalid email or password'); 59 | } 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SingUpController } from './infrastructure/adapters/primary/http/signup/signup.controller'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { SignInController } from './infrastructure/adapters/primary/http/singin/signin.controller'; 5 | import { WhoAmIController } from './infrastructure/adapters/primary/http/whoami/whoami.controller'; 6 | import { SignOutController } from './infrastructure/adapters/primary/http/signout/signout.controller'; 7 | import { JwtModule, JwtService } from '@nestjs/jwt'; 8 | import { JwtStrategy } from './infrastructure/auth-strategies/jwt-strategy'; 9 | import { LocalStrategy } from './infrastructure/auth-strategies/local-strategy'; 10 | import { RefreshJwtStrategy } from './infrastructure/auth-strategies/refresh-token.strategy'; 11 | import { PassportModule } from '@nestjs/passport'; 12 | import { ConfigService } from '@nestjs/config'; 13 | import { UserDao } from './infrastructure/adapters/secondary/db/dao/user.dao'; 14 | import { CreateUserService } from './application/create-user/create-user.service'; 15 | import { AuthRepository } from './infrastructure/adapters/secondary/db/user.repository'; 16 | import { SignUpService } from './application/signup/signup.service'; 17 | import { SignInService } from './application/signin/signin.service'; 18 | import { ValidateUserService } from './application/validate-user/validate-user.service'; 19 | import { EncryptionFacadeService } from './application/encryption-facade/encryption.facade.service'; 20 | import { JwtFacadeService } from './application/jwt-facade/jwt.facade.service'; 21 | import { RefreshTokenController } from './infrastructure/adapters/primary/http/refresh-token/refresh-token.controller'; 22 | import { RefreshTokenService } from './application/refresh-token/refresh-token.service'; 23 | 24 | @Module({ 25 | imports: [ 26 | TypeOrmModule.forFeature([UserDao]), 27 | // Config JWT Auth 28 | PassportModule, 29 | JwtModule.registerAsync({ 30 | useFactory: async (configService: ConfigService) => { 31 | return { 32 | // secret: configService.jwtKey, 33 | secretOrPrivateKey: configService.getOrThrow('JWT_KEY'), 34 | signOptions: { 35 | // expiresIn: '60s', lo estoy definiendo en el servicio 36 | // expiresIn: '7d', 37 | }, 38 | }; 39 | }, 40 | inject: [ConfigService], 41 | }), 42 | ], 43 | providers: [ 44 | SignUpService, 45 | SignInService, 46 | JwtService, 47 | JwtStrategy, 48 | ValidateUserService, 49 | LocalStrategy, 50 | RefreshJwtStrategy, 51 | EncryptionFacadeService, 52 | JwtFacadeService, 53 | CreateUserService, 54 | AuthRepository, 55 | RefreshTokenService, 56 | // { 57 | // provide: APP_INTERCEPTOR, // Interceptor para recuperar la información del usuario fresca de la base de datos 58 | // useClass: CurrentUserInterceptor, 59 | // }, 60 | ], 61 | controllers: [SingUpController, SignInController, WhoAmIController, SignOutController, RefreshTokenController], 62 | }) 63 | export class AuthModule {} 64 | -------------------------------------------------------------------------------- /src/auth/domain/entity/user.ts: -------------------------------------------------------------------------------- 1 | import { EmailValueObject } from '@shared/domain/value-objects/email.value.object'; 2 | import { PasswordValueObject } from '@shared/domain/value-objects/password.value.object'; 3 | 4 | export class User { 5 | name: string; 6 | 7 | constructor( 8 | private email: EmailValueObject, 9 | private password: PasswordValueObject, 10 | private isAdmin: boolean = false, 11 | ) {} 12 | 13 | setName(name: string) { 14 | this.name = name; 15 | } 16 | 17 | toJSON() { 18 | return { 19 | email: this.email.getValue, 20 | password: this.password.getValue, 21 | name: this.name, 22 | isAdmin: this.isAdmin, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/auth/domain/ports/db/user.repository.ts: -------------------------------------------------------------------------------- 1 | export interface IAuthRepositoryInterface { 2 | create(data: D): D; 3 | save(entity: any, options?: any): Promise; 4 | findByEmail(email: string): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/auth/domain/ports/primary/http/refresh-token.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IRefreshTokenController { 2 | refreshToken(user, request: { refreshToken: string }): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/auth/domain/ports/primary/http/signin.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUsersSignInController { 2 | signin(body: P): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/auth/domain/ports/primary/http/signout.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ISignOutController { 2 | whoami(request): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/auth/domain/ports/primary/http/signup.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUsersSignUpController { 2 | signup(body: P): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/auth/domain/ports/primary/http/whoami.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IWhoAmIController { 2 | whoami(request): R; 3 | } 4 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/refresh-token/dto/refresh-token.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract'; 2 | import { Expose } from 'class-transformer'; 3 | 4 | export class RefreshTokenResponseDto extends ResponseBaseDto { 5 | @Expose() 6 | token: string; 7 | 8 | @Expose() 9 | refreshToken: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/refresh-token/refresh-token.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RefreshTokenController } from './refresh-token.controller'; 3 | import { RefreshTokenService } from '@auth/application/refresh-token/refresh-token.service'; 4 | 5 | describe('RefreshTokenController', () => { 6 | let controller: RefreshTokenController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [RefreshTokenController], 11 | providers: [ 12 | { 13 | provide: RefreshTokenService, 14 | useValue: { refreshTokens: async () => ({ token: '', refreshToken: '' }) }, 15 | }, 16 | ], 17 | }).compile(); 18 | 19 | controller = module.get(RefreshTokenController); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(controller).toBeDefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/refresh-token/refresh-token.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; 2 | import { IRefreshTokenController } from '@auth/domain/ports/primary/http/refresh-token.controller.interface'; 3 | import { ValidateRefTokenAndNewTokens } from '@auth/infrastructure/decorators/new-refresh-token.decorator'; 4 | import { CurrentUser } from '@shared/infrastructure/decorators/current-user.decorator'; 5 | import { RefreshTokenResponseDto } from './dto/refresh-token.response.dto'; 6 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator'; 7 | import { RefreshTokenService } from '@auth/application/refresh-token/refresh-token.service'; 8 | 9 | @Controller('auth') 10 | export class RefreshTokenController implements IRefreshTokenController { 11 | constructor(private refreshTokenService: RefreshTokenService) {} 12 | 13 | @Post('/refresh-token') 14 | @ValidateRefTokenAndNewTokens() 15 | @HttpCode(HttpStatus.OK) 16 | @SerializeResponseDto(RefreshTokenResponseDto) 17 | async refreshToken(@CurrentUser() user): Promise { 18 | //el guard coge el token de la cabecera y al user del token y lo mete en el request 19 | return this.refreshTokenService.refreshTokens(user.email, user.refreshToken); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/refresh-token/request.http: -------------------------------------------------------------------------------- 1 | 2 | POST http://{{host}}/api/{{version}}/auth/refresh-token HTTP/1.1 3 | Content-Type: {{contentType}} 4 | Authorization: Bearer {{refreshToken}} 5 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/signout/request.http: -------------------------------------------------------------------------------- 1 | 2 | POST http://{{host}}/api/{{version}}/auth/signout HTTP/1.1 3 | Content-Type: {{contentType}} 4 | Authorization: Bearer {{token}} -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/signout/signout.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SignOutController } from './signout.controller'; 3 | 4 | describe('SignOutController', () => { 5 | let controller: SignOutController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [SignOutController], 10 | providers: [], 11 | }).compile(); 12 | 13 | controller = module.get(SignOutController); 14 | }); 15 | 16 | it('should be defined', () => { 17 | expect(controller).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/signout/signout.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpCode, HttpStatus, Post, Request } from '@nestjs/common'; 2 | import { ISignOutController } from '@auth/domain/ports/primary/http/signout.controller.interface'; 3 | 4 | @Controller('auth') 5 | export class SignOutController implements ISignOutController { 6 | @Post('/signout') 7 | @HttpCode(HttpStatus.OK) 8 | async whoami(@Request() request): Promise { 9 | request.user = null; // TODO: Implementar lógica de signout, borrar el refresh token de la base de datos 10 | return { ok: true }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/signup/dto/signup.request.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class SignUpRequestDto { 4 | @IsNotEmpty() 5 | @IsEmail() 6 | email: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | password: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/signup/dto/user.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract'; 2 | import { Expose } from 'class-transformer'; 3 | 4 | export class UserResponseDto extends ResponseBaseDto { 5 | @Expose() 6 | id: number; 7 | 8 | @Expose() 9 | email: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/signup/request.http: -------------------------------------------------------------------------------- 1 | 2 | POST http://{{host}}/api/{{version}}/auth/signup HTTP/1.1 3 | Content-Type: {{contentType}} 4 | 5 | { 6 | "email": "yasniel@gmail.com", 7 | "password": "yasniel" 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/signup/signup.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; 2 | import { SignUpRequestDto } from './dto/signup.request.dto'; 3 | import { IUsersSignUpController } from '@auth/domain/ports/primary/http/signup.controller.interface'; 4 | import { UserResponseDto } from './dto/user.response.dto'; 5 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator'; 6 | import { SignUpService } from '@auth/application/signup/signup.service'; 7 | import { Public } from '@shared/infrastructure/decorators/public.decorator'; 8 | 9 | @Controller('auth') 10 | export class SingUpController implements IUsersSignUpController { 11 | constructor(private singUpService: SignUpService) {} 12 | 13 | @Public() 14 | @Post('/signup') 15 | @HttpCode(HttpStatus.CREATED) 16 | @SerializeResponseDto(UserResponseDto) 17 | async signup(@Body() body: SignUpRequestDto): Promise { 18 | const user: UserResponseDto = await this.singUpService.signup(body.email, body.password); 19 | return user; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/signup/singup.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SingUpController } from './signup.controller'; 3 | import { SignUpService } from '@auth/application/signup/signup.service'; 4 | 5 | describe('UsersController', () => { 6 | let controller: SingUpController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [SingUpController], 11 | providers: [{ provide: SignUpService, useValue: jest.mock }], 12 | }).compile(); 13 | 14 | controller = module.get(SingUpController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/singin/dto/signin.request.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class SignInRequestDto { 4 | @IsNotEmpty() 5 | @IsEmail() 6 | email: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | password: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/singin/dto/signin.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract'; 2 | import { Expose } from 'class-transformer'; 3 | 4 | export class SignInResponseDto extends ResponseBaseDto { 5 | @Expose() 6 | token: string; 7 | 8 | @Expose() 9 | refreshToken: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/singin/request.http: -------------------------------------------------------------------------------- 1 | 2 | POST http://{{host}}/api/{{version}}/auth/signin HTTP/1.1 3 | Content-Type: {{contentType}} 4 | 5 | { 6 | "email": "yasniel@gmail.com", 7 | "password": "yasniel" 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/singin/signin.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; 2 | import { SignInResponseDto } from './dto/signin.response.dto'; 3 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator'; 4 | import { IUsersSignInController } from '@auth/domain/ports/primary/http/signin.controller.interface'; 5 | 6 | import { SignInRequestDto } from './dto/signin.request.dto'; 7 | import { SignInService } from '@auth/application/signin/signin.service'; 8 | import { Public } from '@shared/infrastructure/decorators/public.decorator'; 9 | 10 | @Controller('auth') 11 | export class SignInController implements IUsersSignInController { 12 | constructor(private signInService: SignInService) {} 13 | 14 | @Public() 15 | @Post('/signin') 16 | @HttpCode(HttpStatus.OK) 17 | @SerializeResponseDto(SignInResponseDto) 18 | async signin(@Body() body: SignInRequestDto): Promise { 19 | const { token, refreshToken } = await this.signInService.signin(body.email, body.password); 20 | return { token, refreshToken }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/singin/singin.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SignInController } from './signin.controller'; 3 | import { SignInService } from '@auth/application/signin/signin.service'; 4 | 5 | describe('SignInController', () => { 6 | let controller: SignInController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [SignInController], 11 | providers: [ 12 | { 13 | provide: SignInService, 14 | useValue: { signin: async () => ({ token: 'token', refreshToken: 'refreshToken' }) }, 15 | }, 16 | ], 17 | }).compile(); 18 | 19 | controller = module.get(SignInController); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(controller).toBeDefined(); 24 | }); 25 | 26 | it('should singin user', async () => { 27 | const response = await controller.signin({ email: 'test@gmail.com', password: 'test' }); 28 | expect(response).toEqual({ token: 'token', refreshToken: 'refreshToken' }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/whoami/dto/user.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract'; 2 | import { Expose } from 'class-transformer'; 3 | 4 | export class UserResponseDto extends ResponseBaseDto { 5 | @Expose() 6 | id: number; 7 | 8 | @Expose() 9 | email: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/whoami/request.http: -------------------------------------------------------------------------------- 1 | 2 | POST http://{{host}}/api/{{version}}/auth/whoami HTTP/1.1 3 | Content-Type: {{contentType}} 4 | Authorization: Bearer {{token}} 5 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/whoami/whoami.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { WhoAmIController } from './whoami.controller'; 3 | 4 | describe('WhoAmIController', () => { 5 | let controller: WhoAmIController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [WhoAmIController], 10 | providers: [], 11 | }).compile(); 12 | 13 | controller = module.get(WhoAmIController); 14 | }); 15 | 16 | it('should be defined', () => { 17 | expect(controller).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/primary/http/whoami/whoami.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; 2 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator'; 3 | import { IWhoAmIController } from '@auth/domain/ports/primary/http/whoami.controller.interface'; 4 | import { UserResponseDto } from './dto/user.response.dto'; 5 | import { CurrentUser } from '@shared/infrastructure/decorators/current-user.decorator'; 6 | @Controller('auth') 7 | export class WhoAmIController implements IWhoAmIController { 8 | @Post('/whoami') 9 | @HttpCode(HttpStatus.OK) 10 | @SerializeResponseDto(UserResponseDto) 11 | whoami(@CurrentUser() user): UserResponseDto { 12 | return user; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/secondary/db/dao/user.dao.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity('User') 4 | export class UserDao { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column({ unique: true }) 9 | email: string; 10 | 11 | @Column() 12 | password: string; 13 | 14 | @Column({ 15 | unique: false, 16 | nullable: true, 17 | }) 18 | name?: string; 19 | 20 | @Column({ default: true }) 21 | isAdmin: boolean; 22 | 23 | @Column({ 24 | nullable: true, 25 | length: 500, 26 | }) 27 | refreshToken: string; 28 | 29 | @CreateDateColumn() 30 | createdAt: Date; 31 | } 32 | -------------------------------------------------------------------------------- /src/auth/infrastructure/adapters/secondary/db/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { Repository, SaveOptions } from 'typeorm'; 2 | import { UserDao } from './dao/user.dao'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { IAuthRepositoryInterface } from '@auth/domain/ports/db/user.repository'; 6 | 7 | @Injectable() 8 | export class AuthRepository implements IAuthRepositoryInterface { 9 | constructor( 10 | @InjectRepository(UserDao) 11 | private repository: Repository, 12 | ) {} 13 | 14 | create(data: UserDao): UserDao { 15 | return this.repository.create(data); 16 | } 17 | 18 | findByEmail(email: string): Promise { 19 | return this.repository.findOneBy({ email }); 20 | } 21 | 22 | save(entity: UserDao, options?: SaveOptions): Promise { 23 | return this.repository.save(entity, options); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/auth/infrastructure/auth-strategies/jwt-strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | 6 | @Injectable() 7 | export class JwtStrategy extends PassportStrategy(Strategy) { 8 | constructor(protected configService: ConfigService) { 9 | super({ 10 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 11 | ignoreExpiration: false, 12 | secretOrKey: configService.getOrThrow('JWT_KEY'), 13 | }); 14 | } 15 | 16 | async validate(payload: any) { 17 | return { id: payload.id, email: payload.email, isAdmin: payload.isAdmin }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/auth/infrastructure/auth-strategies/local-strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ValidateUserService } from '@auth/application/validate-user/validate-user.service'; 4 | import { Strategy } from 'passport-local'; 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | constructor(private validateUserService: ValidateUserService) { 9 | super(); 10 | } 11 | 12 | async validate(email: string, password: string) { 13 | // const user = await this.validateUserService.validate(email, password); 14 | // if (!user) { 15 | // throw new UnauthorizedException(); 16 | // } 17 | // return user; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/auth/infrastructure/auth-strategies/refresh-token.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { ExtractJwt, Strategy } from 'passport-jwt'; 5 | import { Request } from 'express'; 6 | @Injectable() 7 | export class RefreshJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { 8 | constructor(protected configService: ConfigService) { 9 | super({ 10 | // jwtFromRequest: ExtractJwt.fromBodyField('refresh'), 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: false, 13 | passReqToCallback: true, 14 | secretOrKey: configService.getOrThrow('JWT_KEY'), 15 | }); 16 | } 17 | 18 | async validate(req: Request, payload: any) { 19 | const refreshToken = req.get('Authorization').replace('Bearer', '').trim(); 20 | return { ...payload, refreshToken }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/auth/infrastructure/decorators/new-refresh-token.decorator.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { RefreshJwtGuard } from '../guards/refresh-jwt-auth.guard'; 3 | 4 | export function ValidateRefTokenAndNewTokens(): MethodDecorator & ClassDecorator { 5 | return UseGuards(new RefreshJwtGuard()); 6 | } 7 | -------------------------------------------------------------------------------- /src/auth/infrastructure/guards/is-admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext } from '@nestjs/common'; 2 | 3 | export class IsAdminGuard implements CanActivate { 4 | canActivate(context: ExecutionContext) { 5 | const request = context.switchToHttp().getRequest(); 6 | 7 | if (!request.user) { 8 | return false; 9 | } 10 | 11 | if (request.user.isAdmin) { 12 | return true; 13 | } 14 | 15 | return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/auth/infrastructure/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | import { IS_PUBLIC_KEY } from '@shared/infrastructure/decorators/public.decorator'; 5 | 6 | @Injectable() 7 | export class JwtAuthGuard extends AuthGuard('jwt') { 8 | constructor(private reflector: Reflector) { 9 | super({ 10 | passReqToCallback: true, 11 | }); 12 | } 13 | 14 | canActivate(context: ExecutionContext) { 15 | const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ 16 | context.getHandler(), 17 | context.getClass(), 18 | ]); 19 | if (isPublic) { 20 | return true; 21 | } 22 | return super.canActivate(context); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/auth/infrastructure/guards/local-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalAuthGuard extends AuthGuard('local') {} 6 | -------------------------------------------------------------------------------- /src/auth/infrastructure/guards/refresh-jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class RefreshJwtGuard extends AuthGuard('jwt-refresh') {} 6 | -------------------------------------------------------------------------------- /src/auth/infrastructure/interceptors/current-user.interceptor.ts: -------------------------------------------------------------------------------- 1 | // import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; 2 | // import { FindByEmailService } from '@auth/application/services/find-by-email/find-by-email.service'; 3 | // import { Observable } from 'rxjs'; 4 | 5 | // export class CurrentUserInterceptor implements NestInterceptor { 6 | // constructor(private readonly findByEmailService: FindByEmailService) {} 7 | 8 | // async intercept(context: ExecutionContext, next: CallHandler): Promise> { 9 | // const request = context.switchToHttp().getRequest(); 10 | // if (request.user) { 11 | // const user = await this.findByEmailService.find(request.user.email); 12 | // request.user = user; 13 | // } 14 | 15 | // return next.handle(); 16 | // } 17 | // } 18 | -------------------------------------------------------------------------------- /src/config/constants.ts: -------------------------------------------------------------------------------- 1 | export const isProd = process.env.NODE_ENV === 'prod'; 2 | export const isDev = process.env.NODE_ENV === 'dev'; 3 | export const isQa = process.env.NODE_ENV === 'qa'; 4 | export const isTest = process.env.NODE_ENV === 'test'; 5 | export const isStaging = process.env.NODE_ENV === 'staging'; 6 | export const isLocal = process.env.NODE_ENV === 'local'; 7 | -------------------------------------------------------------------------------- /src/config/db/data-source.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, DataSourceOptions } from 'typeorm'; 2 | import * as dotenv from 'dotenv'; 3 | 4 | dotenv.config(); 5 | 6 | export const dataSourceOptions: DataSourceOptions = { 7 | type: 'mysql', 8 | host: 'localhost', 9 | port: 3306, 10 | username: 'user', 11 | password: 'password', 12 | database: 'db', 13 | entities: ['dist/**/**.dao{.ts,.js}'], 14 | migrations: ['dist/**/**.migration{.ts,.js}'], 15 | }; 16 | 17 | const dataSource = new DataSource(dataSourceOptions); 18 | export default dataSource; 19 | -------------------------------------------------------------------------------- /src/config/db/database.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { isLocal, isTest } from '../constants'; 3 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import * as dotenv from 'dotenv'; 6 | 7 | dotenv.config(); 8 | 9 | const typeOrmConfig = (configService: ConfigService) => 10 | ({ 11 | type: configService.getOrThrow('DB_TYPE'), 12 | host: configService.getOrThrow('DB_HOST'), 13 | port: configService.getOrThrow('DB_PORT'), 14 | username: configService.getOrThrow('DB_USERNAME'), 15 | password: configService.getOrThrow('DB_PASSWORD'), 16 | database: configService.getOrThrow('DB_DATABASE'), 17 | entities: [join(__dirname, 'src/**/**.dao{.ts,.js}')], 18 | migrations: [join(__dirname, 'src/**/**.migration{.ts,.js}')], 19 | synchronize: isLocal || isTest ? true : false, // esto solo es para desarrollo, para produccion usamos migraciones 20 | logging: false, // esto es para debugear las consultas a la base de datos 21 | } as TypeOrmModuleOptions); 22 | 23 | export default typeOrmConfig; 24 | -------------------------------------------------------------------------------- /src/config/db/migration-new-way.ts: -------------------------------------------------------------------------------- 1 | const { MigrationInterface, QueryRunner, Table } = require('typeorm'); 2 | 3 | module.exports = class initialSchema1625847615203 { 4 | name = 'initialSchema1625847615203'; 5 | 6 | async up(queryRunner) { 7 | await queryRunner.createTable( 8 | new Table({ 9 | name: 'user', 10 | columns: [ 11 | { 12 | name: 'id', 13 | type: 'integer', 14 | isPrimary: true, 15 | isGenerated: true, 16 | generationStrategy: 'increment', 17 | }, 18 | { 19 | name: 'email', 20 | type: 'varchar', 21 | }, 22 | { 23 | name: 'password', 24 | type: 'varchar', 25 | }, 26 | { 27 | name: 'admin', 28 | type: 'boolean', 29 | default: 'true', 30 | }, 31 | ], 32 | }), 33 | ); 34 | 35 | await queryRunner.createTable( 36 | new Table({ 37 | name: 'report', 38 | columns: [ 39 | { 40 | name: 'id', 41 | type: 'integer', 42 | isPrimary: true, 43 | isGenerated: true, 44 | generationStrategy: 'increment', 45 | }, 46 | { name: 'approved', type: 'boolean', default: 'false' }, 47 | { name: 'price', type: 'float' }, 48 | { name: 'make', type: 'varchar' }, 49 | { name: 'model', type: 'varchar' }, 50 | { name: 'year', type: 'integer' }, 51 | { name: 'lng', type: 'float' }, 52 | { name: 'lat', type: 'float' }, 53 | { name: 'mileage', type: 'integer' }, 54 | { name: 'userId', type: 'integer' }, 55 | ], 56 | }), 57 | ); 58 | } 59 | 60 | async down(queryRunner) { 61 | await queryRunner.query(`DROP TABLE ""report""`); 62 | await queryRunner.query(`DROP TABLE ""user""`); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/config/db/migrations/1692199497672-NewMigration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class NewMigration1692199497672 implements MigrationInterface { 4 | name = 'NewMigration1692199497672'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`DROP INDEX \`IDX_4a257d2c9837248d70640b3e36\` ON \`User\``); 8 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`refreshToken\``); 9 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`updatedAt\``); 10 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`approved\``); 11 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`lat\``); 12 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`lng\``); 13 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`mileage\``); 14 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`model\``); 15 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`name\``); 16 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`model\` varchar(255) NULL`); 17 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`lng\` int NULL`); 18 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`lat\` int NULL`); 19 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`mileage\` int NULL`); 20 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`approved\` tinyint NOT NULL DEFAULT 0`); 21 | await queryRunner.query(`ALTER TABLE \`User\` ADD \`name\` varchar(255) NULL`); 22 | await queryRunner.query(`ALTER TABLE \`User\` ADD \`refreshToken\` varchar(500) NULL`); 23 | await queryRunner.query( 24 | `ALTER TABLE \`User\` ADD \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)`, 25 | ); 26 | await queryRunner.query(`ALTER TABLE \`Report\` DROP FOREIGN KEY \`FK_abc8ce53e6ef1567f06400344fd\``); 27 | await queryRunner.query(`ALTER TABLE \`Report\` CHANGE \`userId\` \`userId\` int NOT NULL`); 28 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`isAdmin\` \`isAdmin\` tinyint NOT NULL`); 29 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`isAdmin\` \`isAdmin\` tinyint NOT NULL`); 30 | await queryRunner.query( 31 | `ALTER TABLE \`User\` CHANGE \`createdAt\` \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)`, 32 | ); 33 | await queryRunner.query(`ALTER TABLE \`User\` ADD UNIQUE INDEX \`IDX_4a257d2c9837248d70640b3e36\` (\`email\`)`); 34 | await queryRunner.query( 35 | `ALTER TABLE \`User\` CHANGE \`createdAt\` \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)`, 36 | ); 37 | await queryRunner.query( 38 | `ALTER TABLE \`Report\` ADD CONSTRAINT \`FK_abc8ce53e6ef1567f06400344fd\` FOREIGN KEY (\`userId\`) REFERENCES \`User\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, 39 | ); 40 | } 41 | 42 | public async down(queryRunner: QueryRunner): Promise { 43 | await queryRunner.query(`ALTER TABLE \`Report\` DROP FOREIGN KEY \`FK_abc8ce53e6ef1567f06400344fd\``); 44 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`createdAt\` \`createdAt\` datetime NOT NULL`); 45 | await queryRunner.query(`ALTER TABLE \`User\` DROP INDEX \`IDX_4a257d2c9837248d70640b3e36\``); 46 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`createdAt\` \`createdAt\` datetime NOT NULL`); 47 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`isAdmin\` \`isAdmin\` tinyint NOT NULL DEFAULT '1'`); 48 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`isAdmin\` \`isAdmin\` tinyint NOT NULL DEFAULT '1'`); 49 | await queryRunner.query(`ALTER TABLE \`Report\` CHANGE \`userId\` \`userId\` int NULL`); 50 | await queryRunner.query( 51 | `ALTER TABLE \`Report\` ADD CONSTRAINT \`FK_abc8ce53e6ef1567f06400344fd\` FOREIGN KEY (\`userId\`) REFERENCES \`User\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, 52 | ); 53 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`updatedAt\``); 54 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`refreshToken\``); 55 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`name\``); 56 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`approved\``); 57 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`mileage\``); 58 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`lat\``); 59 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`lng\``); 60 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`model\``); 61 | await queryRunner.query(`ALTER TABLE \`User\` ADD \`name\` varchar(255) NULL`); 62 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`model\` varchar(255) NULL`); 63 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`mileage\` int NULL`); 64 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`lng\` int NULL`); 65 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`lat\` int NULL`); 66 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`approved\` tinyint NOT NULL DEFAULT '0'`); 67 | await queryRunner.query( 68 | `ALTER TABLE \`User\` ADD \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)`, 69 | ); 70 | await queryRunner.query(`ALTER TABLE \`User\` ADD \`refreshToken\` varchar(500) NULL`); 71 | await queryRunner.query(`CREATE UNIQUE INDEX \`IDX_4a257d2c9837248d70640b3e36\` ON \`User\` (\`email\`)`); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/config/environments/dev.env: -------------------------------------------------------------------------------- 1 | ENV=dev 2 | PORT=3000 3 | PROJECT_ID=example 4 | CORS_ALLOWED_ORIGIN=* 5 | JWT_KEY=jwtexamplekey 6 | SALT_ROUNDS=10 7 | 8 | DB_TYPE=mysql 9 | DB_HOST=db #service name porque estamos usando docker-compose 10 | DB_PORT=3306 11 | DB_USERNAME=user 12 | DB_PASSWORD=password 13 | DB_DATABASE=db -------------------------------------------------------------------------------- /src/config/environments/local.env: -------------------------------------------------------------------------------- 1 | ENV=local 2 | PORT=3000 3 | PROJECT_ID=example 4 | CORS_ALLOWED_ORIGIN=* 5 | JWT_KEY=jwtexamplekey 6 | SALT_ROUNDS=10 7 | 8 | DB_TYPE=sqlite 9 | DB_HOST= 10 | DB_PORT= 11 | DB_USERNAME= 12 | DB_PASSWORD= 13 | DB_DATABASE=db.sqlite -------------------------------------------------------------------------------- /src/config/environments/prod.env: -------------------------------------------------------------------------------- 1 | ENV=prod 2 | PORT=3000 3 | PROJECT_ID=example 4 | CORS_ALLOWED_ORIGIN=https://example.com 5 | JWT_KEY=jwtexamplekey 6 | SALT_ROUNDS=10 7 | 8 | DB_TYPE=mysql 9 | DB_HOST= 10 | DB_PORT= 11 | DB_USERNAME= 12 | DB_PASSWORD= 13 | DB_DATABASE= -------------------------------------------------------------------------------- /src/config/environments/qa.env: -------------------------------------------------------------------------------- 1 | ENV=test 2 | PORT=3000 3 | PROJECT_ID=example 4 | CORS_ALLOWED_ORIGIN=* 5 | JWT_KEY=jwtexamplekey 6 | SALT_ROUNDS=10 7 | 8 | DB_TYPE=mysql 9 | DB_HOST= 10 | DB_PORT= 11 | DB_USERNAME= 12 | DB_PASSWORD= 13 | DB_DATABASE= -------------------------------------------------------------------------------- /src/config/environments/staging.env: -------------------------------------------------------------------------------- 1 | ENV=staging 2 | PORT=3000 3 | PROJECT_ID=example 4 | CORS_ALLOWED_ORIGIN=* 5 | JWT_KEY=jwtexamplekey 6 | SALT_ROUNDS=10 7 | 8 | DB_TYPE=mysql 9 | DB_HOST= 10 | DB_PORT= 11 | DB_USERNAME= 12 | DB_PASSWORD= 13 | DB_DATABASE= -------------------------------------------------------------------------------- /src/config/environments/test.env: -------------------------------------------------------------------------------- 1 | ENV=test 2 | PORT=3000 3 | PROJECT_ID=example 4 | CORS_ALLOWED_ORIGIN=* 5 | JWT_KEY=jwtexamplekey 6 | SALT_ROUNDS=10 7 | 8 | DB_TYPE=sqlite 9 | DB_HOST= 10 | DB_PORT= 11 | DB_USERNAME= 12 | DB_PASSWORD= 13 | DB_DATABASE=:memory: -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { setupApp } from './setup-app'; 3 | import { NestFactory } from '@nestjs/core'; 4 | import { NestExpressApplication } from '@nestjs/platform-express'; 5 | import { AppModule } from '@app/app.module'; 6 | 7 | async function bootstrap() { 8 | const app: NestExpressApplication = await NestFactory.create(AppModule); 9 | const configService = app.get(ConfigService); 10 | setupApp(app); 11 | await app.listen(configService.getOrThrow('PORT')); 12 | } 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /src/reports/application/approved-report/approved-report.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { ApprovedReportService } from './approved-report.service'; 3 | import { getRepositoryToken } from '@nestjs/typeorm'; 4 | import { ReportDao } from '@reports/infrastructure/adapters/secondary/db/dao/report.dao'; 5 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository'; 6 | 7 | describe('ApprovedReportService', () => { 8 | let service: ApprovedReportService; 9 | const mockRepository = { 10 | findOneBy: jest.fn().mockImplementation((dao: ReportDao) => { 11 | return Promise.resolve({ 12 | id: Math.ceil(Math.random() * 10), 13 | ...dao, 14 | }); 15 | }), 16 | }; 17 | 18 | beforeEach(async () => { 19 | const moduleRef = await Test.createTestingModule({ 20 | imports: [], // Add 21 | controllers: [], // Add 22 | providers: [ 23 | ApprovedReportService, 24 | ReportRepository, 25 | { 26 | provide: getRepositoryToken(ReportDao), 27 | useValue: mockRepository, 28 | }, 29 | ], // Add 30 | }).compile(); 31 | 32 | service = moduleRef.get(ApprovedReportService); 33 | }); 34 | 35 | it('should be defined', () => { 36 | expect(service).toBeDefined(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/reports/application/approved-report/approved-report.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { ReportDao } from '@reports/infrastructure/adapters/secondary/db/dao/report.dao'; 3 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository'; 4 | import { Report } from '@src/reports/domain/entity/report'; 5 | import { User } from '@src/reports/domain/entity/user'; 6 | import { EmailValueObject } from '@src/shared/domain/value-objects/email.value.object'; 7 | 8 | @Injectable() 9 | export class ApprovedReportService { 10 | constructor(private reportRepository: ReportRepository) {} 11 | 12 | async changeApproved(id: number, isApproved: boolean): Promise { 13 | const reportDao = await this.reportRepository.findById(id); 14 | 15 | if (!reportDao) { 16 | throw new NotFoundException('Report not found!'); 17 | } 18 | 19 | const user = new User(reportDao.user.id, new EmailValueObject(reportDao.user.email), reportDao.user.name); 20 | 21 | const report = new Report(reportDao.price, reportDao.make, reportDao.year); 22 | report.setOptionalFields(reportDao.model, reportDao.lng, reportDao.lat, reportDao.mileage); 23 | report.setUser(user); 24 | report.setApproved(isApproved); 25 | 26 | return await this.reportRepository.save(report.toJSON() as ReportDao); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/reports/application/create-report/create-report.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { CreateReportService } from './create-report.service'; 3 | import { getRepositoryToken } from '@nestjs/typeorm'; 4 | import { ReportDao } from '@reports/infrastructure/adapters/secondary/db/dao/report.dao'; 5 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository'; 6 | 7 | describe('CreateReportService', () => { 8 | let service: CreateReportService; 9 | const mockRepository = { 10 | create: jest.fn().mockImplementation((dao: ReportDao) => { 11 | return Promise.resolve({ 12 | id: Math.ceil(Math.random() * 10), 13 | ...dao, 14 | }); 15 | }), 16 | }; 17 | 18 | beforeEach(async () => { 19 | const moduleRef = await Test.createTestingModule({ 20 | imports: [], // Add 21 | controllers: [], // Add 22 | providers: [ 23 | CreateReportService, 24 | ReportRepository, 25 | { 26 | provide: getRepositoryToken(ReportDao), 27 | useValue: mockRepository, 28 | }, 29 | ], // Add 30 | }).compile(); 31 | 32 | service = moduleRef.get(CreateReportService); 33 | }); 34 | 35 | it('should be defined', () => { 36 | expect(service).toBeDefined(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/reports/application/create-report/create-report.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ReportDao } from '@reports/infrastructure/adapters/secondary/db/dao/report.dao'; 3 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository'; 4 | import { CreateReportRequestDto } from '@reports/infrastructure/adapters/primary/http/create-report/dto/report.request.dto'; 5 | import { Report } from '@reports/domain/entity/report'; 6 | 7 | @Injectable() 8 | export class CreateReportService { 9 | constructor(private reportRepository: ReportRepository) {} 10 | 11 | async create(reportDto: CreateReportRequestDto, user): Promise { 12 | const report = new Report(reportDto.price, reportDto.make, reportDto.year); 13 | report.setOptionalFields(reportDto.model, reportDto.lng, reportDto.lat, reportDto.mileage); 14 | report.setUser(user); 15 | report.setApproved(false); 16 | 17 | const reportCreated = this.reportRepository.create(report.toJSON() as ReportDao); 18 | reportCreated.userId = user.userId; 19 | return await this.reportRepository.save(reportCreated); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/reports/application/get-estimate/get-estimate.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { GetEstimateService } from './get-estimate.service'; 3 | import { getRepositoryToken } from '@nestjs/typeorm'; 4 | import { ReportDao } from '@reports/infrastructure/adapters/secondary/db/dao/report.dao'; 5 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository'; 6 | 7 | describe('GetEstimateService', () => { 8 | let service: GetEstimateService; 9 | const mockRepository = { 10 | findOneBy: jest.fn().mockImplementation((dao: ReportDao) => { 11 | return Promise.resolve({ 12 | id: Math.ceil(Math.random() * 10), 13 | ...dao, 14 | }); 15 | }), 16 | }; 17 | 18 | beforeEach(async () => { 19 | const moduleRef = await Test.createTestingModule({ 20 | imports: [], // Add 21 | controllers: [], // Add 22 | providers: [ 23 | GetEstimateService, 24 | ReportRepository, 25 | { 26 | provide: getRepositoryToken(ReportDao), 27 | useValue: mockRepository, 28 | }, 29 | ], // Add 30 | }).compile(); 31 | 32 | service = moduleRef.get(GetEstimateService); 33 | }); 34 | 35 | it('should be defined', () => { 36 | expect(service).toBeDefined(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/reports/application/get-estimate/get-estimate.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository'; 3 | import { GetEstimateRequestDto } from '@reports/infrastructure/adapters/primary/http/get-estimate/dto/get-estimate.request.dto'; 4 | import { GetEstimateResponseDto } from '@reports/infrastructure/adapters/primary/http/get-estimate/dto/get-estimate.response.dto'; 5 | 6 | @Injectable() 7 | export class GetEstimateService { 8 | constructor(private reportRepository: ReportRepository) {} 9 | 10 | async getEstimate(query: GetEstimateRequestDto): Promise { 11 | const price: GetEstimateResponseDto = await this.reportRepository.getByQueryBuilder(query); 12 | return price; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/reports/domain/entity/report.ts: -------------------------------------------------------------------------------- 1 | import { MileageError } from '../errors/mileage-error'; 2 | import { PriceError } from '../errors/price-error'; 3 | import { YearError } from '../errors/year-error'; 4 | import { User } from './user'; 5 | 6 | export class Report { 7 | model?: string; 8 | lng?: number; 9 | lat?: number; 10 | mileage?: number; 11 | approved: boolean; 12 | user: User; 13 | 14 | constructor(private price: number, private make: string, private year: number) { 15 | if (year < 1930 || year > 2050) { 16 | throw new YearError(); 17 | } 18 | if (price < 0 || price > 1000000) { 19 | throw new PriceError(); 20 | } 21 | } 22 | 23 | setUser(user: User) { 24 | this.user = user; 25 | } 26 | 27 | setApproved(approved: boolean) { 28 | this.approved = approved; 29 | } 30 | 31 | setOptionalFields(model?: string, lng?: number, lat?: number, mileage?: number) { 32 | this.model = model; 33 | this.lng = lng; 34 | this.lat = lat; 35 | if (mileage < 0 || mileage > 1000000) { 36 | throw new MileageError(); 37 | } 38 | this.mileage = mileage; 39 | } 40 | 41 | toJSON() { 42 | return { 43 | price: this.price, 44 | make: this.make, 45 | model: this.model, 46 | year: this.year, 47 | lng: this.lng, 48 | lat: this.lat, 49 | mileage: this.mileage, 50 | approved: this.approved, 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/reports/domain/entity/user.ts: -------------------------------------------------------------------------------- 1 | import { EmailValueObject } from '@src/shared/domain/value-objects/email.value.object'; 2 | 3 | export class User { 4 | constructor(private id: number, private email: EmailValueObject, private name: string) {} 5 | 6 | async toJSON() { 7 | return { 8 | id: this.id, 9 | email: this.email.getValue, 10 | name: this.name, 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/reports/domain/errors/mileage-error.ts: -------------------------------------------------------------------------------- 1 | export class MileageError extends Error { 2 | constructor() { 3 | super('Price must be between 0 and 1000000'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/reports/domain/errors/price-error.ts: -------------------------------------------------------------------------------- 1 | export class PriceError extends Error { 2 | constructor() { 3 | super('Price must be between 0 and 1000000'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/reports/domain/errors/year-error.ts: -------------------------------------------------------------------------------- 1 | export class YearError extends Error { 2 | constructor() { 3 | super('Year must be between 1930 and 2050'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/reports/domain/ports/primary/http/approved-report.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IApprovedReportController { 2 | approvedReport(params: string, body: Q): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/reports/domain/ports/primary/http/create-report.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ICreateReportController { 2 | create(body: Q, user): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/reports/domain/ports/primary/http/get-estimate.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IGetEstimateController { 2 | getEstimate(query: Q): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/reports/domain/ports/secondary/db/user.repository.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IReportRepositoryInterface { 2 | create(entity: D, options?: any): D; 3 | save(entity: D, options?: any): Promise; 4 | find(options?: any): Promise; 5 | findById(id: number): Promise; 6 | remove(entity: D, options?: any): Promise; 7 | getByQueryBuilder(entity: Partial, options?: any): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/approved-report/approved-report.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ApprovedReportController } from './approved-report.controller'; 3 | import { ApprovedReportService } from '@reports/application/approved-report/approved-report.service'; 4 | 5 | describe('ApprovedReportController', () => { 6 | let controller: ApprovedReportController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [ApprovedReportController], 11 | providers: [ 12 | { 13 | provide: ApprovedReportService, 14 | useValue: { 15 | approved: async () => { 16 | return; 17 | }, 18 | }, 19 | }, 20 | ], 21 | }).compile(); 22 | 23 | controller = module.get(ApprovedReportController); 24 | }); 25 | 26 | it('should be defined', () => { 27 | expect(controller).toBeDefined(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/approved-report/approved-report.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpCode, HttpStatus, Param, Patch } from '@nestjs/common'; 2 | import { ApprovedReportService } from '@reports/application/approved-report/approved-report.service'; 3 | import { IApprovedReportController } from '@reports/domain/ports/primary/http/approved-report.controller.interface'; 4 | import { ApprovedReportRequestDto } from './dto/approved.request.dto'; 5 | import { IsAdmin } from '@shared/infrastructure/decorators/is-admin.decorator'; 6 | 7 | @Controller('reports') 8 | export class ApprovedReportController implements IApprovedReportController { 9 | constructor(private approvedReportService: ApprovedReportService) {} 10 | 11 | @Patch('/:id') 12 | @IsAdmin() 13 | @HttpCode(HttpStatus.NO_CONTENT) 14 | async approvedReport(@Param('id') id: string, @Body() { isApproved }: ApprovedReportRequestDto): Promise { 15 | this.approvedReportService.changeApproved(parseInt(id), isApproved); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/approved-report/dto/approved.request.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsNotEmpty } from 'class-validator'; 2 | 3 | export class ApprovedReportRequestDto { 4 | @IsBoolean() 5 | @IsNotEmpty() 6 | isApproved: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/approved-report/request.http: -------------------------------------------------------------------------------- 1 | 2 | PATCH http://{{host}}/api/{{version}}/reports/1 HTTP/1.1 3 | Content-Type: {{contentType}} 4 | Authorization: Bearer {{token}} 5 | 6 | { 7 | "isApproved": true 8 | } -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/create-report/create-report.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CreateReportController } from './create-report.controller'; 3 | import { CreateReportService } from '@reports/application/create-report/create-report.service'; 4 | 5 | describe('CreateReportController', () => { 6 | let controller: CreateReportController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [CreateReportController], 11 | providers: [ 12 | { 13 | provide: CreateReportService, 14 | useValue: { create: async () => [{ id: 1, email: 'test@gmail.com', name: 'Test' }] }, 15 | }, 16 | ], 17 | }).compile(); 18 | 19 | controller = module.get(CreateReportController); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(controller).toBeDefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/create-report/create-report.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; 2 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator'; 3 | import { ICreateReportController } from '@reports/domain/ports/primary/http/create-report.controller.interface'; 4 | import { CreateReportRequestDto } from './dto/report.request.dto'; 5 | import { CreatedReportRequestDto } from './dto/report.response.dto'; 6 | import { CreateReportService } from '@reports/application/create-report/create-report.service'; 7 | import { CurrentUser } from '@shared/infrastructure/decorators/current-user.decorator'; 8 | 9 | @Controller('reports') 10 | export class CreateReportController 11 | implements ICreateReportController 12 | { 13 | constructor(private createReportService: CreateReportService) {} 14 | 15 | @Post('/') 16 | @HttpCode(HttpStatus.CREATED) 17 | @SerializeResponseDto(CreatedReportRequestDto) 18 | async create(@Body() body: CreateReportRequestDto, @CurrentUser() user): Promise { 19 | return this.createReportService.create(body, user); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/create-report/dto/report.request.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsLatitude, IsLongitude, IsNumber, IsString, Max, Min } from 'class-validator'; 2 | 3 | export class CreateReportRequestDto { 4 | @IsString() 5 | make: string; 6 | 7 | @IsString() 8 | model?: string; 9 | 10 | @IsNumber() 11 | @Min(1930) 12 | @Max(2050) 13 | year: number; 14 | 15 | @IsNumber() 16 | @Min(0) 17 | @Max(1000000) 18 | mileage?: number; 19 | 20 | @IsLongitude() 21 | lng?: number; 22 | 23 | @IsLatitude() 24 | lat?: number; 25 | 26 | @IsNumber() 27 | @Min(0) 28 | @Max(1000000) 29 | price: number; 30 | } 31 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/create-report/dto/report.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { Expose } from 'class-transformer'; 2 | 3 | export class CreatedReportRequestDto { 4 | @Expose() 5 | id: number; 6 | 7 | @Expose() 8 | make: string; 9 | 10 | @Expose() 11 | model?: string; 12 | 13 | @Expose() 14 | approved: boolean; 15 | 16 | @Expose() 17 | userId: number; 18 | } 19 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/create-report/request.http: -------------------------------------------------------------------------------- 1 | 2 | POST http://{{host}}/api/{{version}}/reports HTTP/1.1 3 | Content-Type: {{contentType}} 4 | Authorization: Bearer {{token}} 5 | 6 | { 7 | "make": "Ferrary", 8 | "model": "WUIG78", 9 | "year": 2022, 10 | "mileage": 100, 11 | "lng": 45, 12 | "lat": 45, 13 | "price": 324 14 | } 15 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/get-estimate/dto/get-estimate.request.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { IsString } from 'class-validator'; 3 | 4 | export class GetEstimateRequestDto { 5 | @IsString() 6 | make?: string; 7 | 8 | @IsString() 9 | model?: string; 10 | 11 | @Transform(({ value }) => parseInt(value)) 12 | year?: number; 13 | 14 | @Transform(({ value }) => parseInt(value)) 15 | mileage?: number; 16 | 17 | @Transform(({ value }) => parseFloat(value)) 18 | lng?: number; 19 | 20 | @Transform(({ value }) => parseFloat(value)) 21 | lat?: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/get-estimate/dto/get-estimate.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { Expose } from 'class-transformer'; 2 | 3 | export class GetEstimateResponseDto { 4 | @Expose() 5 | price?: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/get-estimate/get-estimate.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { GetEstimateController } from './get-estimate.controller'; 3 | import { GetEstimateService } from '@reports/application/get-estimate/get-estimate.service'; 4 | 5 | describe('GetEstimateController', () => { 6 | let controller: GetEstimateController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [GetEstimateController], 11 | providers: [ 12 | { 13 | provide: GetEstimateService, 14 | useValue: { 15 | getEstimate: async () => ({ 16 | price: 1000, 17 | }), 18 | }, 19 | }, 20 | ], 21 | }).compile(); 22 | 23 | controller = module.get(GetEstimateController); 24 | }); 25 | 26 | it('should be defined', () => { 27 | expect(controller).toBeDefined(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/get-estimate/get-estimate.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpCode, HttpStatus, Patch, Query } from '@nestjs/common'; 2 | import { GetEstimateRequestDto } from './dto/get-estimate.request.dto'; 3 | import { IGetEstimateController } from '@reports/domain/ports/primary/http/get-estimate.controller.interface'; 4 | import { GetEstimateService } from '@reports/application/get-estimate/get-estimate.service'; 5 | import { GetEstimateResponseDto } from './dto/get-estimate.response.dto'; 6 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator'; 7 | 8 | @Controller('reports') 9 | export class GetEstimateController implements IGetEstimateController { 10 | constructor(private getEstimateService: GetEstimateService) {} 11 | 12 | @Patch('/') 13 | @HttpCode(HttpStatus.OK) 14 | @SerializeResponseDto(GetEstimateResponseDto) 15 | async getEstimate(@Query() query: GetEstimateRequestDto): Promise { 16 | return this.getEstimateService.getEstimate(query); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/primary/http/get-estimate/request.http: -------------------------------------------------------------------------------- 1 | 2 | PATCH http://{{host}}/api/{{version}}/reports?make=toyota&model=corolla&lng=12&lat=12&mileage=200&year=2023 HTTP/1.1 3 | Content-Type: {{contentType}} 4 | Authorization: Bearer {{token}} 5 | 6 | ###other 7 | PATCH http://{{host}}/api/{{version}}/reports?make=Ferrary&model=WUIG78 8 | Content-Type: {{contentType}} 9 | Authorization: Bearer {{token}} 10 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/secondary/db/dao/report.dao.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, Column, Entity, ManyToOne } from 'typeorm'; 2 | import { UserDao } from './user.dao'; 3 | 4 | @Entity('Report') 5 | export class ReportDao { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | price: number; 11 | 12 | @Column() 13 | make: string; 14 | 15 | @Column({ 16 | nullable: true, 17 | }) 18 | model?: string; 19 | 20 | @Column() 21 | year: number; 22 | 23 | @Column({ 24 | nullable: true, 25 | }) 26 | lng?: number; 27 | 28 | @Column({ 29 | nullable: true, 30 | }) 31 | lat?: number; 32 | 33 | @Column({ 34 | nullable: true, 35 | }) 36 | mileage?: number; 37 | 38 | @Column({ default: false }) 39 | approved: boolean; 40 | 41 | @Column() 42 | userId: number; 43 | 44 | @ManyToOne(() => UserDao, (user) => user.reports) 45 | user: UserDao; 46 | } 47 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/secondary/db/dao/user.dao.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { ReportDao } from './report.dao'; 3 | import { Exclude } from 'class-transformer'; 4 | 5 | @Entity('User') 6 | export class UserDao { 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column() 11 | email: string; 12 | 13 | @Column() 14 | @Exclude() 15 | password: string; 16 | 17 | @Column({ 18 | unique: false, 19 | nullable: true, 20 | }) 21 | name?: string; 22 | 23 | @Column() 24 | isAdmin: boolean; 25 | 26 | @Column() 27 | createdAt: Date; 28 | 29 | @OneToMany(() => ReportDao, (report) => report.user) 30 | reports?: ReportDao[]; 31 | } 32 | -------------------------------------------------------------------------------- /src/reports/infrastructure/adapters/secondary/db/report.repository.ts: -------------------------------------------------------------------------------- 1 | import { FindManyOptions, RemoveOptions, Repository, SaveOptions } from 'typeorm'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ReportDao } from './dao/report.dao'; 5 | import { IReportRepositoryInterface } from '@reports/domain/ports/secondary/db/user.repository.interface'; 6 | 7 | @Injectable() 8 | export class ReportRepository implements IReportRepositoryInterface { 9 | constructor( 10 | @InjectRepository(ReportDao) 11 | private repository: Repository, 12 | ) {} 13 | 14 | create(data: ReportDao): ReportDao { 15 | return this.repository.create(data); 16 | } 17 | 18 | save(entity: ReportDao, options?: SaveOptions): Promise { 19 | return this.repository.save(entity, options); 20 | } 21 | 22 | find(options?: FindManyOptions): Promise { 23 | return this.repository.find(options); 24 | } 25 | 26 | findById(id: number): Promise { 27 | return this.repository.findOneBy({ id }); 28 | } 29 | 30 | remove(entity: ReportDao, options?: RemoveOptions): Promise { 31 | return this.repository.remove(entity, options); 32 | } 33 | 34 | // Estimado Promedio de precio de un auto 35 | getByQueryBuilder(query: Partial): Promise { 36 | return this.repository 37 | .createQueryBuilder() 38 | .select('AVG(price)', 'price') 39 | .where('make=:make', { make: query.make }) 40 | .orWhere('model=:model', { model: query.model }) 41 | .orWhere('year - :year', { year: query.year }) 42 | .orWhere('lng - :lng BETWEEN -5 AND +5', { lng: query.lng }) 43 | .orWhere('lat - :lat BETWEEN -3 AND +3', { lat: query.lat }) 44 | .andWhere('approved IS TRUE') 45 | .orderBy('ABS(mileage - :mileage)', 'DESC') 46 | .setParameters({ mileage: query.mileage }) // esto es por el orderBy 47 | .limit(3) 48 | .getRawOne(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/reports/reports.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { ReportDao } from './infrastructure/adapters/secondary/db/dao/report.dao'; 4 | import { CreateReportController } from './infrastructure/adapters/primary/http/create-report/create-report.controller'; 5 | import { CreateReportService } from './application/create-report/create-report.service'; 6 | import { ReportRepository } from './infrastructure/adapters/secondary/db/report.repository'; 7 | import { ApprovedReportController } from './infrastructure/adapters/primary/http/approved-report/approved-report.controller'; 8 | import { ApprovedReportService } from './application/approved-report/approved-report.service'; 9 | import { GetEstimateController } from './infrastructure/adapters/primary/http/get-estimate/get-estimate.controller'; 10 | import { GetEstimateService } from './application/get-estimate/get-estimate.service'; 11 | 12 | @Module({ 13 | imports: [TypeOrmModule.forFeature([ReportDao])], 14 | providers: [CreateReportService, ReportRepository, ApprovedReportService, GetEstimateService], 15 | controllers: [CreateReportController, ApprovedReportController, GetEstimateController], 16 | }) 17 | export class ReportsModule {} 18 | -------------------------------------------------------------------------------- /src/setup-app.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, ValidationPipe } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import * as path from 'path'; 4 | 5 | export const setupApp = (app: INestApplication) => { 6 | const packageJson = require(path.resolve('package.json')); 7 | process.env.API_VERSION = packageJson.version; 8 | 9 | // app.set('trust proxy', true); //esto no me acuerdo para que era 10 | 11 | // esto es para validar los DTO 12 | app.useGlobalPipes( 13 | new ValidationPipe({ 14 | transform: true, // Automatically transform payloads to DTO instances 15 | whitelist: true, // esto sirve para evitar que se meta churre en el endpoind 16 | forbidNonWhitelisted: true, // Throw an error if payload contains non-whitelisted properties 17 | }), 18 | ); 19 | app.setGlobalPrefix('api/v1'); 20 | 21 | const configService = app.get(ConfigService); 22 | app.enableCors({ 23 | origin: configService.getOrThrow('CORS_ALLOWED_ORIGIN'), 24 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', 25 | preflightContinue: false, 26 | optionsSuccessStatus: 204, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/shared/domain/errors/value-required-error.ts: -------------------------------------------------------------------------------- 1 | export class ValueRequiredError extends Error { 2 | constructor(private value: string) { 3 | super(`Value is required: ${value}`); 4 | this.name = 'ValueRequiredError'; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/domain/value-objects/email.value.object.ts: -------------------------------------------------------------------------------- 1 | import { EMAIL_PATTERN } from '@users/constants'; 2 | import { ValueObjectBase } from './value-object-base.abstract'; 3 | import { ValueRequiredError } from '../errors/value-required-error'; 4 | 5 | export class EmailValueObject extends ValueObjectBase { 6 | constructor(value: string) { 7 | super(value); 8 | this.setPattern(EMAIL_PATTERN); 9 | if (!value) { 10 | throw new ValueRequiredError('email'); 11 | } 12 | 13 | if (!this.isValid(value)) { 14 | throw new ValueRequiredError('email'); 15 | } 16 | } 17 | 18 | get getDomain(): string { 19 | return this.value.split('@')[1]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/shared/domain/value-objects/password.value.object.ts: -------------------------------------------------------------------------------- 1 | import { ValueRequiredError } from '../errors/value-required-error'; 2 | import { ValueObjectBase } from './value-object-base.abstract'; 3 | 4 | export class PasswordValueObject extends ValueObjectBase { 5 | constructor(value: string) { 6 | super(value); 7 | if (!value) { 8 | throw new ValueRequiredError('password'); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/domain/value-objects/value-object-base.abstract.ts: -------------------------------------------------------------------------------- 1 | import { shallowEqual } from 'shallow-equal-object'; 2 | 3 | export abstract class ValueObjectBase { 4 | protected readonly value: T; 5 | private PATTERN: RegExp; 6 | 7 | constructor(value: T) { 8 | this.value = Object.freeze(value); 9 | } 10 | 11 | public equals(vo?: ValueObjectBase): boolean { 12 | if (vo === null || vo === undefined) { 13 | return false; 14 | } 15 | if (vo.value === undefined) { 16 | return false; 17 | } 18 | return shallowEqual(this.value, vo.value); 19 | } 20 | 21 | public get getValue(): T { 22 | return this.value; 23 | } 24 | 25 | isValid(value): boolean { 26 | return this.PATTERN.test(value); 27 | } 28 | 29 | setPattern(newPattern: RegExp): void { 30 | this.PATTERN = newPattern; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/infrastructure/decorators/current-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common'; 2 | 3 | export const CurrentUser = createParamDecorator((data: never, context: ExecutionContext) => { 4 | const request = context.switchToHttp().getRequest(); 5 | // Por defecto la estrategia de jwt guarda el user del jwt en request.user 6 | return request.user; 7 | }); 8 | -------------------------------------------------------------------------------- /src/shared/infrastructure/decorators/is-admin.decorator.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { IsAdminGuard } from '../../../auth/infrastructure/guards/is-admin.guard'; 3 | 4 | export function IsAdmin(): MethodDecorator & ClassDecorator { 5 | return UseGuards(new IsAdminGuard()); 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/infrastructure/decorators/public.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const IS_PUBLIC_KEY = 'isPublic'; 4 | export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); 5 | -------------------------------------------------------------------------------- /src/shared/infrastructure/decorators/serialize.decorator.ts: -------------------------------------------------------------------------------- 1 | import { UseInterceptors } from '@nestjs/common'; 2 | import { ResponseBaseDto } from '../response-base.dto.abstract'; 3 | import { SerializeInterceptor } from '../interceptors/serialize.interceptor'; 4 | 5 | export function SerializeResponseDto(dto: ResponseBaseDto): MethodDecorator & ClassDecorator { 6 | return UseInterceptors(new SerializeInterceptor(dto)); 7 | } 8 | -------------------------------------------------------------------------------- /src/shared/infrastructure/interceptors/serialize.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; 2 | import { ClassConstructor, plainToInstance } from 'class-transformer'; 3 | import { Observable, map } from 'rxjs'; 4 | import { ResponseBaseDto } from '../response-base.dto.abstract'; 5 | 6 | @Injectable() 7 | export class SerializeInterceptor implements NestInterceptor { 8 | constructor(private dto: ResponseBaseDto) {} 9 | intercept(context: ExecutionContext, next: CallHandler): Observable { 10 | return next.handle().pipe( 11 | map((data: any) => { 12 | return plainToInstance(this.dto as ClassConstructor, data, { 13 | excludeExtraneousValues: true, 14 | }); 15 | }), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/infrastructure/request-base.dto.abstract.ts: -------------------------------------------------------------------------------- 1 | export abstract class RequestBaseDto {} 2 | -------------------------------------------------------------------------------- /src/shared/infrastructure/response-base.dto.abstract.ts: -------------------------------------------------------------------------------- 1 | export abstract class ResponseBaseDto {} 2 | -------------------------------------------------------------------------------- /src/users/application/find-user-by-id/find-user-by-id.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { FindUserByIdService } from './find-user-by-id.service'; 3 | import { getRepositoryToken } from '@nestjs/typeorm'; 4 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 5 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository'; 6 | 7 | describe('FindUserByIdService', () => { 8 | let service: FindUserByIdService; 9 | const mockRepository = { 10 | findById: jest.fn().mockImplementation((dao: UserDao) => { 11 | return Promise.resolve({ 12 | id: Math.ceil(Math.random() * 10), 13 | ...dao, 14 | }); 15 | }), 16 | }; 17 | 18 | beforeEach(async () => { 19 | const moduleRef = await Test.createTestingModule({ 20 | imports: [], // Add 21 | controllers: [], // Add 22 | providers: [ 23 | FindUserByIdService, 24 | UserRepository, 25 | { 26 | provide: getRepositoryToken(UserDao), 27 | useValue: mockRepository.findById(), 28 | }, 29 | ], // Add 30 | }).compile(); 31 | 32 | service = moduleRef.get(FindUserByIdService); 33 | }); 34 | 35 | it('should be defined', () => { 36 | expect(service).toBeDefined(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/users/application/find-user-by-id/find-user-by-id.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 3 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository'; 4 | 5 | @Injectable() 6 | export class FindUserByIdService { 7 | constructor(private userRepository: UserRepository) {} 8 | 9 | async find(id: number): Promise { 10 | if (!id) { 11 | return null; 12 | } 13 | 14 | const user: UserDao = await this.userRepository.findById(id); 15 | 16 | if (!user) { 17 | throw new NotFoundException('User not found!'); 18 | } 19 | 20 | return user; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/users/application/find-users/find-users.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { getRepositoryToken } from '@nestjs/typeorm'; 3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 4 | import { FindUsersService } from './find-users.service'; 5 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository'; 6 | 7 | describe('FindUsersService', () => { 8 | let service: FindUsersService; 9 | const mockRepository = { 10 | find: jest.fn().mockImplementation((dao: UserDao) => { 11 | return Promise.resolve([ 12 | { 13 | id: Math.ceil(Math.random() * 10), 14 | ...dao, 15 | }, 16 | { 17 | id: Math.ceil(Math.random() * 10), 18 | ...dao, 19 | }, 20 | ]); 21 | }), 22 | }; 23 | 24 | beforeEach(async () => { 25 | const moduleRef = await Test.createTestingModule({ 26 | imports: [], // Add 27 | controllers: [], // Add 28 | providers: [ 29 | FindUsersService, 30 | UserRepository, 31 | { 32 | provide: getRepositoryToken(UserDao), 33 | useValue: mockRepository, 34 | }, 35 | ], // Add 36 | }).compile(); 37 | 38 | service = moduleRef.get(FindUsersService); 39 | }); 40 | 41 | it('should be defined', () => { 42 | expect(service).toBeDefined(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/users/application/find-users/find-users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 3 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository'; 4 | 5 | @Injectable() 6 | export class FindUsersService { 7 | constructor(private userRepository: UserRepository) {} 8 | 9 | async find(): Promise { 10 | const users: UserDao[] = await this.userRepository.find({ relations: ['reports'] }); 11 | 12 | return users; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/users/application/remove-user/remove-user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { RemoveUserService } from './remove-user.service'; 3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 4 | import { getRepositoryToken } from '@nestjs/typeorm'; 5 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository'; 6 | 7 | describe('RemoveUserService', () => { 8 | let service: RemoveUserService; 9 | const mockRepository = { 10 | findById: jest.fn().mockImplementation((dao: UserDao) => { 11 | return Promise.resolve({ 12 | id: Math.ceil(Math.random() * 10), 13 | ...dao, 14 | }); 15 | }), 16 | remove: jest.fn().mockImplementation((dao: UserDao) => { 17 | return Promise.resolve({ 18 | id: Math.ceil(Math.random() * 10), 19 | ...dao, 20 | }); 21 | }), 22 | }; 23 | 24 | beforeEach(async () => { 25 | const moduleRef = await Test.createTestingModule({ 26 | imports: [], // Add 27 | controllers: [], // Add 28 | providers: [ 29 | RemoveUserService, 30 | UserRepository, 31 | { 32 | provide: getRepositoryToken(UserDao), 33 | useValue: mockRepository, 34 | }, 35 | ], // Add 36 | }).compile(); 37 | 38 | service = moduleRef.get(RemoveUserService); 39 | }); 40 | 41 | it('should be defined', () => { 42 | expect(service).toBeDefined(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/users/application/remove-user/remove-user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 3 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository'; 4 | 5 | @Injectable() 6 | export class RemoveUserService { 7 | constructor(private userRepository: UserRepository) {} 8 | 9 | async remove(id: number): Promise { 10 | const user: UserDao = await this.userRepository.findById(id); 11 | 12 | if (!user) { 13 | throw new NotFoundException('User not found!'); 14 | } 15 | 16 | return this.userRepository.remove(user); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/users/application/update-user/update-user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { UpdateUserService } from './update-user.service'; 3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 4 | import { getRepositoryToken } from '@nestjs/typeorm'; 5 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository'; 6 | 7 | describe('UpdateUserService', () => { 8 | let service: UpdateUserService; 9 | const mockRepository = { 10 | findById: jest.fn().mockImplementation((dao: UserDao) => { 11 | return Promise.resolve({ 12 | id: Math.ceil(Math.random() * 10), 13 | ...dao, 14 | }); 15 | }), 16 | save: jest.fn().mockImplementation((dao: UserDao) => { 17 | return Promise.resolve({ 18 | id: Math.ceil(Math.random() * 10), 19 | ...dao, 20 | }); 21 | }), 22 | }; 23 | 24 | beforeEach(async () => { 25 | const moduleRef = await Test.createTestingModule({ 26 | imports: [], // Add 27 | controllers: [], // Add 28 | providers: [ 29 | UpdateUserService, 30 | UserRepository, 31 | { 32 | provide: getRepositoryToken(UserDao), 33 | useValue: mockRepository, 34 | }, 35 | ], // Add 36 | }).compile(); 37 | 38 | service = moduleRef.get(UpdateUserService); 39 | }); 40 | 41 | it('should be defined', () => { 42 | expect(service).toBeDefined(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/users/application/update-user/update-user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 3 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository'; 4 | 5 | @Injectable() 6 | export class UpdateUserService { 7 | constructor(private userRepository: UserRepository) {} 8 | 9 | async update(id: number, attrs: Partial) { 10 | const user: UserDao = await this.userRepository.findById(id); 11 | 12 | if (!user) { 13 | throw new NotFoundException('User not found!'); 14 | } 15 | 16 | Object.assign(user, attrs); // le asignamos lo que esta en attrs a lo que esta en user 17 | 18 | return this.userRepository.save(user); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/users/constants.ts: -------------------------------------------------------------------------------- 1 | export const EMAIL_PATTERN = /^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/; 2 | -------------------------------------------------------------------------------- /src/users/domain/entity/user.ts: -------------------------------------------------------------------------------- 1 | import { EmailValueObject } from '@shared/domain/value-objects/email.value.object'; 2 | 3 | export class User { 4 | constructor(private email: EmailValueObject, private name: string) {} 5 | 6 | async toJSON() { 7 | return { 8 | email: this.email.getValue, 9 | name: this.name, 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/users/domain/ports/primary/api/find-user-by-id.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IFindUserByIdController { 2 | findUserById(param: P): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/users/domain/ports/primary/api/find-users.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IFindUsersController { 2 | find(query: Q): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/users/domain/ports/primary/api/remove-user.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IRemoveUserController { 2 | remove(param: P): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/users/domain/ports/primary/api/update.controller.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUpdateUserController { 2 | update(id: string, body: B): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/users/domain/ports/secondary/db/user.repository.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUserRepositoryInterface { 2 | save(entity: D, options?: any): Promise; 3 | find(options?: any): Promise; 4 | findById(id: number): Promise; 5 | remove(entity: D, options?: any): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/find-user-by-id/dto/user.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract'; 2 | import { Expose } from 'class-transformer'; 3 | 4 | export class UserResponseDto extends ResponseBaseDto { 5 | @Expose() 6 | id: number; 7 | 8 | @Expose() 9 | email: string; 10 | 11 | @Expose() 12 | name?: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/find-user-by-id/find-user-by-id.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { FindUserByIdService } from '@users/application/find-user-by-id/find-user-by-id.service'; 3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 4 | import { FindUserByIdController } from './find-user-by-id.controller'; 5 | 6 | describe('UsersController', () => { 7 | let controller: FindUserByIdController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [FindUserByIdController], 12 | providers: [ 13 | { 14 | provide: FindUserByIdService, 15 | useValue: { find: async () => ({ id: 1, email: 'test@gmail.com', name: 'Test' } as UserDao) }, 16 | }, 17 | ], 18 | }).compile(); 19 | 20 | controller = module.get(FindUserByIdController); 21 | }); 22 | 23 | it('should be defined', () => { 24 | expect(controller).toBeDefined(); 25 | }); 26 | 27 | it('should find the user by id', async () => { 28 | const user = await controller.findUserById('1'); 29 | expect(user.id).toBe(1); 30 | expect(user.email).toBe('test@gmail.com'); 31 | expect(user.name).toBe('Test'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/find-user-by-id/find-user-by-id.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, HttpCode, HttpStatus, Param } from '@nestjs/common'; 2 | import { UserResponseDto } from './dto/user.response.dto'; 3 | import { IFindUserByIdController } from '@users/domain/ports/primary/api/find-user-by-id.controller.interface'; 4 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator'; 5 | import { FindUserByIdService } from '@users/application/find-user-by-id/find-user-by-id.service'; 6 | 7 | @Controller('users') 8 | export class FindUserByIdController implements IFindUserByIdController { 9 | constructor(private findUserByIdService: FindUserByIdService) {} 10 | 11 | @Get('/:id') 12 | @HttpCode(HttpStatus.OK) 13 | @SerializeResponseDto(UserResponseDto) 14 | async findUserById(@Param('id') id: string): Promise { 15 | return this.findUserByIdService.find(parseInt(id)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/find-user-by-id/request.http: -------------------------------------------------------------------------------- 1 | 2 | GET http://{{host}}/api/{{version}}/users/1 HTTP/1.1 3 | Content-Type: {{contentType}} 4 | Authorization: Bearer {{token}} 5 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/find-users/dto/product.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { Expose } from 'class-transformer'; 2 | 3 | export class Product { 4 | @Expose() 5 | id: number; 6 | 7 | @Expose() 8 | price: number; 9 | } 10 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/find-users/dto/user.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract'; 2 | import { Expose, Type } from 'class-transformer'; 3 | import { Product } from './product.response.dto'; 4 | 5 | export class UserResponseDto extends ResponseBaseDto { 6 | @Expose() 7 | id: number; 8 | 9 | @Expose() 10 | email: string; 11 | 12 | @Expose() 13 | name?: string; 14 | 15 | @Expose() 16 | createdAt: Date; 17 | 18 | @Expose() 19 | updatedAt?: Date; 20 | 21 | @Expose() 22 | @Type(() => Product) 23 | reports?: Product[]; 24 | } 25 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/find-users/find-users.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { FindUsersController } from './find-users.controller'; 3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 4 | import { FindUsersService } from '@users/application/find-users/find-users.service'; 5 | 6 | describe('FindUsersController', () => { 7 | let controller: FindUsersController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [FindUsersController], 12 | providers: [ 13 | { 14 | provide: FindUsersService, 15 | useValue: { find: async () => [{ id: 1, email: 'test@gmail.com', name: 'Test' }] as UserDao[] }, 16 | }, 17 | ], 18 | }).compile(); 19 | 20 | controller = module.get(FindUsersController); 21 | }); 22 | 23 | it('should be defined', () => { 24 | expect(controller).toBeDefined(); 25 | }); 26 | 27 | it('should find all users', async () => { 28 | const users = await controller.find(); 29 | expect(users.length).toBe(1); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/find-users/find-users.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, HttpCode, HttpStatus } from '@nestjs/common'; 2 | import { UserResponseDto } from './dto/user.response.dto'; 3 | import { IFindUsersController } from '@users/domain/ports/primary/api/find-users.controller.interface'; 4 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator'; 5 | import { FindUsersService } from '@users/application/find-users/find-users.service'; 6 | 7 | @Controller('users') 8 | export class FindUsersController implements IFindUsersController { 9 | constructor(private findUsersService: FindUsersService) {} 10 | 11 | @Get('/') 12 | @HttpCode(HttpStatus.OK) 13 | @SerializeResponseDto(UserResponseDto) 14 | async find(): Promise { 15 | return this.findUsersService.find(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/find-users/request.http: -------------------------------------------------------------------------------- 1 | 2 | GET http://{{host}}/api/{{version}}/users HTTP/1.1 3 | Content-Type: {{contentType}} 4 | Authorization: Bearer {{token}} 5 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/remove-user/dto/user.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { Expose } from 'class-transformer'; 2 | 3 | export class UserResponseDto { 4 | @Expose() 5 | email: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/remove-user/remove-user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RemoveUserController } from './remove-user.controller'; 3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 4 | import { RemoveUserService } from '@users/application/remove-user/remove-user.service'; 5 | 6 | describe('RemoveUserController', () => { 7 | let controller: RemoveUserController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [RemoveUserController], 12 | providers: [ 13 | { 14 | provide: RemoveUserService, 15 | useValue: { remove: async () => ({ id: 1, email: 'test@gmail.com', name: 'Test' } as UserDao) }, 16 | }, 17 | ], 18 | }).compile(); 19 | 20 | controller = module.get(RemoveUserController); 21 | }); 22 | 23 | it('should be defined', () => { 24 | expect(controller).toBeDefined(); 25 | }); 26 | 27 | it('should remove an user', async () => { 28 | const user = await controller.remove('1'); 29 | expect(user.email).toBe('test@gmail.com'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/remove-user/remove-user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Delete, HttpCode, HttpStatus, Param } from '@nestjs/common'; 2 | import { UserResponseDto } from './dto/user.response.dto'; 3 | import { IRemoveUserController } from '@users/domain/ports/primary/api/remove-user.controller.interface'; 4 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator'; 5 | import { RemoveUserService } from '@users/application/remove-user/remove-user.service'; 6 | 7 | @Controller('users') 8 | export class RemoveUserController implements IRemoveUserController { 9 | constructor(private removeUserService: RemoveUserService) {} 10 | 11 | @Delete('/:id') 12 | @HttpCode(HttpStatus.OK) 13 | @SerializeResponseDto(UserResponseDto) 14 | async remove(@Param('id') id: string): Promise { 15 | return this.removeUserService.remove(parseInt(id)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/remove-user/request.http: -------------------------------------------------------------------------------- 1 | 2 | DELETE http://{{host}}/api/{{version}}/users/2 HTTP/1.1 3 | Content-Type: {{contentType}} 4 | Authorization: Bearer {{token}} 5 | 6 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/update-user/dto/user.request.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsOptional, IsString } from 'class-validator'; 2 | 3 | export class UpdateRequestDto { 4 | @IsOptional() 5 | @IsEmail() 6 | email?: string; 7 | 8 | @IsOptional() 9 | @IsString() 10 | password?: string; 11 | 12 | @IsOptional() 13 | @IsString() 14 | name?: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/update-user/dto/user.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract'; 2 | import { Expose } from 'class-transformer'; 3 | export class UserResponseDto extends ResponseBaseDto { 4 | @Expose() 5 | email: string; 6 | 7 | @Expose() 8 | name?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/update-user/request.http: -------------------------------------------------------------------------------- 1 | 2 | PATCH http://{{host}}/api/{{version}}/users/2 HTTP/1.1 3 | Content-Type: {{contentType}} 4 | Authorization: Bearer {{token}} 5 | 6 | { 7 | "email": "yasniel22222@gmail.com", 8 | "password": "yasniell", 9 | "name": "Yasniel2" 10 | } -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/update-user/update-user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UpdateUserController } from './update-user.controller'; 3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 4 | import { UpdateUserService } from '@users/application/update-user/update-user.service'; 5 | 6 | describe('UsersController', () => { 7 | let controller: UpdateUserController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [UpdateUserController], 12 | providers: [ 13 | { 14 | provide: UpdateUserService, 15 | useValue: { update: async () => ({ id: 1, email: 'test@gmail.com', name: 'Test' } as UserDao) }, 16 | }, 17 | ], 18 | }).compile(); 19 | 20 | controller = module.get(UpdateUserController); 21 | }); 22 | 23 | it('should be defined', () => { 24 | expect(controller).toBeDefined(); 25 | }); 26 | 27 | it('should remove an user', async () => { 28 | const user = await controller.update('1', { email: 'test@gmail.com', name: 'Test' }); 29 | expect(user.email).toBe('test@gmail.com'); 30 | expect(user.name).toBe('Test'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/primary/http/update-user/update-user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpCode, HttpStatus, Param, Patch } from '@nestjs/common'; 2 | import { UpdateRequestDto } from './dto/user.request.dto'; 3 | import { UserResponseDto } from './dto/user.response.dto'; 4 | import { IUpdateUserController } from '@users/domain/ports/primary/api/update.controller.interface'; 5 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator'; 6 | import { UpdateUserService } from '@users/application/update-user/update-user.service'; 7 | 8 | @Controller('users') 9 | export class UpdateUserController implements IUpdateUserController { 10 | constructor(private updateUserService: UpdateUserService) {} 11 | 12 | @Patch('/:id') 13 | @HttpCode(HttpStatus.OK) 14 | @SerializeResponseDto(UserResponseDto) 15 | async update(@Param('id') id: string, @Body() body: UpdateRequestDto): Promise { 16 | return this.updateUserService.update(parseInt(id), body); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/secondary/db/dao/report.dao.ts: -------------------------------------------------------------------------------- 1 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 2 | import { PrimaryGeneratedColumn, Column, Entity, ManyToOne } from 'typeorm'; 3 | 4 | @Entity('Report') 5 | export class ReportDao { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | make: string; 11 | 12 | @Column() 13 | price: number; 14 | 15 | @Column() 16 | year: number; 17 | 18 | @ManyToOne(() => UserDao, (user) => user.reports) 19 | user: UserDao; 20 | } 21 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/secondary/db/dao/user.dao.ts: -------------------------------------------------------------------------------- 1 | import { Exclude, Type } from 'class-transformer'; 2 | import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; 3 | import { ReportDao } from './report.dao'; 4 | 5 | @Entity('User') 6 | export class UserDao { 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column() 11 | email: string; 12 | 13 | @Column() 14 | @Exclude() 15 | password: string; 16 | 17 | @Column({ 18 | unique: false, 19 | nullable: true, 20 | }) 21 | name?: string; 22 | 23 | @Column({ 24 | nullable: true, 25 | length: 500, 26 | }) 27 | @Exclude() 28 | refreshToken: string; 29 | 30 | @Column() 31 | isAdmin: boolean; 32 | 33 | @CreateDateColumn() 34 | createdAt: Date; 35 | 36 | @UpdateDateColumn() 37 | updatedAt?: Date; 38 | 39 | @OneToMany((type) => ReportDao, (report) => report.user) 40 | @Type() 41 | reports?: ReportDao[]; 42 | 43 | // @AfterInsert() 44 | // logInstert() { 45 | // // eslint-disable-next-line no-console 46 | // console.log('Insert ID: ', this.id); 47 | // } 48 | 49 | // @AfterUpdate() 50 | // logUpdate() { 51 | // // eslint-disable-next-line no-console 52 | // console.log('Update ID: ', this.id); 53 | // } 54 | 55 | // @AfterRemove() 56 | // logRemove() { 57 | // // eslint-disable-next-line no-console 58 | // console.log('Remove ID: ', this.id); 59 | // } 60 | } 61 | -------------------------------------------------------------------------------- /src/users/infrastructure/adapters/secondary/db/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { IUserRepositoryInterface } from '@users/domain/ports/secondary/db/user.repository.interface'; 2 | import { FindManyOptions, RemoveOptions, Repository, SaveOptions } from 'typeorm'; 3 | import { UserDao } from './dao/user.dao'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { Injectable } from '@nestjs/common'; 6 | 7 | @Injectable() 8 | export class UserRepository implements IUserRepositoryInterface { 9 | constructor( 10 | @InjectRepository(UserDao) 11 | private repository: Repository, 12 | ) {} 13 | 14 | save(entity: UserDao, options?: SaveOptions): Promise { 15 | return this.repository.save(entity, options); 16 | } 17 | 18 | find(options?: FindManyOptions): Promise { 19 | return this.repository.find(options); 20 | } 21 | 22 | findById(id: number): Promise { 23 | return this.repository.findOneBy({ id }); 24 | } 25 | 26 | remove(entity: UserDao, options?: RemoveOptions): Promise { 27 | return this.repository.remove(entity, options); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao'; 4 | import { FindUserByIdController } from './infrastructure/adapters/primary/http/find-user-by-id/find-user-by-id.controller'; 5 | import { FindUsersController } from './infrastructure/adapters/primary/http/find-users/find-users.controller'; 6 | import { RemoveUserController } from './infrastructure/adapters/primary/http/remove-user/remove-user.controller'; 7 | import { UpdateUserController } from './infrastructure/adapters/primary/http/update-user/update-user.controller'; 8 | import { FindUserByIdService } from './application/find-user-by-id/find-user-by-id.service'; 9 | import { UserRepository } from './infrastructure/adapters/secondary/db/user.repository'; 10 | import { FindUsersService } from './application/find-users/find-users.service'; 11 | import { RemoveUserService } from './application/remove-user/remove-user.service'; 12 | import { UpdateUserService } from './application/update-user/update-user.service'; 13 | 14 | @Module({ 15 | imports: [TypeOrmModule.forFeature([UserDao])], 16 | exports: [], 17 | providers: [FindUserByIdService, FindUsersService, RemoveUserService, UpdateUserService, UserRepository], 18 | controllers: [FindUserByIdController, FindUsersController, RemoveUserController, UpdateUserController], 19 | }) 20 | export class UsersModule {} 21 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from '@app/app.module'; 5 | import { setupApp } from '@src/setup-app'; 6 | 7 | describe('AppController (e2e)', () => { 8 | let app: INestApplication; 9 | 10 | beforeEach(async () => { 11 | const moduleFixture: TestingModule = await Test.createTestingModule({ 12 | imports: [AppModule], 13 | }).compile(); 14 | 15 | app = moduleFixture.createNestApplication(); 16 | setupApp(app); 17 | await app.init(); 18 | }); 19 | 20 | it('/ (GET)', () => { 21 | return request(app.getHttpServer()).get('/api/v1').expect(200).expect({ version: '0.0.1', env: 'qa' }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/auth/signin.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { setupApp } from '@src/setup-app'; 2 | import { AppModule } from '@app/app.module'; 3 | import { INestApplication } from '@nestjs/common'; 4 | import { Test, TestingModule } from '@nestjs/testing'; 5 | import * as request from 'supertest'; 6 | import { ConfigService } from '@nestjs/config'; 7 | describe('Auth signin (e2e)', () => { 8 | let app: INestApplication; 9 | const email = 'test@gmail.com'; 10 | 11 | beforeEach(async () => { 12 | const moduleFixture: TestingModule = await Test.createTestingModule({ 13 | imports: [AppModule], 14 | providers: [ConfigService], 15 | }).compile(); 16 | 17 | app = moduleFixture.createNestApplication(); 18 | setupApp(app); 19 | await app.init(); 20 | }); 21 | 22 | it('handles a signin request / (POST)', async () => { 23 | await request(app.getHttpServer()) 24 | .post('/api/v1/auth/signup') 25 | .send({ email, password: 'test' }) 26 | .expect(201) 27 | .then((res) => { 28 | const { id, email: emailNewUser } = res.body; 29 | expect(id).toBeDefined(); 30 | expect(emailNewUser).toEqual(email); 31 | }); 32 | 33 | return request(app.getHttpServer()) 34 | .post('/api/v1/auth/signin') 35 | .send({ email, password: 'test' }) 36 | .expect(200) 37 | .then((res) => { 38 | const { token } = res.body; 39 | expect(token).toBeDefined(); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/auth/signout.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { setupApp } from '@src/setup-app'; 2 | import { AppModule } from '@app/app.module'; 3 | import { INestApplication } from '@nestjs/common'; 4 | import { Test, TestingModule } from '@nestjs/testing'; 5 | import * as request from 'supertest'; 6 | 7 | describe('Auth signout (e2e)', () => { 8 | let app: INestApplication; 9 | const email = 'test@gmail.com'; 10 | 11 | beforeEach(async () => { 12 | const moduleFixture: TestingModule = await Test.createTestingModule({ 13 | imports: [AppModule], 14 | }).compile(); 15 | 16 | app = moduleFixture.createNestApplication(); 17 | setupApp(app); 18 | await app.init(); 19 | }); 20 | 21 | it('handles a signout request / (POST)', () => { 22 | return request(app.getHttpServer()) 23 | .post('/api/v1/auth/signout') 24 | .send({ email, password: 'test' }) 25 | .set( 26 | 'Authorization', 27 | 'Bearer ' + 28 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJpYXQiOjE2OTEzNjk2ODl9.tnCeh4KgF68oJW5xjXL9EErdRpcAi0XSnFYOryEAHRs', 29 | ) 30 | .expect(200) 31 | .then((res) => { 32 | const { ok } = res.body; 33 | expect(ok).toBeDefined(); 34 | expect(ok).toBeTruthy(); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/auth/signup.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { setupApp } from '@src/setup-app'; 2 | import { AppModule } from '@app/app.module'; 3 | import { INestApplication } from '@nestjs/common'; 4 | import { Test, TestingModule } from '@nestjs/testing'; 5 | import * as request from 'supertest'; 6 | 7 | describe('Auth signout (e2e)', () => { 8 | let app: INestApplication; 9 | 10 | beforeEach(async () => { 11 | const moduleFixture: TestingModule = await Test.createTestingModule({ 12 | imports: [AppModule], 13 | }).compile(); 14 | 15 | app = moduleFixture.createNestApplication(); 16 | setupApp(app); 17 | await app.init(); 18 | }); 19 | 20 | it('handles a signup request / (POST)', () => { 21 | const email = 'test@gmail.com'; 22 | return request(app.getHttpServer()) 23 | .post('/api/v1/auth/signup') 24 | .send({ email, password: 'test' }) 25 | .expect(201) 26 | .then((res) => { 27 | const { id, email: emailNewUser } = res.body; 28 | expect(id).toBeDefined(); 29 | expect(emailNewUser).toEqual(email); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/auth/whoami.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { setupApp } from '@src/setup-app'; 2 | import { AppModule } from '@app/app.module'; 3 | import { INestApplication } from '@nestjs/common'; 4 | import { Test, TestingModule } from '@nestjs/testing'; 5 | import * as request from 'supertest'; 6 | describe('Auth whoami (e2e)', () => { 7 | let app: INestApplication; 8 | const email = 'test@gmail.com'; 9 | 10 | beforeEach(async () => { 11 | const moduleFixture: TestingModule = await Test.createTestingModule({ 12 | imports: [AppModule], 13 | }).compile(); 14 | 15 | app = moduleFixture.createNestApplication(); 16 | setupApp(app); 17 | await app.init(); 18 | }); 19 | 20 | it('handles a whoami request / (POST)', async () => { 21 | await request(app.getHttpServer()) 22 | .post('/api/v1/auth/signup') 23 | .send({ email, password: 'test' }) 24 | .expect(201) 25 | .then((res) => { 26 | const { id, email: emailNewUser } = res.body; 27 | expect(id).toBeDefined(); 28 | expect(emailNewUser).toEqual(email); 29 | }); 30 | 31 | return request(app.getHttpServer()) 32 | .post('/api/v1/auth/whoami') 33 | .send({ email, password: 'test' }) 34 | .set( 35 | 'Authorization', 36 | 'Bearer ' + 37 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJpYXQiOjE2OTEzNjk2ODl9.tnCeh4KgF68oJW5xjXL9EErdRpcAi0XSnFYOryEAHRs', 38 | ) 39 | .expect(200) 40 | .then((res) => { 41 | const { email: emailNewUser } = res.body; 42 | expect(emailNewUser).toBeDefined(); 43 | expect(email).toEqual(emailNewUser); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | }, 9 | "moduleNameMapper": { 10 | "^@src(.*)$": "/../src$1", 11 | "^@app(.*)$": "/../src/app$1", 12 | "^@config(.*)$": "/../src/config$1", 13 | "^@users(.*)$": "/../src/users$1", 14 | "^@auth(.*)$": "/../src/auth$1", 15 | "^@reports(.*)$": "/../src/reports$1", 16 | "^@utils(.*)$": "/../src/utils$1", 17 | "^@shared(.*)$": "/../src/shared$1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "paths": { 21 | "@src/*": ["src/*"], 22 | "@app/*": ["src/app/*"], 23 | "@config/*": ["src/config/*"], 24 | "@users/*": ["src/users/*"], 25 | "@auth/*": ["src/auth/*"], 26 | "@reports/*": ["src/reports/*"], 27 | "@utils/*": ["src/utils/*"], 28 | "@shared/*": ["src/shared/*"], 29 | } 30 | } 31 | } 32 | --------------------------------------------------------------------------------