├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── database-overview.png ├── logo.svg └── projectOverview.jpg └── services ├── AuthenticationService ├── .env.development.example ├── .env.test.example ├── .gitignore ├── jest.config.ts ├── jest.setup.ts ├── package.json ├── serverless-secrets.example.json ├── serverless.yml ├── src │ ├── container │ │ └── index.ts │ ├── controllers │ │ ├── authentication │ │ │ └── SignInUserController.ts │ │ ├── errors │ │ │ └── MissingParamError.ts │ │ ├── ports │ │ │ ├── IController.ts │ │ │ ├── IHttpRequest.ts │ │ │ └── IHttpResponse.ts │ │ └── utils │ │ │ ├── HttpResponses.ts │ │ │ └── IsRequiredParamsMissing.ts │ ├── infra │ │ ├── authentication │ │ │ ├── JwtAuthenticationTokenProvider.ts │ │ │ └── config.ts │ │ ├── database │ │ │ └── prisma │ │ │ │ ├── PrismaClient.ts │ │ │ │ ├── migrations │ │ │ │ ├── 20220406010128_add_users │ │ │ │ │ └── migration.sql │ │ │ │ └── migration_lock.toml │ │ │ │ ├── repositories │ │ │ │ └── PrismaUsersRepository.ts │ │ │ │ └── schema.prisma │ │ ├── encoder │ │ │ └── BcryptEncoder.ts │ │ └── serverless │ │ │ ├── functions │ │ │ ├── EnsureAdmin.ts │ │ │ ├── EnsureAuthenticated.ts │ │ │ └── SignInUser.ts │ │ │ └── utils │ │ │ └── commonMiddleware.ts │ ├── logic │ │ └── Either.ts │ ├── middlewares │ │ ├── ensureAdminMiddleware.ts │ │ ├── ensureAuthenticatedMiddleware.ts │ │ └── ports │ │ │ ├── IHttpRequest.ts │ │ │ └── IMiddleware.ts │ └── useCases │ │ └── authentication │ │ ├── SignInUserUseCase.ts │ │ ├── errors │ │ └── IncorrectCredentialsError.ts │ │ └── ports │ │ ├── IAuthenticationTokenProvider.ts │ │ ├── IEncoder.ts │ │ ├── IUserData.ts │ │ └── IUsersRepository.ts ├── tests │ ├── controllers │ │ └── SignInUserController.spec.ts │ ├── doubles │ │ ├── UsersActions.ts │ │ └── repositories │ │ │ └── UsersRepositoryInMemory.ts │ ├── infra │ │ └── serverless │ │ │ ├── EnsureAdmin.spec.ts │ │ │ ├── EnsureAuthenticated.spec.ts │ │ │ └── SignInUser.spec.ts │ ├── middlewares │ │ ├── EnsureAdminMiddleware.spec.ts │ │ └── EnsureAuthenticatedMiddleware.spec.ts │ └── useCases │ │ └── SignInUserUseCase.spec.ts ├── tsconfig.json └── yarn.lock ├── CartService ├── .env.development.example ├── .env.test.example ├── .gitignore ├── @types │ └── express │ │ └── index.d.ts ├── package.json ├── src │ ├── container │ │ └── index.ts │ ├── controllers │ │ ├── errors │ │ │ └── MissingParamError.ts │ │ ├── ports │ │ │ ├── IController.ts │ │ │ ├── IHttpRequest.ts │ │ │ └── IHttpResponse.ts │ │ └── utils │ │ │ ├── HttpResponses.ts │ │ │ └── IsRequiredParamsMissing.ts │ ├── infra │ │ ├── database │ │ │ └── prisma │ │ │ │ ├── PrismaClient.ts │ │ │ │ └── schema.prisma │ │ └── http │ │ │ ├── app.ts │ │ │ ├── routes │ │ │ └── RouteAdapter.ts │ │ │ └── server.ts │ └── logic │ │ └── Either.ts └── tsconfig.json ├── OrderService ├── .env.development.example ├── .env.test.example ├── .gitignore ├── Dockerfile ├── docker-compose.yml ├── jest.config.ts ├── jest.setup.ts ├── package.json ├── serverless-secrets.example.json ├── serverless.yml ├── src │ ├── container │ │ └── index.ts │ ├── controllers │ │ ├── errors │ │ │ └── MissingParamError.ts │ │ ├── orders │ │ │ ├── ListOrdersByUserController.ts │ │ │ └── PlaceOrderController.ts │ │ ├── ports │ │ │ ├── IController.ts │ │ │ ├── IHttpRequest.ts │ │ │ ├── IHttpResponse.ts │ │ │ └── IServerlessHttpRequest.ts │ │ └── utils │ │ │ ├── HttpResponses.ts │ │ │ └── IsRequiredParamsMissing.ts │ ├── domain │ │ └── entities │ │ │ └── Order │ │ │ ├── errors │ │ │ └── InvalidOrderTotalError.ts │ │ │ └── index.ts │ ├── infra │ │ ├── database │ │ │ └── prisma │ │ │ │ ├── PrismaClient.ts │ │ │ │ ├── migrations │ │ │ │ ├── 20220406140833_create_orders │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20220406210700_add_product_amount │ │ │ │ │ └── migration.sql │ │ │ │ └── migration_lock.toml │ │ │ │ ├── repositories │ │ │ │ └── PrismaOrdersRepository.ts │ │ │ │ └── schema.prisma │ │ ├── messaging │ │ │ ├── SQSClient.ts │ │ │ └── SQSMessagingAdapter.ts │ │ └── serverless │ │ │ ├── functions │ │ │ ├── ListOrdersByUser.ts │ │ │ └── PlaceOrder.ts │ │ │ └── utils │ │ │ └── commonMiddleware.ts │ ├── logic │ │ └── Either.ts │ └── useCases │ │ └── orders │ │ ├── ListOrdersByUserUseCase.ts │ │ ├── PlaceOrderUseCase.ts │ │ └── ports │ │ ├── IMessagingAdapter.ts │ │ ├── IOrderData.ts │ │ └── IOrdersRepository.ts ├── tests │ ├── Entities │ │ └── Order.spec.ts │ ├── controllers │ │ ├── ListOrdersByUserController.spec.ts │ │ └── PlaceOrderController.spec.ts │ ├── doubles │ │ ├── FakeMessagingAdapter.ts │ │ ├── OrdersActions.ts │ │ └── repositories │ │ │ └── OrdersRepositoryInMemory.ts │ ├── infra │ │ └── serverless │ │ │ ├── ListOrdersByUser.spec.ts │ │ │ └── PlaceOrder.spec.ts │ └── useCases │ │ └── orders │ │ ├── ListOrdersByUserUseCase.spec.ts │ │ └── PlaceOrderUseCase.spec.ts ├── tsconfig.json └── yarn.lock ├── ProductService ├── .env.development.example ├── .env.test.example ├── .gitignore ├── Dockerfile ├── docker-compose.yml ├── jest.config.ts ├── jest.setup.ts ├── package.json ├── serverless-secrets.example.json ├── serverless.yml ├── src │ ├── container │ │ └── index.ts │ ├── controllers │ │ ├── categories │ │ │ ├── CreateCategoryController.ts │ │ │ ├── DeleteCategoryController.ts │ │ │ ├── ListAllCategoriesController.ts │ │ │ └── UpdateCategoryController.ts │ │ ├── errors │ │ │ └── MissingParamError.ts │ │ ├── ports │ │ │ ├── IController.ts │ │ │ ├── IHttpRequest.ts │ │ │ ├── IHttpResponse.ts │ │ │ └── IServerlessHttpRequest.ts │ │ ├── products │ │ │ ├── CreateProductController.ts │ │ │ ├── DeleteProductController.ts │ │ │ ├── ListAllProductsController.ts │ │ │ ├── ReduceProductsStockController.ts │ │ │ └── UpdateProductController.ts │ │ └── utils │ │ │ ├── HttpResponses.ts │ │ │ └── IsRequiredParamsMissing.ts │ ├── domain │ │ └── entities │ │ │ ├── Category │ │ │ ├── errors │ │ │ │ └── InvalidCategoryNameError.ts │ │ │ └── index.ts │ │ │ └── Product │ │ │ ├── errors │ │ │ ├── InvalidProductNameError.ts │ │ │ ├── InvalidProductPriceError.ts │ │ │ └── InvalidProductStockError.ts │ │ │ └── index.ts │ ├── infra │ │ ├── database │ │ │ └── prisma │ │ │ │ ├── PrismaClient.ts │ │ │ │ ├── migrations │ │ │ │ ├── 20220404192944_add_products_and_categories │ │ │ │ │ └── migration.sql │ │ │ │ └── migration_lock.toml │ │ │ │ ├── repositories │ │ │ │ ├── PrismaCategoriesRepository.ts │ │ │ │ └── PrismaProductsRepository.ts │ │ │ │ └── schema.prisma │ │ ├── http │ │ │ ├── app.ts │ │ │ ├── routes │ │ │ │ ├── RouteAdapter.ts │ │ │ │ ├── categories.routes.ts │ │ │ │ ├── index.ts │ │ │ │ └── products.routes.ts │ │ │ └── server.ts │ │ └── serverless │ │ │ ├── functions │ │ │ ├── CreateCategory.ts │ │ │ ├── CreateProduct.ts │ │ │ ├── DeleteCategory.ts │ │ │ ├── DeleteProduct.ts │ │ │ ├── ReduceProductStock.ts │ │ │ └── UpdateCategory.ts │ │ │ └── utils │ │ │ └── commonMiddleware.ts │ ├── logic │ │ └── Either.ts │ └── useCases │ │ ├── categories │ │ ├── CreateCategoryUseCase.ts │ │ ├── DeleteCategoryUseCase.ts │ │ ├── ListAllCategoriesUseCase.ts │ │ ├── UpdateCategoryUseCase.ts │ │ ├── errors │ │ │ ├── CategoryAlreadyExistsError.ts │ │ │ └── CategoryNotFoundError.ts │ │ └── ports │ │ │ ├── ICategoriesRepository.ts │ │ │ └── ICategoryData.ts │ │ └── products │ │ ├── CreateProductUseCase.ts │ │ ├── DeleteProductUseCase.ts │ │ ├── ListAllProductsUseCase.ts │ │ ├── ReduceProductsStockUseCase.ts │ │ ├── UpdateProductUseCase.ts │ │ ├── errors │ │ ├── ProductAlreadyExistsError.ts │ │ ├── ProductInsufficientStockError.ts │ │ └── ProductNotFoundError.ts │ │ └── ports │ │ ├── IProductData.ts │ │ └── IProductsRepository.ts ├── tests │ ├── Entities │ │ ├── Category.spec.ts │ │ └── Product.spec.ts │ ├── controllers │ │ ├── categories │ │ │ ├── CreateCategoryController.spec.ts │ │ │ ├── DeleteCategoryController.spec.ts │ │ │ ├── ListAllCategoriesController.spec.ts │ │ │ └── UpdateCategoryController.spec.ts │ │ └── products │ │ │ ├── CreateProductController.spec.ts │ │ │ ├── DeleteProductController.spec.ts │ │ │ ├── ListAllProductsController.spec.ts │ │ │ ├── ReduceProductsStockController.spec.ts │ │ │ └── UpdateProductController.spec.ts │ ├── doubles │ │ ├── CategoriesActions.ts │ │ ├── ProductsActions.ts │ │ └── repositories │ │ │ ├── CategoriesRepositoryInMemory.ts │ │ │ └── ProductsRepositoryInMemory.ts │ ├── infra │ │ ├── http │ │ │ └── routes │ │ │ │ ├── categories │ │ │ │ └── ListAllCategoriesRoute.spec.ts │ │ │ │ └── products │ │ │ │ └── ListAllProductsRoute.spec.ts │ │ └── serverless │ │ │ ├── CreateCategory.spec.ts │ │ │ ├── CreateProduct.spec.ts │ │ │ ├── DeleteCategory.spec.ts │ │ │ ├── DeleteProduct.spec.ts │ │ │ ├── ReduceProductsStock.spec.ts │ │ │ └── UpdateCategory.spec.ts │ └── useCases │ │ ├── categories │ │ ├── CreateCategoryUseCase.spec.ts │ │ ├── DeleteCategoryUseCase.spec.ts │ │ ├── ListAllCategoriesUseCase.spec.ts │ │ └── UpdateCategoryUseCase.spec.ts │ │ └── products │ │ ├── CreateProductUseCase.spec.ts │ │ ├── DeleteProductUseCase.spec.ts │ │ ├── ListAllProductsUseCase.spec.ts │ │ ├── ReduceProductStockUseCase.spec.ts │ │ └── UpdateProductUseCase.spec.ts ├── tsconfig.json └── yarn.lock └── UserService ├── .env.development.example ├── .env.test.example ├── .gitignore ├── @types └── express │ └── index.d.ts ├── Dockerfile ├── docker-compose.yml ├── jest.config.ts ├── jest.setup.ts ├── package.json ├── src ├── container │ └── index.ts ├── controllers │ ├── errors │ │ └── MissingParamError.ts │ ├── ports │ │ ├── IController.ts │ │ ├── IHttpRequest.ts │ │ └── IHttpResponse.ts │ ├── users │ │ ├── AdminUpdateUserController.ts │ │ ├── CreateUserController.ts │ │ ├── DeleteUserController.ts │ │ ├── ListAllUsersController.ts │ │ ├── UpdateUserController.ts │ │ └── UserProfileController.ts │ └── utils │ │ ├── HttpResponses.ts │ │ └── IsRequiredParamsMissing.ts ├── domain │ └── entities │ │ └── User │ │ ├── ValidateEmail.ts │ │ ├── errors │ │ ├── InvalidEmailError.ts │ │ ├── InvalidNameError.ts │ │ └── InvalidPasswordError.ts │ │ └── index.ts ├── infra │ ├── authentication │ │ ├── JwtAuthenticationTokenProvider.ts │ │ └── config.ts │ ├── database │ │ └── prisma │ │ │ ├── PrismaClient.ts │ │ │ ├── migrations │ │ │ ├── 20220331134924_add_users │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ │ ├── repositories │ │ │ └── PrismaUsersRepository.ts │ │ │ └── schema.prisma │ ├── encoder │ │ └── BcryptEncoder.ts │ └── http │ │ ├── app.ts │ │ ├── middlewares │ │ ├── MiddlewareAdapter.ts │ │ ├── adaptedEnsureAdmin.ts │ │ └── adaptedEnsureAuthenticated.ts │ │ ├── routes │ │ ├── RouteAdapter.ts │ │ ├── index.ts │ │ └── users.routes.ts │ │ └── server.ts ├── logic │ └── Either.ts ├── middlewares │ ├── ensureAdmin.ts │ ├── ensureAuthenticated.ts │ ├── ports │ │ ├── IHttpRequest.ts │ │ ├── IHttpResponse.ts │ │ └── IMiddleware.ts │ └── utils │ │ └── HttpResponses.ts └── useCases │ └── users │ ├── CreateUserUseCase.ts │ ├── DeleteUserUseCase.ts │ ├── ListAllUsersUseCase.ts │ ├── UpdateUserUseCase.ts │ ├── UserProfileUseCase.ts │ ├── errors │ ├── EmailIsAlreadyTakenError.ts │ ├── IncorrectPasswordError.ts │ ├── UnmatchedPasswordError.ts │ ├── UserAlreadyExistsError.ts │ └── UserNotFoundError.ts │ └── ports │ ├── IAuthenticationTokenProvider.ts │ ├── IEncoder.ts │ ├── IListUsersResponse.ts │ ├── IUpdatedUserData.ts │ ├── IUserData.ts │ └── IUsersRepository.ts ├── tests ├── Entities │ └── User.spec.ts ├── controllers │ └── users │ │ ├── AdminUpdateUserController.spec.ts │ │ ├── CreateUserController.spec.ts │ │ ├── DeleteUserController.spec.ts │ │ ├── ListAllUsersController.spec.ts │ │ ├── UpdateUserController.spec.ts │ │ └── UserProfileController.spec.ts ├── doubles │ ├── FakeEncoder.ts │ ├── UserIdTestMiddleware.ts │ ├── UsersActions.ts │ └── repositories │ │ └── UsersRepositoryInMemory.ts ├── infra │ └── http │ │ └── routes │ │ └── users │ │ ├── AdminUpdateUserRoute.spec.ts │ │ ├── CreateUserRoute.spec.ts │ │ ├── DeleteUserRoute.spec.ts │ │ ├── ListAllUsersRoute.spec.ts │ │ ├── UpdateUserRoute.spec.ts │ │ └── UserProfileRoute.spec.ts └── useCases │ └── users │ ├── CreateUserUseCase.spec.ts │ ├── DeleteUserUseCase.spec.ts │ ├── ListAllUsersCase.spec.ts │ ├── UpdateUserUseCase.spec.ts │ └── UserProfileUseCase.spec.ts ├── tsconfig.json └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | 5 | push: 6 | branches: [ main ] 7 | paths: 8 | - services/UserService/** 9 | - services/ProductService/** 10 | 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Setup Nodejs 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '16.14.0' 24 | 25 | - name: Install Dependencies 26 | run: | 27 | cd ./services/UserService 28 | yarn 29 | cd ../ProductService 30 | yarn 31 | 32 | - name: Build 33 | run: | 34 | cd ./services/UserService 35 | yarn build 36 | cd ../ProductService 37 | yarn build 38 | 39 | - uses: appleboy/scp-action@master 40 | with: 41 | host: ${{ secrets.SSH_HOST }} 42 | username: ${{ secrets.SSH_USER }} 43 | port: ${{ secrets.SSH_PORT }} 44 | key: ${{ secrets.SSH_KEY }} 45 | source: "., !services/UserService/node_modules, !services/ProductService/node_modules" 46 | target: "~/app/GoTech" 47 | 48 | - name: Update API 49 | uses: appleboy/ssh-action@master 50 | with: 51 | host: ${{ secrets.SSH_HOST }} 52 | username: ${{ secrets.SSH_USER }} 53 | key: ${{ secrets.SSH_KEY }} 54 | port: ${{ secrets.SSH_PORT }} 55 | script: | 56 | PATH=$PATH:/home/${{ secrets.SSH_USER }}/.nvm/versions/node/v16.14.0/bin/ 57 | cd ~/app/GoTech/services/UserService 58 | yarn 59 | yarn prisma:migrate 60 | pm2 restart UserService 61 | cd ../ProductService 62 | yarn 63 | yarn prisma:migrate 64 | pm2 restart ProductService 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .env.development 4 | .env.test 5 | .env 6 | .vscode 7 | .serverless 8 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eric Viana Kivel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/database-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erickivel/GoTech/2d91007862fe29933879a13f4d8d428b8f841820/assets/database-overview.png -------------------------------------------------------------------------------- /assets/projectOverview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erickivel/GoTech/2d91007862fe29933879a13f4d8d428b8f841820/assets/projectOverview.jpg -------------------------------------------------------------------------------- /services/AuthenticationService/.env.development.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://docker:password@user_service_database:5432/user_service_database?schema=public" 2 | TOKEN_SECRET_KEY= -------------------------------------------------------------------------------- /services/AuthenticationService/.env.test.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://prisma_test:password@localhost:5433/user_service_test_database?schema=public" -------------------------------------------------------------------------------- /services/AuthenticationService/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | jspm_packages 3 | coverage 4 | .serverless 5 | serverless-secrets.json 6 | .env.development 7 | .env.test -------------------------------------------------------------------------------- /services/AuthenticationService/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; -------------------------------------------------------------------------------- /services/AuthenticationService/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authentication-service", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "test": "dotenv -e .env.test -- prisma migrate dev --schema ./src/infra/database/prisma/schema.prisma && dotenv -e .env.test -- jest --runInBand" 8 | }, 9 | "devDependencies": { 10 | "@types/aws-lambda": "^8.10.61", 11 | "@types/bcrypt": "^5.0.0", 12 | "@types/jest": "^27.4.1", 13 | "@types/jsonwebtoken": "^8.5.8", 14 | "@types/node": "^17.0.21", 15 | "git-commit-msg-linter": "^4.1.1", 16 | "jest": "^27.5.1", 17 | "prisma": "^3.14.0", 18 | "serverless-dotenv-plugin": "^3.12.2", 19 | "serverless-plugin-typescript": "^1.1.9", 20 | "ts-jest": "^27.1.3", 21 | "ts-node-dev": "^1.1.8", 22 | "typescript": "^4.6.2" 23 | }, 24 | "dependencies": { 25 | "@middy/core": "^2.5.7", 26 | "@middy/http-cors": "^2.5.7", 27 | "@middy/http-error-handler": "^2.5.7", 28 | "@middy/http-event-normalizer": "^2.5.7", 29 | "@middy/http-json-body-parser": "^2.5.7", 30 | "@prisma/client": "^3.14.0", 31 | "bcrypt": "^5.0.1", 32 | "dotenv-cli": "^5.0.0", 33 | "jsonwebtoken": "^8.5.1", 34 | "reflect-metadata": "^0.1.13", 35 | "tsyringe": "^4.6.0" 36 | } 37 | } -------------------------------------------------------------------------------- /services/AuthenticationService/serverless-secrets.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "SECURITY_GROUP_ID": "", 3 | "SUBNET_ID1": "", 4 | "SUBNET_ID2": "", 5 | "SUBNET_ID3": "" 6 | } -------------------------------------------------------------------------------- /services/AuthenticationService/serverless.yml: -------------------------------------------------------------------------------- 1 | service: authentication-service-serverless 2 | 3 | frameworkVersion: '3' 4 | 5 | plugins: 6 | - serverless-plugin-typescript 7 | - serverless-dotenv-plugin 8 | 9 | provider: 10 | name: aws 11 | runtime: nodejs14.x 12 | memorySize: 512 13 | region: sa-east-1 14 | vpc: 15 | securityGroupIds: 16 | - ${file(./serverless-secrets.json):SECURITY_GROUP_ID} 17 | subnetIds: 18 | - ${file(./serverless-secrets.json):SUBNET_ID1} 19 | - ${file(./serverless-secrets.json):SUBNET_ID2} 20 | - ${file(./serverless-secrets.json):SUBNET_ID3} 21 | 22 | functions: 23 | EnsureAuthenticated: 24 | handler: src/infra/serverless/functions/EnsureAuthenticated.handle 25 | EnsureAdmin: 26 | handler: src/infra/serverless/functions/EnsureAdmin.handle 27 | SignInUser: 28 | handler: src/infra/serverless/functions/SignInUser.handle 29 | events: 30 | - http: 31 | path: /sessions 32 | method: POST 33 | 34 | package: 35 | exclude: 36 | - ./node_modules/prisma/** 37 | 38 | custom: 39 | dotenv: 40 | path: ".env.development" -------------------------------------------------------------------------------- /services/AuthenticationService/src/container/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { JwtAuthenticationTokenProvider } from "../infra/authentication/JwtAuthenticationTokenProvider"; 4 | import { PrismaUsersRepository } from "../infra/database/prisma/repositories/PrismaUsersRepository"; 5 | import { BcryptEncoder } from "../infra/encoder/BcryptEncoder"; 6 | import { IAuthenticationTokenProvider } from "../useCases/authentication/ports/IAuthenticationTokenProvider"; 7 | import { IEncoder } from "../useCases/authentication/ports/IEncoder"; 8 | import { IUsersRepository } from "../useCases/authentication/ports/IUsersRepository"; 9 | 10 | container.registerSingleton( 11 | "UsersRepository", 12 | PrismaUsersRepository 13 | ); 14 | 15 | container.registerSingleton( 16 | "Encoder", 17 | BcryptEncoder 18 | ); 19 | 20 | container.registerSingleton( 21 | "AuthenticationTokenProvider", 22 | JwtAuthenticationTokenProvider 23 | ); 24 | -------------------------------------------------------------------------------- /services/AuthenticationService/src/controllers/authentication/SignInUserController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { IncorrectCredentialsError } from "../../useCases/authentication/errors/IncorrectCredentialsError"; 4 | import { SignInUserUseCase } from "../../useCases/authentication/SignInUserUseCase"; 5 | import { MissingParamError } from "../errors/MissingParamError"; 6 | import { IController } from "../ports/IController"; 7 | import { IHttpRequest } from "../ports/IHttpRequest"; 8 | import { IHttpResponse } from "../ports/IHttpResponse"; 9 | import { badRequest, forbidden, ok, serverError } from "../utils/HttpResponses"; 10 | import { IsRequiredParamsMissing } from "../utils/IsRequiredParamsMissing"; 11 | 12 | export class SignInUserController implements IController { 13 | readonly requiredParams = ["email", "password"]; 14 | 15 | async handle(request: IHttpRequest): Promise { 16 | try { 17 | const signInUserUseCase = container.resolve(SignInUserUseCase); 18 | 19 | const requiredParamsMissing = IsRequiredParamsMissing(request.body, this.requiredParams) 20 | 21 | if (requiredParamsMissing) { 22 | return badRequest(new MissingParamError(requiredParamsMissing).message); 23 | }; 24 | 25 | const { email, password } = request.body; 26 | 27 | 28 | const response = await signInUserUseCase.execute({ 29 | email, 30 | password 31 | }); 32 | 33 | if (response.value instanceof IncorrectCredentialsError) { 34 | return forbidden(response.value.message); 35 | }; 36 | 37 | return ok(response.value); 38 | } catch (error) { 39 | console.error(error); 40 | return serverError(error); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /services/AuthenticationService/src/controllers/errors/MissingParamError.ts: -------------------------------------------------------------------------------- 1 | export class MissingParamError extends Error { 2 | constructor(missingParams: string) { 3 | super(`Missing parameter(s): ${missingParams}.`); 4 | this.name = "MissingParamError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/controllers/ports/IController.ts: -------------------------------------------------------------------------------- 1 | import { IHttpRequest } from "./IHttpRequest"; 2 | import { IHttpResponse } from "./IHttpResponse"; 3 | 4 | export interface IController { 5 | readonly requiredParams?: string[]; 6 | handle(request: IHttpRequest): Promise; 7 | } -------------------------------------------------------------------------------- /services/AuthenticationService/src/controllers/ports/IHttpRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpRequest { 2 | user?: { 3 | id?: string; 4 | }; 5 | body?: any; 6 | params?: any; 7 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/controllers/ports/IHttpResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpResponse { 2 | statusCode: number; 3 | body: string; 4 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/controllers/utils/HttpResponses.ts: -------------------------------------------------------------------------------- 1 | import { IHttpResponse } from '../ports/IHttpResponse' 2 | 3 | export const ok = (data: any): IHttpResponse => ({ 4 | statusCode: 200, 5 | body: data 6 | }) 7 | 8 | export const forbidden = (error: any): IHttpResponse => ({ 9 | statusCode: 403, 10 | body: error 11 | }) 12 | 13 | export const badRequest = (error: any): IHttpResponse => ({ 14 | statusCode: 400, 15 | body: error 16 | }) 17 | 18 | export const serverError = (error: any): IHttpResponse => ({ 19 | statusCode: 500, 20 | body: error 21 | }) -------------------------------------------------------------------------------- /services/AuthenticationService/src/controllers/utils/IsRequiredParamsMissing.ts: -------------------------------------------------------------------------------- 1 | export function IsRequiredParamsMissing(receivedParams: any, requiredParams: string[]): string { 2 | const missingParams: string[] = []; 3 | 4 | requiredParams.forEach(param => { 5 | if (!Object.keys(receivedParams).includes(param)) { 6 | missingParams.push(param); 7 | } 8 | 9 | return false; 10 | }); 11 | 12 | return missingParams.join(', '); 13 | } -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/authentication/JwtAuthenticationTokenProvider.ts: -------------------------------------------------------------------------------- 1 | import { sign, verify } from 'jsonwebtoken'; 2 | 3 | import { IAuthenticationTokenProvider } from "../../useCases/authentication/ports/IAuthenticationTokenProvider"; 4 | import authConfig from './config'; 5 | 6 | const { secretKey, expiresIn } = authConfig; 7 | 8 | export class JwtAuthenticationTokenProvider implements IAuthenticationTokenProvider { 9 | generateToken(subject: string): string { 10 | const token = sign({}, secretKey, { 11 | subject, 12 | expiresIn, 13 | }); 14 | 15 | return token; 16 | } 17 | 18 | verify(token: string): string | undefined { 19 | try { 20 | const { sub } = verify(token, secretKey); 21 | 22 | return sub?.toString(); 23 | } catch (err) { 24 | return undefined; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/authentication/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | secretKey: process.env.TOKEN_SECRET_KEY || "secretKey", 3 | expiresIn: '1d', 4 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/database/prisma/PrismaClient.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const prismaClient = new PrismaClient(); -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/database/prisma/migrations/20220406010128_add_users/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "users" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | "password" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "isAdmin" BOOLEAN NOT NULL DEFAULT false, 10 | 11 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); 16 | -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/database/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/database/prisma/repositories/PrismaUsersRepository.ts: -------------------------------------------------------------------------------- 1 | import { IUserData } from '../../../../useCases/authentication/ports/IUserData'; 2 | import { IUsersRepository } from '../../../../useCases/authentication/ports/IUsersRepository'; 3 | import { prismaClient } from '../PrismaClient'; 4 | 5 | export class PrismaUsersRepository implements IUsersRepository { 6 | async create(data: IUserData): Promise> { 7 | const userCreated = await prismaClient.users.create({ 8 | data: { 9 | id: data.id, 10 | name: data.name, 11 | email: data.email, 12 | password: data.password, 13 | createdAt: data.createdAt, 14 | }, 15 | select: { 16 | id: true, 17 | name: true, 18 | email: true, 19 | createdAt: true, 20 | updatedAt: true, 21 | } 22 | }); 23 | 24 | return userCreated; 25 | }; 26 | 27 | async findByEmail(email: string): Promise { 28 | const user = await prismaClient.users.findFirst({ 29 | where: { 30 | email, 31 | } 32 | }); 33 | 34 | return user; 35 | }; 36 | 37 | async findById(id: string): Promise { 38 | const user = await prismaClient.users.findFirst({ 39 | where: { 40 | id, 41 | }, 42 | }); 43 | 44 | return user; 45 | }; 46 | } -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/database/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | binaryTargets = ["native", "rhel-openssl-1.0.x"] 4 | } 5 | 6 | datasource db { 7 | url = env("DATABASE_URL") 8 | provider = "postgresql" 9 | } 10 | 11 | model Users { 12 | id String @id @default(uuid()) 13 | name String 14 | email String @unique 15 | password String 16 | createdAt DateTime @default(now()) 17 | updatedAt DateTime @default(now()) 18 | isAdmin Boolean @default(false) 19 | 20 | @@map("users") 21 | } 22 | -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/encoder/BcryptEncoder.ts: -------------------------------------------------------------------------------- 1 | import { compare, hash } from 'bcrypt'; 2 | 3 | import { IEncoder } from "../../useCases/authentication/ports/IEncoder"; 4 | 5 | export class BcryptEncoder implements IEncoder { 6 | async encode(plain: string): Promise { 7 | const hashedString = await hash(plain, 8); 8 | 9 | return hashedString; 10 | }; 11 | 12 | async compare(plain: string, hash: string): Promise { 13 | const isEqual = await compare(plain, hash); 14 | 15 | return isEqual; 16 | } 17 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/serverless/functions/EnsureAdmin.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { container } from 'tsyringe'; 3 | import "../../../container"; 4 | 5 | import { EnsureAdmin } from '../../../middlewares/ensureAdminMiddleware'; 6 | 7 | export const handle = async (event: any) => { 8 | const ensureAdminMiddleware = container.resolve(EnsureAdmin); 9 | 10 | const response = await ensureAdminMiddleware.handle(event); 11 | 12 | if (response.isLeft()) { 13 | return { 14 | "isAuthorized": false, 15 | } 16 | } 17 | 18 | const user = response.value; 19 | 20 | return { 21 | "isAuthorized": true, 22 | "context": { 23 | "user": user, 24 | }, 25 | }; 26 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/serverless/functions/EnsureAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { container } from 'tsyringe'; 3 | import "../../../container"; 4 | 5 | import { EnsureAuthenticated } from '../../../middlewares/ensureAuthenticatedMiddleware'; 6 | 7 | export const handle = async (event: any) => { 8 | const ensureAuthenticatedMiddleware = container.resolve(EnsureAuthenticated); 9 | 10 | const response = await ensureAuthenticatedMiddleware.handle(event); 11 | 12 | if (response.isLeft()) { 13 | return { 14 | "isAuthorized": false, 15 | } 16 | } 17 | 18 | const user = response.value; 19 | 20 | return { 21 | "isAuthorized": true, 22 | "context": { 23 | "user": user, 24 | }, 25 | }; 26 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/serverless/functions/SignInUser.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "../../../container"; 3 | 4 | import { SignInUserController } from "../../../controllers/authentication/SignInUserController" 5 | import { MiddyMiddleware } from "../utils/commonMiddleware"; 6 | 7 | const SignInUser = async (event: any) => { 8 | const signInUserController = new SignInUserController(); 9 | 10 | const response = await signInUserController.handle(event); 11 | 12 | return { 13 | statusCode: response.statusCode, 14 | body: JSON.stringify(response.body) 15 | }; 16 | }; 17 | 18 | export const handle = MiddyMiddleware(SignInUser); -------------------------------------------------------------------------------- /services/AuthenticationService/src/infra/serverless/utils/commonMiddleware.ts: -------------------------------------------------------------------------------- 1 | import middy from '@middy/core'; 2 | import httpJsonBodyParser from '@middy/http-json-body-parser'; 3 | import httpEventNormalizer from '@middy/http-event-normalizer'; 4 | import httpErrorHandler from '@middy/http-error-handler'; 5 | import cors from '@middy/http-cors'; 6 | 7 | export function MiddyMiddleware(handler: any) { 8 | return middy(handler) 9 | .use([ 10 | httpJsonBodyParser(), 11 | httpEventNormalizer(), 12 | httpErrorHandler(), 13 | cors(), 14 | ]); 15 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/logic/Either.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | export type Either = Left | Right; 3 | export class Left { 4 | readonly value: L; 5 | 6 | constructor(value: L) { 7 | this.value = value; 8 | } 9 | 10 | isLeft(): this is Left { 11 | return true; 12 | } 13 | 14 | isRight(): this is Right { 15 | return false; 16 | } 17 | } 18 | 19 | export class Right { 20 | readonly value: A; 21 | 22 | constructor(value: A) { 23 | this.value = value; 24 | } 25 | 26 | isLeft(): this is Left { 27 | return false; 28 | } 29 | 30 | isRight(): this is Right { 31 | return true; 32 | } 33 | } 34 | 35 | export const left = (l: L): Either => { 36 | return new Left(l); 37 | }; 38 | 39 | export const right = (a: A): Either => { 40 | return new Right(a); 41 | }; 42 | -------------------------------------------------------------------------------- /services/AuthenticationService/src/middlewares/ensureAdminMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import { Either, left, right } from "../logic/Either"; 3 | 4 | import { IAuthenticationTokenProvider } from "../useCases/authentication/ports/IAuthenticationTokenProvider"; 5 | import { IUsersRepository } from "../useCases/authentication/ports/IUsersRepository"; 6 | import { IHttpRequest } from "./ports/IHttpRequest"; 7 | import { IMiddleware } from "./ports/IMiddleware"; 8 | 9 | interface IResponse { 10 | id: string; 11 | name: string; 12 | email: string; 13 | } 14 | 15 | @injectable() 16 | export class EnsureAdmin implements IMiddleware { 17 | constructor( 18 | @inject("AuthenticationTokenProvider") 19 | private authenticationTokenProvider: IAuthenticationTokenProvider, 20 | @inject("UsersRepository") 21 | private usersRepository: IUsersRepository, 22 | ) { } 23 | 24 | async handle(request: IHttpRequest): Promise> { 25 | try { 26 | if (!request.headers.authorization) { 27 | return left(false); 28 | } 29 | 30 | const authHeader = request.headers.authorization; 31 | 32 | const [, token] = authHeader.split(" "); 33 | 34 | const user_id = this.authenticationTokenProvider.verify( 35 | token, 36 | ); 37 | 38 | if (!user_id) { 39 | return left(false); 40 | } 41 | 42 | const user = await this.usersRepository.findById(user_id); 43 | 44 | if (!user) { 45 | return left(false); 46 | } 47 | 48 | if (!user.isAdmin) { 49 | return left(false); 50 | } 51 | 52 | const userResponse = { 53 | id: user.id, 54 | name: user.name, 55 | email: user.email, 56 | } 57 | 58 | return right(userResponse); 59 | } catch (error) { 60 | console.error(error); 61 | return left(false); 62 | }; 63 | }; 64 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/middlewares/ensureAuthenticatedMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import { Either, left, right } from "../logic/Either"; 3 | 4 | import { IAuthenticationTokenProvider } from "../useCases/authentication/ports/IAuthenticationTokenProvider"; 5 | import { IUsersRepository } from "../useCases/authentication/ports/IUsersRepository"; 6 | import { IHttpRequest } from "./ports/IHttpRequest"; 7 | import { IMiddleware } from "./ports/IMiddleware"; 8 | 9 | interface IResponse { 10 | id: string; 11 | name: string; 12 | email: string; 13 | } 14 | 15 | @injectable() 16 | export class EnsureAuthenticated implements IMiddleware { 17 | constructor( 18 | @inject("AuthenticationTokenProvider") 19 | private authenticationTokenProvider: IAuthenticationTokenProvider, 20 | @inject("UsersRepository") 21 | private usersRepository: IUsersRepository, 22 | ) { } 23 | 24 | async handle(request: IHttpRequest): Promise> { 25 | try { 26 | if (!request.headers.authorization) { 27 | return left(false); 28 | } 29 | 30 | const authHeader = request.headers.authorization; 31 | 32 | const [, token] = authHeader.split(" "); 33 | 34 | const user_id = this.authenticationTokenProvider.verify( 35 | token, 36 | ); 37 | 38 | if (!user_id) { 39 | return left(false); 40 | } 41 | 42 | const user = await this.usersRepository.findById(user_id); 43 | 44 | if (!user) { 45 | return left(false); 46 | } 47 | 48 | const userResponse = { 49 | id: user.id, 50 | name: user.name, 51 | email: user.email, 52 | } 53 | 54 | return right(userResponse); 55 | } catch (error) { 56 | console.error(error); 57 | return left(false); 58 | }; 59 | }; 60 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/middlewares/ports/IHttpRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpRequest { 2 | user?: { 3 | id: string; 4 | }; 5 | headers: { 6 | authorization?: string; 7 | }; 8 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/middlewares/ports/IMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { IHttpRequest } from "./IHttpRequest"; 2 | 3 | export interface IMiddleware { 4 | handle(request: IHttpRequest): Promise 5 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/useCases/authentication/SignInUserUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { Either, left, right } from "../../logic/Either"; 4 | import { IncorrectCredentialsError } from "./errors/IncorrectCredentialsError"; 5 | import { IAuthenticationTokenProvider } from "./ports/IAuthenticationTokenProvider"; 6 | import { IEncoder } from "./ports/IEncoder"; 7 | 8 | import { IUsersRepository } from "./ports/IUsersRepository"; 9 | 10 | interface IRequest { 11 | email: string; 12 | password: string; 13 | }; 14 | 15 | interface IResponse { 16 | user: { 17 | name: string; 18 | email: string; 19 | }, 20 | token: string; 21 | }; 22 | 23 | @injectable() 24 | export class SignInUserUseCase { 25 | constructor( 26 | @inject("UsersRepository") 27 | private usersRepository: IUsersRepository, 28 | @inject("Encoder") 29 | private encoder: IEncoder, 30 | @inject("AuthenticationTokenProvider") 31 | private authenticationTokenProvider: IAuthenticationTokenProvider, 32 | ) { } 33 | 34 | async execute({ email, password }: IRequest): Promise> { 35 | const user = await this.usersRepository.findByEmail(email); 36 | 37 | if (!user) { 38 | return left(new IncorrectCredentialsError()); 39 | } 40 | 41 | const passwordMatch = await this.encoder.compare(password, user.password); 42 | 43 | if (!passwordMatch) { 44 | return left(new IncorrectCredentialsError()); 45 | } 46 | 47 | const token = this.authenticationTokenProvider.generateToken(user.id); 48 | 49 | return right({ 50 | user: { 51 | name: user.name, 52 | email: user.email 53 | }, 54 | token, 55 | }); 56 | }; 57 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/useCases/authentication/errors/IncorrectCredentialsError.ts: -------------------------------------------------------------------------------- 1 | export class IncorrectCredentialsError extends Error { 2 | constructor() { 3 | super("Email or password incorrect"); 4 | this.name = "IncorrectCredentialsError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/useCases/authentication/ports/IAuthenticationTokenProvider.ts: -------------------------------------------------------------------------------- 1 | export interface IAuthenticationTokenProvider { 2 | generateToken(subject: string): string; 3 | verify(token: string): string | undefined; 4 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/useCases/authentication/ports/IEncoder.ts: -------------------------------------------------------------------------------- 1 | export interface IEncoder { 2 | encode(plain: string): Promise; 3 | compare(plain: string, hash: string): Promise; 4 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/src/useCases/authentication/ports/IUserData.ts: -------------------------------------------------------------------------------- 1 | export interface IUserData { 2 | id: string; 3 | name: string; 4 | email: string; 5 | password: string; 6 | createdAt: Date; 7 | updatedAt: Date; 8 | isAdmin: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /services/AuthenticationService/src/useCases/authentication/ports/IUsersRepository.ts: -------------------------------------------------------------------------------- 1 | import { IUserData } from "./IUserData"; 2 | 3 | export interface IUsersRepository { 4 | create(data: IUserData): Promise>; 5 | findByEmail(email: string): Promise; 6 | findById(id: string): Promise; 7 | }; 8 | -------------------------------------------------------------------------------- /services/AuthenticationService/tests/doubles/UsersActions.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { IUserData } from "../../src/useCases/authentication/ports/IUserData"; 4 | import { IUsersRepository } from "../../src/useCases/authentication/ports/IUsersRepository"; 5 | 6 | @injectable() 7 | export class UsersActions { 8 | constructor( 9 | @inject("UsersRepository") 10 | private usersRepository: IUsersRepository, 11 | ) { } 12 | 13 | async create(data: IUserData): Promise> { 14 | const user = await this.usersRepository.create(data); 15 | 16 | return user; 17 | }; 18 | }; -------------------------------------------------------------------------------- /services/AuthenticationService/tests/doubles/repositories/UsersRepositoryInMemory.ts: -------------------------------------------------------------------------------- 1 | import { IUserData } from "../../../src/useCases/authentication/ports/IUserData"; 2 | import { IUsersRepository } from "../../../src/useCases/authentication/ports/IUsersRepository"; 3 | 4 | export class UsersRepositoryInMemory implements IUsersRepository { 5 | users: IUserData[] = []; 6 | 7 | async create(data: IUserData): Promise> { 8 | this.users.push(data); 9 | 10 | const user = { 11 | id: data.id, 12 | name: data.name, 13 | email: data.email, 14 | createdAt: data.createdAt, 15 | updatedAt: data.updatedAt, 16 | }; 17 | 18 | return user; 19 | }; 20 | 21 | async findByEmail(email: string): Promise { 22 | const user = this.users.find((user) => user.email === email); 23 | 24 | return user || null; 25 | }; 26 | 27 | async findById(id: string): Promise { 28 | const user = this.users.find((user) => user.id === id); 29 | 30 | return user || null; 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /services/AuthenticationService/tests/infra/serverless/EnsureAdmin.spec.ts: -------------------------------------------------------------------------------- 1 | import { JwtAuthenticationTokenProvider } from "../../../src/infra/authentication/JwtAuthenticationTokenProvider"; 2 | import { prismaClient } from "../../../src/infra/database/prisma/PrismaClient"; 3 | import { handle } from "../../../src/infra/serverless/functions/EnsureAdmin"; 4 | 5 | describe("Ensure Admin Serverless Function", () => { 6 | let dateNow: Date; 7 | let jwt: string; 8 | 9 | beforeAll(async () => { 10 | dateNow = new Date(); 11 | const jwtAuthenticationProvider = new JwtAuthenticationTokenProvider() 12 | jwt = jwtAuthenticationProvider.generateToken("admin-id"); 13 | 14 | await prismaClient.$connect(); 15 | await prismaClient.users.create({ 16 | data: { 17 | id: "admin-id", 18 | name: "Admin", 19 | email: "admin@example.com", 20 | password: "password", 21 | isAdmin: true, 22 | createdAt: dateNow, 23 | updatedAt: dateNow, 24 | } 25 | }); 26 | }); 27 | 28 | afterAll(async () => { 29 | await prismaClient.users.deleteMany(); 30 | await prismaClient.$disconnect(); 31 | }); 32 | 33 | it("should return isAuthorized true and user data", async () => { 34 | const event = { 35 | headers: { 36 | authorization: `Bearer ${jwt}` 37 | } 38 | }; 39 | 40 | const placeOrderServerless = await handle(event); 41 | 42 | const expectedResponse = { 43 | "isAuthorized": true, 44 | "context": { 45 | "user": { 46 | "id": "admin-id", 47 | "name": "Admin", 48 | "email": "admin@example.com" 49 | }, 50 | }, 51 | } 52 | 53 | expect(placeOrderServerless).toEqual(expectedResponse); 54 | }); 55 | }); -------------------------------------------------------------------------------- /services/AuthenticationService/tests/infra/serverless/EnsureAuthenticated.spec.ts: -------------------------------------------------------------------------------- 1 | import { JwtAuthenticationTokenProvider } from "../../../src/infra/authentication/JwtAuthenticationTokenProvider"; 2 | import { prismaClient } from "../../../src/infra/database/prisma/PrismaClient"; 3 | import { handle } from "../../../src/infra/serverless/functions/EnsureAuthenticated"; 4 | 5 | describe("Ensure Authenticated Serverless Function", () => { 6 | let dateNow: Date; 7 | let jwt: string; 8 | 9 | beforeAll(async () => { 10 | dateNow = new Date(); 11 | const jwtAuthenticationProvider = new JwtAuthenticationTokenProvider() 12 | jwt = jwtAuthenticationProvider.generateToken("user-id"); 13 | 14 | await prismaClient.$connect(); 15 | await prismaClient.users.create({ 16 | data: { 17 | id: "user-id", 18 | name: "User", 19 | email: "user@example.com", 20 | password: "password", 21 | isAdmin: false, 22 | createdAt: dateNow, 23 | updatedAt: dateNow, 24 | } 25 | }); 26 | }); 27 | 28 | afterAll(async () => { 29 | await prismaClient.users.deleteMany(); 30 | await prismaClient.$disconnect(); 31 | }); 32 | 33 | it("should return isAuthorized true and user data", async () => { 34 | const event = { 35 | headers: { 36 | authorization: `Bearer ${jwt}` 37 | } 38 | }; 39 | 40 | const placeOrderServerless = await handle(event); 41 | 42 | const expectedResponse = { 43 | "isAuthorized": true, 44 | "context": { 45 | "user": { 46 | "id": "user-id", 47 | "name": "User", 48 | "email": "user@example.com" 49 | }, 50 | }, 51 | } 52 | 53 | expect(placeOrderServerless).toEqual(expectedResponse); 54 | }); 55 | }); -------------------------------------------------------------------------------- /services/AuthenticationService/tests/infra/serverless/SignInUser.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "aws-lambda"; 2 | import { JwtAuthenticationTokenProvider } from "../../../src/infra/authentication/JwtAuthenticationTokenProvider"; 3 | import { prismaClient } from "../../../src/infra/database/prisma/PrismaClient"; 4 | import { BcryptEncoder } from "../../../src/infra/encoder/BcryptEncoder"; 5 | import { handle } from "../../../src/infra/serverless/functions/SignInUser"; 6 | 7 | describe("SignIn User Serverless Function", () => { 8 | let dateNow: Date; 9 | 10 | beforeAll(async () => { 11 | dateNow = new Date(); 12 | const bcryptEncoder = new BcryptEncoder() 13 | const hashedPassword = await bcryptEncoder.encode("user-password"); 14 | 15 | await prismaClient.$connect(); 16 | await prismaClient.users.create({ 17 | data: { 18 | id: "user-id", 19 | name: "User", 20 | email: "user@example.com", 21 | password: hashedPassword, 22 | isAdmin: false, 23 | createdAt: dateNow, 24 | updatedAt: dateNow, 25 | } 26 | }); 27 | }); 28 | 29 | afterAll(async () => { 30 | await prismaClient.users.deleteMany(); 31 | await prismaClient.$disconnect(); 32 | }); 33 | 34 | it("should return status code 200 and user and token on the body", async () => { 35 | const event = { 36 | body: { 37 | email: "user@example.com", 38 | password: "user-password", 39 | } 40 | }; 41 | 42 | const context = {} as Context; 43 | 44 | const signInUserServerless = await handle(event, context); 45 | 46 | const bodyParsed = JSON.parse(signInUserServerless.body); 47 | 48 | expect(bodyParsed).toHaveProperty("user"); 49 | expect(bodyParsed.user.name).toEqual("User"); 50 | expect(bodyParsed).toHaveProperty("token"); 51 | expect(signInUserServerless.statusCode).toEqual(200); 52 | }); 53 | }); -------------------------------------------------------------------------------- /services/CartService/.env.development.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://docker:password@ecommerce_database:5432/ecommerce_database?schema=public" -------------------------------------------------------------------------------- /services/CartService/.env.test.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://prisma_test:password@localhost:5433/ecommerce_test_database?schema=public" -------------------------------------------------------------------------------- /services/CartService/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .env.development 4 | .env.test -------------------------------------------------------------------------------- /services/CartService/@types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | declare namespace Express { 3 | export interface Request { 4 | user: { 5 | id: string; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /services/CartService/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cart-service", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": {}, 7 | "devDependencies": { 8 | "@types/express": "^4.17.13", 9 | "@types/jest": "^27.4.1", 10 | "@types/node": "^17.0.21", 11 | "git-commit-msg-linter": "^4.1.1", 12 | "jest": "^27.5.1", 13 | "prisma": "^3.10.0", 14 | "supertest": "^6.2.2", 15 | "ts-jest": "^27.1.3", 16 | "ts-node-dev": "^1.1.8", 17 | "typescript": "^4.6.2" 18 | }, 19 | "dependencies": { 20 | "@prisma/client": "^3.10.0", 21 | "@types/supertest": "^2.0.11", 22 | "dotenv-cli": "^5.0.0", 23 | "express": "^4.17.3", 24 | "reflect-metadata": "^0.1.13", 25 | "tsyringe": "^4.6.0" 26 | } 27 | } -------------------------------------------------------------------------------- /services/CartService/src/container/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erickivel/GoTech/2d91007862fe29933879a13f4d8d428b8f841820/services/CartService/src/container/index.ts -------------------------------------------------------------------------------- /services/CartService/src/controllers/errors/MissingParamError.ts: -------------------------------------------------------------------------------- 1 | export class MissingParamError extends Error { 2 | constructor(missingParams: string) { 3 | super(`Missing parameter(s): ${missingParams}.`); 4 | this.name = "MissingParamError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/CartService/src/controllers/ports/IController.ts: -------------------------------------------------------------------------------- 1 | import { IHttpRequest } from "./IHttpRequest"; 2 | import { IHttpResponse } from "./IHttpResponse"; 3 | 4 | export interface IController { 5 | readonly requiredParams?: string[]; 6 | handle(request: IHttpRequest): Promise; 7 | } -------------------------------------------------------------------------------- /services/CartService/src/controllers/ports/IHttpRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpRequest { 2 | user?: { 3 | id?: string; 4 | }; 5 | body?: any; 6 | params?: any; 7 | }; -------------------------------------------------------------------------------- /services/CartService/src/controllers/ports/IHttpResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpResponse { 2 | statusCode: number; 3 | body: any; 4 | }; -------------------------------------------------------------------------------- /services/CartService/src/controllers/utils/HttpResponses.ts: -------------------------------------------------------------------------------- 1 | import { IHttpResponse } from '../ports/IHttpResponse' 2 | 3 | export const ok = (data: any): IHttpResponse => ({ 4 | statusCode: 200, 5 | body: data 6 | }) 7 | 8 | export const created = (data: any): IHttpResponse => ({ 9 | statusCode: 201, 10 | body: data 11 | }) 12 | 13 | export const updated = (data: any): IHttpResponse => ({ 14 | statusCode: 201, 15 | body: data 16 | }) 17 | 18 | export const unauthorized = (error: any): IHttpResponse => ({ 19 | statusCode: 401, 20 | body: error 21 | }) 22 | 23 | export const forbidden = (error: any): IHttpResponse => ({ 24 | statusCode: 403, 25 | body: error 26 | }) 27 | 28 | export const badRequest = (error: any): IHttpResponse => ({ 29 | statusCode: 400, 30 | body: error 31 | }) 32 | 33 | export const serverError = (error: any): IHttpResponse => ({ 34 | statusCode: 500, 35 | body: error 36 | }) -------------------------------------------------------------------------------- /services/CartService/src/controllers/utils/IsRequiredParamsMissing.ts: -------------------------------------------------------------------------------- 1 | export function IsRequiredParamsMissing(receivedParams: any, requiredParams: string[]): string { 2 | const missingParams: string[] = []; 3 | 4 | requiredParams.forEach(param => { 5 | if (!Object.keys(receivedParams).includes(param)) { 6 | missingParams.push(param); 7 | } 8 | 9 | return false; 10 | }); 11 | 12 | return missingParams.join(', '); 13 | } -------------------------------------------------------------------------------- /services/CartService/src/infra/database/prisma/PrismaClient.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const prismaClient = new PrismaClient(); -------------------------------------------------------------------------------- /services/CartService/src/infra/database/prisma/schema.prisma: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erickivel/GoTech/2d91007862fe29933879a13f4d8d428b8f841820/services/CartService/src/infra/database/prisma/schema.prisma -------------------------------------------------------------------------------- /services/CartService/src/infra/http/app.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | 3 | import "../../container"; 4 | 5 | import express from "express"; 6 | 7 | import { router } from './routes'; 8 | 9 | const app = express(); 10 | 11 | app.use(express.json()); 12 | 13 | app.use(router); 14 | 15 | export { app }; -------------------------------------------------------------------------------- /services/CartService/src/infra/http/routes/RouteAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | import { IController } from "../../../controllers/ports/IController"; 4 | 5 | export function routeAdapter(controller: IController) { 6 | return async (request: Request, response: Response) => { 7 | const httpResponse = await controller.handle(request); 8 | response.status(httpResponse.statusCode).json(httpResponse.body); 9 | }; 10 | }; -------------------------------------------------------------------------------- /services/CartService/src/infra/http/server.ts: -------------------------------------------------------------------------------- 1 | import { app } from "./app"; 2 | 3 | app.listen(3333, () => { 4 | console.log("🚀 App is running on port 3333!") 5 | }); -------------------------------------------------------------------------------- /services/CartService/src/logic/Either.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | export type Either = Left | Right; 3 | export class Left { 4 | readonly value: L; 5 | 6 | constructor(value: L) { 7 | this.value = value; 8 | } 9 | 10 | isLeft(): this is Left { 11 | return true; 12 | } 13 | 14 | isRight(): this is Right { 15 | return false; 16 | } 17 | } 18 | 19 | export class Right { 20 | readonly value: A; 21 | 22 | constructor(value: A) { 23 | this.value = value; 24 | } 25 | 26 | isLeft(): this is Left { 27 | return false; 28 | } 29 | 30 | isRight(): this is Right { 31 | return true; 32 | } 33 | } 34 | 35 | export const left = (l: L): Either => { 36 | return new Left(l); 37 | }; 38 | 39 | export const right = (a: A): Either => { 40 | return new Right(a); 41 | }; 42 | -------------------------------------------------------------------------------- /services/OrderService/.env.development.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://docker:password@order_service_database:5432/order_service_database?schema=public" 2 | 3 | SQS_URL= -------------------------------------------------------------------------------- /services/OrderService/.env.test.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://prisma_test:password@localhost:5433/order_service_test_database?schema=public" -------------------------------------------------------------------------------- /services/OrderService/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | jspm_packages 3 | coverage 4 | .serverless 5 | serverless-secrets.json 6 | .env.development 7 | .env.test -------------------------------------------------------------------------------- /services/OrderService/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /usr/app 4 | 5 | COPY package.json ./ 6 | 7 | RUN yarn 8 | 9 | COPY . . 10 | 11 | EXPOSE 3333 12 | 13 | CMD ["yarn", "dev"] 14 | -------------------------------------------------------------------------------- /services/OrderService/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | order_service_database: 5 | image: postgres 6 | container_name: order_service_database 7 | restart: always 8 | ports: 9 | - 5432:5432 10 | environment: 11 | - POSTGRES_USER=docker 12 | - POSTGRES_PASSWORD=password 13 | - POSTGRES_DB=order_service_database 14 | volumes: 15 | - pgdata:/data/postgres 16 | order_service_test_database: 17 | image: postgres 18 | container_name: order_service_test_database 19 | restart: always 20 | ports: 21 | - 5433:5432 22 | environment: 23 | - POSTGRES_USER=prisma_test 24 | - POSTGRES_PASSWORD=password 25 | - POSTGRES_DB=order_service_test_database 26 | volumes: 27 | - pgdata:/data/postgres 28 | order_service_api: 29 | build: . 30 | container_name: order_service_api 31 | ports: 32 | - 3333:3333 33 | volumes: 34 | - .:/usr/app 35 | links: 36 | - order_service_database 37 | depends_on: 38 | - order_service_database 39 | 40 | volumes: 41 | pgdata: 42 | driver: local 43 | -------------------------------------------------------------------------------- /services/OrderService/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; -------------------------------------------------------------------------------- /services/OrderService/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "order-service", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "prisma:migrate": "dotenv -e .env.development -- prisma migrate dev --schema ./src/infra/database/prisma/schema.prisma", 8 | "dev": "dotenv -e .env.development -- prisma migrate dev --schema ./src/infra/database/prisma/schema.prisma && dotenv -e .env.development -- ts-node-dev --inspect --transpile-only --ignore-watch node_modules --respawn src/infra/http/server.ts", 9 | "test": "dotenv -e .env.test -- prisma migrate dev --schema ./src/infra/database/prisma/schema.prisma && dotenv -e .env.test -- jest --runInBand" 10 | }, 11 | "devDependencies": { 12 | "@types/aws-lambda": "^8.10.93", 13 | "@types/jest": "^27.4.1", 14 | "@types/node": "^17.0.21", 15 | "aws-sdk": "^2.1111.0", 16 | "git-commit-msg-linter": "^4.1.1", 17 | "jest": "^27.5.1", 18 | "prisma": "^3.10.0", 19 | "serverless-dotenv-plugin": "^3.12.2", 20 | "serverless-plugin-typescript": "^1.1.9", 21 | "serverless-pseudo-parameters": "^2.6.1", 22 | "ts-jest": "^27.1.3", 23 | "ts-node-dev": "^1.1.8", 24 | "typescript": "^4.6.2" 25 | }, 26 | "dependencies": { 27 | "@middy/core": "^2.5.7", 28 | "@middy/http-cors": "^2.5.7", 29 | "@middy/http-error-handler": "^2.5.7", 30 | "@middy/http-event-normalizer": "^2.5.7", 31 | "@middy/http-json-body-parser": "^2.5.7", 32 | "@prisma/client": "^3.10.0", 33 | "dotenv-cli": "^5.0.0", 34 | "reflect-metadata": "^0.1.13", 35 | "tsyringe": "^4.6.0" 36 | } 37 | } -------------------------------------------------------------------------------- /services/OrderService/serverless-secrets.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "SECURITY_GROUP_ID": "", 3 | "SUBNET_ID1": "", 4 | "SUBNET_ID2": "", 5 | "SUBNET_ID3": "", 6 | "SQS_ARN": "" 7 | } -------------------------------------------------------------------------------- /services/OrderService/serverless.yml: -------------------------------------------------------------------------------- 1 | service: order-service-serverless 2 | 3 | frameworkVersion: '3' 4 | 5 | plugins: 6 | - serverless-plugin-typescript 7 | - serverless-dotenv-plugin 8 | 9 | provider: 10 | name: aws 11 | runtime: nodejs14.x 12 | memorySize: 512 13 | region: sa-east-1 14 | vpc: 15 | securityGroupIds: 16 | - ${file(./serverless-secrets.json):SECURITY_GROUP_ID} 17 | subnetIds: 18 | - ${file(./serverless-secrets.json):SUBNET_ID1} 19 | - ${file(./serverless-secrets.json):SUBNET_ID2} 20 | - ${file(./serverless-secrets.json):SUBNET_ID3} 21 | iam: 22 | role: 23 | statements: 24 | - Effect: 'Allow' 25 | Action: 26 | - sqs:SendMessage 27 | Resource: ${file(./serverless-secrets.json):SQS_ARN} 28 | 29 | resources: 30 | Resources: 31 | SQS: 32 | Type: AWS::SQS::Queue 33 | Properties: 34 | QueueName: orders-NewOrder 35 | 36 | functions: 37 | PlaceOrder: 38 | handler: src/infra/serverless/functions/PlaceOrder.handle 39 | events: 40 | - http: 41 | method: POST 42 | path: /orders 43 | cors: true 44 | ListOrdersByUser: 45 | handler: src/infra/serverless/functions/ListOrdersByUser.handle 46 | events: 47 | - http: 48 | method: GET 49 | path: /orders 50 | cors: true 51 | 52 | package: 53 | exclude: 54 | - ./node_modules/prisma/** 55 | 56 | custom: 57 | dotenv: 58 | path: ".env.development" -------------------------------------------------------------------------------- /services/OrderService/src/container/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { PrismaOrdersRepository } from "../infra/database/prisma/repositories/PrismaOrdersRepository"; 4 | import { SQSMessagingAdapter } from "../infra/messaging/SQSMessagingAdapter"; 5 | import { IMessagingAdapter } from "../useCases/orders/ports/IMessagingAdapter"; 6 | import { IOrdersRepository } from "../useCases/orders/ports/IOrdersRepository"; 7 | 8 | container.registerSingleton( 9 | "OrdersRepository", 10 | PrismaOrdersRepository 11 | ); 12 | 13 | container.registerSingleton( 14 | "MessagingAdapter", 15 | SQSMessagingAdapter 16 | ); -------------------------------------------------------------------------------- /services/OrderService/src/controllers/errors/MissingParamError.ts: -------------------------------------------------------------------------------- 1 | export class MissingParamError extends Error { 2 | constructor(missingParams: string) { 3 | super(`Missing parameter(s): ${missingParams}.`); 4 | this.name = "MissingParamError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/OrderService/src/controllers/orders/ListOrdersByUserController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { ListOrdersByUserUseCase } from "../../useCases/orders/ListOrdersByUserUseCase"; 4 | import { IController } from "../ports/IController"; 5 | import { IHttpResponse } from "../ports/IHttpResponse"; 6 | import { IServerlessHttpRequest } from "../ports/IServerlessHttpRequest"; 7 | import { ok, serverError, unauthorized } from "../utils/HttpResponses"; 8 | 9 | export class ListOrdersByUserController implements IController { 10 | async handle(request: IServerlessHttpRequest): Promise { 11 | try { 12 | const authorizer = request.requestContext.authorizer 13 | 14 | if (!authorizer?.user || !authorizer.user?.id) { 15 | return unauthorized("User is not authenticated!"); 16 | }; 17 | 18 | const listOrdersByUserUseCase = container.resolve(ListOrdersByUserUseCase); 19 | 20 | const response = await listOrdersByUserUseCase.execute({ userId: authorizer.user.id }); 21 | 22 | return ok(response.value); 23 | } catch (error) { 24 | console.error(error); 25 | return serverError(error); 26 | }; 27 | }; 28 | }; -------------------------------------------------------------------------------- /services/OrderService/src/controllers/orders/PlaceOrderController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { PlaceOrderUseCase } from "../../useCases/orders/PlaceOrderUseCase"; 4 | import { MissingParamError } from "../errors/MissingParamError"; 5 | import { IController } from "../ports/IController"; 6 | import { IHttpResponse } from "../ports/IHttpResponse"; 7 | import { IServerlessHttpRequest } from "../ports/IServerlessHttpRequest"; 8 | import { badRequest, created, forbidden, serverError, unauthorized } from "../utils/HttpResponses"; 9 | import { IsRequiredParamsMissing } from "../utils/IsRequiredParamsMissing"; 10 | 11 | export class PlaceOrderController implements IController { 12 | requiredParams = ["products"]; 13 | 14 | async handle(request: any): Promise { 15 | try { 16 | const authorizer = request.requestContext.authorizer.lambda; 17 | 18 | if (!authorizer?.user || !authorizer.user?.id) { 19 | return unauthorized("User is not authenticated!"); 20 | }; 21 | 22 | const requiredParamsMissing = IsRequiredParamsMissing(request.body, this.requiredParams) 23 | 24 | if (requiredParamsMissing) { 25 | return badRequest(new MissingParamError(requiredParamsMissing).message); 26 | }; 27 | 28 | const { products } = request.body; 29 | 30 | const placeOrderUseCase = container.resolve(PlaceOrderUseCase); 31 | 32 | const user = { 33 | id: authorizer.user.id, 34 | name: authorizer.user.name, 35 | email: authorizer.user.email, 36 | }; 37 | 38 | const response = await placeOrderUseCase.execute({ products, user }); 39 | 40 | if (response.isLeft()) { 41 | return forbidden(response.value.message); 42 | } 43 | 44 | return created(response.value); 45 | } catch (error) { 46 | console.error(error); 47 | return serverError(error); 48 | }; 49 | }; 50 | }; -------------------------------------------------------------------------------- /services/OrderService/src/controllers/ports/IController.ts: -------------------------------------------------------------------------------- 1 | import { IHttpRequest } from "./IHttpRequest"; 2 | import { IHttpResponse } from "./IHttpResponse"; 3 | import { IServerlessHttpRequest } from "./IServerlessHttpRequest"; 4 | 5 | export interface IController { 6 | readonly requiredParams?: string[]; 7 | handle(request: IHttpRequest | IServerlessHttpRequest): Promise; 8 | } -------------------------------------------------------------------------------- /services/OrderService/src/controllers/ports/IHttpRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpRequest { 2 | user?: { 3 | id?: string; 4 | }; 5 | body?: any; 6 | params?: any; 7 | }; -------------------------------------------------------------------------------- /services/OrderService/src/controllers/ports/IHttpResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpResponse { 2 | statusCode: number; 3 | body: any; 4 | }; -------------------------------------------------------------------------------- /services/OrderService/src/controllers/ports/IServerlessHttpRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IServerlessHttpRequest { 2 | requestContext: { 3 | authorizer?: { 4 | user?: { 5 | id: string, 6 | name: string 7 | email: string 8 | }, 9 | }, 10 | }, 11 | pathParameters?: any, 12 | body?: any; 13 | }; -------------------------------------------------------------------------------- /services/OrderService/src/controllers/utils/HttpResponses.ts: -------------------------------------------------------------------------------- 1 | import { IHttpResponse } from '../ports/IHttpResponse' 2 | 3 | export const ok = (data: any): IHttpResponse => ({ 4 | statusCode: 200, 5 | body: data 6 | }) 7 | 8 | export const created = (data: any): IHttpResponse => ({ 9 | statusCode: 201, 10 | body: data 11 | }) 12 | 13 | export const updated = (data: any): IHttpResponse => ({ 14 | statusCode: 201, 15 | body: data 16 | }) 17 | 18 | export const unauthorized = (error: any): IHttpResponse => ({ 19 | statusCode: 401, 20 | body: error 21 | }) 22 | 23 | export const forbidden = (error: any): IHttpResponse => ({ 24 | statusCode: 403, 25 | body: error 26 | }) 27 | 28 | export const badRequest = (error: any): IHttpResponse => ({ 29 | statusCode: 400, 30 | body: error 31 | }) 32 | 33 | export const serverError = (error: any): IHttpResponse => ({ 34 | statusCode: 500, 35 | body: error 36 | }) -------------------------------------------------------------------------------- /services/OrderService/src/controllers/utils/IsRequiredParamsMissing.ts: -------------------------------------------------------------------------------- 1 | export function IsRequiredParamsMissing(receivedParams: any, requiredParams: string[]): string { 2 | const missingParams: string[] = []; 3 | 4 | requiredParams.forEach(param => { 5 | if (!Object.keys(receivedParams).includes(param)) { 6 | missingParams.push(param); 7 | } 8 | 9 | return false; 10 | }); 11 | 12 | return missingParams.join(', '); 13 | } -------------------------------------------------------------------------------- /services/OrderService/src/domain/entities/Order/errors/InvalidOrderTotalError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidOrderTotalError extends Error { 2 | constructor(total: number) { 3 | super(`"${total}" is an invalid order total.`); 4 | this.name = "InvalidOrderTotalError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/OrderService/src/domain/entities/Order/index.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | import { Either, left, right } from '../../../logic/Either'; 4 | import { InvalidOrderTotalError } from './errors/InvalidOrderTotalError'; 5 | 6 | type User = { 7 | id: string; 8 | name: string; 9 | email: string; 10 | } 11 | 12 | type Product = { 13 | id: string; 14 | name: string; 15 | price: number; 16 | amount: number; 17 | }; 18 | 19 | type OrderProps = { 20 | id?: string; 21 | user: User; 22 | products: Product[]; 23 | total: number; 24 | createdAt?: Date; 25 | updatedAt?: Date; 26 | }; 27 | 28 | export class Order { 29 | public readonly id: string; 30 | public readonly user: User; 31 | public readonly products: Product[]; 32 | public readonly total: number; 33 | public readonly createdAt: Date; 34 | public readonly updatedAt: Date; 35 | 36 | private constructor(props: OrderProps) { 37 | this.id = props.id || crypto.randomUUID(); 38 | this.user = props.user; 39 | this.products = props.products; 40 | this.total = props.total; 41 | this.createdAt = props.createdAt || new Date(); 42 | this.updatedAt = props.updatedAt || new Date(); 43 | }; 44 | 45 | static create( 46 | props: OrderProps 47 | ): Either< 48 | | InvalidOrderTotalError, 49 | Order 50 | > { 51 | const validate = Order.validate(props); 52 | 53 | if (validate.isLeft()) { 54 | return left(validate.value); 55 | }; 56 | 57 | const order = new Order(props); 58 | 59 | return right(order); 60 | }; 61 | 62 | public static validate( 63 | props: OrderProps 64 | ): Either< 65 | | InvalidOrderTotalError, 66 | true 67 | > { 68 | const { total } = props; 69 | 70 | let [, decimalPlaces] = String(total).split("."); 71 | 72 | decimalPlaces = decimalPlaces === undefined ? "0" : decimalPlaces; 73 | 74 | if (decimalPlaces.length > 2 || Number(total) < 0) { 75 | return left(new InvalidOrderTotalError(total)) 76 | }; 77 | 78 | return right(true); 79 | }; 80 | }; -------------------------------------------------------------------------------- /services/OrderService/src/infra/database/prisma/PrismaClient.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const prismaClient = new PrismaClient(); -------------------------------------------------------------------------------- /services/OrderService/src/infra/database/prisma/migrations/20220406140833_create_orders/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "users" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | 7 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE "products" ( 12 | "id" TEXT NOT NULL, 13 | "name" TEXT NOT NULL, 14 | "price" DECIMAL(8,2) NOT NULL, 15 | "amount" INTEGER NOT NULL, 16 | 17 | CONSTRAINT "products_pkey" PRIMARY KEY ("id") 18 | ); 19 | 20 | -- CreateTable 21 | CREATE TABLE "orders" ( 22 | "id" TEXT NOT NULL, 23 | "total" DECIMAL(8,2) NOT NULL, 24 | "userId" TEXT NOT NULL, 25 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 26 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 27 | 28 | CONSTRAINT "orders_pkey" PRIMARY KEY ("id") 29 | ); 30 | 31 | -- CreateTable 32 | CREATE TABLE "_OrdersToProducts" ( 33 | "A" TEXT NOT NULL, 34 | "B" TEXT NOT NULL 35 | ); 36 | 37 | -- CreateIndex 38 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); 39 | 40 | -- CreateIndex 41 | CREATE UNIQUE INDEX "products_name_key" ON "products"("name"); 42 | 43 | -- CreateIndex 44 | CREATE UNIQUE INDEX "_OrdersToProducts_AB_unique" ON "_OrdersToProducts"("A", "B"); 45 | 46 | -- CreateIndex 47 | CREATE INDEX "_OrdersToProducts_B_index" ON "_OrdersToProducts"("B"); 48 | 49 | -- AddForeignKey 50 | ALTER TABLE "orders" ADD CONSTRAINT "orders_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 51 | 52 | -- AddForeignKey 53 | ALTER TABLE "_OrdersToProducts" ADD FOREIGN KEY ("A") REFERENCES "orders"("id") ON DELETE CASCADE ON UPDATE CASCADE; 54 | 55 | -- AddForeignKey 56 | ALTER TABLE "_OrdersToProducts" ADD FOREIGN KEY ("B") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE; 57 | -------------------------------------------------------------------------------- /services/OrderService/src/infra/database/prisma/migrations/20220406210700_add_product_amount/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `amount` on the `products` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "products" DROP COLUMN "amount"; 9 | 10 | -- CreateTable 11 | CREATE TABLE "productAmount" ( 12 | "id" TEXT NOT NULL, 13 | "amount" INTEGER NOT NULL, 14 | "productId" TEXT NOT NULL, 15 | "ordersId" TEXT NOT NULL, 16 | 17 | CONSTRAINT "productAmount_pkey" PRIMARY KEY ("id") 18 | ); 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "productAmount" ADD CONSTRAINT "productAmount_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "productAmount" ADD CONSTRAINT "productAmount_ordersId_fkey" FOREIGN KEY ("ordersId") REFERENCES "orders"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 25 | -------------------------------------------------------------------------------- /services/OrderService/src/infra/database/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /services/OrderService/src/infra/database/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | binaryTargets = ["native", "rhel-openssl-1.0.x"] 4 | } 5 | 6 | datasource db { 7 | url = env("DATABASE_URL") 8 | provider = "postgresql" 9 | } 10 | 11 | model Users { 12 | id String @id 13 | name String 14 | email String @unique 15 | 16 | orders Orders[] 17 | 18 | @@map("users") 19 | } 20 | 21 | model Products { 22 | id String @id 23 | name String @unique 24 | price Decimal @db.Decimal(8, 2) 25 | 26 | orders Orders[] 27 | ProductAmount ProductAmount[] 28 | 29 | @@map("products") 30 | } 31 | 32 | model ProductAmount { 33 | id String @id @default(uuid()) 34 | amount Int 35 | productId String 36 | ordersId String 37 | 38 | product Products @relation(fields: [productId], references: [id]) 39 | Orders Orders @relation(fields: [ordersId], references: [id]) 40 | 41 | @@map("productAmount") 42 | } 43 | 44 | model Orders { 45 | id String @id @default(uuid()) 46 | total Decimal @db.Decimal(8, 2) 47 | userId String 48 | createdAt DateTime @default(now()) 49 | updatedAt DateTime @default(now()) 50 | 51 | user Users @relation(fields: [userId], references: [id]) 52 | products Products[] 53 | productsAmount ProductAmount[] 54 | 55 | @@map("orders") 56 | } 57 | -------------------------------------------------------------------------------- /services/OrderService/src/infra/messaging/SQSClient.ts: -------------------------------------------------------------------------------- 1 | import { SQS } from "aws-sdk"; 2 | 3 | export const sqsClient = new SQS({ 4 | region: "sa-east-1", 5 | apiVersion: '2012-11-05' 6 | }); -------------------------------------------------------------------------------- /services/OrderService/src/infra/messaging/SQSMessagingAdapter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IMessagingAdapter } from "../../useCases/orders/ports/IMessagingAdapter"; 3 | import { sqsClient } from "./SQSClient"; 4 | 5 | export class SQSMessagingAdapter implements IMessagingAdapter { 6 | async sendMessage(message: string): Promise { 7 | try { 8 | await sqsClient.sendMessage({ 9 | QueueUrl: process.env.SQS_URL || "sqsurl", 10 | MessageBody: message, 11 | }).promise(); 12 | } catch (error) { 13 | console.error(error); 14 | } 15 | }; 16 | } -------------------------------------------------------------------------------- /services/OrderService/src/infra/serverless/functions/ListOrdersByUser.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "../../../container"; 3 | 4 | import { ListOrdersByUserController } from "../../../controllers/orders/ListOrdersByUserController"; 5 | import { MiddyMiddleware } from "../utils/commonMiddleware"; 6 | 7 | const ListOrdersByUser = async (event: any) => { 8 | const listOrdersByUserController = new ListOrdersByUserController(); 9 | 10 | console.log(event) 11 | 12 | const response = await listOrdersByUserController.handle(event); 13 | 14 | return { 15 | statusCode: response.statusCode, 16 | body: JSON.stringify(response.body) 17 | }; 18 | }; 19 | 20 | export const handle = MiddyMiddleware(ListOrdersByUser); -------------------------------------------------------------------------------- /services/OrderService/src/infra/serverless/functions/PlaceOrder.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "../../../container"; 3 | 4 | import { PlaceOrderController } from "../../../controllers/orders/PlaceOrderController"; 5 | import { MiddyMiddleware } from "../utils/commonMiddleware"; 6 | 7 | const PlaceOrder = async (event: any) => { 8 | const placeOrderController = new PlaceOrderController(); 9 | 10 | console.log(event); 11 | 12 | const response = await placeOrderController.handle(event); 13 | 14 | return { 15 | statusCode: response.statusCode, 16 | body: JSON.stringify(response.body) 17 | }; 18 | }; 19 | 20 | export const handle = MiddyMiddleware(PlaceOrder); -------------------------------------------------------------------------------- /services/OrderService/src/infra/serverless/utils/commonMiddleware.ts: -------------------------------------------------------------------------------- 1 | import middy from '@middy/core'; 2 | import httpJsonBodyParser from '@middy/http-json-body-parser'; 3 | import httpEventNormalizer from '@middy/http-event-normalizer'; 4 | import httpErrorHandler from '@middy/http-error-handler'; 5 | import cors from '@middy/http-cors'; 6 | 7 | export function MiddyMiddleware(handler: any) { 8 | return middy(handler) 9 | .use([ 10 | httpJsonBodyParser(), 11 | httpEventNormalizer(), 12 | httpErrorHandler(), 13 | cors(), 14 | ]); 15 | }; -------------------------------------------------------------------------------- /services/OrderService/src/logic/Either.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | export type Either = Left | Right; 3 | export class Left { 4 | readonly value: L; 5 | 6 | constructor(value: L) { 7 | this.value = value; 8 | } 9 | 10 | isLeft(): this is Left { 11 | return true; 12 | } 13 | 14 | isRight(): this is Right { 15 | return false; 16 | } 17 | } 18 | 19 | export class Right { 20 | readonly value: A; 21 | 22 | constructor(value: A) { 23 | this.value = value; 24 | } 25 | 26 | isLeft(): this is Left { 27 | return false; 28 | } 29 | 30 | isRight(): this is Right { 31 | return true; 32 | } 33 | } 34 | 35 | export const left = (l: L): Either => { 36 | return new Left(l); 37 | }; 38 | 39 | export const right = (a: A): Either => { 40 | return new Right(a); 41 | }; 42 | -------------------------------------------------------------------------------- /services/OrderService/src/useCases/orders/ListOrdersByUserUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'tsyringe'; 2 | 3 | import { Either, right } from "../../logic/Either"; 4 | 5 | import { IOrderData } from './ports/IOrderData'; 6 | import { IOrdersRepository } from './ports/IOrdersRepository'; 7 | 8 | interface IRequest { 9 | userId: string; 10 | }; 11 | 12 | @injectable() 13 | export class ListOrdersByUserUseCase { 14 | constructor( 15 | @inject("OrdersRepository") 16 | private ordersRepository: IOrdersRepository, 17 | ) { } 18 | 19 | async execute({ userId }: IRequest): Promise< 20 | Either 21 | > { 22 | const orders = await this.ordersRepository.filterByUserId(userId); 23 | 24 | return right(orders); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /services/OrderService/src/useCases/orders/PlaceOrderUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'tsyringe'; 2 | 3 | import { Either, left, right } from "../../logic/Either"; 4 | 5 | import { InvalidOrderTotalError } from '../../domain/entities/Order/errors/InvalidOrderTotalError'; 6 | import { IOrderData } from './ports/IOrderData'; 7 | import { IOrdersRepository } from './ports/IOrdersRepository'; 8 | import { Order } from '../../domain/entities/Order'; 9 | import { IMessagingAdapter } from './ports/IMessagingAdapter'; 10 | 11 | type User = { 12 | id: string; 13 | name: string; 14 | email: string; 15 | } 16 | 17 | type Product = { 18 | id: string; 19 | name: string; 20 | price: number; 21 | amount: number; 22 | }; 23 | 24 | interface IRequest { 25 | user: User; 26 | products: Product[]; 27 | }; 28 | 29 | @injectable() 30 | export class PlaceOrderUseCase { 31 | constructor( 32 | @inject("OrdersRepository") 33 | private ordersRepository: IOrdersRepository, 34 | 35 | @inject("MessagingAdapter") 36 | private messagingAdapter: IMessagingAdapter 37 | ) { } 38 | 39 | async execute({ user, products }: IRequest): Promise< 40 | Either< 41 | | InvalidOrderTotalError, 42 | IOrderData 43 | > 44 | > { 45 | const total = products.reduce((acc, product) => { 46 | return acc += (product.price * product.amount); 47 | }, 0) 48 | 49 | const orderOrError = Order.create({ 50 | user, 51 | products, 52 | total: Number(total.toFixed(2)), 53 | }); 54 | 55 | if (orderOrError.isLeft()) { 56 | return left(orderOrError.value); 57 | } 58 | 59 | const order = await this.ordersRepository.create(orderOrError.value); 60 | 61 | await this.messagingAdapter.sendMessage(JSON.stringify( 62 | { 63 | products: products.map(product => { 64 | return { 65 | id: product.id, 66 | amount: product.amount, 67 | } 68 | }) 69 | } 70 | )); 71 | 72 | return right(order); 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /services/OrderService/src/useCases/orders/ports/IMessagingAdapter.ts: -------------------------------------------------------------------------------- 1 | export interface IMessagingAdapter { 2 | sendMessage(message: string): Promise; 3 | }; -------------------------------------------------------------------------------- /services/OrderService/src/useCases/orders/ports/IOrderData.ts: -------------------------------------------------------------------------------- 1 | type User = { 2 | id: string; 3 | name: string; 4 | email: string; 5 | } 6 | 7 | type Product = { 8 | id: string; 9 | name: string; 10 | price: number; 11 | amount: number; 12 | }; 13 | 14 | export interface IOrderData { 15 | id: string; 16 | user: User; 17 | products: Product[]; 18 | total: number; 19 | createdAt: Date; 20 | updatedAt: Date; 21 | } -------------------------------------------------------------------------------- /services/OrderService/src/useCases/orders/ports/IOrdersRepository.ts: -------------------------------------------------------------------------------- 1 | import { IOrderData } from "./IOrderData"; 2 | 3 | export interface IOrdersRepository { 4 | create(data: IOrderData): Promise; 5 | filterByUserId(userId: string): Promise; 6 | }; -------------------------------------------------------------------------------- /services/OrderService/tests/doubles/FakeMessagingAdapter.ts: -------------------------------------------------------------------------------- 1 | import { IMessagingAdapter } from "../../src/useCases/orders/ports/IMessagingAdapter"; 2 | 3 | export class FakeMessagingAdapter implements IMessagingAdapter { 4 | async sendMessage(message: string): Promise { 5 | return; 6 | }; 7 | } -------------------------------------------------------------------------------- /services/OrderService/tests/doubles/OrdersActions.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { IOrderData } from "../../src/useCases/orders/ports/IOrderData"; 4 | import { IOrdersRepository } from "../../src/useCases/orders/ports/IOrdersRepository"; 5 | 6 | @injectable() 7 | export class OrdersActions { 8 | constructor( 9 | @inject("OrdersRepository") 10 | private ordersRepository: IOrdersRepository, 11 | ) { } 12 | 13 | async create(data: IOrderData): Promise { 14 | const order = await this.ordersRepository.create(data); 15 | 16 | return order; 17 | }; 18 | }; -------------------------------------------------------------------------------- /services/OrderService/tests/doubles/repositories/OrdersRepositoryInMemory.ts: -------------------------------------------------------------------------------- 1 | import { IOrderData } from "../../../src/useCases/orders/ports/IOrderData"; 2 | import { IOrdersRepository } from "../../../src/useCases/orders/ports/IOrdersRepository"; 3 | 4 | export class OrdersRepositoryInMemory implements IOrdersRepository { 5 | orders: IOrderData[] = []; 6 | 7 | async create(data: IOrderData): Promise { 8 | this.orders.push(data); 9 | 10 | return data; 11 | }; 12 | 13 | async filterByUserId(userId: string): Promise { 14 | const orders = this.orders.filter(order => order.user.id === userId); 15 | 16 | return orders; 17 | } 18 | }; -------------------------------------------------------------------------------- /services/OrderService/tests/infra/serverless/ListOrdersByUser.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "aws-lambda"; 2 | 3 | import { prismaClient } from "../../../src/infra/database/prisma/PrismaClient"; 4 | import { handle as PlaceOrder } from "../../../src/infra/serverless/functions/PlaceOrder"; 5 | import { handle as ListOrdersByUser } from "../../../src/infra/serverless/functions/ListOrdersByUser"; 6 | 7 | describe("List Orders By User Serverless Function", () => { 8 | beforeAll(async () => { 9 | await prismaClient.$connect(); 10 | }); 11 | 12 | afterAll(async () => { 13 | await prismaClient.productAmount.deleteMany(); 14 | await prismaClient.products.deleteMany(); 15 | await prismaClient.orders.deleteMany(); 16 | await prismaClient.users.deleteMany(); 17 | await prismaClient.$disconnect(); 18 | }); 19 | 20 | it("should return status code 200 and the user's orders", async () => { 21 | const placeOrderEvent = { 22 | requestContext: { 23 | authorizer: { 24 | user: { 25 | id: "user-id", 26 | name: "User Name", 27 | email: "user@example.com" 28 | }, 29 | }, 30 | }, 31 | body: { 32 | products: [ 33 | { 34 | id: "product-id-1", 35 | name: "Product 1", 36 | price: 254.59, 37 | amount: 1 38 | }, 39 | ] 40 | } 41 | }; 42 | const context = {} as Context; 43 | 44 | await PlaceOrder(placeOrderEvent, context); 45 | 46 | const listOrdersByUserEvent = { 47 | requestContext: { 48 | authorizer: { 49 | user: { 50 | id: "user-id", 51 | name: "User Name", 52 | email: "user@example.com" 53 | }, 54 | }, 55 | }, 56 | }; 57 | 58 | const listOrdersByUserServerless = await ListOrdersByUser(listOrdersByUserEvent, context); 59 | 60 | const bodyParsed = JSON.parse(listOrdersByUserServerless.body); 61 | 62 | expect(bodyParsed[0]).toHaveProperty("id"); 63 | expect(bodyParsed[0]).toHaveProperty("user"); 64 | expect(bodyParsed[0].user.id).toEqual("user-id"); 65 | expect(bodyParsed[0]).toHaveProperty("products"); 66 | expect(bodyParsed[0]).toHaveProperty("total"); 67 | expect(listOrdersByUserServerless.statusCode).toEqual(200); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /services/OrderService/tests/infra/serverless/PlaceOrder.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "aws-lambda"; 2 | import { prismaClient } from "../../../src/infra/database/prisma/PrismaClient"; 3 | import { handle } from "../../../src/infra/serverless/functions/PlaceOrder"; 4 | 5 | describe("Place Order Serverless Function", () => { 6 | beforeAll(async () => { 7 | await prismaClient.$connect(); 8 | }); 9 | 10 | afterAll(async () => { 11 | await prismaClient.productAmount.deleteMany(); 12 | await prismaClient.products.deleteMany(); 13 | await prismaClient.orders.deleteMany(); 14 | await prismaClient.users.deleteMany(); 15 | await prismaClient.$disconnect(); 16 | }); 17 | 18 | it("should return status code 201 and the created order", async () => { 19 | const event = { 20 | requestContext: { 21 | authorizer: { 22 | user: { 23 | id: "user-id", 24 | name: "User Name", 25 | email: "user@example.com" 26 | }, 27 | }, 28 | }, 29 | body: { 30 | products: [ 31 | { 32 | id: "product-id-1", 33 | name: "Product 1", 34 | price: 254.59, 35 | amount: 1 36 | }, 37 | { 38 | id: "product-id-2", 39 | name: "Product 2", 40 | price: 129.42, 41 | amount: 3 42 | } 43 | ] 44 | } 45 | }; 46 | const context = {} as Context; 47 | 48 | const placeOrderServerless = await handle(event, context); 49 | 50 | const bodyParsed = JSON.parse(placeOrderServerless.body); 51 | 52 | expect(bodyParsed).toHaveProperty("id"); 53 | expect(bodyParsed).toHaveProperty("user"); 54 | expect(bodyParsed.user.id).toEqual("user-id"); 55 | expect(bodyParsed).toHaveProperty("products"); 56 | expect(bodyParsed).toHaveProperty("total"); 57 | expect(placeOrderServerless.statusCode).toEqual(201); 58 | }); 59 | }); -------------------------------------------------------------------------------- /services/OrderService/tests/useCases/orders/PlaceOrderUseCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { InvalidOrderTotalError } from "../../../src/domain/entities/Order/errors/InvalidOrderTotalError"; 2 | import { PlaceOrderUseCase } from "../../../src/useCases/orders/PlaceOrderUseCase"; 3 | import { FakeMessagingAdapter } from "../../doubles/FakeMessagingAdapter"; 4 | import { OrdersRepositoryInMemory } from "../../doubles/repositories/OrdersRepositoryInMemory"; 5 | 6 | describe("Place Order UseCase", () => { 7 | let ordersRepositoryInMemory: OrdersRepositoryInMemory; 8 | let fakeMessagingAdapter: FakeMessagingAdapter; 9 | let placeOrderUseCase: PlaceOrderUseCase; 10 | 11 | beforeEach(() => { 12 | ordersRepositoryInMemory = new OrdersRepositoryInMemory(); 13 | fakeMessagingAdapter = new FakeMessagingAdapter() 14 | placeOrderUseCase = new PlaceOrderUseCase(ordersRepositoryInMemory, fakeMessagingAdapter); 15 | }); 16 | 17 | it("should place an order", async () => { 18 | const order = { 19 | user: { 20 | id: "user-id", 21 | name: "User Name", 22 | email: "user@example.com" 23 | }, 24 | products: [ 25 | { 26 | id: "product-id", 27 | name: "Product 1", 28 | price: 254.59, 29 | amount: 1 30 | }, 31 | { 32 | id: "product-id-2", 33 | name: "Product 2", 34 | price: 129.42, 35 | amount: 3 36 | } 37 | ], 38 | }; 39 | 40 | const orderOrError = await placeOrderUseCase.execute(order); 41 | 42 | expect(orderOrError.isRight).toBeTruthy(); 43 | expect(orderOrError.value).toHaveProperty("id"); 44 | expect(orderOrError.value).toHaveProperty("user"); 45 | expect(orderOrError.value).toHaveProperty("products"); 46 | expect(orderOrError.value).toHaveProperty("total"); 47 | if (orderOrError.isRight()) { 48 | expect(orderOrError.value.total).toEqual(642.85); // 254.59 + (129.42 * 3) 49 | }; 50 | }); 51 | }); -------------------------------------------------------------------------------- /services/ProductService/.env.development.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://docker:password@product_service_database:5432/product_service_database?schema=public" -------------------------------------------------------------------------------- /services/ProductService/.env.test.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://prisma_test:password@localhost:5433/product_service_test_database?schema=public" 2 | TEST="true" -------------------------------------------------------------------------------- /services/ProductService/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | jspm_packages 3 | coverage 4 | .serverless 5 | serverless-secrets.json 6 | .env.development 7 | .env.test 8 | -------------------------------------------------------------------------------- /services/ProductService/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /usr/app 4 | 5 | COPY package.json ./ 6 | 7 | RUN yarn 8 | 9 | COPY . . 10 | 11 | EXPOSE 3333 12 | 13 | CMD ["yarn", "dev"] 14 | -------------------------------------------------------------------------------- /services/ProductService/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | product_service_database: 5 | image: postgres 6 | container_name: product_service_database 7 | restart: always 8 | ports: 9 | - 5432:5432 10 | environment: 11 | - POSTGRES_USER=docker 12 | - POSTGRES_PASSWORD=password 13 | - POSTGRES_DB=product_service_database 14 | volumes: 15 | - pgdata:/data/postgres 16 | product_service_test_database: 17 | image: postgres 18 | container_name: product_service_test_database 19 | restart: always 20 | ports: 21 | - 5433:5432 22 | environment: 23 | - POSTGRES_USER=prisma_test 24 | - POSTGRES_PASSWORD=password 25 | - POSTGRES_DB=product_service_test_database 26 | volumes: 27 | - pgdata:/data/postgres 28 | product_service_api: 29 | build: . 30 | container_name: product_service_api 31 | ports: 32 | - 3334:3334 33 | volumes: 34 | - .:/usr/app 35 | links: 36 | - product_service_database 37 | depends_on: 38 | - product_service_database 39 | 40 | volumes: 41 | pgdata: 42 | driver: local 43 | -------------------------------------------------------------------------------- /services/ProductService/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; -------------------------------------------------------------------------------- /services/ProductService/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product-service", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "prisma migrate deploy --schema ./src/infra/database/prisma/schema.prisma && dotenv -e .env -- node ./dist/src/infra/http/server.js", 9 | "prisma:migrate": "dotenv -e .env -- prisma migrate deploy --schema ./src/infra/database/prisma/schema.prisma", 10 | "dev": "dotenv -e .env.development -- prisma migrate dev --schema ./src/infra/database/prisma/schema.prisma && dotenv -e .env.development -- ts-node-dev --inspect --transpile-only --ignore-watch node_modules --respawn src/infra/http/server.ts", 11 | "test": "dotenv -e .env.test -- prisma migrate dev --schema ./src/infra/database/prisma/schema.prisma && dotenv -e .env.test -- jest --runInBand" 12 | }, 13 | "devDependencies": { 14 | "@types/express": "^4.17.13", 15 | "@types/jest": "^27.4.1", 16 | "@types/node": "^17.0.21", 17 | "git-commit-msg-linter": "^4.1.1", 18 | "jest": "^27.5.1", 19 | "prisma": "^3.14.0", 20 | "serverless-dotenv-plugin": "^3.12.2", 21 | "serverless-plugin-typescript": "^1.1.9", 22 | "supertest": "^6.2.2", 23 | "ts-jest": "^27.1.3", 24 | "ts-node-dev": "^1.1.8", 25 | "typescript": "^4.6.2" 26 | }, 27 | "dependencies": { 28 | "@middy/core": "^2.5.7", 29 | "@middy/http-cors": "^2.5.7", 30 | "@middy/http-error-handler": "^2.5.7", 31 | "@middy/http-event-normalizer": "^2.5.7", 32 | "@middy/http-json-body-parser": "^2.5.7", 33 | "@prisma/client": "^3.14.0", 34 | "@types/supertest": "^2.0.11", 35 | "dotenv-cli": "^5.0.0", 36 | "express": "^4.17.3", 37 | "reflect-metadata": "^0.1.13", 38 | "tsyringe": "^4.6.0" 39 | } 40 | } -------------------------------------------------------------------------------- /services/ProductService/serverless-secrets.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "SECURITY_GROUP_ID": "", 3 | "SUBNET_ID1": "", 4 | "SUBNET_ID2": "", 5 | "SUBNET_ID3": "", 6 | "SQS_ARN": "" 7 | } -------------------------------------------------------------------------------- /services/ProductService/serverless.yml: -------------------------------------------------------------------------------- 1 | service: product-service-serverless 2 | 3 | frameworkVersion: '3' 4 | 5 | plugins: 6 | - serverless-plugin-typescript 7 | - serverless-dotenv-plugin 8 | 9 | provider: 10 | name: aws 11 | runtime: nodejs14.x 12 | memorySize: 512 13 | region: sa-east-1 14 | vpc: 15 | securityGroupIds: 16 | - ${file(./serverless-secrets.json):SECURITY_GROUP_ID} 17 | subnetIds: 18 | - ${file(./serverless-secrets.json):SUBNET_ID1} 19 | - ${file(./serverless-secrets.json):SUBNET_ID2} 20 | - ${file(./serverless-secrets.json):SUBNET_ID3} 21 | 22 | functions: 23 | CreateCategory: 24 | handler: src/infra/serverless/functions/CreateCategory.handle 25 | events: 26 | - http: 27 | method: POST 28 | path: /categories 29 | cors: true 30 | DeleteCategory: 31 | handler: src/infra/serverless/functions/DeleteCategory.handle 32 | events: 33 | - http: 34 | method: DELETE 35 | path: /categories/delete/{id} 36 | cors: true 37 | UpdateCategory: 38 | handler: src/infra/serverless/functions/UpdateCategory.handle 39 | events: 40 | - http: 41 | method: PUT 42 | path: /categories/update/{id} 43 | cors: true 44 | CreateProduct: 45 | handler: src/infra/serverless/functions/CreateProduct.handle 46 | events: 47 | - http: 48 | method: POST 49 | path: /products 50 | cors: true 51 | DeleteProduct: 52 | handler: src/infra/serverless/functions/DeleteProduct.handle 53 | events: 54 | - http: 55 | method: DELETE 56 | path: /products/delete/{id} 57 | cors: true 58 | ReduceProductStock: 59 | handler: src/infra/serverless/functions/ReduceProductStock.handle 60 | events: 61 | - sqs: 62 | arn: ${file(./serverless-secrets.json):SQS_ARN} 63 | batchSize: 1 64 | 65 | package: 66 | exclude: 67 | - ./node_modules/prisma/** 68 | - ./node_modules/express/** 69 | 70 | custom: 71 | dotenv: 72 | path: ".env" -------------------------------------------------------------------------------- /services/ProductService/src/container/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { PrismaCategoriesRepository } from "../infra/database/prisma/repositories/PrismaCategoriesRepository"; 4 | import { PrismaProductsRepository } from "../infra/database/prisma/repositories/PrismaProductsRepository"; 5 | import { ICategoriesRepository } from "../useCases/categories/ports/ICategoriesRepository"; 6 | import { IProductsRepository } from "../useCases/products/ports/IProductsRepository"; 7 | 8 | container.registerSingleton( 9 | "CategoriesRepository", 10 | PrismaCategoriesRepository 11 | ); 12 | 13 | container.registerSingleton( 14 | "ProductsRepository", 15 | PrismaProductsRepository 16 | ); -------------------------------------------------------------------------------- /services/ProductService/src/controllers/categories/CreateCategoryController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { CreateCategoryUseCase } from "../../useCases/categories/CreateCategoryUseCase"; 4 | import { CategoryAlreadyExistsError } from "../../useCases/categories/errors/CategoryAlreadyExistsError"; 5 | import { MissingParamError } from "../errors/MissingParamError"; 6 | import { IController } from "../ports/IController"; 7 | import { IHttpResponse } from "../ports/IHttpResponse"; 8 | import { IServerlessHttpRequest } from "../ports/IServerlessHttpRequest"; 9 | import { badRequest, created, forbidden, serverError, unauthorized } from "../utils/HttpResponses"; 10 | import { IsRequiredParamsMissing } from "../utils/IsRequiredParamsMissing"; 11 | 12 | export class CreateCategoryController implements IController { 13 | requiredParams = ["name"]; 14 | 15 | async handle(request: IServerlessHttpRequest): Promise { 16 | try { 17 | const authorizer = request.requestContext.authorizer 18 | 19 | if (!authorizer?.user || !authorizer.user?.id) { 20 | return unauthorized("User is not authenticated!"); 21 | }; 22 | 23 | const requiredParamsMissing = IsRequiredParamsMissing(request.body, this.requiredParams) 24 | 25 | if (requiredParamsMissing) { 26 | return badRequest(new MissingParamError(requiredParamsMissing).message); 27 | }; 28 | 29 | const { name } = request.body; 30 | 31 | const createCategoryUseCase = container.resolve(CreateCategoryUseCase); 32 | 33 | const response = await createCategoryUseCase.execute({ name }); 34 | 35 | if (response.isRight()) { 36 | return created(response.value); 37 | }; 38 | 39 | if (response.value instanceof CategoryAlreadyExistsError) { 40 | return forbidden(response.value.message); 41 | }; 42 | 43 | return badRequest(response.value.message); 44 | } catch (error) { 45 | return serverError(error); 46 | }; 47 | }; 48 | }; -------------------------------------------------------------------------------- /services/ProductService/src/controllers/categories/DeleteCategoryController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { DeleteCategoryUseCase } from "../../useCases/categories/DeleteCategoryUseCase"; 4 | import { MissingParamError } from "../errors/MissingParamError"; 5 | import { IController } from "../ports/IController"; 6 | import { IHttpResponse } from "../ports/IHttpResponse"; 7 | import { IServerlessHttpRequest } from "../ports/IServerlessHttpRequest"; 8 | import { badRequest, forbidden, ok, serverError, unauthorized } from "../utils/HttpResponses"; 9 | 10 | export class DeleteCategoryController implements IController { 11 | async handle(request: IServerlessHttpRequest): Promise { 12 | try { 13 | const authorizer = request.requestContext.authorizer 14 | 15 | if (!authorizer?.user || !authorizer.user?.id) { 16 | return unauthorized("User is not authenticated!"); 17 | }; 18 | 19 | if (!request.pathParameters.id) { 20 | return badRequest(new MissingParamError("category_id").message); 21 | }; 22 | 23 | const { id } = request.pathParameters; 24 | 25 | const deleteCategoryUseCase = container.resolve(DeleteCategoryUseCase); 26 | 27 | const response = await deleteCategoryUseCase.execute({ category_id: id }); 28 | 29 | if (response.isLeft()) { 30 | return forbidden(response.value.message); 31 | }; 32 | 33 | return ok("Category deleted successfully!"); 34 | } catch (error) { 35 | return serverError(error); 36 | }; 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /services/ProductService/src/controllers/categories/ListAllCategoriesController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { ListAllCategoriesUseCase } from "../../useCases/categories/ListAllCategoriesUseCase"; 4 | import { IController } from "../ports/IController"; 5 | import { IHttpRequest } from "../ports/IHttpRequest"; 6 | import { IHttpResponse } from "../ports/IHttpResponse"; 7 | import { ok, serverError } from "../utils/HttpResponses"; 8 | 9 | export class ListAllCategoriesController implements IController { 10 | async handle(request: IHttpRequest): Promise { 11 | try { 12 | const listAllCategoriesUseCase = container.resolve(ListAllCategoriesUseCase); 13 | 14 | const response = await listAllCategoriesUseCase.execute(); 15 | 16 | return ok(response.value); 17 | } catch (error) { 18 | return serverError(error); 19 | }; 20 | }; 21 | }; -------------------------------------------------------------------------------- /services/ProductService/src/controllers/categories/UpdateCategoryController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { CategoryAlreadyExistsError } from "../../useCases/categories/errors/CategoryAlreadyExistsError"; 4 | import { UpdateCategoryUseCase } from "../../useCases/categories/UpdateCategoryUseCase"; 5 | import { MissingParamError } from "../errors/MissingParamError"; 6 | import { IController } from "../ports/IController"; 7 | import { IHttpResponse } from "../ports/IHttpResponse"; 8 | import { IServerlessHttpRequest } from "../ports/IServerlessHttpRequest"; 9 | import { badRequest, forbidden, serverError, unauthorized, updated } from "../utils/HttpResponses"; 10 | import { IsRequiredParamsMissing } from "../utils/IsRequiredParamsMissing"; 11 | 12 | export class UpdateCategoryController implements IController { 13 | requiredParams = ["name"]; 14 | 15 | async handle(request: IServerlessHttpRequest): Promise { 16 | try { 17 | const authorizer = request.requestContext.authorizer 18 | 19 | if (!authorizer?.user || !authorizer.user?.id) { 20 | return unauthorized("User is not authenticated!"); 21 | }; 22 | 23 | const requiredParamsMissing = IsRequiredParamsMissing(request.body, this.requiredParams) 24 | 25 | if (requiredParamsMissing) { 26 | return badRequest(new MissingParamError(requiredParamsMissing).message); 27 | }; 28 | 29 | if (!request.pathParameters.id) { 30 | return badRequest(new MissingParamError("category_id").message); 31 | }; 32 | 33 | const { name } = request.body; 34 | const { id } = request.pathParameters; 35 | 36 | const updateCategoryUseCase = container.resolve(UpdateCategoryUseCase); 37 | 38 | const response = await updateCategoryUseCase.execute({ name, category_id: id }); 39 | 40 | if (response.isRight()) { 41 | return updated(response.value); 42 | }; 43 | 44 | if (response.value instanceof CategoryAlreadyExistsError) { 45 | return forbidden(response.value.message); 46 | }; 47 | 48 | return badRequest(response.value.message); 49 | } catch (error) { 50 | return serverError(error); 51 | }; 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /services/ProductService/src/controllers/errors/MissingParamError.ts: -------------------------------------------------------------------------------- 1 | export class MissingParamError extends Error { 2 | constructor(missingParams: string) { 3 | super(`Missing parameter(s): ${missingParams}.`); 4 | this.name = "MissingParamError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/ProductService/src/controllers/ports/IController.ts: -------------------------------------------------------------------------------- 1 | import { IHttpRequest } from "./IHttpRequest"; 2 | import { IHttpResponse } from "./IHttpResponse"; 3 | import { IServerlessHttpRequest } from "./IServerlessHttpRequest"; 4 | 5 | export interface IController { 6 | readonly requiredParams?: string[]; 7 | handle(request: IHttpRequest | IServerlessHttpRequest): Promise; 8 | } -------------------------------------------------------------------------------- /services/ProductService/src/controllers/ports/IHttpRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpRequest { 2 | user?: { 3 | id?: string; 4 | }; 5 | body?: any; 6 | params?: any; 7 | }; -------------------------------------------------------------------------------- /services/ProductService/src/controllers/ports/IHttpResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpResponse { 2 | statusCode: number; 3 | body: any; 4 | }; -------------------------------------------------------------------------------- /services/ProductService/src/controllers/ports/IServerlessHttpRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IServerlessHttpRequest { 2 | requestContext: { 3 | authorizer?: { 4 | user?: { 5 | id: string; 6 | name: string; 7 | email: string; 8 | }, 9 | }, 10 | }, 11 | pathParameters?: any, 12 | body?: any, 13 | Records?: [ 14 | { 15 | body: string 16 | } 17 | ] 18 | }; -------------------------------------------------------------------------------- /services/ProductService/src/controllers/products/CreateProductController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { CategoryNotFoundError } from "../../useCases/categories/errors/CategoryNotFoundError"; 4 | import { ProductAlreadyExistsError } from "../../useCases/products/errors/ProductAlreadyExistsError"; 5 | import { CreateProductUseCase } from "../../useCases/products/CreateProductUseCase"; 6 | import { MissingParamError } from "../errors/MissingParamError"; 7 | import { IController } from "../ports/IController"; 8 | import { IHttpResponse } from "../ports/IHttpResponse"; 9 | import { IServerlessHttpRequest } from "../ports/IServerlessHttpRequest"; 10 | import { badRequest, created, forbidden, serverError, unauthorized } from "../utils/HttpResponses"; 11 | import { IsRequiredParamsMissing } from "../utils/IsRequiredParamsMissing"; 12 | 13 | export class CreateProductController implements IController { 14 | requiredParams = ["name", "stock", "price", "categoryId"]; 15 | 16 | async handle(request: IServerlessHttpRequest): Promise { 17 | try { 18 | const authorizer = request.requestContext.authorizer 19 | 20 | if (!authorizer?.user || !authorizer.user?.id) { 21 | return unauthorized("User is not authenticated!"); 22 | }; 23 | 24 | const requiredParamsMissing = IsRequiredParamsMissing(request.body, this.requiredParams) 25 | 26 | if (requiredParamsMissing) { 27 | return badRequest(new MissingParamError(requiredParamsMissing).message); 28 | }; 29 | 30 | const { name, stock, price, categoryId } = request.body; 31 | 32 | const createProductUseCase = container.resolve(CreateProductUseCase); 33 | 34 | const response = await createProductUseCase.execute({ name, stock, price, categoryId }); 35 | 36 | if (response.isRight()) { 37 | return created(response.value); 38 | }; 39 | 40 | if ( 41 | response.value instanceof ProductAlreadyExistsError 42 | || response.value instanceof CategoryNotFoundError) { 43 | return forbidden(response.value.message); 44 | }; 45 | 46 | return badRequest(response.value.message); 47 | } catch (error) { 48 | return serverError(error); 49 | }; 50 | }; 51 | }; -------------------------------------------------------------------------------- /services/ProductService/src/controllers/products/DeleteProductController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { DeleteProductUseCase } from "../../useCases/products/DeleteProductUseCase"; 4 | import { MissingParamError } from "../errors/MissingParamError"; 5 | import { IController } from "../ports/IController"; 6 | import { IHttpResponse } from "../ports/IHttpResponse"; 7 | import { badRequest, forbidden, ok, serverError, unauthorized } from "../utils/HttpResponses"; 8 | 9 | export class DeleteProductController implements IController { 10 | async handle(request: any): Promise { 11 | try { 12 | const authorizer = request.requestContext.authorizer.lambda; 13 | 14 | if (!authorizer?.user || !authorizer.user?.id) { 15 | return unauthorized("User is not authenticated!"); 16 | }; 17 | 18 | if (!request.pathParameters.id) { 19 | return badRequest(new MissingParamError("product id").message); 20 | } 21 | 22 | const { id } = request.pathParameters; 23 | 24 | const deleteProductUseCase = container.resolve(DeleteProductUseCase); 25 | 26 | const response = await deleteProductUseCase.execute({ product_id: id }); 27 | 28 | if (response.isLeft()) { 29 | return forbidden(response.value.message); 30 | } 31 | 32 | return ok("Product deleted successfully!"); 33 | } catch (error) { 34 | return serverError(error); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /services/ProductService/src/controllers/products/ListAllProductsController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { ListAllProductsUseCase } from "../../useCases/products/ListAllProductsUseCase"; 4 | import { IController } from "../ports/IController"; 5 | import { IHttpRequest } from "../ports/IHttpRequest"; 6 | import { IHttpResponse } from "../ports/IHttpResponse"; 7 | import { ok, serverError } from "../utils/HttpResponses"; 8 | 9 | export class ListAllProductsController implements IController { 10 | async handle(request: IHttpRequest): Promise { 11 | try { 12 | const listAllProductsUseCase = container.resolve(ListAllProductsUseCase); 13 | 14 | const response = await listAllProductsUseCase.execute(); 15 | 16 | return ok(response.value); 17 | } catch (error) { 18 | return serverError(error); 19 | }; 20 | }; 21 | }; -------------------------------------------------------------------------------- /services/ProductService/src/controllers/products/ReduceProductsStockController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { IController } from "../ports/IController"; 4 | import { IHttpResponse } from "../ports/IHttpResponse"; 5 | import { IServerlessHttpRequest } from "../ports/IServerlessHttpRequest"; 6 | import { badRequest, forbidden, ok, serverError } from "../utils/HttpResponses"; 7 | import { ReduceProductsStockUseCase } from "../../useCases/products/ReduceProductsStockUseCase"; 8 | 9 | export class ReduceProductsStockController implements IController { 10 | async handle(request: IServerlessHttpRequest): Promise { 11 | try { 12 | if (!request.Records) { 13 | return badRequest("Message not received!"); 14 | }; 15 | 16 | const messageInString = request.Records[0].body; 17 | 18 | const messageParsed = JSON.parse(messageInString); 19 | 20 | const reduceProductsStockUseCase = container.resolve(ReduceProductsStockUseCase); 21 | 22 | const response = await reduceProductsStockUseCase.execute({ productsInfos: messageParsed.products }); 23 | 24 | if (response.isRight()) { 25 | return ok("Products Stock Updated!"); 26 | }; 27 | 28 | return forbidden(response.value.message); 29 | } catch (error) { 30 | return serverError(error); 31 | }; 32 | }; 33 | }; -------------------------------------------------------------------------------- /services/ProductService/src/controllers/utils/HttpResponses.ts: -------------------------------------------------------------------------------- 1 | import { IHttpResponse } from '../ports/IHttpResponse' 2 | 3 | export const ok = (data: any): IHttpResponse => ({ 4 | statusCode: 200, 5 | body: data 6 | }) 7 | 8 | export const created = (data: any): IHttpResponse => ({ 9 | statusCode: 201, 10 | body: data 11 | }) 12 | 13 | export const updated = (data: any): IHttpResponse => ({ 14 | statusCode: 201, 15 | body: data 16 | }) 17 | 18 | export const unauthorized = (error: any): IHttpResponse => ({ 19 | statusCode: 401, 20 | body: error 21 | }) 22 | 23 | export const forbidden = (error: any): IHttpResponse => ({ 24 | statusCode: 403, 25 | body: error 26 | }) 27 | 28 | export const badRequest = (error: any): IHttpResponse => ({ 29 | statusCode: 400, 30 | body: error 31 | }) 32 | 33 | export const serverError = (error: any): IHttpResponse => ({ 34 | statusCode: 500, 35 | body: error 36 | }) -------------------------------------------------------------------------------- /services/ProductService/src/controllers/utils/IsRequiredParamsMissing.ts: -------------------------------------------------------------------------------- 1 | export function IsRequiredParamsMissing(receivedParams: any, requiredParams: string[]): string { 2 | const missingParams: string[] = []; 3 | 4 | requiredParams.forEach(param => { 5 | if (!Object.keys(receivedParams).includes(param)) { 6 | missingParams.push(param); 7 | } 8 | 9 | return false; 10 | }); 11 | 12 | return missingParams.join(', '); 13 | } -------------------------------------------------------------------------------- /services/ProductService/src/domain/entities/Category/errors/InvalidCategoryNameError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidCategoryNameError extends Error { 2 | constructor(name: string) { 3 | super(`"${name}" is an invalid category name`); 4 | this.name = "InvalidCategoryNameError" 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/ProductService/src/domain/entities/Category/index.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | import { Either, left, right } from '../../../logic/Either'; 4 | import { InvalidCategoryNameError } from './errors/InvalidCategoryNameError'; 5 | 6 | type CategoryProps = { 7 | id?: string; 8 | name: string; 9 | createdAt?: Date; 10 | updatedAt?: Date; 11 | } 12 | 13 | export class Category { 14 | public readonly id: string; 15 | public readonly name: string; 16 | public readonly createdAt: Date; 17 | public readonly updatedAt: Date; 18 | 19 | constructor(props: CategoryProps) { 20 | this.id = props.id || crypto.randomUUID(); 21 | this.name = props.name; 22 | this.createdAt = props.createdAt || new Date(); 23 | this.updatedAt = props.updatedAt || new Date(); 24 | }; 25 | 26 | static create( 27 | props: CategoryProps 28 | ): Either { 29 | const validate = Category.validate(props) 30 | 31 | if (validate.isLeft()) { 32 | return left(validate.value); 33 | }; 34 | 35 | const category = new Category(props); 36 | 37 | return right(category); 38 | }; 39 | 40 | public static validate(props: CategoryProps): Either { 41 | const { name } = props; 42 | 43 | if (!name || name.trim().length > 255 || name.trim().length < 2) { 44 | return left(new InvalidCategoryNameError(name)); 45 | }; 46 | 47 | return right(true); 48 | }; 49 | }; -------------------------------------------------------------------------------- /services/ProductService/src/domain/entities/Product/errors/InvalidProductNameError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidProductNameError extends Error { 2 | constructor(name: string) { 3 | super(`"${name}" is an invalid product name`); 4 | this.name = "InvalidProductNameError"; 5 | } 6 | } -------------------------------------------------------------------------------- /services/ProductService/src/domain/entities/Product/errors/InvalidProductPriceError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidProductPriceError extends Error { 2 | constructor(price: number) { 3 | super(`"${price}" is an invalid product price`); 4 | this.name = "InvalidProductPriceError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/ProductService/src/domain/entities/Product/errors/InvalidProductStockError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidProductStockError extends Error { 2 | constructor(stock: number) { 3 | super(`"${stock}" is an invalid product stock`); 4 | this.name = "InvalidProductStockError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/ProductService/src/infra/database/prisma/PrismaClient.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const prismaClient = new PrismaClient(); -------------------------------------------------------------------------------- /services/ProductService/src/infra/database/prisma/migrations/20220404192944_add_products_and_categories/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "categories" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | 8 | CONSTRAINT "categories_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- CreateTable 12 | CREATE TABLE "products" ( 13 | "id" TEXT NOT NULL, 14 | "name" TEXT NOT NULL, 15 | "stock" INTEGER NOT NULL, 16 | "price" DECIMAL(8,2) NOT NULL, 17 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | "categoryId" TEXT, 20 | 21 | CONSTRAINT "products_pkey" PRIMARY KEY ("id") 22 | ); 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "products" ADD CONSTRAINT "products_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "categories"("id") ON DELETE SET NULL ON UPDATE CASCADE; 26 | -------------------------------------------------------------------------------- /services/ProductService/src/infra/database/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /services/ProductService/src/infra/database/prisma/repositories/PrismaCategoriesRepository.ts: -------------------------------------------------------------------------------- 1 | import { ICategoriesRepository } from "../../../../useCases/categories/ports/ICategoriesRepository"; 2 | import { ICategoryData } from "../../../../useCases/categories/ports/ICategoryData"; 3 | import { prismaClient } from "../PrismaClient"; 4 | 5 | export class PrismaCategoriesRepository implements ICategoriesRepository { 6 | async create(data: ICategoryData): Promise { 7 | const categoryCreated = await prismaClient.categories.create({ 8 | data: { 9 | id: data.id, 10 | name: data.name, 11 | createdAt: data.createdAt, 12 | updatedAt: data.updatedAt, 13 | } 14 | }); 15 | 16 | return categoryCreated; 17 | } 18 | async findByName(name: string): Promise { 19 | const categoryOrNull = await prismaClient.categories.findFirst({ 20 | where: { 21 | name, 22 | } 23 | }); 24 | 25 | return categoryOrNull; 26 | }; 27 | 28 | async listAll(): Promise { 29 | const categories = await prismaClient.categories.findMany(); 30 | 31 | return categories; 32 | }; 33 | 34 | async findById(id: string): Promise { 35 | const categoryOrNull = await prismaClient.categories.findFirst({ 36 | where: { 37 | id, 38 | }, 39 | }); 40 | 41 | return categoryOrNull; 42 | }; 43 | 44 | async update(data: ICategoryData): Promise { 45 | const categoryUpdated = await prismaClient.categories.update({ 46 | where: { 47 | id: data.id, 48 | }, 49 | data: { 50 | name: data.name, 51 | updatedAt: data.updatedAt, 52 | }, 53 | }); 54 | 55 | return categoryUpdated; 56 | }; 57 | 58 | async deleteOne(id: string): Promise { 59 | await prismaClient.categories.delete({ 60 | where: { 61 | id, 62 | }, 63 | }); 64 | }; 65 | }; -------------------------------------------------------------------------------- /services/ProductService/src/infra/database/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | binaryTargets = ["native", "rhel-openssl-1.0.x"] 4 | } 5 | 6 | datasource db { 7 | url = env("DATABASE_URL") 8 | provider = "postgresql" 9 | } 10 | 11 | model Categories { 12 | id String @id @default(uuid()) 13 | name String 14 | createdAt DateTime @default(now()) 15 | updatedAt DateTime @default(now()) 16 | 17 | products Products[] 18 | 19 | @@map("categories") 20 | } 21 | 22 | model Products { 23 | id String @id @default(uuid()) 24 | name String 25 | stock Int 26 | price Decimal @db.Decimal(8, 2) 27 | createdAt DateTime @default(now()) 28 | updatedAt DateTime @default(now()) 29 | 30 | category Categories? @relation(fields: [categoryId], references: [id]) 31 | categoryId String? 32 | 33 | @@map("products") 34 | } 35 | -------------------------------------------------------------------------------- /services/ProductService/src/infra/http/app.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | 3 | import "../../container"; 4 | 5 | import express from "express"; 6 | 7 | import { router } from './routes'; 8 | 9 | const app = express(); 10 | 11 | app.use(express.json()); 12 | 13 | app.use(router); 14 | 15 | export { app }; -------------------------------------------------------------------------------- /services/ProductService/src/infra/http/routes/RouteAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | import { IController } from "../../../controllers/ports/IController"; 4 | 5 | export function routeAdapter(controller: IController) { 6 | return async (request: Request, response: Response) => { 7 | const httpResponse = await controller.handle(request); 8 | response.status(httpResponse.statusCode).json(httpResponse.body); 9 | }; 10 | }; -------------------------------------------------------------------------------- /services/ProductService/src/infra/http/routes/categories.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import { ListAllCategoriesController } from "../../../controllers/categories/ListAllCategoriesController"; 4 | import { routeAdapter } from "./RouteAdapter"; 5 | 6 | export const categoriesRoutes = Router(); 7 | 8 | const listAllCategoriesController = new ListAllCategoriesController(); 9 | 10 | categoriesRoutes.get("/", routeAdapter(listAllCategoriesController)); -------------------------------------------------------------------------------- /services/ProductService/src/infra/http/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { categoriesRoutes } from './categories.routes'; 4 | import { productsRoutes } from './products.routes'; 5 | 6 | const router = Router(); 7 | 8 | router.use("/categories", categoriesRoutes); 9 | router.use("/", productsRoutes); 10 | 11 | export { router }; -------------------------------------------------------------------------------- /services/ProductService/src/infra/http/routes/products.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import { ListAllProductsController } from "../../../controllers/products/ListAllProductsController"; 4 | import { routeAdapter } from "./RouteAdapter"; 5 | 6 | export const productsRoutes = Router(); 7 | 8 | const listAllProductsController = new ListAllProductsController(); 9 | 10 | productsRoutes.get("/", routeAdapter(listAllProductsController)); -------------------------------------------------------------------------------- /services/ProductService/src/infra/http/server.ts: -------------------------------------------------------------------------------- 1 | import { app } from "./app"; 2 | 3 | app.listen(3334, () => { 4 | console.log("🚀 App is running on port 3334!") 5 | }); -------------------------------------------------------------------------------- /services/ProductService/src/infra/serverless/functions/CreateCategory.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "../../../container"; 3 | 4 | import { CreateCategoryController } from "../../../controllers/categories/CreateCategoryController"; 5 | import { MiddyMiddleware } from "../utils/commonMiddleware"; 6 | 7 | const CreateCategory = async (event: any) => { 8 | const createCategoryController = new CreateCategoryController(); 9 | 10 | const response = await createCategoryController.handle(event); 11 | 12 | return { 13 | statusCode: response.statusCode, 14 | body: JSON.stringify(response.body) 15 | }; 16 | }; 17 | 18 | export const handle = MiddyMiddleware(CreateCategory); -------------------------------------------------------------------------------- /services/ProductService/src/infra/serverless/functions/CreateProduct.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "../../../container"; 3 | 4 | import { CreateProductController } from "../../../controllers/products/CreateProductController"; 5 | import { MiddyMiddleware } from "../utils/commonMiddleware"; 6 | 7 | const CreateProduct = async (event: any) => { 8 | const createProductController = new CreateProductController(); 9 | 10 | const response = await createProductController.handle(event); 11 | 12 | return { 13 | statusCode: response.statusCode, 14 | body: JSON.stringify(response.body) 15 | }; 16 | }; 17 | 18 | export const handle = MiddyMiddleware(CreateProduct); -------------------------------------------------------------------------------- /services/ProductService/src/infra/serverless/functions/DeleteCategory.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "../../../container"; 3 | 4 | import { DeleteCategoryController } from "../../../controllers/categories/DeleteCategoryController"; 5 | import { MiddyMiddleware } from "../utils/commonMiddleware"; 6 | 7 | const DeleteCategory = async (event: any) => { 8 | const deleteCategoryController = new DeleteCategoryController(); 9 | 10 | const response = await deleteCategoryController.handle(event); 11 | 12 | return { 13 | statusCode: response.statusCode, 14 | body: JSON.stringify(response.body) 15 | }; 16 | }; 17 | 18 | export const handle = MiddyMiddleware(DeleteCategory); -------------------------------------------------------------------------------- /services/ProductService/src/infra/serverless/functions/DeleteProduct.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "../../../container"; 3 | 4 | import { DeleteProductController } from "../../../controllers/products/DeleteProductController"; 5 | import { MiddyMiddleware } from "../utils/commonMiddleware"; 6 | 7 | const DeleteProduct = async (event: any) => { 8 | const deleteProductController = new DeleteProductController(); 9 | 10 | console.log("Event:", event); 11 | 12 | const response = await deleteProductController.handle(event); 13 | 14 | console.log("Response:", response); 15 | 16 | return { 17 | statusCode: response.statusCode, 18 | body: JSON.stringify(response.body) 19 | }; 20 | }; 21 | 22 | export const handle = MiddyMiddleware(DeleteProduct); -------------------------------------------------------------------------------- /services/ProductService/src/infra/serverless/functions/ReduceProductStock.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "../../../container"; 3 | import { ReduceProductsStockController } from "../../../controllers/products/ReduceProductsStockController"; 4 | 5 | import { MiddyMiddleware } from "../utils/commonMiddleware"; 6 | 7 | const ReduceProductsStock = async (event: any) => { 8 | const reduceProductsStockController = new ReduceProductsStockController(); 9 | 10 | const response = await reduceProductsStockController.handle(event); 11 | 12 | return { 13 | statusCode: response.statusCode, 14 | body: JSON.stringify(response.body) 15 | }; 16 | }; 17 | 18 | export const handle = MiddyMiddleware(ReduceProductsStock); -------------------------------------------------------------------------------- /services/ProductService/src/infra/serverless/functions/UpdateCategory.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import "../../../container"; 3 | 4 | import { UpdateCategoryController } from "../../../controllers/categories/UpdateCategoryController"; 5 | import { MiddyMiddleware } from "../utils/commonMiddleware"; 6 | 7 | const UpdateCategory = async (event: any) => { 8 | const updateCategoryController = new UpdateCategoryController(); 9 | 10 | const response = await updateCategoryController.handle(event); 11 | 12 | return { 13 | statusCode: response.statusCode, 14 | body: JSON.stringify(response.body), 15 | }; 16 | }; 17 | 18 | export const handle = MiddyMiddleware(UpdateCategory); 19 | 20 | -------------------------------------------------------------------------------- /services/ProductService/src/infra/serverless/utils/commonMiddleware.ts: -------------------------------------------------------------------------------- 1 | import middy from '@middy/core'; 2 | import httpJsonBodyParser from '@middy/http-json-body-parser'; 3 | import httpEventNormalizer from '@middy/http-event-normalizer'; 4 | import httpErrorHandler from '@middy/http-error-handler'; 5 | import cors from '@middy/http-cors'; 6 | 7 | export function MiddyMiddleware(handler: any) { 8 | return middy(handler) 9 | .use([ 10 | httpJsonBodyParser(), 11 | httpEventNormalizer(), 12 | httpErrorHandler(), 13 | cors(), 14 | ]); 15 | }; -------------------------------------------------------------------------------- /services/ProductService/src/logic/Either.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | export type Either = Left | Right; 3 | export class Left { 4 | readonly value: L; 5 | 6 | constructor(value: L) { 7 | this.value = value; 8 | } 9 | 10 | isLeft(): this is Left { 11 | return true; 12 | } 13 | 14 | isRight(): this is Right { 15 | return false; 16 | } 17 | } 18 | 19 | export class Right { 20 | readonly value: A; 21 | 22 | constructor(value: A) { 23 | this.value = value; 24 | } 25 | 26 | isLeft(): this is Left { 27 | return false; 28 | } 29 | 30 | isRight(): this is Right { 31 | return true; 32 | } 33 | } 34 | 35 | export const left = (l: L): Either => { 36 | return new Left(l); 37 | }; 38 | 39 | export const right = (a: A): Either => { 40 | return new Right(a); 41 | }; 42 | -------------------------------------------------------------------------------- /services/ProductService/src/useCases/categories/CreateCategoryUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'tsyringe'; 2 | 3 | import { Category } from '../../domain/entities/Category'; 4 | import { InvalidCategoryNameError } from '../../domain/entities/Category/errors/InvalidCategoryNameError'; 5 | import { Either, left, right } from "../../logic/Either"; 6 | import { CategoryAlreadyExistsError } from './errors/CategoryAlreadyExistsError'; 7 | import { ICategoriesRepository } from './ports/ICategoriesRepository'; 8 | import { ICategoryData } from './ports/ICategoryData'; 9 | 10 | 11 | interface IRequest { 12 | name: string; 13 | } 14 | 15 | @injectable() 16 | export class CreateCategoryUseCase { 17 | constructor( 18 | @inject("CategoriesRepository") 19 | private categoriesRepository: ICategoriesRepository 20 | ) { } 21 | 22 | async execute({ 23 | name, 24 | }: IRequest): Promise< 25 | Either< 26 | | CategoryAlreadyExistsError 27 | | InvalidCategoryNameError, 28 | ICategoryData 29 | > 30 | > { 31 | const categoryExists = await this.categoriesRepository.findByName(name); 32 | 33 | if (categoryExists) { 34 | return left(new CategoryAlreadyExistsError(name)); 35 | } 36 | 37 | const categoryOrError = Category.create({ 38 | name, 39 | }); 40 | 41 | if (categoryOrError.isLeft()) { 42 | return left(categoryOrError.value); 43 | } 44 | 45 | const category = await this.categoriesRepository.create(categoryOrError.value); 46 | 47 | return right(category); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /services/ProductService/src/useCases/categories/DeleteCategoryUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { Either, left, right } from "../../logic/Either"; 4 | import { CategoryNotFoundError } from "./errors/CategoryNotFoundError"; 5 | import { ICategoriesRepository } from "./ports/ICategoriesRepository"; 6 | 7 | interface IRequest { 8 | category_id: string; 9 | }; 10 | 11 | @injectable() 12 | export class DeleteCategoryUseCase { 13 | constructor( 14 | @inject("CategoriesRepository") 15 | private categoriesRepository: ICategoriesRepository 16 | ) { } 17 | 18 | async execute({ category_id }: IRequest): Promise< 19 | Either> { 20 | const category = await this.categoriesRepository.findById(category_id); 21 | 22 | if (!category) { 23 | return left(new CategoryNotFoundError(category_id)) 24 | }; 25 | 26 | await this.categoriesRepository.deleteOne(category_id); 27 | 28 | return right(null); 29 | } 30 | } -------------------------------------------------------------------------------- /services/ProductService/src/useCases/categories/ListAllCategoriesUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'tsyringe'; 2 | 3 | import { Either, right } from "../../logic/Either"; 4 | import { ICategoriesRepository } from './ports/ICategoriesRepository'; 5 | import { ICategoryData } from './ports/ICategoryData'; 6 | 7 | 8 | @injectable() 9 | export class ListAllCategoriesUseCase { 10 | constructor( 11 | @inject("CategoriesRepository") 12 | private categoriesRepository: ICategoriesRepository 13 | ) { } 14 | 15 | async execute(): Promise> { 16 | const categories = await this.categoriesRepository.listAll(); 17 | 18 | return right(categories); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /services/ProductService/src/useCases/categories/UpdateCategoryUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { Category } from "../../domain/entities/Category"; 4 | import { InvalidCategoryNameError } from "../../domain/entities/Category/errors/InvalidCategoryNameError"; 5 | import { Either, left, right } from "../../logic/Either"; 6 | import { CategoryAlreadyExistsError } from "./errors/CategoryAlreadyExistsError"; 7 | import { CategoryNotFoundError } from "./errors/CategoryNotFoundError"; 8 | import { ICategoriesRepository } from "./ports/ICategoriesRepository"; 9 | import { ICategoryData } from "./ports/ICategoryData"; 10 | 11 | interface IRequest { 12 | category_id: string; 13 | name: string; 14 | }; 15 | 16 | @injectable() 17 | export class UpdateCategoryUseCase { 18 | constructor( 19 | @inject("CategoriesRepository") 20 | private categoriesRepository: ICategoriesRepository 21 | ) { } 22 | 23 | async execute({ 24 | category_id, name 25 | }: IRequest): Promise< 26 | Either< 27 | | CategoryNotFoundError 28 | | CategoryAlreadyExistsError 29 | | InvalidCategoryNameError, 30 | ICategoryData 31 | > 32 | > { 33 | const category = await this.categoriesRepository.findById(category_id); 34 | 35 | if (!category) { 36 | return left(new CategoryNotFoundError(category_id)) 37 | }; 38 | 39 | const categoryAlreadyExists = await this.categoriesRepository.findByName(name); 40 | 41 | if (categoryAlreadyExists && categoryAlreadyExists.id !== category.id) { 42 | return left(new CategoryAlreadyExistsError(name)); 43 | }; 44 | 45 | const paramsToUpdate = { 46 | id: category.id, 47 | name, 48 | createdAt: category.createdAt, 49 | updatedAt: new Date(), 50 | } 51 | 52 | const categoryOrError = Category.create(paramsToUpdate); 53 | 54 | if (categoryOrError.isLeft()) { 55 | return left(categoryOrError.value); 56 | }; 57 | 58 | const categoryUpdated = await this.categoriesRepository.update(categoryOrError.value); 59 | 60 | return right(categoryUpdated); 61 | } 62 | } -------------------------------------------------------------------------------- /services/ProductService/src/useCases/categories/errors/CategoryAlreadyExistsError.ts: -------------------------------------------------------------------------------- 1 | export class CategoryAlreadyExistsError extends Error { 2 | constructor(name: string) { 3 | super(`Category "${name}" already exists`); 4 | this.name = "CategoryAlreadyExistsError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/ProductService/src/useCases/categories/errors/CategoryNotFoundError.ts: -------------------------------------------------------------------------------- 1 | export class CategoryNotFoundError extends Error { 2 | constructor(id: string) { 3 | super(`Category with id: "${id}" not found.`); 4 | this.name = "CategoryNotFoundError" 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/ProductService/src/useCases/categories/ports/ICategoriesRepository.ts: -------------------------------------------------------------------------------- 1 | import { ICategoryData } from "./ICategoryData"; 2 | 3 | export interface ICategoriesRepository { 4 | create(data: ICategoryData): Promise; 5 | findByName(name: string): Promise; 6 | findById(id: string): Promise; 7 | listAll(): Promise; 8 | update(data: ICategoryData): Promise; 9 | deleteOne(id: string): Promise; 10 | }; -------------------------------------------------------------------------------- /services/ProductService/src/useCases/categories/ports/ICategoryData.ts: -------------------------------------------------------------------------------- 1 | export interface ICategoryData { 2 | id: string; 3 | name: string; 4 | createdAt: Date; 5 | updatedAt: Date; 6 | }; -------------------------------------------------------------------------------- /services/ProductService/src/useCases/products/DeleteProductUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import { Either, left, right } from "../../logic/Either"; 3 | import { ProductNotFoundError } from "./errors/ProductNotFoundError"; 4 | import { IProductsRepository } from "./ports/IProductsRepository"; 5 | 6 | interface IRequest { 7 | product_id: string; 8 | } 9 | 10 | @injectable() 11 | export class DeleteProductUseCase { 12 | constructor( 13 | @inject("ProductsRepository") 14 | private productsRepository: IProductsRepository 15 | ) { } 16 | 17 | async execute({ product_id }: IRequest): Promise< 18 | Either> { 19 | const product = await this.productsRepository.findById(product_id); 20 | 21 | if (!product) { 22 | return left(new ProductNotFoundError(product_id)); 23 | } 24 | 25 | await this.productsRepository.deleteOne(product_id); 26 | 27 | return right(null); 28 | } 29 | } -------------------------------------------------------------------------------- /services/ProductService/src/useCases/products/ListAllProductsUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'tsyringe'; 2 | 3 | import { Either, right } from "../../logic/Either"; 4 | import { IProductData } from './ports/IProductData'; 5 | import { IProductsRepository } from './ports/IProductsRepository'; 6 | 7 | 8 | @injectable() 9 | export class ListAllProductsUseCase { 10 | constructor( 11 | @inject("ProductsRepository") 12 | private productsRepository: IProductsRepository 13 | ) { } 14 | 15 | async execute(): Promise> { 16 | const categories = await this.productsRepository.listAll(); 17 | 18 | return right(categories); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /services/ProductService/src/useCases/products/ReduceProductsStockUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'tsyringe'; 2 | 3 | import { Either, Left, left, right } from "../../logic/Either"; 4 | import { ProductInsufficientStockError } from './errors/ProductInsufficientStockError'; 5 | import { ProductNotFoundError } from './errors/ProductNotFoundError'; 6 | import { IProductsRepository } from './ports/IProductsRepository'; 7 | 8 | type ProductInfo = { 9 | id: string 10 | amount: number; 11 | } 12 | 13 | interface IRequest { 14 | productsInfos: ProductInfo[]; 15 | } 16 | 17 | @injectable() 18 | export class ReduceProductsStockUseCase { 19 | constructor( 20 | @inject("ProductsRepository") 21 | private productsRepository: IProductsRepository 22 | ) { } 23 | 24 | async execute({ productsInfos }: IRequest): Promise< 25 | Either< 26 | | ProductNotFoundError 27 | | ProductInsufficientStockError, 28 | null 29 | > 30 | > { 31 | const products = await Promise.all(productsInfos.map(async product => { 32 | return await this.productsRepository.findById(product.id); 33 | })); 34 | 35 | for (const product of products) { 36 | if (product === null) { 37 | return left(new ProductNotFoundError()); 38 | }; 39 | 40 | const productInfo = productsInfos.find(productInfo => product.id === productInfo.id); 41 | 42 | if (!productInfo) { 43 | return left(new ProductNotFoundError(product.id)) 44 | }; 45 | 46 | if (productInfo.amount > product.stock) { 47 | return left(new ProductInsufficientStockError(product.id)); 48 | }; 49 | } 50 | 51 | await Promise.all(productsInfos.map(async product => { 52 | const productInRepository = products.find(p => p?.id === product.id); 53 | 54 | if (!productInRepository) { 55 | return left(new ProductNotFoundError(product.id)) 56 | }; 57 | 58 | const stockUpdated = Number(productInRepository?.stock) - Number(product.amount); 59 | 60 | return await this.productsRepository.updateStock(product.id, stockUpdated).then(); 61 | })) 62 | 63 | return right(null); 64 | } 65 | } -------------------------------------------------------------------------------- /services/ProductService/src/useCases/products/errors/ProductAlreadyExistsError.ts: -------------------------------------------------------------------------------- 1 | export class ProductAlreadyExistsError extends Error { 2 | constructor(name: string) { 3 | super(`Product "${name}" already exists`); 4 | this.name = "ProductAlreadyExistsError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/ProductService/src/useCases/products/errors/ProductInsufficientStockError.ts: -------------------------------------------------------------------------------- 1 | export class ProductInsufficientStockError extends Error { 2 | constructor(id: string) { 3 | super(`Product with id "${id}" has insufficient stock`); 4 | this.name = "ProductInsufficientStockError"; 5 | } 6 | } -------------------------------------------------------------------------------- /services/ProductService/src/useCases/products/errors/ProductNotFoundError.ts: -------------------------------------------------------------------------------- 1 | export class ProductNotFoundError extends Error { 2 | constructor(id?: string) { 3 | if (id) { 4 | super(`Product with id "${id}" not found`); 5 | } else { 6 | super(`Product not found`); 7 | } 8 | this.name = "ProductNotFoundError"; 9 | } 10 | } -------------------------------------------------------------------------------- /services/ProductService/src/useCases/products/ports/IProductData.ts: -------------------------------------------------------------------------------- 1 | import { Category } from "../../../domain/entities/Category"; 2 | 3 | export interface IProductData { 4 | id: string; 5 | name: string; 6 | stock: number; 7 | price: number; 8 | categoryId: string | null; 9 | category?: Category | null; 10 | createdAt: Date; 11 | updatedAt: Date; 12 | }; -------------------------------------------------------------------------------- /services/ProductService/src/useCases/products/ports/IProductsRepository.ts: -------------------------------------------------------------------------------- 1 | import { IProductData } from "./IProductData"; 2 | 3 | export interface IProductsRepository { 4 | create(data: IProductData): Promise; 5 | findByName(name: string): Promise; 6 | findById(id: string): Promise; 7 | listAll(): Promise; 8 | updateStock(id: string, newStock: number): Promise; 9 | deleteOne(product_id: string): Promise; 10 | update(data: IProductData): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /services/ProductService/tests/Entities/Category.spec.ts: -------------------------------------------------------------------------------- 1 | import { Category } from "../../src/domain/entities/Category"; 2 | import { InvalidCategoryNameError } from "../../src/domain/entities/Category/errors/InvalidCategoryNameError"; 3 | 4 | describe("Category Validator", () => { 5 | it("should create a category", () => { 6 | const category = { 7 | name: "Valid Category Name", 8 | }; 9 | 10 | const createdCategoryOrError = Category.create(category); 11 | 12 | const dateNow = new Date(); 13 | 14 | const categoryWithAllParams = { 15 | id: "fake-id", 16 | name: "Valid Category Name", 17 | createdAt: dateNow, 18 | updatedAt: dateNow, 19 | }; 20 | 21 | const createdCategoryWithAllParamsOrError = Category.create(categoryWithAllParams); 22 | 23 | expect(createdCategoryOrError.isRight()).toBeTruthy(); 24 | expect(createdCategoryOrError.value).toMatchObject(category); 25 | expect(createdCategoryWithAllParamsOrError.isRight()).toBeTruthy(); 26 | expect(createdCategoryWithAllParamsOrError.value).toEqual(categoryWithAllParams); 27 | }); 28 | 29 | it("should not create a category with invalid category name", () => { 30 | const shortName = "a" 31 | 32 | const shortNameCategory = { 33 | name: shortName, 34 | }; 35 | 36 | const shortNameCreatedCategoryOrError = Category.create(shortNameCategory); 37 | 38 | const largeName = "n".repeat(256); 39 | 40 | const largeNameCategory = { 41 | name: largeName 42 | }; 43 | 44 | const largeNameCategoryOrError = Category.create(largeNameCategory); 45 | 46 | expect(shortNameCreatedCategoryOrError.isLeft()).toBeTruthy(); 47 | expect(shortNameCreatedCategoryOrError.value).toEqual(new InvalidCategoryNameError(shortName)); 48 | expect(largeNameCategoryOrError.isLeft()).toBeTruthy(); 49 | expect(largeNameCategoryOrError.value).toEqual(new InvalidCategoryNameError(largeName)); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /services/ProductService/tests/controllers/categories/ListAllCategoriesController.spec.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { ListAllCategoriesController } from "../../../src/controllers/categories/ListAllCategoriesController"; 4 | import { ICategoriesRepository } from "../../../src/useCases/categories/ports/ICategoriesRepository"; 5 | import { CategoriesActions } from "../../doubles/CategoriesActions"; 6 | import { CategoriesRepositoryInMemory } from "../../doubles/repositories/CategoriesRepositoryInMemory"; 7 | 8 | describe("List All Categories Controller", () => { 9 | let listAllCategoriesController: ListAllCategoriesController; 10 | 11 | beforeEach(() => { 12 | container.registerSingleton("CategoriesRepository", CategoriesRepositoryInMemory); 13 | listAllCategoriesController = new ListAllCategoriesController(); 14 | }); 15 | 16 | afterAll(() => { 17 | container.clearInstances(); 18 | }); 19 | 20 | 21 | it("should return status code 200 and all categories if user is authenticated", async () => { 22 | const categoriesActions = container.resolve(CategoriesActions); 23 | 24 | const dateNow = new Date(); 25 | 26 | await categoriesActions.create({ 27 | id: "fake-id-1", 28 | name: "Category 1", 29 | createdAt: dateNow, 30 | updatedAt: dateNow, 31 | }); 32 | 33 | await categoriesActions.create({ 34 | id: "fake-id-2", 35 | name: "Category 2", 36 | createdAt: dateNow, 37 | updatedAt: dateNow, 38 | }); 39 | 40 | const result = await listAllCategoriesController.handle({}); 41 | 42 | const expectedResponse = [ 43 | { 44 | id: "fake-id-1", 45 | name: "Category 1", 46 | createdAt: dateNow, 47 | updatedAt: dateNow, 48 | }, 49 | { 50 | id: "fake-id-2", 51 | name: "Category 2", 52 | createdAt: dateNow, 53 | updatedAt: dateNow, 54 | } 55 | ]; 56 | 57 | expect(result.statusCode).toBe(200); 58 | expect(result.body).toEqual(expectedResponse); 59 | }); 60 | }); -------------------------------------------------------------------------------- /services/ProductService/tests/doubles/CategoriesActions.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { ICategoriesRepository } from "../../src/useCases/categories/ports/ICategoriesRepository"; 4 | import { ICategoryData } from "../../src/useCases/categories/ports/ICategoryData"; 5 | 6 | @injectable() 7 | export class CategoriesActions { 8 | constructor( 9 | @inject("CategoriesRepository") 10 | private categoriesRepository: ICategoriesRepository, 11 | ) { } 12 | 13 | async create(data: ICategoryData): Promise { 14 | const category = await this.categoriesRepository.create(data); 15 | 16 | return category; 17 | }; 18 | 19 | async findByName(name: string): Promise { 20 | const category = await this.categoriesRepository.findByName(name); 21 | 22 | return category; 23 | }; 24 | 25 | async listAll(): Promise { 26 | const categories = await this.categoriesRepository.listAll(); 27 | 28 | return categories; 29 | } 30 | }; -------------------------------------------------------------------------------- /services/ProductService/tests/doubles/ProductsActions.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { IProductData } from "../../src/useCases/products/ports/IProductData"; 4 | import { IProductsRepository } from "../../src/useCases/products/ports/IProductsRepository"; 5 | 6 | @injectable() 7 | export class ProductsActions { 8 | constructor( 9 | @inject("ProductsRepository") 10 | private productsRepository: IProductsRepository, 11 | ) { } 12 | 13 | async create(data: IProductData): Promise { 14 | const product = await this.productsRepository.create(data); 15 | 16 | return product; 17 | }; 18 | 19 | async listAll(): Promise { 20 | const products = await this.productsRepository.listAll(); 21 | 22 | return products; 23 | } 24 | }; -------------------------------------------------------------------------------- /services/ProductService/tests/doubles/repositories/CategoriesRepositoryInMemory.ts: -------------------------------------------------------------------------------- 1 | import { ICategoriesRepository } from "../../../src/useCases/categories/ports/ICategoriesRepository"; 2 | import { ICategoryData } from "../../../src/useCases/categories/ports/ICategoryData"; 3 | 4 | export class CategoriesRepositoryInMemory implements ICategoriesRepository { 5 | categories: ICategoryData[] = []; 6 | 7 | async create(data: ICategoryData): Promise { 8 | this.categories.push(data); 9 | 10 | return data; 11 | }; 12 | 13 | async findByName(name: string): Promise { 14 | const category = this.categories.find(category => category.name === name); 15 | 16 | return category || null; 17 | }; 18 | 19 | async listAll(): Promise { 20 | return this.categories; 21 | }; 22 | 23 | async findById(id: string): Promise { 24 | const category = this.categories.find(category => category.id === id); 25 | 26 | return category || null; 27 | }; 28 | 29 | async update(data: ICategoryData): Promise { 30 | const categoryIndex = this.categories.findIndex(category => category.id === data.id); 31 | 32 | this.categories[categoryIndex] = data; 33 | 34 | return this.categories[categoryIndex]; 35 | }; 36 | 37 | async deleteOne(id: string): Promise { 38 | const categoryIndex = this.categories.findIndex(category => category.id === id); 39 | 40 | this.categories.splice(categoryIndex, 1); 41 | } 42 | } -------------------------------------------------------------------------------- /services/ProductService/tests/doubles/repositories/ProductsRepositoryInMemory.ts: -------------------------------------------------------------------------------- 1 | import { IProductData } from "../../../src/useCases/products/ports/IProductData"; 2 | import { IProductsRepository } from "../../../src/useCases/products/ports/IProductsRepository"; 3 | 4 | export class ProductsRepositoryInMemory implements IProductsRepository { 5 | products: IProductData[] = []; 6 | 7 | async create(data: IProductData): Promise { 8 | this.products.push(data); 9 | 10 | return data; 11 | } 12 | 13 | async findByName(name: string): Promise { 14 | const product = this.products.find((product) => product.name === name); 15 | 16 | return product || null; 17 | } 18 | 19 | async listAll(): Promise { 20 | return this.products; 21 | } 22 | 23 | async findById(id: string): Promise { 24 | const product = this.products.find((product) => product.id === id); 25 | 26 | return product || null; 27 | } 28 | 29 | async updateStock(id: string, newStock: number): Promise { 30 | const findIndex = this.products.findIndex((product) => product.id === id); 31 | 32 | this.products[findIndex].stock = newStock; 33 | } 34 | 35 | async deleteOne(id: string): Promise { 36 | const productIndex = this.products.findIndex( 37 | (product) => product.id === id 38 | ); 39 | 40 | this.products.splice(productIndex, 1); 41 | } 42 | 43 | async update(data: IProductData): Promise { 44 | const productIndex = this.products.findIndex( 45 | (product) => product.id === data.id 46 | ); 47 | 48 | this.products[productIndex] = data; 49 | 50 | return this.products[productIndex]; 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /services/ProductService/tests/infra/http/routes/categories/ListAllCategoriesRoute.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import { prismaClient } from '../../../../../src/infra/database/prisma/PrismaClient'; 4 | import { app } from '../../../../../src/infra/http/app'; 5 | 6 | describe("List All Categories Route", () => { 7 | let dateNow: Date; 8 | 9 | beforeAll(async () => { 10 | dateNow = new Date(); 11 | 12 | await prismaClient.$connect(); 13 | await prismaClient.categories.create({ 14 | data: { 15 | id: "fake-id-1", 16 | name: "Category-1", 17 | createdAt: dateNow, 18 | updatedAt: dateNow, 19 | } 20 | }); 21 | 22 | await prismaClient.categories.create({ 23 | data: { 24 | id: "fake-id-2", 25 | name: "Category-2", 26 | createdAt: dateNow, 27 | updatedAt: dateNow, 28 | } 29 | }); 30 | }); 31 | 32 | afterAll(async () => { 33 | await prismaClient.categories.deleteMany(); 34 | await prismaClient.$disconnect() 35 | }); 36 | 37 | it("should return status code 200 and all the categories", async () => { 38 | const response = await request(app) 39 | .get("/categories") 40 | .expect(200); 41 | 42 | const createdCategory = await prismaClient.categories.findMany(); 43 | 44 | const repositoryExpectedResponse = [ 45 | { 46 | id: "fake-id-1", 47 | name: "Category-1", 48 | createdAt: dateNow, 49 | updatedAt: dateNow, 50 | }, 51 | { 52 | id: "fake-id-2", 53 | name: "Category-2", 54 | createdAt: dateNow, 55 | updatedAt: dateNow, 56 | } 57 | ]; 58 | 59 | const bodyExpectedResponse = [ 60 | { 61 | id: "fake-id-1", 62 | name: "Category-1", 63 | createdAt: dateNow.toISOString(), 64 | updatedAt: dateNow.toISOString(), 65 | }, 66 | { 67 | id: "fake-id-2", 68 | name: "Category-2", 69 | createdAt: dateNow.toISOString(), 70 | updatedAt: dateNow.toISOString(), 71 | } 72 | ]; 73 | 74 | expect(createdCategory).toEqual(repositoryExpectedResponse); 75 | expect(response.body).toEqual(bodyExpectedResponse); 76 | }); 77 | }); -------------------------------------------------------------------------------- /services/ProductService/tests/infra/serverless/CreateCategory.spec.ts: -------------------------------------------------------------------------------- 1 | import { prismaClient } from "../../../src/infra/database/prisma/PrismaClient"; 2 | import { handle } from "../../../src/infra/serverless/functions/CreateCategory"; 3 | 4 | describe("Create Category Serverless Function", () => { 5 | beforeAll(async () => { 6 | await prismaClient.$connect(); 7 | }); 8 | 9 | afterAll(async () => { 10 | await prismaClient.categories.deleteMany(); 11 | await prismaClient.$disconnect(); 12 | }); 13 | 14 | it("should return status code 201 and the created category", async () => { 15 | const event = { 16 | requestContext: { 17 | authorizer: { 18 | user: { 19 | id: "admin-id", 20 | name: "Admin", 21 | email: "admin@example.com" 22 | }, 23 | }, 24 | }, 25 | body: { 26 | name: "Category Name" 27 | } 28 | }; 29 | const context = {}; 30 | 31 | const createCategoryServerless = await handle(event, context); 32 | 33 | const bodyParsed = JSON.parse(createCategoryServerless.body); 34 | 35 | expect(bodyParsed).toHaveProperty("id"); 36 | expect(bodyParsed).toHaveProperty("name"); 37 | expect(bodyParsed.name).toEqual("Category Name"); 38 | expect(createCategoryServerless.statusCode).toEqual(201); 39 | }); 40 | }); -------------------------------------------------------------------------------- /services/ProductService/tests/infra/serverless/CreateProduct.spec.ts: -------------------------------------------------------------------------------- 1 | import { prismaClient } from "../../../src/infra/database/prisma/PrismaClient"; 2 | import { handle } from "../../../src/infra/serverless/functions/CreateProduct"; 3 | 4 | describe("Create Product Serverless Function", () => { 5 | let dateNow: Date; 6 | 7 | beforeAll(async () => { 8 | dateNow = new Date(); 9 | 10 | await prismaClient.$connect(); 11 | await prismaClient.categories.create({ 12 | data: { 13 | id: "category-id", 14 | name: "Category", 15 | createdAt: dateNow, 16 | updatedAt: dateNow, 17 | } 18 | }); 19 | }); 20 | 21 | afterAll(async () => { 22 | await prismaClient.products.deleteMany(); 23 | await prismaClient.categories.deleteMany(); 24 | await prismaClient.$disconnect(); 25 | }); 26 | 27 | it("should return status code 201 and the created product", async () => { 28 | const event = { 29 | requestContext: { 30 | authorizer: { 31 | user: { 32 | id: "admin-id", 33 | name: "Admin", 34 | email: "admin@example.com" 35 | }, 36 | }, 37 | }, 38 | body: { 39 | name: "Product Name", 40 | stock: 42, 41 | price: 25.45, 42 | categoryId: "category-id" 43 | } 44 | }; 45 | const context = {}; 46 | 47 | const createProductServerless = await handle(event, context); 48 | 49 | const bodyParsed = JSON.parse(createProductServerless.body); 50 | 51 | expect(bodyParsed).toHaveProperty("id"); 52 | expect(bodyParsed).toHaveProperty("name"); 53 | expect(bodyParsed.name).toEqual("Product Name"); 54 | expect(bodyParsed).toHaveProperty("stock"); 55 | expect(bodyParsed).toHaveProperty("price"); 56 | expect(bodyParsed).toHaveProperty("categoryId"); 57 | expect(createProductServerless.statusCode).toEqual(201); 58 | }); 59 | }); -------------------------------------------------------------------------------- /services/ProductService/tests/infra/serverless/DeleteCategory.spec.ts: -------------------------------------------------------------------------------- 1 | import { prismaClient } from "../../../src/infra/database/prisma/PrismaClient"; 2 | import { handle } from "../../../src/infra/serverless/functions/DeleteCategory"; 3 | 4 | describe("Delete Category Serverless Function", () => { 5 | let dateNow: Date; 6 | 7 | beforeAll(async () => { 8 | dateNow = new Date(); 9 | 10 | await prismaClient.$connect(); 11 | await prismaClient.categories.create({ 12 | data: { 13 | id: "category-id-1", 14 | name: "Category 1", 15 | createdAt: dateNow, 16 | updatedAt: dateNow, 17 | } 18 | }); 19 | 20 | await prismaClient.categories.create({ 21 | data: { 22 | id: "category-id-2", 23 | name: "Category 2", 24 | createdAt: dateNow, 25 | updatedAt: dateNow, 26 | } 27 | }); 28 | }); 29 | 30 | afterAll(async () => { 31 | await prismaClient.categories.deleteMany(); 32 | await prismaClient.$disconnect(); 33 | }); 34 | 35 | it("should return status code 200 if the category is deleted", async () => { 36 | const event = { 37 | requestContext: { 38 | authorizer: { 39 | user: { 40 | id: "admin-id", 41 | name: "Admin", 42 | email: "admin@example.com" 43 | }, 44 | }, 45 | }, 46 | pathParameters: { 47 | id: "category-id-1" 48 | } 49 | }; 50 | const context = {}; 51 | 52 | const deleteCategoryServerless = await handle(event, context); 53 | 54 | const categories = await prismaClient.categories.findMany(); 55 | 56 | const expectedResponse = [ 57 | { 58 | id: "category-id-2", 59 | name: "Category 2", 60 | createdAt: dateNow, 61 | updatedAt: dateNow, 62 | } 63 | ] 64 | 65 | expect(categories).toEqual(expectedResponse); 66 | expect(deleteCategoryServerless.statusCode).toEqual(200); 67 | }); 68 | }); -------------------------------------------------------------------------------- /services/ProductService/tests/infra/serverless/UpdateCategory.spec.ts: -------------------------------------------------------------------------------- 1 | import { prismaClient } from "../../../src/infra/database/prisma/PrismaClient"; 2 | import { handle } from "../../../src/infra/serverless/functions/UpdateCategory"; 3 | 4 | describe("Update Category Serverless Function", () => { 5 | let dateNow: Date; 6 | 7 | beforeAll(async () => { 8 | dateNow = new Date(); 9 | 10 | await prismaClient.$connect(); 11 | await prismaClient.categories.create({ 12 | data: { 13 | id: "category-id", 14 | name: "Category", 15 | createdAt: dateNow, 16 | updatedAt: dateNow, 17 | } 18 | }); 19 | }); 20 | 21 | afterAll(async () => { 22 | await prismaClient.categories.deleteMany(); 23 | await prismaClient.$disconnect(); 24 | }); 25 | 26 | it("should return status code 201 and the updated category", async () => { 27 | const event = { 28 | requestContext: { 29 | authorizer: { 30 | user: { 31 | id: "admin-id", 32 | name: "Admin", 33 | email: "admin@example.com" 34 | }, 35 | }, 36 | }, 37 | body: { 38 | name: "New Name" 39 | }, 40 | pathParameters: { 41 | id: "category-id" 42 | } 43 | }; 44 | const context = {}; 45 | 46 | const deleteCategoryServerless = await handle(event, context); 47 | 48 | const bodyParsed = JSON.parse(deleteCategoryServerless.body); 49 | 50 | const categories = await prismaClient.categories.findFirst({ 51 | where: { 52 | id: "category-id" 53 | } 54 | }); 55 | 56 | const expectedResponse = { 57 | id: "category-id", 58 | name: "New Name", 59 | createdAt: dateNow, 60 | }; 61 | 62 | expect(categories).toMatchObject(expectedResponse); 63 | expect(bodyParsed).toHaveProperty("id"); 64 | expect(bodyParsed).toHaveProperty("name"); 65 | expect(bodyParsed.name).toEqual("New Name"); 66 | expect(deleteCategoryServerless.statusCode).toEqual(201); 67 | }); 68 | }); -------------------------------------------------------------------------------- /services/ProductService/tests/useCases/categories/ListAllCategoriesUseCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { ListAllCategoriesUseCase } from "../../../src/useCases/categories/ListAllCategoriesUseCase"; 2 | import { CategoriesRepositoryInMemory } from "../../doubles/repositories/CategoriesRepositoryInMemory"; 3 | 4 | describe("List All Categories UseCase", () => { 5 | let categoriesRepositoryInMemory: CategoriesRepositoryInMemory; 6 | let listAllCategoriesUseCase: ListAllCategoriesUseCase; 7 | 8 | beforeEach(() => { 9 | categoriesRepositoryInMemory = new CategoriesRepositoryInMemory() 10 | listAllCategoriesUseCase = new ListAllCategoriesUseCase(categoriesRepositoryInMemory); 11 | }); 12 | 13 | it("should list all categories", async () => { 14 | const dateNow = new Date(); 15 | 16 | await categoriesRepositoryInMemory.create({ 17 | id: "fake-id-1", 18 | name: "Category 1", 19 | createdAt: dateNow, 20 | updatedAt: dateNow, 21 | }); 22 | 23 | await categoriesRepositoryInMemory.create({ 24 | id: "fake-id-2", 25 | name: "Category 2", 26 | createdAt: dateNow, 27 | updatedAt: dateNow, 28 | }); 29 | 30 | const categories = await listAllCategoriesUseCase.execute(); 31 | 32 | const expectedResponse = [ 33 | { 34 | id: "fake-id-1", 35 | name: "Category 1", 36 | createdAt: dateNow, 37 | updatedAt: dateNow, 38 | }, 39 | { 40 | id: "fake-id-2", 41 | name: "Category 2", 42 | createdAt: dateNow, 43 | updatedAt: dateNow, 44 | }, 45 | ]; 46 | 47 | expect(categories.isRight()).toBeTruthy(); 48 | expect(categories.value).toEqual(expectedResponse); 49 | }); 50 | }); -------------------------------------------------------------------------------- /services/UserService/.env.development.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://docker:password@user_service_database:5432/user_service_database?schema=public" 2 | 3 | TOKEN_SECRET_KEY="" -------------------------------------------------------------------------------- /services/UserService/.env.test.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://prisma_test:password@localhost:5433/user_service_test_database?schema=public" 2 | TEST="true" -------------------------------------------------------------------------------- /services/UserService/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .env.development 4 | .env.test 5 | .env -------------------------------------------------------------------------------- /services/UserService/@types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | declare namespace Express { 3 | export interface Request { 4 | user: { 5 | id: string; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /services/UserService/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /usr/app 4 | 5 | COPY package.json ./ 6 | 7 | RUN yarn 8 | 9 | COPY . . 10 | 11 | EXPOSE 3333 12 | 13 | CMD ["yarn", "dev"] 14 | -------------------------------------------------------------------------------- /services/UserService/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | user_service_database: 5 | image: postgres 6 | container_name: user_service_database 7 | restart: always 8 | ports: 9 | - 5432:5432 10 | environment: 11 | - POSTGRES_USER=docker 12 | - POSTGRES_PASSWORD=password 13 | - POSTGRES_DB=user_service_database 14 | volumes: 15 | - pgdata:/data/postgres 16 | user_service_test_database: 17 | image: postgres 18 | container_name: user_service_test_database 19 | restart: always 20 | ports: 21 | - 5433:5432 22 | environment: 23 | - POSTGRES_USER=prisma_test 24 | - POSTGRES_PASSWORD=password 25 | - POSTGRES_DB=user_service_test_database 26 | volumes: 27 | - pgdata:/data/postgres 28 | user_service_api: 29 | build: . 30 | container_name: user_service_api 31 | ports: 32 | - 3333:3333 33 | volumes: 34 | - .:/usr/app 35 | links: 36 | - user_service_database 37 | depends_on: 38 | - user_service_database 39 | 40 | volumes: 41 | pgdata: 42 | driver: local 43 | -------------------------------------------------------------------------------- /services/UserService/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; -------------------------------------------------------------------------------- /services/UserService/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-service", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "prisma migrate deploy --schema ./src/infra/database/prisma/schema.prisma && dotenv -e .env -- node ./dist/src/infra/http/server.js", 9 | "prisma:migrate": "dotenv -e .env -- prisma migrate deploy --schema ./src/infra/database/prisma/schema.prisma", 10 | "dev": "dotenv -e .env.development -- prisma migrate dev --schema ./src/infra/database/prisma/schema.prisma && dotenv -e .env.development -- ts-node-dev --inspect --transpile-only --ignore-watch node_modules --respawn src/infra/http/server.ts", 11 | "test": "dotenv -e .env.test -- prisma migrate dev --schema ./src/infra/database/prisma/schema.prisma && dotenv -e .env.test -- jest --runInBand" 12 | }, 13 | "devDependencies": { 14 | "@types/bcrypt": "^5.0.0", 15 | "@types/express": "^4.17.13", 16 | "@types/jest": "^27.4.1", 17 | "@types/jsonwebtoken": "^8.5.8", 18 | "@types/node": "^17.0.21", 19 | "git-commit-msg-linter": "^4.1.1", 20 | "jest": "^27.5.1", 21 | "prisma": "^3.10.0", 22 | "supertest": "^6.2.2", 23 | "ts-jest": "^27.1.3", 24 | "ts-node-dev": "^1.1.8", 25 | "typescript": "^4.6.2" 26 | }, 27 | "dependencies": { 28 | "@prisma/client": "^3.10.0", 29 | "@types/supertest": "^2.0.11", 30 | "bcrypt": "^5.0.1", 31 | "dotenv-cli": "^5.0.0", 32 | "express": "^4.17.3", 33 | "jsonwebtoken": "^8.5.1", 34 | "reflect-metadata": "^0.1.13", 35 | "tsyringe": "^4.6.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/UserService/src/container/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | import { JwtAuthenticationTokenProvider } from "../infra/authentication/JwtAuthenticationTokenProvider"; 3 | 4 | import { PrismaUsersRepository } from "../infra/database/prisma/repositories/PrismaUsersRepository"; 5 | import { BcryptEncoder } from "../infra/encoder/BcryptEncoder"; 6 | import { IAuthenticationTokenProvider } from "../useCases/users/ports/IAuthenticationTokenProvider"; 7 | import { IEncoder } from "../useCases/users/ports/IEncoder"; 8 | import { IUsersRepository } from "../useCases/users/ports/IUsersRepository"; 9 | 10 | container.registerSingleton( 11 | "UsersRepository", 12 | PrismaUsersRepository 13 | ); 14 | 15 | container.registerSingleton( 16 | "Encoder", 17 | BcryptEncoder 18 | ); 19 | 20 | container.registerSingleton( 21 | "AuthenticationTokenProvider", 22 | JwtAuthenticationTokenProvider 23 | ); -------------------------------------------------------------------------------- /services/UserService/src/controllers/errors/MissingParamError.ts: -------------------------------------------------------------------------------- 1 | export class MissingParamError extends Error { 2 | constructor(missingParams: string) { 3 | super(`Missing parameter(s): ${missingParams}.`); 4 | this.name = "MissingParamError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/UserService/src/controllers/ports/IController.ts: -------------------------------------------------------------------------------- 1 | import { IHttpRequest } from "./IHttpRequest"; 2 | import { IHttpResponse } from "./IHttpResponse"; 3 | 4 | export interface IController { 5 | readonly requiredParams?: string[]; 6 | handle(request: IHttpRequest): Promise; 7 | } -------------------------------------------------------------------------------- /services/UserService/src/controllers/ports/IHttpRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpRequest { 2 | user?: { 3 | id?: string; 4 | }; 5 | body?: any; 6 | params?: any; 7 | }; -------------------------------------------------------------------------------- /services/UserService/src/controllers/ports/IHttpResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpResponse { 2 | statusCode: number; 3 | body: any; 4 | }; -------------------------------------------------------------------------------- /services/UserService/src/controllers/users/AdminUpdateUserController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { UpdateUserUseCase } from "../../useCases/users/UpdateUserUseCase"; 4 | import { MissingParamError } from "../errors/MissingParamError"; 5 | import { IController } from "../ports/IController"; 6 | import { IHttpRequest } from "../ports/IHttpRequest"; 7 | import { IHttpResponse } from "../ports/IHttpResponse"; 8 | import { badRequest, forbidden, serverError, unauthorized, updated } from "../utils/HttpResponses"; 9 | import { IsRequiredParamsMissing } from "../utils/IsRequiredParamsMissing"; 10 | 11 | export class AdminUpdateUserController implements IController { 12 | requiredParams = ["name", "email", "user_id_to_be_updated"]; 13 | 14 | async handle(request: IHttpRequest): Promise { 15 | try { 16 | const updateUserUseCase = container.resolve(UpdateUserUseCase); 17 | 18 | const requiredParamsMissing = IsRequiredParamsMissing(request.body, this.requiredParams) 19 | 20 | if (requiredParamsMissing) { 21 | return badRequest(new MissingParamError(requiredParamsMissing).message); 22 | }; 23 | 24 | if (!request.user || !request.user.id) { 25 | return unauthorized("Admin user is not authenticated!"); 26 | }; 27 | 28 | const { name, email, user_id_to_be_updated } = request.body; 29 | 30 | const response = await updateUserUseCase.execute({ 31 | user_id: user_id_to_be_updated, 32 | name, 33 | email, 34 | }); 35 | 36 | 37 | if (response.isLeft()) { 38 | return forbidden(response.value.message); 39 | }; 40 | 41 | return updated(response.value); 42 | } catch (error) { 43 | return serverError(error); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /services/UserService/src/controllers/users/CreateUserController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { CreateUserUseCase } from "../../useCases/users/CreateUserUseCase"; 4 | import { UserAlreadyExistsError } from "../../useCases/users/errors/UserAlreadyExistsError"; 5 | import { MissingParamError } from "../errors/MissingParamError"; 6 | import { IController } from "../ports/IController"; 7 | import { IHttpRequest } from "../ports/IHttpRequest"; 8 | import { IHttpResponse } from "../ports/IHttpResponse"; 9 | import { badRequest, created, forbidden, serverError } from "../utils/HttpResponses"; 10 | import { IsRequiredParamsMissing } from "../utils/IsRequiredParamsMissing"; 11 | 12 | export class CreateUserController implements IController { 13 | readonly requiredParams = ["name", "email", "password"]; 14 | 15 | async handle(request: IHttpRequest): Promise { 16 | try { 17 | const createUserUseCase = container.resolve(CreateUserUseCase); 18 | 19 | const requiredParamsMissing = IsRequiredParamsMissing(request.body, this.requiredParams) 20 | 21 | if (requiredParamsMissing) { 22 | return badRequest(new MissingParamError(requiredParamsMissing).message); 23 | }; 24 | 25 | const { name, email, password } = request.body; 26 | 27 | const response = await createUserUseCase.execute({ 28 | name, 29 | email, 30 | password 31 | }); 32 | 33 | 34 | if (response.isRight()) { 35 | return created("User Created!"); 36 | } 37 | 38 | if (response.value instanceof UserAlreadyExistsError) { 39 | return forbidden(response.value.message); 40 | }; 41 | 42 | return badRequest(response.value.message); 43 | } catch (error) { 44 | return serverError(error); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /services/UserService/src/controllers/users/DeleteUserController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { DeleteUserUseCase } from "../../useCases/users/DeleteUserUseCase"; 4 | import { MissingParamError } from "../errors/MissingParamError"; 5 | import { IController } from "../ports/IController"; 6 | import { IHttpRequest } from "../ports/IHttpRequest"; 7 | import { IHttpResponse } from "../ports/IHttpResponse"; 8 | import { badRequest, forbidden, ok, serverError, unauthorized } from "../utils/HttpResponses"; 9 | import { IsRequiredParamsMissing } from "../utils/IsRequiredParamsMissing"; 10 | 11 | export class DeleteUserController implements IController { 12 | requiredParams = ["user_id"]; 13 | 14 | async handle(request: IHttpRequest): Promise { 15 | try { 16 | const deleteUserUseCase = container.resolve(DeleteUserUseCase); 17 | 18 | const requiredParamsMissing = IsRequiredParamsMissing(request.params, this.requiredParams) 19 | 20 | if (requiredParamsMissing) { 21 | return badRequest(new MissingParamError(requiredParamsMissing).message); 22 | }; 23 | 24 | const { user_id } = request.params; 25 | 26 | if (!request.user || !request.user.id) { 27 | return unauthorized("User is not authenticated!"); 28 | }; 29 | 30 | const response = await deleteUserUseCase.execute({ userIdToBeDeleted: user_id }); 31 | 32 | if (response.isLeft()) { 33 | return forbidden(response.value.message) 34 | } 35 | 36 | return ok("User deleted!"); 37 | } catch (error) { 38 | return serverError(error); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /services/UserService/src/controllers/users/ListAllUsersController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { ListAllUsersUseCase } from "../../useCases/users/ListAllUsersUseCase"; 4 | import { IController } from "../ports/IController"; 5 | import { IHttpRequest } from "../ports/IHttpRequest"; 6 | import { IHttpResponse } from "../ports/IHttpResponse"; 7 | import { ok, serverError, unauthorized } from "../utils/HttpResponses"; 8 | 9 | export class ListAllUsersController implements IController { 10 | async handle(request: IHttpRequest): Promise { 11 | try { 12 | const listAllUsersUseCase = container.resolve(ListAllUsersUseCase); 13 | 14 | if (!request.user || !request.user.id) { 15 | return unauthorized("User is not authenticated!"); 16 | }; 17 | 18 | const response = await listAllUsersUseCase.execute(); 19 | 20 | return ok(response.value); 21 | } catch (error) { 22 | return serverError(error); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /services/UserService/src/controllers/users/UserProfileController.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { UserNotFoundError } from "../../useCases/users/errors/UserNotFoundError"; 4 | import { UserProfileUseCase } from "../../useCases/users/UserProfileUseCase"; 5 | import { MissingParamError } from "../errors/MissingParamError"; 6 | import { IController } from "../ports/IController"; 7 | import { IHttpRequest } from "../ports/IHttpRequest"; 8 | import { IHttpResponse } from "../ports/IHttpResponse"; 9 | import { badRequest, forbidden, ok, serverError } from "../utils/HttpResponses"; 10 | 11 | export class UserProfileController implements IController { 12 | async handle(request: IHttpRequest): Promise { 13 | try { 14 | const userProfileUseCase = container.resolve(UserProfileUseCase); 15 | 16 | if (!request.user || !request.user.id) { 17 | return badRequest(new MissingParamError("user id").message); 18 | }; 19 | 20 | const { id } = request.user; 21 | 22 | const response = await userProfileUseCase.execute({ 23 | user_id: id 24 | }); 25 | 26 | if (response.value instanceof UserNotFoundError) { 27 | return forbidden(response.value.message); 28 | }; 29 | 30 | return ok(response.value); 31 | } catch (error) { 32 | return serverError(error); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /services/UserService/src/controllers/utils/HttpResponses.ts: -------------------------------------------------------------------------------- 1 | import { IHttpResponse } from '../ports/IHttpResponse' 2 | 3 | export const ok = (data: any): IHttpResponse => ({ 4 | statusCode: 200, 5 | body: data 6 | }) 7 | 8 | export const created = (data: any): IHttpResponse => ({ 9 | statusCode: 201, 10 | body: data 11 | }) 12 | 13 | export const updated = (data: any): IHttpResponse => ({ 14 | statusCode: 201, 15 | body: data 16 | }) 17 | 18 | export const unauthorized = (error: any): IHttpResponse => ({ 19 | statusCode: 401, 20 | body: error 21 | }) 22 | 23 | export const forbidden = (error: any): IHttpResponse => ({ 24 | statusCode: 403, 25 | body: error 26 | }) 27 | 28 | export const badRequest = (error: any): IHttpResponse => ({ 29 | statusCode: 400, 30 | body: error 31 | }) 32 | 33 | export const serverError = (error: any): IHttpResponse => ({ 34 | statusCode: 500, 35 | body: error 36 | }) -------------------------------------------------------------------------------- /services/UserService/src/controllers/utils/IsRequiredParamsMissing.ts: -------------------------------------------------------------------------------- 1 | export function IsRequiredParamsMissing(receivedParams: any, requiredParams: string[]): string { 2 | const missingParams: string[] = []; 3 | 4 | requiredParams.forEach(param => { 5 | if (!Object.keys(receivedParams).includes(param)) { 6 | missingParams.push(param); 7 | } 8 | 9 | return false; 10 | }); 11 | 12 | return missingParams.join(', '); 13 | } -------------------------------------------------------------------------------- /services/UserService/src/domain/entities/User/ValidateEmail.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | export function valid(email: string): boolean { 3 | const maxEmailSize = 320; 4 | if (emptyOrTooLarge(email, maxEmailSize) || nonConformant(email)) { 5 | return false; 6 | } 7 | 8 | const [local, domain] = email.split("@"); 9 | const maxLocalSize = 64; 10 | const maxDomainSize = 255; 11 | if ( 12 | emptyOrTooLarge(local, maxLocalSize) || 13 | emptyOrTooLarge(domain, maxDomainSize) 14 | ) { 15 | return false; 16 | } 17 | 18 | if (somePartIsTooLargeIn(domain)) { 19 | return false; 20 | } 21 | 22 | return true; 23 | } 24 | 25 | function emptyOrTooLarge(str: string, maxSize: number): boolean { 26 | if (!str || str.length > maxSize) { 27 | return true; 28 | } 29 | 30 | return false; 31 | } 32 | 33 | function nonConformant(email: string): boolean { 34 | const emailRegex = 35 | /^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; 36 | 37 | return !emailRegex.test(email); 38 | } 39 | 40 | function somePartIsTooLargeIn(domain: string): boolean { 41 | const maxPartSize = 63; 42 | const domainParts = domain.split("."); 43 | return domainParts.some((part) => { 44 | return part.length > maxPartSize; 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /services/UserService/src/domain/entities/User/errors/InvalidEmailError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidEmailError extends Error { 2 | constructor(email: string) { 3 | super(`"${email}" is an invalid email`); 4 | this.name = "InvalidEmailError"; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /services/UserService/src/domain/entities/User/errors/InvalidNameError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidNameError extends Error { 2 | constructor(name: string) { 3 | super(`"${name}" is an invalid name`); 4 | this.name = "InvalidNameError"; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /services/UserService/src/domain/entities/User/errors/InvalidPasswordError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidPasswordError extends Error { 2 | constructor() { 3 | super("Invalid Password"); 4 | this.name = "InvalidPasswordError"; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /services/UserService/src/domain/entities/User/index.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | import { Either, left, right } from "../../../logic/Either"; 4 | import { InvalidEmailError } from "./errors/InvalidEmailError"; 5 | import { InvalidNameError } from "./errors/InvalidNameError"; 6 | import { InvalidPasswordError } from "./errors/InvalidPasswordError"; 7 | import { valid } from "./ValidateEmail"; 8 | 9 | type UserProps = { 10 | id?: string; 11 | name: string; 12 | email: string; 13 | password: string; 14 | createdAt?: Date; 15 | updatedAt?: Date; 16 | isAdmin?: boolean; 17 | }; 18 | 19 | export class User { 20 | public readonly id: string; 21 | public readonly name: string; 22 | public readonly email: string; 23 | public readonly password: string; 24 | public readonly createdAt: Date; 25 | public readonly updatedAt: Date; 26 | public readonly isAdmin: boolean; 27 | 28 | private constructor(props: UserProps) { 29 | this.id = props.id || crypto.randomUUID(); 30 | this.name = props.name; 31 | this.email = props.email; 32 | this.password = props.password; 33 | this.createdAt = props.createdAt || new Date(); 34 | this.updatedAt = props.updatedAt || new Date(); 35 | this.isAdmin = props.isAdmin || false; 36 | }; 37 | 38 | static create( 39 | props: UserProps 40 | ): Either { 41 | const validate = User.validate(props); 42 | 43 | if (validate.isLeft()) { 44 | return left(validate.value); 45 | } 46 | 47 | const user = new User(props); 48 | 49 | return right(user); 50 | } 51 | 52 | public static validate( 53 | props: Omit 54 | ): Either { 55 | const { name, email, password } = props; 56 | 57 | if (!name || name.trim().length > 255 || name.trim().length < 2) { 58 | return left(new InvalidNameError(name)); 59 | } 60 | 61 | if (!valid(email)) { 62 | return left(new InvalidEmailError(email)); 63 | } 64 | 65 | if (!password || password.length > 255 || password.length < 6) { 66 | return left(new InvalidPasswordError()); 67 | } 68 | 69 | return right(true); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /services/UserService/src/infra/authentication/JwtAuthenticationTokenProvider.ts: -------------------------------------------------------------------------------- 1 | import { sign, verify } from 'jsonwebtoken'; 2 | 3 | import { IAuthenticationTokenProvider } from "../../useCases/users/ports/IAuthenticationTokenProvider"; 4 | import authConfig from './config'; 5 | 6 | const { secretKey, expiresIn } = authConfig; 7 | 8 | export class JwtAuthenticationTokenProvider implements IAuthenticationTokenProvider { 9 | generateToken(subject: string): string { 10 | const token = sign({}, secretKey, { 11 | subject, 12 | expiresIn, 13 | }); 14 | 15 | return token; 16 | } 17 | 18 | verify(token: string): string | undefined { 19 | try { 20 | const { sub } = verify(token, secretKey); 21 | 22 | return sub?.toString(); 23 | } catch (err) { 24 | return undefined; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /services/UserService/src/infra/authentication/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | secretKey: process.env.TOKEN_SECRET_KEY || "secretKey", 3 | expiresIn: '1d', 4 | }; -------------------------------------------------------------------------------- /services/UserService/src/infra/database/prisma/PrismaClient.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const prismaClient = new PrismaClient(); -------------------------------------------------------------------------------- /services/UserService/src/infra/database/prisma/migrations/20220331134924_add_users/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "users" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | "password" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "isAdmin" BOOLEAN NOT NULL DEFAULT false, 10 | 11 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); 16 | -------------------------------------------------------------------------------- /services/UserService/src/infra/database/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /services/UserService/src/infra/database/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | url = env("DATABASE_URL") 3 | provider = "postgresql" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Users { 11 | id String @id @default(uuid()) 12 | name String 13 | email String @unique 14 | password String 15 | createdAt DateTime @default(now()) 16 | updatedAt DateTime @default(now()) 17 | isAdmin Boolean @default(false) 18 | 19 | @@map("users") 20 | } 21 | -------------------------------------------------------------------------------- /services/UserService/src/infra/encoder/BcryptEncoder.ts: -------------------------------------------------------------------------------- 1 | import { compare, hash } from 'bcrypt'; 2 | 3 | import { IEncoder } from "../../useCases/users/ports/IEncoder"; 4 | 5 | export class BcryptEncoder implements IEncoder { 6 | async encode(plain: string): Promise { 7 | const hashedString = await hash(plain, 8); 8 | 9 | return hashedString; 10 | }; 11 | 12 | async compare(plain: string, hash: string): Promise { 13 | const isEqual = await compare(plain, hash); 14 | 15 | return isEqual; 16 | } 17 | }; -------------------------------------------------------------------------------- /services/UserService/src/infra/http/app.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | 3 | import "../../container"; 4 | 5 | import express from "express"; 6 | 7 | import { router } from './routes'; 8 | 9 | const app = express(); 10 | 11 | app.use(express.json()); 12 | 13 | app.use(router); 14 | 15 | export { app }; -------------------------------------------------------------------------------- /services/UserService/src/infra/http/middlewares/MiddlewareAdapter.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | import { IMiddleware } from "../../../middlewares/ports/IMiddleware"; 4 | 5 | export function MiddlewareRouteAdapter(middleware: IMiddleware) { 6 | return async ( 7 | request: Request, response: Response, next: NextFunction 8 | ): Promise => { 9 | const httpResponse = await middleware.handle(request); 10 | 11 | if (httpResponse.statusCode !== 200) { 12 | response.status(httpResponse.statusCode).json(httpResponse.body); 13 | } 14 | 15 | if (httpResponse.statusCode === 200) { 16 | next(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /services/UserService/src/infra/http/middlewares/adaptedEnsureAdmin.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { EnsureAdmin } from "../../../middlewares/ensureAdmin"; 4 | import { MiddlewareRouteAdapter } from "./MiddlewareAdapter"; 5 | 6 | const ensureAdmin = container.resolve(EnsureAdmin); 7 | 8 | export const adaptedEnsureAdmin = MiddlewareRouteAdapter(ensureAdmin); -------------------------------------------------------------------------------- /services/UserService/src/infra/http/middlewares/adaptedEnsureAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import { container } from "tsyringe"; 2 | 3 | import { EnsureAuthenticated } from "../../../middlewares/ensureAuthenticated"; 4 | import { MiddlewareRouteAdapter } from "./MiddlewareAdapter"; 5 | 6 | const ensureAuthenticated = container.resolve(EnsureAuthenticated); 7 | 8 | export const adaptedEnsureAuthenticated = MiddlewareRouteAdapter(ensureAuthenticated); -------------------------------------------------------------------------------- /services/UserService/src/infra/http/routes/RouteAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | import { IController } from "../../../controllers/ports/IController"; 4 | 5 | export function routeAdapter(controller: IController) { 6 | return async (request: Request, response: Response) => { 7 | const httpResponse = await controller.handle(request); 8 | response.status(httpResponse.statusCode).json(httpResponse.body); 9 | }; 10 | }; -------------------------------------------------------------------------------- /services/UserService/src/infra/http/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { usersRoutes } from './users.routes'; 4 | 5 | const router = Router(); 6 | 7 | router.use("/", usersRoutes); 8 | 9 | export { router }; -------------------------------------------------------------------------------- /services/UserService/src/infra/http/server.ts: -------------------------------------------------------------------------------- 1 | import { app } from "./app"; 2 | 3 | app.listen(3333, () => { 4 | console.log("🚀 App is running on port 3333!") 5 | }); -------------------------------------------------------------------------------- /services/UserService/src/logic/Either.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | export type Either = Left | Right; 3 | export class Left { 4 | readonly value: L; 5 | 6 | constructor(value: L) { 7 | this.value = value; 8 | } 9 | 10 | isLeft(): this is Left { 11 | return true; 12 | } 13 | 14 | isRight(): this is Right { 15 | return false; 16 | } 17 | } 18 | 19 | export class Right { 20 | readonly value: A; 21 | 22 | constructor(value: A) { 23 | this.value = value; 24 | } 25 | 26 | isLeft(): this is Left { 27 | return false; 28 | } 29 | 30 | isRight(): this is Right { 31 | return true; 32 | } 33 | } 34 | 35 | export const left = (l: L): Either => { 36 | return new Left(l); 37 | }; 38 | 39 | export const right = (a: A): Either => { 40 | return new Right(a); 41 | }; 42 | -------------------------------------------------------------------------------- /services/UserService/src/middlewares/ensureAdmin.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { IAuthenticationTokenProvider } from "../useCases/users/ports/IAuthenticationTokenProvider"; 4 | import { IUsersRepository } from "../useCases/users/ports/IUsersRepository"; 5 | import { IHttpRequest } from "./ports/IHttpRequest"; 6 | import { IHttpResponse } from "./ports/IHttpResponse"; 7 | import { IMiddleware } from "./ports/IMiddleware"; 8 | import { unauthorized, ok, serverError } from "./utils/HttpResponses"; 9 | 10 | @injectable() 11 | export class EnsureAdmin implements IMiddleware { 12 | constructor( 13 | @inject("AuthenticationTokenProvider") 14 | private authenticationTokenProvider: IAuthenticationTokenProvider, 15 | @inject("UsersRepository") 16 | private usersRepository: IUsersRepository, 17 | ) { } 18 | 19 | async handle(request: IHttpRequest): Promise { 20 | 21 | try { 22 | if (!request.headers.authorization) { 23 | return unauthorized("Token is missing!") 24 | } 25 | 26 | const authHeader = request.headers.authorization; 27 | 28 | const [, token] = authHeader.split(" "); 29 | 30 | 31 | const user_id = this.authenticationTokenProvider.verify( 32 | token, 33 | ); 34 | 35 | if (!user_id) { 36 | return unauthorized("Token is invalid!"); 37 | } 38 | 39 | const user = await this.usersRepository.findById(user_id); 40 | 41 | if (!user) { 42 | return unauthorized("User not found!"); 43 | } 44 | 45 | if (!user.isAdmin) { 46 | return unauthorized("User is not an admin!"); 47 | } 48 | 49 | request.user = { 50 | id: user_id, 51 | } 52 | return ok("User is authenticated!"); 53 | } catch (error) { 54 | return serverError(error); 55 | }; 56 | }; 57 | }; -------------------------------------------------------------------------------- /services/UserService/src/middlewares/ensureAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { IAuthenticationTokenProvider } from "../useCases/users/ports/IAuthenticationTokenProvider"; 4 | import { IHttpRequest } from "./ports/IHttpRequest"; 5 | import { IHttpResponse } from "./ports/IHttpResponse"; 6 | import { IMiddleware } from "./ports/IMiddleware"; 7 | import { unauthorized, ok, serverError } from "./utils/HttpResponses"; 8 | 9 | @injectable() 10 | export class EnsureAuthenticated implements IMiddleware { 11 | constructor( 12 | @inject("AuthenticationTokenProvider") 13 | private authenticationTokenProvider: IAuthenticationTokenProvider, 14 | ) { } 15 | 16 | async handle(request: IHttpRequest): Promise { 17 | 18 | try { 19 | if (!request.headers.authorization) { 20 | return unauthorized("Token is missing!") 21 | } 22 | 23 | const authHeader = request.headers.authorization; 24 | 25 | const [, token] = authHeader.split(" "); 26 | 27 | 28 | const user_id = this.authenticationTokenProvider.verify( 29 | token, 30 | ); 31 | 32 | if (!user_id) { 33 | return unauthorized("Token is invalid!"); 34 | } 35 | 36 | request.user = { 37 | id: user_id, 38 | } 39 | return ok("User is authenticated!"); 40 | } catch (error) { 41 | return serverError(error); 42 | }; 43 | }; 44 | }; -------------------------------------------------------------------------------- /services/UserService/src/middlewares/ports/IHttpRequest.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpRequest { 2 | user?: { 3 | id: string; 4 | }; 5 | headers: { 6 | authorization?: string; 7 | }; 8 | }; -------------------------------------------------------------------------------- /services/UserService/src/middlewares/ports/IHttpResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpResponse { 2 | statusCode: number; 3 | body: string; 4 | } -------------------------------------------------------------------------------- /services/UserService/src/middlewares/ports/IMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { IHttpRequest } from "./IHttpRequest"; 2 | import { IHttpResponse } from "./IHttpResponse"; 3 | 4 | export interface IMiddleware { 5 | handle(request: IHttpRequest): Promise 6 | }; -------------------------------------------------------------------------------- /services/UserService/src/middlewares/utils/HttpResponses.ts: -------------------------------------------------------------------------------- 1 | import { IHttpResponse } from '../ports/IHttpResponse' 2 | 3 | export const ok = (data: any): IHttpResponse => ({ 4 | statusCode: 200, 5 | body: data 6 | }) 7 | 8 | export const unauthorized = (error: any): IHttpResponse => ({ 9 | statusCode: 401, 10 | body: error 11 | }) 12 | 13 | export const serverError = (error: any): IHttpResponse => ({ 14 | statusCode: 500, 15 | body: error 16 | }) -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/CreateUserUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'tsyringe'; 2 | 3 | import { User } from "../../domain/entities/User"; 4 | import { InvalidEmailError } from "../../domain/entities/User/errors/InvalidEmailError"; 5 | import { InvalidNameError } from "../../domain/entities/User/errors/InvalidNameError"; 6 | import { InvalidPasswordError } from "../../domain/entities/User/errors/InvalidPasswordError"; 7 | import { Either, left, right } from "../../logic/Either"; 8 | import { UserAlreadyExistsError } from "./errors/UserAlreadyExistsError"; 9 | import { IEncoder } from "./ports/IEncoder"; 10 | import { IUsersRepository } from "./ports/IUsersRepository"; 11 | 12 | interface IRequest { 13 | name: string; 14 | email: string; 15 | password: string; 16 | } 17 | 18 | export interface IResponse { 19 | id: string; 20 | name: string; 21 | email: string; 22 | }; 23 | 24 | @injectable() 25 | export class CreateUserUseCase { 26 | constructor( 27 | @inject("UsersRepository") 28 | private usersRepository: IUsersRepository, 29 | @inject("Encoder") 30 | private encoder: IEncoder, 31 | ) { } 32 | 33 | async execute({ 34 | name, 35 | email, 36 | password, 37 | }: IRequest): Promise< 38 | Either< 39 | | UserAlreadyExistsError 40 | | InvalidNameError 41 | | InvalidEmailError 42 | | InvalidPasswordError, 43 | IResponse 44 | > 45 | > { 46 | const userExists = await this.usersRepository.findByEmail(email); 47 | 48 | if (userExists) { 49 | return left(new UserAlreadyExistsError(email)); 50 | } 51 | 52 | const userOrError = User.create({ 53 | name, 54 | email, 55 | password, 56 | }); 57 | 58 | if (userOrError.isLeft()) { 59 | return left(userOrError.value); 60 | } 61 | 62 | const passwordHash = await this.encoder.encode(userOrError.value.password); 63 | 64 | const user = await this.usersRepository.create({ 65 | ...userOrError.value, 66 | password: passwordHash 67 | }); 68 | 69 | const userMapped = { 70 | id: user.id, 71 | name: user.name, 72 | email: user.email, 73 | } 74 | 75 | return right(userMapped); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/DeleteUserUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { Either, left, right } from "../../logic/Either"; 4 | import { UserNotFoundError } from "./errors/UserNotFoundError"; 5 | import { IUsersRepository } from "./ports/IUsersRepository"; 6 | 7 | interface IRequest { 8 | userIdToBeDeleted: string; 9 | }; 10 | 11 | @injectable() 12 | export class DeleteUserUseCase { 13 | constructor( 14 | @inject("UsersRepository") 15 | private usersRepository: IUsersRepository 16 | ) { } 17 | 18 | async execute({ userIdToBeDeleted }: IRequest): Promise> { 19 | const user = await this.usersRepository.findById(userIdToBeDeleted); 20 | 21 | if (!user) { 22 | return left(new UserNotFoundError(userIdToBeDeleted)); 23 | }; 24 | 25 | await this.usersRepository.deleteOne(userIdToBeDeleted); 26 | 27 | return right(null); 28 | }; 29 | }; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/ListAllUsersUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { Either, right } from "../../logic/Either"; 4 | import { IListUsersResponse } from "./ports/IListUsersResponse"; 5 | import { IUsersRepository } from "./ports/IUsersRepository"; 6 | 7 | @injectable() 8 | export class ListAllUsersUseCase { 9 | constructor( 10 | @inject("UsersRepository") 11 | private usersRepository: IUsersRepository 12 | ) { } 13 | 14 | async execute(): Promise> { 15 | const users = await this.usersRepository.listAll(); 16 | 17 | return right(users); 18 | }; 19 | }; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/UserProfileUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { Either, left, right } from "../../logic/Either"; 4 | import { UserNotFoundError } from "./errors/UserNotFoundError"; 5 | import { IUsersRepository } from "./ports/IUsersRepository"; 6 | 7 | interface IRequest { 8 | user_id: string; 9 | }; 10 | 11 | interface IResponse { 12 | id: string; 13 | name: string; 14 | email: string; 15 | } 16 | 17 | @injectable() 18 | export class UserProfileUseCase { 19 | constructor( 20 | @inject("UsersRepository") 21 | private usersRepository: IUsersRepository 22 | ) { } 23 | 24 | async execute({ user_id }: IRequest): Promise> { 25 | const user = await this.usersRepository.findById(user_id); 26 | 27 | if (!user) { 28 | return left(new UserNotFoundError(user_id)); 29 | }; 30 | 31 | const userMapped: IResponse = { 32 | id: user.id, 33 | name: user.name, 34 | email: user.email, 35 | }; 36 | 37 | return right(userMapped); 38 | }; 39 | }; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/errors/EmailIsAlreadyTakenError.ts: -------------------------------------------------------------------------------- 1 | export class EmailIsAlreadyTakenError extends Error { 2 | constructor(email: string) { 3 | super(`Email: "${email}" is already taken!`); 4 | this.name = "EmailIsAlreadyTakenError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/errors/IncorrectPasswordError.ts: -------------------------------------------------------------------------------- 1 | export class IncorrectPasswordError extends Error { 2 | constructor() { 3 | super("Password incorrect"); 4 | this.name = "IncorrectPasswordError" 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/errors/UnmatchedPasswordError.ts: -------------------------------------------------------------------------------- 1 | export class UnmatchedPasswordError extends Error { 2 | constructor() { 3 | super("Passwords not match"); 4 | this.name = "UnmatchedPasswordError" 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/errors/UserAlreadyExistsError.ts: -------------------------------------------------------------------------------- 1 | export class UserAlreadyExistsError extends Error { 2 | constructor(email: string) { 3 | super(`User with email "${email}" already exists!`); 4 | this.name = "UserAlreadyExistsError"; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/errors/UserNotFoundError.ts: -------------------------------------------------------------------------------- 1 | export class UserNotFoundError extends Error { 2 | constructor(id: string) { 3 | super(`User with id "${id}" not found`); 4 | this.name = "UserNotFoundError"; 5 | }; 6 | }; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/ports/IAuthenticationTokenProvider.ts: -------------------------------------------------------------------------------- 1 | export interface IAuthenticationTokenProvider { 2 | generateToken(subject: string): string; 3 | verify(token: string): string | undefined; 4 | }; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/ports/IEncoder.ts: -------------------------------------------------------------------------------- 1 | export interface IEncoder { 2 | encode(plain: string): Promise; 3 | compare(plain: string, hash: string): Promise; 4 | }; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/ports/IListUsersResponse.ts: -------------------------------------------------------------------------------- 1 | import { IUserData } from "./IUserData"; 2 | 3 | export type IListUsersResponse = Omit[]; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/ports/IUpdatedUserData.ts: -------------------------------------------------------------------------------- 1 | export interface IUpdatedUserData { 2 | id: string; 3 | name: string; 4 | email: string; 5 | }; -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/ports/IUserData.ts: -------------------------------------------------------------------------------- 1 | export interface IUserData { 2 | id: string; 3 | name: string; 4 | email: string; 5 | password: string; 6 | createdAt: Date; 7 | updatedAt: Date; 8 | isAdmin: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /services/UserService/src/useCases/users/ports/IUsersRepository.ts: -------------------------------------------------------------------------------- 1 | import { IListUsersResponse } from "./IListUsersResponse"; 2 | import { IUpdatedUserData } from "./IUpdatedUserData"; 3 | import { IUserData } from "./IUserData"; 4 | 5 | export interface IUsersRepository { 6 | create(data: IUserData): Promise>; 7 | findByEmail(email: string): Promise; 8 | findById(id: string): Promise; 9 | update(data: IUserData): Promise; 10 | listAll(): Promise; 11 | deleteOne(id: string): Promise 12 | }; 13 | -------------------------------------------------------------------------------- /services/UserService/tests/doubles/FakeEncoder.ts: -------------------------------------------------------------------------------- 1 | import { IEncoder } from "../../src/useCases/users/ports/IEncoder"; 2 | 3 | export class FakeEncoder implements IEncoder { 4 | async encode(plain: string): Promise { 5 | const hash = `${plain}Encripted`; 6 | 7 | return hash; 8 | }; 9 | 10 | async compare(plain: string, hash: string): Promise { 11 | const plainHashed = `${plain}Encripted`; 12 | 13 | if (plainHashed === hash) { 14 | return true; 15 | } 16 | 17 | return false; 18 | }; 19 | } -------------------------------------------------------------------------------- /services/UserService/tests/doubles/UserIdTestMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | export async function UserIdTestMiddleware( 4 | request: Request, response: Response, next: NextFunction 5 | ): Promise { 6 | try { 7 | if (!request.headers.userid) { 8 | return response.status(401).json("User id missing!"); 9 | } 10 | 11 | const userId = request.headers.userid; 12 | 13 | request.user = { 14 | id: userId.toString(), 15 | } 16 | 17 | next(); 18 | } catch (error) { 19 | response.status(500).json(error); 20 | }; 21 | }; -------------------------------------------------------------------------------- /services/UserService/tests/doubles/UsersActions.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | 3 | import { IListUsersResponse } from "../../src/useCases/users/ports/IListUsersResponse"; 4 | import { IUserData } from "../../src/useCases/users/ports/IUserData"; 5 | import { IUsersRepository } from "../../src/useCases/users/ports/IUsersRepository"; 6 | 7 | @injectable() 8 | export class UsersActions { 9 | constructor( 10 | @inject("UsersRepository") 11 | private usersRepository: IUsersRepository, 12 | ) { } 13 | 14 | async create(data: IUserData): Promise> { 15 | const user = await this.usersRepository.create(data); 16 | 17 | return user; 18 | }; 19 | 20 | async listAll(): Promise { 21 | const users = await this.usersRepository.listAll(); 22 | 23 | return users; 24 | }; 25 | 26 | async findByEmail(email: string): Promise { 27 | const user = await this.usersRepository.findByEmail(email); 28 | 29 | return user; 30 | } 31 | }; -------------------------------------------------------------------------------- /services/UserService/tests/doubles/repositories/UsersRepositoryInMemory.ts: -------------------------------------------------------------------------------- 1 | import { IListUsersResponse } from "../../../src/useCases/users/ports/IListUsersResponse"; 2 | import { IUpdatedUserData } from "../../../src/useCases/users/ports/IUpdatedUserData"; 3 | import { IUserData } from "../../../src/useCases/users/ports/IUserData"; 4 | import { IUsersRepository } from "../../../src/useCases/users/ports/IUsersRepository"; 5 | 6 | export class UsersRepositoryInMemory implements IUsersRepository { 7 | users: IUserData[] = []; 8 | 9 | async create(data: IUserData): Promise> { 10 | this.users.push(data); 11 | 12 | const user = { 13 | id: data.id, 14 | name: data.name, 15 | email: data.email, 16 | createdAt: data.createdAt, 17 | updatedAt: data.updatedAt, 18 | }; 19 | 20 | return user; 21 | }; 22 | 23 | async findByEmail(email: string): Promise { 24 | const user = this.users.find((user) => user.email === email); 25 | 26 | return user || null; 27 | }; 28 | 29 | async findById(id: string): Promise { 30 | const user = this.users.find((user) => user.id === id); 31 | 32 | return user || null; 33 | }; 34 | 35 | async update(data: IUserData): Promise { 36 | const userIndex = this.users.findIndex(user => user.id === data.id); 37 | 38 | this.users[userIndex] = data; 39 | 40 | 41 | const userUpdated = { 42 | id: this.users[userIndex].id, 43 | name: this.users[userIndex].name, 44 | email: this.users[userIndex].email, 45 | } 46 | 47 | return userUpdated; 48 | }; 49 | 50 | async listAll(): Promise { 51 | const usersFormatted = this.users.map(user => { 52 | return { 53 | id: user.id, 54 | name: user.name, 55 | email: user.email, 56 | createdAt: user.createdAt, 57 | updatedAt: user.updatedAt, 58 | } 59 | }); 60 | 61 | return usersFormatted; 62 | } 63 | 64 | async deleteOne(id: string): Promise { 65 | const userIndex = this.users.findIndex(user => user.id === id); 66 | 67 | this.users.splice(userIndex, 1); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /services/UserService/tests/infra/http/routes/users/AdminUpdateUserRoute.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import { prismaClient } from '../../../../../src/infra/database/prisma/PrismaClient'; 4 | import { BcryptEncoder } from "../../../../../src/infra/encoder/BcryptEncoder"; 5 | import { app } from '../../../../../src/infra/http/app'; 6 | 7 | describe("Admin Update User Route", () => { 8 | let hashedPassword: string; 9 | let dateNow: Date; 10 | 11 | beforeAll(async () => { 12 | const bcryptEncoder = new BcryptEncoder(); 13 | 14 | hashedPassword = await bcryptEncoder.encode("password"); 15 | 16 | dateNow = new Date(); 17 | 18 | await prismaClient.$connect(); 19 | 20 | await prismaClient.$connect(); 21 | await prismaClient.users.create({ 22 | data: { 23 | id: "admin-id", 24 | email: "admin@example.com", 25 | name: "Admin", 26 | password: hashedPassword, 27 | isAdmin: true, 28 | createdAt: dateNow, 29 | updatedAt: dateNow, 30 | } 31 | }); 32 | 33 | await prismaClient.users.create({ 34 | data: { 35 | id: "fake-id", 36 | email: "user@example.com", 37 | name: "User", 38 | password: hashedPassword, 39 | isAdmin: false, 40 | createdAt: dateNow, 41 | updatedAt: dateNow, 42 | } 43 | }); 44 | }); 45 | 46 | afterAll(async () => { 47 | await prismaClient.users.deleteMany(); 48 | await prismaClient.$disconnect() 49 | }); 50 | 51 | it("should return status code 201 and body with user updated if user is authenticated and is an admin", async () => { 52 | const response = await request(app) 53 | .put("/users/admin/update") 54 | .send({ 55 | user_id_to_be_updated: "fake-id", 56 | name: "New Name", 57 | email: "new-email@example.com", 58 | }) 59 | .set({ 60 | userid: "admin-id" 61 | }) 62 | .expect(201); 63 | 64 | const expectedResponse = { 65 | id: "fake-id", 66 | email: "new-email@example.com", 67 | name: "New Name", 68 | }; 69 | 70 | expect(response.body).toEqual(expectedResponse); 71 | }); 72 | }); -------------------------------------------------------------------------------- /services/UserService/tests/infra/http/routes/users/CreateUserRoute.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import { prismaClient } from '../../../../../src/infra/database/prisma/PrismaClient'; 4 | import { app } from '../../../../../src/infra/http/app'; 5 | 6 | 7 | 8 | describe("Create User Route", () => { 9 | beforeAll(async () => { 10 | await prismaClient.$connect(); 11 | }); 12 | 13 | afterAll(async () => { 14 | await prismaClient.users.deleteMany(); 15 | await prismaClient.$disconnect() 16 | }); 17 | 18 | it("should return status code 201 when user is created", async () => { 19 | const response = await request(app) 20 | .post("/users") 21 | .send({ 22 | name: "User", 23 | email: "user@example.com", 24 | password: "password" 25 | }) 26 | .expect(201); 27 | 28 | const createdUser = await prismaClient.users.findMany(); 29 | 30 | expect(createdUser[0]).toHaveProperty("id") 31 | expect(createdUser[0].email).toEqual("user@example.com") 32 | expect(createdUser[0].password).not.toEqual("password") 33 | expect(response.body).toEqual("User Created!"); 34 | }); 35 | }); -------------------------------------------------------------------------------- /services/UserService/tests/infra/http/routes/users/DeleteUserRoute.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import { prismaClient } from '../../../../../src/infra/database/prisma/PrismaClient'; 4 | import { BcryptEncoder } from "../../../../../src/infra/encoder/BcryptEncoder"; 5 | import { app } from '../../../../../src/infra/http/app'; 6 | 7 | describe("Delete User Route", () => { 8 | let hashedPassword: string; 9 | let dateNow: Date; 10 | 11 | beforeAll(async () => { 12 | const bcryptEncoder = new BcryptEncoder(); 13 | 14 | hashedPassword = await bcryptEncoder.encode("password"); 15 | 16 | dateNow = new Date(); 17 | 18 | await prismaClient.$connect(); 19 | await prismaClient.users.create({ 20 | data: { 21 | id: "admin-id", 22 | email: "admin@example.com", 23 | name: "Admin", 24 | password: hashedPassword, 25 | isAdmin: true, 26 | createdAt: dateNow, 27 | updatedAt: dateNow, 28 | } 29 | }); 30 | 31 | await prismaClient.users.create({ 32 | data: { 33 | id: "fake-id-2", 34 | email: "user@example.com", 35 | name: "User", 36 | password: hashedPassword, 37 | isAdmin: false, 38 | createdAt: dateNow, 39 | updatedAt: dateNow, 40 | } 41 | }); 42 | }); 43 | 44 | afterAll(async () => { 45 | await prismaClient.users.deleteMany(); 46 | await prismaClient.$disconnect() 47 | }); 48 | 49 | it("should return status code 200 and body with all users if user is authenticated and is an admin", async () => { 50 | const response = await request(app) 51 | .delete("/users/fake-id-2") 52 | .set({ 53 | userid: "admin-id" 54 | }) 55 | .expect(200); 56 | 57 | const allUsersResponse = await request(app) 58 | .get("/users") 59 | .set({ 60 | userid: "admin-id" 61 | }) 62 | 63 | const expectedResponse = [ 64 | { 65 | id: "admin-id", 66 | email: "admin@example.com", 67 | name: "Admin", 68 | createdAt: dateNow.toISOString(), 69 | updatedAt: dateNow.toISOString(), 70 | } 71 | ] 72 | 73 | expect(response.body).toEqual("User deleted!"); 74 | expect(allUsersResponse.body).toEqual(expectedResponse); 75 | }); 76 | }); -------------------------------------------------------------------------------- /services/UserService/tests/infra/http/routes/users/ListAllUsersRoute.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import { prismaClient } from '../../../../../src/infra/database/prisma/PrismaClient'; 4 | import { BcryptEncoder } from "../../../../../src/infra/encoder/BcryptEncoder"; 5 | import { app } from '../../../../../src/infra/http/app'; 6 | 7 | describe("List All Users Profile Route", () => { 8 | let hashedPassword: string; 9 | let dateNow: Date; 10 | 11 | beforeAll(async () => { 12 | const bcryptEncoder = new BcryptEncoder(); 13 | 14 | hashedPassword = await bcryptEncoder.encode("password"); 15 | 16 | dateNow = new Date(); 17 | 18 | await prismaClient.$connect(); 19 | 20 | await prismaClient.users.create({ 21 | data: { 22 | id: "admin-id", 23 | email: "admin@example.com", 24 | name: "Admin", 25 | password: hashedPassword, 26 | isAdmin: true, 27 | createdAt: dateNow, 28 | updatedAt: dateNow, 29 | } 30 | }); 31 | }); 32 | 33 | afterAll(async () => { 34 | await prismaClient.users.deleteMany(); 35 | await prismaClient.$disconnect() 36 | }); 37 | 38 | it("should return status code 200 and body with all users if user is authenticated and is an admin", async () => { 39 | const response = await request(app) 40 | .get("/users") 41 | .set({ 42 | userid: "admin-id", 43 | }) 44 | .expect(200); 45 | 46 | const expectedResponse = [ 47 | { 48 | id: "admin-id", 49 | email: "admin@example.com", 50 | name: "Admin", 51 | createdAt: dateNow.toISOString(), 52 | updatedAt: dateNow.toISOString(), 53 | } 54 | ] 55 | 56 | expect(response.body).toEqual(expectedResponse); 57 | }); 58 | }); -------------------------------------------------------------------------------- /services/UserService/tests/infra/http/routes/users/UpdateUserRoute.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import { prismaClient } from '../../../../../src/infra/database/prisma/PrismaClient'; 4 | import { BcryptEncoder } from "../../../../../src/infra/encoder/BcryptEncoder"; 5 | import { app } from '../../../../../src/infra/http/app'; 6 | 7 | describe("Update User Route", () => { 8 | let hashedPassword: string; 9 | let dateNow: Date; 10 | 11 | beforeAll(async () => { 12 | const bcryptEncoder = new BcryptEncoder(); 13 | 14 | hashedPassword = await bcryptEncoder.encode("password"); 15 | 16 | dateNow = new Date(); 17 | 18 | await prismaClient.$connect(); 19 | 20 | await prismaClient.users.create({ 21 | data: { 22 | id: "fake-id", 23 | email: "user@example.com", 24 | name: "User", 25 | password: hashedPassword, 26 | isAdmin: false, 27 | createdAt: dateNow, 28 | updatedAt: dateNow, 29 | } 30 | }); 31 | }); 32 | 33 | afterAll(async () => { 34 | await prismaClient.users.deleteMany(); 35 | await prismaClient.$disconnect() 36 | }); 37 | 38 | it("should return status code 201 and body with updated user data if user is authenticated", async () => { 39 | const response = await request(app) 40 | .put("/users/update") 41 | .send({ 42 | name: "New Name", 43 | email: "newuser@example.com", 44 | old_password: "password", 45 | new_password: "newPassword", 46 | confirm_password: "newPassword", 47 | }) 48 | .set({ 49 | userid: "fake-id" 50 | }) 51 | .expect(201); 52 | 53 | const expectedResponse = { 54 | id: "fake-id", 55 | name: "New Name", 56 | email: "newuser@example.com" 57 | } 58 | 59 | expect(response.body).toEqual(expectedResponse); 60 | }); 61 | }); -------------------------------------------------------------------------------- /services/UserService/tests/infra/http/routes/users/UserProfileRoute.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import { prismaClient } from '../../../../../src/infra/database/prisma/PrismaClient'; 4 | import { BcryptEncoder } from "../../../../../src/infra/encoder/BcryptEncoder"; 5 | import { app } from '../../../../../src/infra/http/app'; 6 | 7 | describe("User Profile Route", () => { 8 | beforeAll(async () => { 9 | const bcryptEncoder = new BcryptEncoder(); 10 | 11 | const hashedPassword = await bcryptEncoder.encode("password"); 12 | 13 | await prismaClient.$connect(); 14 | await prismaClient.users.create({ 15 | data: { 16 | id: "fake-id", 17 | email: "user@example.com", 18 | name: "User", 19 | password: hashedPassword, 20 | } 21 | }); 22 | }); 23 | 24 | afterAll(async () => { 25 | await prismaClient.users.deleteMany(); 26 | await prismaClient.$disconnect() 27 | }); 28 | 29 | it("should return status code 200 and body with user data if user is authenticated", async () => { 30 | const response = await request(app) 31 | .get("/users/profile") 32 | .set({ 33 | userid: "fake-id" 34 | }) 35 | .expect(200); 36 | 37 | const expectedResponse = { 38 | id: "fake-id", 39 | name: "User", 40 | email: "user@example.com" 41 | } 42 | 43 | expect(response.body).toEqual(expectedResponse); 44 | }); 45 | }); -------------------------------------------------------------------------------- /services/UserService/tests/useCases/users/ListAllUsersCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { BcryptEncoder } from "../../../src/infra/encoder/BcryptEncoder"; 2 | import { ListAllUsersUseCase } from "../../../src/useCases/users/ListAllUsersUseCase"; 3 | import { UsersRepositoryInMemory } from "../../doubles/repositories/UsersRepositoryInMemory"; 4 | 5 | let usersRepositoryInMemory: UsersRepositoryInMemory; 6 | let listAllUsersUseCase: ListAllUsersUseCase; 7 | 8 | describe("List All Users UseCase", () => { 9 | beforeEach(() => { 10 | usersRepositoryInMemory = new UsersRepositoryInMemory(); 11 | listAllUsersUseCase = new ListAllUsersUseCase( 12 | usersRepositoryInMemory, 13 | ); 14 | }); 15 | 16 | it("should return all users data if user authenticated is an admin", async () => { 17 | const bcryptEncoder = new BcryptEncoder(); 18 | const hashedPassword = await bcryptEncoder.encode("password"); 19 | 20 | const dateNow = new Date(); 21 | 22 | await usersRepositoryInMemory.create({ 23 | id: "fake-id", 24 | name: "Admin", 25 | email: "admin@example.com", 26 | password: hashedPassword, 27 | createdAt: dateNow, 28 | updatedAt: dateNow, 29 | isAdmin: true, 30 | }); 31 | 32 | await usersRepositoryInMemory.create({ 33 | id: "fake-id2", 34 | name: "User", 35 | email: "user@example.com", 36 | password: hashedPassword, 37 | createdAt: dateNow, 38 | updatedAt: dateNow, 39 | isAdmin: false, 40 | }); 41 | 42 | const userOrError = await listAllUsersUseCase.execute(); 43 | 44 | const expectedResponse = [ 45 | { 46 | id: "fake-id", 47 | name: "Admin", 48 | email: "admin@example.com", 49 | createdAt: dateNow, 50 | updatedAt: dateNow, 51 | }, 52 | { 53 | id: "fake-id2", 54 | name: "User", 55 | email: "user@example.com", 56 | createdAt: dateNow, 57 | updatedAt: dateNow, 58 | }, 59 | ]; 60 | 61 | expect(userOrError.isRight()).toEqual(true); 62 | expect(userOrError.value).toEqual(expectedResponse); 63 | }); 64 | }); --------------------------------------------------------------------------------