├── .github ├── release-drafter.yml └── workflows │ ├── ci.yml │ ├── release-drafter-labeler.yml │ └── release-drafter.yml ├── .gitignore ├── CONTRIBUTION.md ├── LICENSE ├── Makefile ├── README.md ├── assets ├── shop-golang-microservices.excalidraw ├── shop-golang-microservices.png ├── shop-golang-microservices.svg ├── vertical-slice-architecture.excalidraw └── vertical-slice-architecture.png ├── deployments └── docker-compose │ └── infrastructure.yaml ├── internal ├── pkg │ ├── go.mod │ ├── go.sum │ ├── gorm_pgsql │ │ ├── db.go │ │ └── generic_repository.go │ ├── grpc │ │ ├── grpc-client.go │ │ ├── grpc-server.go │ │ └── mocks │ │ │ └── grpc_client.go │ ├── http │ │ ├── context_provider.go │ │ └── echo │ │ │ ├── middleware │ │ │ ├── correlation_id_middleware.go │ │ │ └── validate_token_middleware.go │ │ │ └── server │ │ │ └── echo-server.go │ ├── http_client │ │ └── http_client.go │ ├── logger │ │ └── logger.go │ ├── mapper │ │ └── mapper.go │ ├── oauth2 │ │ └── password_credentials.go │ ├── otel │ │ ├── jaeger.go │ │ ├── middleware │ │ │ └── echo_tracer_middleware.go │ │ └── utils.go │ ├── rabbitmq │ │ ├── consumer.go │ │ ├── mocks │ │ │ ├── IConsumer.go │ │ │ └── IPublisher.go │ │ ├── publisher.go │ │ └── rabbitmq.go │ ├── reflection │ │ ├── reflection_helper │ │ │ ├── reflection_helper.go │ │ │ └── reflection_helper_test.go │ │ ├── type_mappper │ │ │ ├── type_mapper.go │ │ │ ├── type_mapper_test.go │ │ │ └── unsafe_types.go │ │ └── type_registry │ │ │ └── type_registry.go │ ├── test │ │ └── container │ │ │ ├── postgres_container │ │ │ ├── postgres_container.go │ │ │ └── postgres_container_test.go │ │ │ └── rabbitmq_container │ │ │ ├── rabbitmq_container.go │ │ │ └── rabbitmq_container_test.go │ └── utils │ │ ├── pagination.go │ │ ├── password.go │ │ └── workers_runner.go └── services │ ├── identity_service │ ├── .env │ ├── cmd │ │ └── main.go │ ├── config │ │ ├── config.development.json │ │ └── config.go │ ├── docs │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ ├── go.mod │ ├── go.sum │ ├── identity │ │ ├── configurations │ │ │ ├── endpoint_configurations.go │ │ │ ├── grpc_configurations.go │ │ │ ├── mediator_configurations.go │ │ │ ├── middleware_configurations.go │ │ │ └── swagger_configurations.go │ │ ├── constants │ │ │ └── constants.go │ │ ├── data │ │ │ ├── contracts │ │ │ │ └── user_repository.go │ │ │ ├── repositories │ │ │ │ └── pg_user_repository.go │ │ │ └── seeds │ │ │ │ └── data_seeder.go │ │ ├── features │ │ │ └── registering_user │ │ │ │ └── v1 │ │ │ │ ├── commands │ │ │ │ ├── register_user.go │ │ │ │ └── register_user_handler.go │ │ │ │ ├── dtos │ │ │ │ ├── register_user_request_dto.go │ │ │ │ └── register_user_response_dto.go │ │ │ │ └── endpoints │ │ │ │ └── register_user_endpoint.go │ │ ├── grpc_server │ │ │ ├── protos │ │ │ │ ├── identity_service.pb.go │ │ │ │ ├── identity_service.proto │ │ │ │ └── identity_service_grpc.pb.go │ │ │ └── services │ │ │ │ └── identity_grpc_server_service.go │ │ ├── mappings │ │ │ └── mapping_profile.go │ │ ├── middlewares │ │ │ └── problem_details_handler.go │ │ └── models │ │ │ └── user.go │ └── server │ │ └── server.go │ ├── inventory_service │ ├── .env │ ├── cmd │ │ └── main.go │ ├── config │ │ ├── config.development.json │ │ └── config.go │ ├── docs │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ ├── go.mod │ ├── go.sum │ ├── inventory │ │ ├── configurations │ │ │ ├── consumrs_configurations.go │ │ │ ├── endpoint_configurations.go │ │ │ ├── mediator_configurations.go │ │ │ ├── middleware_configurations.go │ │ │ └── swagger_configurations.go │ │ ├── constans │ │ │ └── constants.go │ │ ├── consumers │ │ │ ├── events │ │ │ │ ├── inventory_updated.go │ │ │ │ └── product_created.go │ │ │ └── handlers │ │ │ │ └── consume_create_product_handler.go │ │ ├── data │ │ │ ├── contracts │ │ │ │ └── inventory_repository.go │ │ │ ├── inventories_pg_seed.go │ │ │ └── repositories │ │ │ │ └── pg_inventory_repository.go │ │ ├── dtos │ │ │ ├── inventory_dto.go │ │ │ └── product_item_dto.go │ │ ├── mappings │ │ │ └── mappings_profile.go │ │ ├── middlewares │ │ │ └── problem_details_handler.go │ │ └── models │ │ │ ├── inventory.go │ │ │ └── product_item.go │ ├── server │ │ └── server.go │ └── shared │ │ └── delivery │ │ └── inventory_delivery.go │ └── product_service │ ├── .env │ ├── cmd │ └── main.go │ ├── config │ ├── config.development.json │ ├── config.go │ └── config.test.json │ ├── docs │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml │ ├── go.mod │ ├── go.sum │ ├── product │ ├── configurations │ │ ├── endpoint_configurations.go │ │ ├── mediator_configurations.go │ │ ├── middleware_configurations.go │ │ └── swagger_configurations.go │ ├── constants │ │ └── constants.go │ ├── consumers │ │ └── consume_create_product_handler.go │ ├── data │ │ ├── contracts │ │ │ └── product_repository.go │ │ ├── products_pg_seed.go │ │ └── repositories │ │ │ └── pg_product_repository.go │ ├── dtos │ │ └── product_dto.go │ ├── features │ │ ├── creating_product │ │ │ └── v1 │ │ │ │ ├── commands │ │ │ │ ├── create_product.go │ │ │ │ └── create_product_handler.go │ │ │ │ ├── dtos │ │ │ │ ├── create_product_request_dto.go │ │ │ │ └── create_product_response_dto.go │ │ │ │ ├── endpoints │ │ │ │ └── create_product_endpoint.go │ │ │ │ └── events │ │ │ │ └── product_created.go │ │ ├── deleting_product │ │ │ └── v1 │ │ │ │ ├── commands │ │ │ │ ├── delete_product.go │ │ │ │ └── delete_product_handler.go │ │ │ │ ├── dtos │ │ │ │ └── delete_product_request_dto.go │ │ │ │ ├── endpoints │ │ │ │ └── delete_product_endpoint.go │ │ │ │ └── events │ │ │ │ └── product_deleted.go │ │ ├── getting_product_by_id │ │ │ └── v1 │ │ │ │ ├── dtos │ │ │ │ ├── get_product_by_id_request_dto.go │ │ │ │ └── get_product_by_id_response_dto.go │ │ │ │ ├── endpoints │ │ │ │ └── get_product_by_id_endpoint.go │ │ │ │ └── queries │ │ │ │ ├── get_product_by_id.go │ │ │ │ └── get_product_by_id_handler.go │ │ ├── getting_products │ │ │ └── v1 │ │ │ │ ├── dtos │ │ │ │ ├── get_products_request_dto.go │ │ │ │ └── get_products_response_dto.go │ │ │ │ ├── endpoints │ │ │ │ └── get_products_endpoint.go │ │ │ │ └── queries │ │ │ │ ├── get_products.go │ │ │ │ └── get_products_handler.go │ │ ├── searching_product │ │ │ └── v1 │ │ │ │ ├── dtos │ │ │ │ ├── search_products_request_dto.go │ │ │ │ └── search_products_response_dto.go │ │ │ │ ├── endpoints │ │ │ │ └── search_products_endpoint.go │ │ │ │ └── queries │ │ │ │ ├── search_products.go │ │ │ │ └── search_products_handler.go │ │ └── updating_product │ │ │ └── v1 │ │ │ ├── commands │ │ │ ├── update_product.go │ │ │ └── update_product_handler.go │ │ │ ├── dtos │ │ │ ├── update_product_request_dto.go │ │ │ └── update_product_response_dto.go │ │ │ ├── endpoints │ │ │ └── update_product_endpoint.go │ │ │ └── events │ │ │ └── product_updated.go │ ├── grpc_client │ │ └── protos │ │ │ ├── identity_service.pb.go │ │ │ ├── identity_service.proto │ │ │ └── identity_service_grpc.pb.go │ ├── mappings │ │ ├── manual_mappings.go │ │ └── mapping_profile.go │ ├── middlewares │ │ └── problem_details_handler.go │ └── models │ │ └── product.go │ ├── server │ └── server.go │ ├── shared │ ├── delivery │ │ └── product_delivery.go │ └── test_fixture │ │ ├── integration_test_fixture.go │ │ └── unit_test_fixture.go │ └── tests │ ├── end_to_end_tests │ └── features │ │ └── creating_product │ │ └── create_product_end_to_end_test.go │ ├── integration_tests │ └── features │ │ └── creating_product │ │ └── create_product_integration_test.go │ └── unit_tests │ ├── features │ └── creating_product │ │ └── create_product_handler_unit_test.go │ ├── mocks │ ├── IdentityServiceClient.go │ ├── IdentityServiceServer.go │ ├── ProductRepository.go │ └── UnsafeIdentityServiceServer.go │ └── test_data │ └── product.go └── shop.rest /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # https://johanneskonings.dev/github/2021/02/28/github_automatic_releases_and-changelog/ 2 | # https://tiagomichaelsousa.dev/articles/stop-writing-your-changelogs-manually 3 | # https://github.com/release-drafter/release-drafter/issues/551 4 | # https://github.com/release-drafter/release-drafter/pull/1013 5 | # https://github.com/release-drafter/release-drafter/issues/139 6 | # https://github.com/atk4/data/blob/develop/.github/release-drafter.yml 7 | 8 | # This release drafter follows the conventions from https://keepachangelog.com, https://common-changelog.org/ 9 | # https://www.conventionalcommits.org 10 | 11 | name-template: 'v$RESOLVED_VERSION' 12 | tag-template: 'v$RESOLVED_VERSION' 13 | template: | 14 | ## What Changed 👀 15 | $CHANGES 16 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 17 | categories: 18 | - title: 🚀 Features 19 | labels: 20 | - feature 21 | - title: 🐛 Bug Fixes 22 | labels: 23 | - fix 24 | - bug 25 | - title: 🧪 Test 26 | labels: 27 | - test 28 | - title: 👷 CI 29 | labels: 30 | - ci 31 | - title: ♻️ Changes 32 | labels: 33 | - changed 34 | - enhancement 35 | - refactor 36 | - title: ⛔️ Deprecated 37 | labels: 38 | - deprecated 39 | - title: 🔐 Security 40 | labels: 41 | - security 42 | - title: 📄 Documentation 43 | labels: 44 | - docs 45 | - documentation 46 | - title: 🧩 Dependency Updates 47 | labels: 48 | - deps 49 | - dependencies 50 | - title: 🧰 Maintenance 51 | label: 'chore' 52 | - title: 📝 Other changes 53 | ## putting no labels pr to `Other Changes` category with no label - https://github.com/release-drafter/release-drafter/issues/139#issuecomment-480473934 54 | 55 | # https://www.trywilco.com/post/wilco-ci-cd-github-heroku 56 | # https://github.com/release-drafter/release-drafter#autolabeler 57 | # https://github.com/fuxingloh/multi-labeler 58 | 59 | # Using regex for defining rules - https://regexr.com/ - https://regex101.com/ 60 | autolabeler: 61 | - label: 'chore' 62 | branch: 63 | - '/(chore)\/.*/' 64 | - label: 'security' 65 | branch: 66 | - '/(security)\/.*/' 67 | - label: 'refactor' 68 | branch: 69 | - '/(refactor)\/.*/' 70 | - label: 'docs' 71 | branch: 72 | - '/(docs)\/.*/' 73 | - label: 'ci' 74 | branch: 75 | - '/(ci)\/.*/' 76 | - label: 'test' 77 | branch: 78 | - '/(test)\/.*/' 79 | - label: 'bug' 80 | branch: 81 | - '/(fix)\/.*/' 82 | - label: 'feature' 83 | branch: 84 | - '/(feat)\/.*/' 85 | - label: 'minor' 86 | branch: 87 | - '/(feat)\/.*/' 88 | - label: 'patch' 89 | branch: 90 | - '/(fix)\/.*/' 91 | body: 92 | - '/JIRA-[0-9]{1,4}/' 93 | 94 | change-template: '- $TITLE (#$NUMBER)' 95 | exclude-contributors: 96 | - 'meysamhadeli' 97 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 98 | version-resolver: 99 | major: 100 | labels: 101 | - major 102 | minor: 103 | labels: 104 | - minor 105 | patch: 106 | labels: 107 | - patch 108 | default: patch 109 | 110 | exclude-labels: 111 | - skip-changelog 112 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | paths-ignore: 10 | - "README.md" 11 | - "CHANGELOG.md" 12 | pull_request: 13 | branches: ["main"] 14 | 15 | jobs: 16 | 17 | ci: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v3 24 | with: 25 | go-version: 1.23 26 | 27 | - name: pkg build 28 | working-directory: ./internal/pkg 29 | run: go build -v ./... 30 | 31 | - name: identity_service build 32 | working-directory: ./internal/services/identity_service 33 | run: go build -v ./... 34 | 35 | - name: product_service build 36 | working-directory: ./internal/services/product_service 37 | run: go build -v ./... 38 | 39 | - name: inventory_service build 40 | working-directory: ./internal/services/inventory_service 41 | run: go build -v ./... 42 | 43 | - name: product_service test 44 | working-directory: ./internal/services/product_service 45 | run: go test -v ./... 46 | 47 | - name: identity_service test 48 | working-directory: ./internal/services/identity_service 49 | run: go test -v ./... 50 | 51 | - name: inventory_service test 52 | working-directory: ./internal/services/inventory_service 53 | run: go test -v ./... 54 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter-labeler.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter Auto Labeler 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | - labeled 10 | - unlabeled 11 | 12 | jobs: 13 | auto-labeler: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: release-drafter/release-drafter@v5 17 | with: 18 | config-name: release-drafter.yml 19 | disable-releaser: true # only run auto-labeler for PRs 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # https://johanneskonings.dev/github/2021/02/28/github_automatic_releases_and-changelog/ 2 | # https://tiagomichaelsousa.dev/articles/stop-writing-your-changelogs-manually 3 | name: Release Drafter 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | update_release_draft: 12 | name: Release drafter 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Update Release Draft 17 | uses: release-drafter/release-drafter@v5 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Ignore idea 18 | .idea/** 19 | 20 | # Ignore CHANGELOG 21 | CHANGELOG.md 22 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | ## Contribution 2 | 3 | This is great that you'd like to contribute to this project. All change requests should go through the steps described below. 4 | 5 | ## Pull Requests 6 | 7 | **Please, make sure you open an issue before starting with a Pull Request, unless it's a typo or a really obvious error.** Pull requests are the best way to propose changes. 8 | 9 | ## Conventional commits 10 | 11 | Our repository follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) specification. Releasing to GitHub and NuGet is done with the support of [semantic-release](https://semantic-release.gitbook.io/semantic-release/). 12 | 13 | Pull requests should have a title that follows the specification, otherwise, merging is blocked. If you are not familiar with the specification simply ask maintainers to modify. You can also use this cheatsheet if you want: 14 | 15 | - `fix: ` prefix in the title indicates that PR is a bug fix and PATCH release must be triggered. 16 | - `feat: ` prefix in the title indicates that PR is a feature and MINOR release must be triggered. 17 | - `docs: ` prefix in the title indicates that PR is only related to the documentation and there is no need to trigger release. 18 | - `chore: ` prefix in the title indicates that PR is only related to cleanup in the project and there is no need to trigger release. 19 | - `test: ` prefix in the title indicates that PR is only related to tests and there is no need to trigger release. 20 | - `refactor: ` prefix in the title indicates that PR is only related to refactoring and there is no need to trigger release. 21 | 22 | ## Resources 23 | 24 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 25 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 26 | - [GitHub Help](https://help.github.com) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Meysam Hadeli 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: 2 | 3 | ### for running all commands we need bash command lien ### 4 | 5 | ## choco install make 6 | # ============================================================================== 7 | # Run Services 8 | run_products_service: 9 | cd internal/services/product_service/ && go run ./cmd/main.go 10 | 11 | run_identities_service: 12 | cd internal/services/identity_service/ && go run ./cmd/main.go 13 | 14 | # ============================================================================== 15 | # Docker Compose 16 | docker-compose_infra_up: 17 | @echo Starting infrastructure docker-compose up 18 | docker-compose -f deployments/docker-compose/infrastructure.yaml up --build 19 | 20 | docker-compose_infra_down: 21 | @echo Stoping infrastructure docker-compose down 22 | docker-compose -f deployments/docker-compose/infrastructure.yaml down 23 | 24 | ## choco install protoc 25 | ## go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 26 | # ============================================================================== 27 | # Proto Identity Service 28 | 29 | ## grpc-server 30 | proto_identities_get_user_by_id_service: 31 | @echo Generating identity_service proto 32 | protoc --go_out=./internal/services/identity_service/identity/grpc_server/protos --go-grpc_opt=require_unimplemented_servers=false --go-grpc_out=./internal/services/identity_service/identity/grpc_server/protos ./internal/services/identity_service/identity/grpc_server/protos/*.proto 33 | 34 | 35 | ## grpc-client 36 | proto_identities_get_user_by_id_service: 37 | @echo Generating identity_service proto 38 | protoc --go_out=./internal/services/product_service/product/grpc_client/protos --go-grpc_opt=require_unimplemented_servers=false --go-grpc_out=./internal/services/product_service/product/grpc_client/protos ./internal/services/product_service/product/grpc_client/protos/*.proto 39 | 40 | ## go install github.com/swaggo/swag/cmd/swag@v1.8.3 41 | # Swagger products Service #https://github.com/swaggo/swag/issues/817 42 | # ============================================================================== 43 | 44 | swagger_products: 45 | @echo Starting swagger generating 46 | swag init -g ./internal/services/product_service/cmd/main.go -o ./internal/services/product_service/docs --exclude ./internal/services/identity_service, ./internal/services/inventory_service 47 | 48 | swagger_identities: 49 | @echo Starting swagger generating 50 | swag init -g ./internal/services/identity_service/cmd/main.go -o ./internal/services/identity_service/docs --exclude ./internal/services/product_service, ./internal/services/inventory_service 51 | 52 | swagger_inventories: 53 | @echo Starting swagger generating 54 | swag init -g ./internal/services/inventory_service/cmd/main.go -o ./internal/services/inventory_service/docs --exclude ./internal/services/product_service, ./internal/services/identity_service -------------------------------------------------------------------------------- /assets/shop-golang-microservices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meysamhadeli/shop-golang-microservices/f1a30a348ec34af2c27001d8ad30cac733df1026/assets/shop-golang-microservices.png -------------------------------------------------------------------------------- /assets/vertical-slice-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meysamhadeli/shop-golang-microservices/f1a30a348ec34af2c27001d8ad30cac733df1026/assets/vertical-slice-architecture.png -------------------------------------------------------------------------------- /deployments/docker-compose/infrastructure.yaml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | 4 | ####################################################### 5 | # Rabbitmq 6 | ####################################################### 7 | rabbitmq: 8 | container_name: rabbitmq 9 | image: rabbitmq:management 10 | restart: unless-stopped 11 | ports: 12 | - 5672:5672 13 | - 15672:15672 14 | networks: 15 | - shop 16 | 17 | ####################################################### 18 | # Postgress 19 | ####################################################### 20 | postgres: 21 | image: postgres:latest 22 | container_name: postgres 23 | restart: unless-stopped 24 | ports: 25 | - '5432:5432' 26 | environment: 27 | - POSTGRES_USER=postgres 28 | - POSTGRES_PASSWORD=postgres 29 | command: 30 | - "postgres" 31 | - "-c" 32 | - "wal_level=logical" 33 | - "-c" 34 | - "max_prepared_transactions=10" 35 | networks: 36 | - shop 37 | 38 | ####################################################### 39 | # Jaeger 40 | ####################################################### 41 | jaeger: 42 | container_name: jaeger 43 | image: jaegertracing/all-in-one 44 | restart: unless-stopped 45 | networks: 46 | - shop 47 | ports: 48 | - 5775:5775/udp 49 | - 5778:5778 50 | - 6831:6831/udp 51 | - 6832:6832/udp 52 | - 9411:9411 53 | - 14268:14268 54 | - 16686:16686 55 | 56 | 57 | networks: 58 | shop: 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /internal/pkg/gorm_pgsql/generic_repository.go: -------------------------------------------------------------------------------- 1 | package gormpgsql 2 | 3 | import ( 4 | "context" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // gorm generic repository 10 | type genericRepository[T any] struct { 11 | db *gorm.DB 12 | } 13 | 14 | // create new gorm generic repository 15 | func NewGenericRepository[T any](db *gorm.DB) *genericRepository[T] { 16 | return &genericRepository[T]{ 17 | db: db, 18 | } 19 | } 20 | 21 | func (r *genericRepository[T]) Add(entity *T, ctx context.Context) error { 22 | return r.db.WithContext(ctx).Create(&entity).Error 23 | } 24 | 25 | func (r *genericRepository[T]) AddAll(entity *[]T, ctx context.Context) error { 26 | return r.db.WithContext(ctx).Create(&entity).Error 27 | } 28 | 29 | func (r *genericRepository[T]) GetById(id int, ctx context.Context) (*T, error) { 30 | var entity T 31 | err := r.db.WithContext(ctx).Model(&entity).Where("id = ? AND is_active = ?", id, true).FirstOrInit(&entity).Error 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return &entity, nil 37 | } 38 | 39 | func (r *genericRepository[T]) Get(params *T, ctx context.Context) *T { 40 | var entity T 41 | r.db.WithContext(ctx).Where(¶ms).FirstOrInit(&entity) 42 | return &entity 43 | } 44 | 45 | func (r *genericRepository[T]) GetAll(ctx context.Context) (*[]T, error) { 46 | var entities []T 47 | err := r.db.WithContext(ctx).Find(&entities).Error 48 | if err != nil { 49 | return nil, err 50 | } 51 | return &entities, nil 52 | } 53 | 54 | func (r *genericRepository[T]) Where(params *T, ctx context.Context) (*[]T, error) { 55 | var entities []T 56 | err := r.db.WithContext(ctx).Where(¶ms).Find(&entities).Error 57 | if err != nil { 58 | return nil, err 59 | } 60 | return &entities, nil 61 | } 62 | 63 | func (r *genericRepository[T]) Update(entity *T, ctx context.Context) error { 64 | return r.db.WithContext(ctx).Save(&entity).Error 65 | } 66 | 67 | func (r genericRepository[T]) UpdateAll(entities *[]T, ctx context.Context) error { 68 | return r.db.WithContext(ctx).Save(&entities).Error 69 | } 70 | 71 | func (r *genericRepository[T]) Delete(id int, ctx context.Context) error { 72 | var entity T 73 | return r.db.WithContext(ctx).FirstOrInit(&entity).UpdateColumn("is_active", false).Error 74 | } 75 | 76 | func (r *genericRepository[T]) SkipTake(skip int, take int, ctx context.Context) (*[]T, error) { 77 | var entities []T 78 | err := r.db.WithContext(ctx).Offset(skip).Limit(take).Find(&entities).Error 79 | if err != nil { 80 | return nil, err 81 | } 82 | return &entities, nil 83 | } 84 | 85 | func (r *genericRepository[T]) Count(ctx context.Context) int64 { 86 | var entity T 87 | var count int64 88 | r.db.WithContext(ctx).Model(&entity).Count(&count) 89 | return count 90 | } 91 | 92 | func (r *genericRepository[T]) CountWhere(params *T, ctx context.Context) int64 { 93 | var entity T 94 | var count int64 95 | r.db.WithContext(ctx).Model(&entity).Where(¶ms).Count(&count) 96 | return count 97 | } 98 | -------------------------------------------------------------------------------- /internal/pkg/grpc/grpc-client.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "fmt" 5 | "google.golang.org/grpc" 6 | "google.golang.org/grpc/credentials/insecure" 7 | ) 8 | 9 | type grpcClient struct { 10 | conn *grpc.ClientConn 11 | } 12 | 13 | //go:generate mockery --name GrpcClient 14 | type GrpcClient interface { 15 | GetGrpcConnection() *grpc.ClientConn 16 | Close() error 17 | } 18 | 19 | func NewGrpcClient(config *GrpcConfig) (GrpcClient, error) { 20 | // Grpc Client to call Grpc Server 21 | //https://sahansera.dev/building-grpc-client-go/ 22 | //https://github.com/open-telemetry/opentelemetry-go-contrib/blob/df16f32df86b40077c9c90d06f33c4cdb6dd5afa/instrumentation/google.golang.org/grpc/otelgrpc/example_interceptor_test.go 23 | conn, err := grpc.Dial(fmt.Sprintf("%s%s", config.Host, config.Port), 24 | grpc.WithTransportCredentials(insecure.NewCredentials()), 25 | ) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return &grpcClient{conn: conn}, nil 31 | } 32 | 33 | func (g *grpcClient) GetGrpcConnection() *grpc.ClientConn { 34 | return g.conn 35 | } 36 | 37 | func (g *grpcClient) Close() error { 38 | return g.conn.Close() 39 | } 40 | -------------------------------------------------------------------------------- /internal/pkg/grpc/grpc-server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | "github.com/pkg/errors" 9 | "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/keepalive" 12 | "google.golang.org/grpc/reflection" 13 | "net" 14 | "time" 15 | ) 16 | 17 | const ( 18 | maxConnectionIdle = 5 19 | gRPCTimeout = 15 20 | maxConnectionAge = 5 21 | gRPCTime = 10 22 | ) 23 | 24 | type GrpcConfig struct { 25 | Port string `mapstructure:"port"` 26 | Host string `mapstructure:"host"` 27 | Development bool `mapstructure:"development"` 28 | } 29 | 30 | type GrpcServer struct { 31 | Grpc *grpc.Server 32 | Config *GrpcConfig 33 | Log logger.ILogger 34 | } 35 | 36 | func NewGrpcServer(log logger.ILogger, config *GrpcConfig) *GrpcServer { 37 | 38 | unaryServerInterceptors := []grpc.UnaryServerInterceptor{ 39 | otelgrpc.UnaryServerInterceptor(), 40 | } 41 | streamServerInterceptors := []grpc.StreamServerInterceptor{ 42 | otelgrpc.StreamServerInterceptor(), 43 | } 44 | 45 | s := grpc.NewServer( 46 | grpc.KeepaliveParams(keepalive.ServerParameters{ 47 | MaxConnectionIdle: maxConnectionIdle * time.Minute, 48 | Timeout: gRPCTimeout * time.Second, 49 | MaxConnectionAge: maxConnectionAge * time.Minute, 50 | Time: gRPCTime * time.Minute, 51 | }), 52 | //https://github.com/open-telemetry/opentelemetry-go-contrib/tree/00b796d0cdc204fa5d864ec690b2ee9656bb5cfc/instrumentation/google.golang.org/grpc/otelgrpc 53 | //github.com/grpc-ecosystem/go-grpc-middleware 54 | grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( 55 | streamServerInterceptors..., 56 | )), 57 | grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( 58 | unaryServerInterceptors..., 59 | )), 60 | ) 61 | 62 | return &GrpcServer{Grpc: s, Config: config, Log: log} 63 | } 64 | 65 | func (s *GrpcServer) RunGrpcServer(ctx context.Context, configGrpc ...func(grpcServer *grpc.Server)) error { 66 | listen, err := net.Listen("tcp", s.Config.Port) 67 | if err != nil { 68 | return errors.Wrap(err, "net.Listen") 69 | } 70 | 71 | if len(configGrpc) > 0 { 72 | grpcFunc := configGrpc[0] 73 | if grpcFunc != nil { 74 | grpcFunc(s.Grpc) 75 | } 76 | } 77 | 78 | if s.Config.Development { 79 | reflection.Register(s.Grpc) 80 | } 81 | 82 | if len(configGrpc) > 0 { 83 | grpcFunc := configGrpc[0] 84 | if grpcFunc != nil { 85 | grpcFunc(s.Grpc) 86 | } 87 | } 88 | 89 | go func() { 90 | for { 91 | select { 92 | case <-ctx.Done(): 93 | s.Log.Infof("shutting down grpc PORT: {%s}", s.Config.Port) 94 | s.shutdown() 95 | s.Log.Info("grpc exited properly") 96 | return 97 | } 98 | } 99 | }() 100 | 101 | s.Log.Infof("grpc server is listening on port: %s", s.Config.Port) 102 | 103 | err = s.Grpc.Serve(listen) 104 | 105 | if err != nil { 106 | s.Log.Error(fmt.Sprintf("[grpcServer_RunGrpcServer.Serve] grpc server serve error: %+v", err)) 107 | } 108 | 109 | return err 110 | } 111 | 112 | func (s *GrpcServer) shutdown() { 113 | s.Grpc.Stop() 114 | s.Grpc.GracefulStop() 115 | } 116 | -------------------------------------------------------------------------------- /internal/pkg/grpc/mocks/grpc_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.16.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | google_golang_orggrpc "google.golang.org/grpc" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // GrpcClient is an autogenerated mock type for the GrpcClient type 12 | type GrpcClient struct { 13 | mock.Mock 14 | } 15 | 16 | // Close provides a mock function with given fields: 17 | func (_m *GrpcClient) Close() error { 18 | ret := _m.Called() 19 | 20 | var r0 error 21 | if rf, ok := ret.Get(0).(func() error); ok { 22 | r0 = rf() 23 | } else { 24 | r0 = ret.Error(0) 25 | } 26 | 27 | return r0 28 | } 29 | 30 | // GetGrpcConnection provides a mock function with given fields: 31 | func (_m *GrpcClient) GetGrpcConnection() *google_golang_orggrpc.ClientConn { 32 | ret := _m.Called() 33 | 34 | var r0 *google_golang_orggrpc.ClientConn 35 | if rf, ok := ret.Get(0).(func() *google_golang_orggrpc.ClientConn); ok { 36 | r0 = rf() 37 | } else { 38 | if ret.Get(0) != nil { 39 | r0 = ret.Get(0).(*google_golang_orggrpc.ClientConn) 40 | } 41 | } 42 | 43 | return r0 44 | } 45 | 46 | type mockConstructorTestingTNewGrpcClient interface { 47 | mock.TestingT 48 | Cleanup(func()) 49 | } 50 | 51 | // NewGrpcClient creates a new instance of GrpcClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 52 | func NewGrpcClient(t mockConstructorTestingTNewGrpcClient) *GrpcClient { 53 | mock := &GrpcClient{} 54 | mock.Mock.Test(t) 55 | 56 | t.Cleanup(func() { mock.AssertExpectations(t) }) 57 | 58 | return mock 59 | } 60 | -------------------------------------------------------------------------------- /internal/pkg/http/context_provider.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | log "github.com/sirupsen/logrus" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | func NewContext() context.Context { 12 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 13 | 14 | go func() { 15 | for { 16 | select { 17 | case <-ctx.Done(): 18 | log.Info("context is canceled!") 19 | cancel() 20 | return 21 | } 22 | } 23 | }() 24 | 25 | return ctx 26 | } 27 | -------------------------------------------------------------------------------- /internal/pkg/http/echo/middleware/correlation_id_middleware.go: -------------------------------------------------------------------------------- 1 | package echomiddleware 2 | 3 | import ( 4 | "context" 5 | "github.com/labstack/echo/v4" 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | func CorrelationIdMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 10 | return func(c echo.Context) error { 11 | 12 | req := c.Request() 13 | 14 | id := req.Header.Get(echo.HeaderXCorrelationID) 15 | if id == "" { 16 | id = uuid.NewV4().String() 17 | } 18 | 19 | c.Response().Header().Set(echo.HeaderXCorrelationID, id) 20 | newReq := req.WithContext(context.WithValue(req.Context(), echo.HeaderXCorrelationID, id)) 21 | c.SetRequest(newReq) 22 | 23 | return next(c) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/pkg/http/echo/middleware/validate_token_middleware.go: -------------------------------------------------------------------------------- 1 | package echomiddleware 2 | 3 | import ( 4 | "github.com/go-oauth2/oauth2/v4/generates" 5 | "github.com/golang-jwt/jwt" 6 | "github.com/labstack/echo/v4" 7 | "github.com/pkg/errors" 8 | "net/http" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | // ValidateBearerToken from request 14 | func ValidateBearerToken() echo.MiddlewareFunc { 15 | return func(next echo.HandlerFunc) echo.HandlerFunc { 16 | return func(c echo.Context) error { 17 | 18 | // Ignore check authentication in test 19 | env := os.Getenv("APP_ENV") 20 | if env == "test" { 21 | return next(c) 22 | } 23 | 24 | // Parse and verify jwt access token 25 | auth, ok := bearerAuth(c.Request()) 26 | if !ok { 27 | return echo.NewHTTPError(http.StatusUnauthorized, errors.New("parse jwt access token error")) 28 | } 29 | token, err := jwt.ParseWithClaims(auth, &generates.JWTAccessClaims{}, func(t *jwt.Token) (interface{}, error) { 30 | if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { 31 | return nil, echo.NewHTTPError(http.StatusUnauthorized, errors.New("parse signing method error")) 32 | } 33 | return []byte("secret"), nil 34 | }) 35 | if err != nil { 36 | return echo.NewHTTPError(http.StatusUnauthorized, err) 37 | } 38 | 39 | c.Set("token", token) 40 | return next(c) 41 | } 42 | } 43 | } 44 | 45 | // BearerAuth parse bearer token 46 | func bearerAuth(r *http.Request) (string, bool) { 47 | auth := r.Header.Get("Authorization") 48 | prefix := "Bearer " 49 | token := "" 50 | 51 | if auth != "" && strings.HasPrefix(auth, prefix) { 52 | token = auth[len(prefix):] 53 | } else { 54 | token = r.FormValue("access_token") 55 | } 56 | return token, token != "" 57 | } 58 | -------------------------------------------------------------------------------- /internal/pkg/http/echo/server/echo-server.go: -------------------------------------------------------------------------------- 1 | package echoserver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/labstack/echo/v4" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | "time" 9 | ) 10 | 11 | const ( 12 | MaxHeaderBytes = 1 << 20 13 | ReadTimeout = 15 * time.Second 14 | WriteTimeout = 15 * time.Second 15 | ) 16 | 17 | type EchoConfig struct { 18 | Port string `mapstructure:"port" validate:"required"` 19 | Development bool `mapstructure:"development"` 20 | BasePath string `mapstructure:"basePath" validate:"required"` 21 | DebugErrorsResponse bool `mapstructure:"debugErrorsResponse"` 22 | IgnoreLogUrls []string `mapstructure:"ignoreLogUrls"` 23 | Timeout int `mapstructure:"timeout"` 24 | Host string `mapstructure:"host"` 25 | } 26 | 27 | func NewEchoServer() *echo.Echo { 28 | e := echo.New() 29 | return e 30 | } 31 | 32 | func RunHttpServer(ctx context.Context, echo *echo.Echo, log logger.ILogger, cfg *EchoConfig) error { 33 | echo.Server.ReadTimeout = ReadTimeout 34 | echo.Server.WriteTimeout = WriteTimeout 35 | echo.Server.MaxHeaderBytes = MaxHeaderBytes 36 | 37 | go func() { 38 | for { 39 | select { 40 | case <-ctx.Done(): 41 | log.Infof("shutting down Http PORT: {%s}", cfg.Port) 42 | err := echo.Shutdown(ctx) 43 | if err != nil { 44 | log.Errorf("(Shutdown) err: {%v}", err) 45 | return 46 | } 47 | log.Info("server exited properly") 48 | return 49 | } 50 | } 51 | }() 52 | 53 | err := echo.Start(cfg.Port) 54 | 55 | return err 56 | } 57 | 58 | func ApplyVersioningFromHeader(echo *echo.Echo) { 59 | echo.Pre(apiVersion) 60 | } 61 | 62 | func RegisterGroupFunc(groupName string, echo *echo.Echo, builder func(g *echo.Group)) *echo.Echo { 63 | builder(echo.Group(groupName)) 64 | 65 | return echo 66 | } 67 | 68 | // APIVersion Header Based Versioning 69 | func apiVersion(next echo.HandlerFunc) echo.HandlerFunc { 70 | return func(c echo.Context) error { 71 | req := c.Request() 72 | headers := req.Header 73 | 74 | apiVersion := headers.Get("version") 75 | 76 | req.URL.Path = fmt.Sprintf("/%s%s", apiVersion, req.URL.Path) 77 | 78 | return next(c) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/pkg/http_client/http_client.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import ( 4 | "github.com/go-resty/resty/v2" 5 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 6 | "net" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | const ( 12 | timeout = 5 * time.Second 13 | dialContextTimeout = 5 * time.Second 14 | tLSHandshakeTimeout = 5 * time.Second 15 | xaxIdleConns = 20 16 | maxConnsPerHost = 40 17 | retryCount = 3 18 | retryWaitTime = 300 * time.Millisecond 19 | idleConnTimeout = 120 * time.Second 20 | responseHeaderTimeout = 5 * time.Second 21 | ) 22 | 23 | func NewHttpClient() *resty.Client { 24 | 25 | transport := &http.Transport{ 26 | DialContext: (&net.Dialer{ 27 | Timeout: dialContextTimeout, 28 | }).DialContext, 29 | TLSHandshakeTimeout: tLSHandshakeTimeout, 30 | MaxIdleConns: xaxIdleConns, 31 | MaxConnsPerHost: maxConnsPerHost, 32 | IdleConnTimeout: idleConnTimeout, 33 | ResponseHeaderTimeout: responseHeaderTimeout, 34 | } 35 | 36 | client := resty.New(). 37 | SetTimeout(timeout). 38 | SetRetryCount(retryCount). 39 | SetRetryWaitTime(retryWaitTime). 40 | SetTransport(otelhttp.NewTransport(transport)) // use custom transport open-telemetry for tracing http-client 41 | 42 | return client 43 | } 44 | -------------------------------------------------------------------------------- /internal/pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "os" 6 | ) 7 | 8 | // Logger methods interface 9 | // 10 | //go:generate mockery --name ILogger 11 | type ILogger interface { 12 | getLevel() log.Level 13 | Debug(args ...interface{}) 14 | Debugf(format string, args ...interface{}) 15 | Info(args ...interface{}) 16 | Infof(format string, args ...interface{}) 17 | Warn(args ...interface{}) 18 | Warnf(format string, args ...interface{}) 19 | Error(args ...interface{}) 20 | Errorf(format string, args ...interface{}) 21 | Panic(args ...interface{}) 22 | Panicf(format string, args ...interface{}) 23 | Fatal(args ...interface{}) 24 | Fatalf(format string, args ...interface{}) 25 | Trace(args ...interface{}) 26 | Tracef(format string, args ...interface{}) 27 | } 28 | 29 | var ( 30 | Logger ILogger 31 | ) 32 | 33 | type LoggerConfig struct { 34 | LogLevel string `mapstructure:"level"` 35 | } 36 | 37 | // Application logger 38 | type appLogger struct { 39 | level string 40 | logger *log.Logger 41 | } 42 | 43 | // For mapping config logger to email_service logger levels 44 | var loggerLevelMap = map[string]log.Level{ 45 | "debug": log.DebugLevel, 46 | "info": log.InfoLevel, 47 | "warn": log.WarnLevel, 48 | "error": log.ErrorLevel, 49 | "panic": log.PanicLevel, 50 | "fatal": log.FatalLevel, 51 | "trace": log.TraceLevel, 52 | } 53 | 54 | func (l *appLogger) getLevel() log.Level { 55 | 56 | level, exist := loggerLevelMap[l.level] 57 | if !exist { 58 | return log.DebugLevel 59 | } 60 | 61 | return level 62 | } 63 | 64 | // InitLogger Init logger 65 | func InitLogger(cfg *LoggerConfig) ILogger { 66 | 67 | l := &appLogger{level: cfg.LogLevel} 68 | 69 | l.logger = log.StandardLogger() 70 | 71 | logLevel := l.getLevel() 72 | 73 | env := os.Getenv("APP_ENV") 74 | 75 | if env == "production" { 76 | log.SetFormatter(&log.JSONFormatter{}) 77 | } else { 78 | // The TextFormatter is default, you don't actually have to do this. 79 | log.SetFormatter(&log.TextFormatter{ 80 | DisableColors: false, 81 | ForceColors: true, 82 | FullTimestamp: true, 83 | }) 84 | } 85 | 86 | log.SetLevel(logLevel) 87 | 88 | return l 89 | } 90 | 91 | func (l *appLogger) Debug(args ...interface{}) { 92 | l.logger.Debug(args...) 93 | } 94 | 95 | func (l *appLogger) Debugf(format string, args ...interface{}) { 96 | l.logger.Debugf(format, args...) 97 | } 98 | 99 | func (l *appLogger) Info(args ...interface{}) { 100 | l.logger.Info(args...) 101 | } 102 | 103 | func (l *appLogger) Infof(format string, args ...interface{}) { 104 | l.logger.Infof(format, args...) 105 | } 106 | 107 | func (l *appLogger) Trace(args ...interface{}) { 108 | l.logger.Trace(args...) 109 | } 110 | 111 | func (l *appLogger) Tracef(format string, args ...interface{}) { 112 | l.logger.Tracef(format, args...) 113 | } 114 | 115 | func (l *appLogger) Error(args ...interface{}) { 116 | l.logger.Error(args...) 117 | } 118 | 119 | func (l *appLogger) Errorf(format string, args ...interface{}) { 120 | l.logger.Errorf(format, args...) 121 | } 122 | 123 | func (l *appLogger) Warn(args ...interface{}) { 124 | l.logger.Warn(args...) 125 | } 126 | 127 | func (l *appLogger) Warnf(format string, args ...interface{}) { 128 | l.logger.Warnf(format, args...) 129 | } 130 | 131 | func (l *appLogger) Panic(args ...interface{}) { 132 | l.logger.Panic(args...) 133 | } 134 | 135 | func (l *appLogger) Panicf(format string, args ...interface{}) { 136 | l.logger.Panicf(format, args...) 137 | } 138 | 139 | func (l *appLogger) Fatal(args ...interface{}) { 140 | l.logger.Fatal(args...) 141 | } 142 | 143 | func (l *appLogger) Fatalf(format string, args ...interface{}) { 144 | l.logger.Fatalf(format, args...) 145 | } 146 | -------------------------------------------------------------------------------- /internal/pkg/oauth2/password_credentials.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "context" 5 | "github.com/go-oauth2/oauth2/v4" 6 | "github.com/go-oauth2/oauth2/v4/errors" 7 | "github.com/go-oauth2/oauth2/v4/generates" 8 | "github.com/go-oauth2/oauth2/v4/manage" 9 | "github.com/go-oauth2/oauth2/v4/models" 10 | "github.com/go-oauth2/oauth2/v4/server" 11 | "github.com/go-oauth2/oauth2/v4/store" 12 | "github.com/golang-jwt/jwt" 13 | "github.com/labstack/echo/v4" 14 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 15 | uuid "github.com/satori/go.uuid" 16 | "gorm.io/gorm" 17 | "log" 18 | "net/http" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | var ( 24 | srv *server.Server 25 | once sync.Once 26 | manager *manage.Manager 27 | privateKey = []byte(`secret`) 28 | clients = []*models.Client{{ID: "clientId", Secret: "clientSecret"}, {ID: "clientId2", Secret: "clientSecret2"}} 29 | ) 30 | 31 | // User model 32 | type User struct { 33 | UserId uuid.UUID `json:"userId" gorm:"primaryKey"` 34 | FirstName string `json:"firstName"` 35 | LastName string `json:"lastName"` 36 | UserName string `json:"userName"` 37 | Email string `json:"email"` 38 | Password string `json:"password"` 39 | CreatedAt time.Time `json:"createdAt"` 40 | UpdatedAt time.Time `json:"updatedAt"` 41 | } 42 | 43 | func init() { 44 | manager = manage.NewDefaultManager() 45 | manager.MapAccessGenerate(generates.NewJWTAccessGenerate("", privateKey, jwt.SigningMethodHS512)) 46 | manager.MustTokenStorage(store.NewMemoryTokenStore()) 47 | once.Do(func() { 48 | srv = server.NewDefaultServer(manager) 49 | }) 50 | } 51 | 52 | func clientStore(clients ...*models.Client) oauth2.ClientStore { 53 | clientStore := store.NewClientStore() 54 | 55 | for _, client := range clients { 56 | if client != nil { 57 | err := clientStore.Set(client.ID, &models.Client{ 58 | ID: client.ID, 59 | Secret: client.Secret, 60 | Domain: client.Domain, 61 | }) 62 | if err != nil { 63 | return nil 64 | } 65 | } 66 | } 67 | return clientStore 68 | } 69 | 70 | // ref: https://github.com/go-oauth2/oauth2 71 | func RunOauthServer(e *echo.Echo, gorm *gorm.DB) { 72 | 73 | manager.MapClientStorage(clientStore(clients...)) 74 | 75 | srv.SetPasswordAuthorizationHandler(func(ctx context.Context, clientID, username, password string) (userID string, err error) { 76 | 77 | u := User{} 78 | gorm.Where("user_name = ?", username).First(&u) 79 | 80 | // now use p 81 | isMatch, err := utils.ComparePasswords(u.Password, password) 82 | 83 | if err != nil { 84 | return "", err 85 | } 86 | 87 | if isMatch { 88 | return u.UserId.String(), nil 89 | } 90 | return 91 | }) 92 | 93 | srv.SetClientScopeHandler(func(tgr *oauth2.TokenGenerateRequest) (allowed bool, err error) { 94 | if tgr.Scope == "all" { 95 | allowed = true 96 | } 97 | return 98 | }) 99 | // for using querystring 100 | srv.SetAllowGetAccessRequest(true) 101 | srv.SetClientInfoHandler(server.ClientFormHandler) 102 | 103 | srv.SetInternalErrorHandler(func(err error) (re *errors.Response) { 104 | log.Println("Internal Error:", err.Error()) 105 | return 106 | }) 107 | 108 | srv.SetResponseErrorHandler(func(re *errors.Response) { 109 | log.Println("Response Error:", re.Error.Error()) 110 | }) 111 | 112 | e.GET("connect/token", token) 113 | e.GET("validate-token", validateBearerToken) 114 | } 115 | 116 | func validateBearerToken(c echo.Context) error { 117 | token, err := srv.ValidationBearerToken(c.Request()) 118 | if err != nil { 119 | return echo.NewHTTPError(http.StatusUnauthorized, err) 120 | } 121 | return c.JSON(http.StatusOK, token) 122 | } 123 | 124 | func token(c echo.Context) error { 125 | err := srv.HandleTokenRequest(c.Response().Writer, c.Request()) 126 | if err != nil { 127 | return err 128 | } 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /internal/pkg/otel/jaeger.go: -------------------------------------------------------------------------------- 1 | package otel 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 7 | "go.opentelemetry.io/otel" 8 | "go.opentelemetry.io/otel/attribute" 9 | "go.opentelemetry.io/otel/exporters/jaeger" 10 | "go.opentelemetry.io/otel/propagation" 11 | "go.opentelemetry.io/otel/sdk/resource" 12 | tracesdk "go.opentelemetry.io/otel/sdk/trace" 13 | semconv "go.opentelemetry.io/otel/semconv/v1.12.0" 14 | "go.opentelemetry.io/otel/trace" 15 | "os" 16 | ) 17 | 18 | type JaegerConfig struct { 19 | Server string `mapstructure:"server"` 20 | ServiceName string `mapstructure:"serviceName"` 21 | TracerName string `mapstructure:"tracerName"` 22 | } 23 | 24 | func TracerProvider(ctx context.Context, cfg *JaegerConfig, log logger.ILogger) (trace.Tracer, error) { 25 | var serverUrl = fmt.Sprintf(cfg.Server+"%s", "/api/traces") 26 | // Create the Jaeger exporter 27 | exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(serverUrl))) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | env := os.Getenv("APP_ENV") 33 | 34 | if env != "production" { 35 | env = "development" 36 | } 37 | 38 | tp := tracesdk.NewTracerProvider( 39 | // Always be sure to batch in production. 40 | tracesdk.WithBatcher(exporter), 41 | // Record information about this application in a Resource. 42 | tracesdk.WithResource(resource.NewWithAttributes( 43 | semconv.SchemaURL, 44 | semconv.ServiceNameKey.String(cfg.ServiceName), 45 | attribute.String("environment", env), 46 | )), 47 | ) 48 | 49 | go func() { 50 | for { 51 | select { 52 | case <-ctx.Done(): 53 | err = tp.Shutdown(ctx) 54 | log.Info("open-telemetry exited properly") 55 | if err != nil { 56 | return 57 | } 58 | } 59 | } 60 | }() 61 | 62 | otel.SetTracerProvider(tp) 63 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{})) 64 | 65 | t := tp.Tracer(cfg.TracerName) 66 | 67 | return t, nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/pkg/otel/middleware/echo_tracer_middleware.go: -------------------------------------------------------------------------------- 1 | package otelmiddleware 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/labstack/echo/v4" 7 | "go.opentelemetry.io/otel" 8 | "go.opentelemetry.io/otel/attribute" 9 | "go.opentelemetry.io/otel/codes" 10 | "go.opentelemetry.io/otel/propagation" 11 | semconv "go.opentelemetry.io/otel/semconv/v1.12.0" 12 | oteltrace "go.opentelemetry.io/otel/trace" 13 | ) 14 | 15 | func EchoTracerMiddleware(serviceName string) echo.MiddlewareFunc { 16 | 17 | return func(next echo.HandlerFunc) echo.HandlerFunc { 18 | return func(c echo.Context) error { 19 | 20 | request := c.Request() 21 | ctx := request.Context() 22 | 23 | // ref: https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/instrumentation/github.com/labstack/echo/otelecho/echo.go 24 | ctx = otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(request.Header)) 25 | opts := []oteltrace.SpanStartOption{ 26 | oteltrace.WithAttributes(semconv.NetAttributesFromHTTPRequest("tcp", request)...), 27 | oteltrace.WithAttributes(semconv.EndUserAttributesFromHTTPRequest(request)...), 28 | oteltrace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(serviceName, c.Path(), request)...), 29 | oteltrace.WithSpanKind(oteltrace.SpanKindServer), 30 | } 31 | spanName := c.Path() 32 | if spanName == "" { 33 | spanName = fmt.Sprintf("HTTP %s route not found", request.Method) 34 | } 35 | 36 | ctx, span := otel.Tracer("echo-http").Start(ctx, spanName, opts...) 37 | defer span.End() 38 | 39 | // pass the span through the request context 40 | c.SetRequest(request.WithContext(ctx)) 41 | 42 | err := next(c) 43 | 44 | if err != nil { 45 | // invokes the registered HTTP error handler 46 | c.Error(err) 47 | 48 | var echoError *echo.HTTPError 49 | 50 | // handle *HTTPError error type in Echo 51 | if errors.As(err, &echoError) { 52 | c.Response().Status = err.(*echo.HTTPError).Code 53 | err = err.(*echo.HTTPError).Message.(error) 54 | } 55 | 56 | span.SetStatus(codes.Error, "") // set the spanStatus Error for all error stats codes 57 | span.SetAttributes(attribute.String("echo-error", err.Error())) 58 | span.SetAttributes(attribute.Int("status-code", c.Response().Status)) 59 | } 60 | 61 | return err 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/pkg/otel/utils.go: -------------------------------------------------------------------------------- 1 | package otel 2 | 3 | import ( 4 | "context" 5 | 6 | "go.opentelemetry.io/otel" 7 | ) 8 | 9 | //ref: https://devandchill.com/posts/2021/12/go-step-by-step-guide-for-implementing-tracing-on-a-microservices-architecture-2/2/ 10 | 11 | type AmqpHeadersCarrier map[string]interface{} 12 | 13 | func (a AmqpHeadersCarrier) Get(key string) string { 14 | v, ok := a[key] 15 | if !ok { 16 | return "" 17 | } 18 | return v.(string) 19 | } 20 | 21 | func (a AmqpHeadersCarrier) Set(key string, value string) { 22 | a[key] = value 23 | } 24 | 25 | func (a AmqpHeadersCarrier) Keys() []string { 26 | i := 0 27 | r := make([]string, len(a)) 28 | 29 | for k, _ := range a { 30 | r[i] = k 31 | i++ 32 | } 33 | 34 | return r 35 | } 36 | 37 | // InjectAMQPHeaders injects the tracing from the context into the header map 38 | func InjectAMQPHeaders(ctx context.Context) map[string]interface{} { 39 | h := make(AmqpHeadersCarrier) 40 | otel.GetTextMapPropagator().Inject(ctx, h) 41 | return h 42 | } 43 | 44 | // ExtractAMQPHeaders extracts the tracing from the header and puts it into the context 45 | func ExtractAMQPHeaders(ctx context.Context, headers map[string]interface{}) context.Context { 46 | return otel.GetTextMapPropagator().Extract(ctx, AmqpHeadersCarrier(headers)) 47 | } 48 | -------------------------------------------------------------------------------- /internal/pkg/rabbitmq/mocks/IConsumer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.20.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // IConsumer is an autogenerated mock type for the IConsumer type 8 | type IConsumer[T interface{}] struct { 9 | mock.Mock 10 | } 11 | 12 | // ConsumeMessage provides a mock function with given fields: msg, dependencies 13 | func (_m *IConsumer[T]) ConsumeMessage(msg interface{}, dependencies T) error { 14 | ret := _m.Called(msg, dependencies) 15 | 16 | var r0 error 17 | if rf, ok := ret.Get(0).(func(interface{}, T) error); ok { 18 | r0 = rf(msg, dependencies) 19 | } else { 20 | r0 = ret.Error(0) 21 | } 22 | 23 | return r0 24 | } 25 | 26 | // IsConsumed provides a mock function with given fields: msg 27 | func (_m *IConsumer[T]) IsConsumed(msg interface{}) bool { 28 | ret := _m.Called(msg) 29 | 30 | var r0 bool 31 | if rf, ok := ret.Get(0).(func(interface{}) bool); ok { 32 | r0 = rf(msg) 33 | } else { 34 | r0 = ret.Get(0).(bool) 35 | } 36 | 37 | return r0 38 | } 39 | 40 | type mockConstructorTestingTNewIConsumer interface { 41 | mock.TestingT 42 | Cleanup(func()) 43 | } 44 | 45 | // NewIConsumer creates a new instance of IConsumer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 46 | func NewIConsumer[T interface{}](t mockConstructorTestingTNewIConsumer) *IConsumer[T] { 47 | mock := &IConsumer[T]{} 48 | mock.Mock.Test(t) 49 | 50 | t.Cleanup(func() { mock.AssertExpectations(t) }) 51 | 52 | return mock 53 | } 54 | -------------------------------------------------------------------------------- /internal/pkg/rabbitmq/mocks/IPublisher.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.20.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // IPublisher is an autogenerated mock type for the IPublisher type 8 | type IPublisher struct { 9 | mock.Mock 10 | } 11 | 12 | // IsPublished provides a mock function with given fields: msg 13 | func (_m *IPublisher) IsPublished(msg interface{}) bool { 14 | ret := _m.Called(msg) 15 | 16 | var r0 bool 17 | if rf, ok := ret.Get(0).(func(interface{}) bool); ok { 18 | r0 = rf(msg) 19 | } else { 20 | r0 = ret.Get(0).(bool) 21 | } 22 | 23 | return r0 24 | } 25 | 26 | // PublishMessage provides a mock function with given fields: msg 27 | func (_m *IPublisher) PublishMessage(msg interface{}) error { 28 | ret := _m.Called(msg) 29 | 30 | var r0 error 31 | if rf, ok := ret.Get(0).(func(interface{}) error); ok { 32 | r0 = rf(msg) 33 | } else { 34 | r0 = ret.Error(0) 35 | } 36 | 37 | return r0 38 | } 39 | 40 | type mockConstructorTestingTNewIPublisher interface { 41 | mock.TestingT 42 | Cleanup(func()) 43 | } 44 | 45 | // NewIPublisher creates a new instance of IPublisher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 46 | func NewIPublisher(t mockConstructorTestingTNewIPublisher) *IPublisher { 47 | mock := &IPublisher{} 48 | mock.Mock.Test(t) 49 | 50 | t.Cleanup(func() { mock.AssertExpectations(t) }) 51 | 52 | return mock 53 | } 54 | -------------------------------------------------------------------------------- /internal/pkg/rabbitmq/publisher.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | "github.com/ahmetb/go-linq/v3" 6 | "github.com/iancoleman/strcase" 7 | jsoniter "github.com/json-iterator/go" 8 | "github.com/labstack/echo/v4" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel" 11 | uuid "github.com/satori/go.uuid" 12 | "github.com/streadway/amqp" 13 | "go.opentelemetry.io/otel/attribute" 14 | "go.opentelemetry.io/otel/trace" 15 | "reflect" 16 | "time" 17 | ) 18 | 19 | //go:generate mockery --name IPublisher 20 | type IPublisher interface { 21 | PublishMessage(msg interface{}) error 22 | IsPublished(msg interface{}) bool 23 | } 24 | 25 | var publishedMessages []string 26 | 27 | type Publisher struct { 28 | cfg *RabbitMQConfig 29 | conn *amqp.Connection 30 | log logger.ILogger 31 | jaegerTracer trace.Tracer 32 | ctx context.Context 33 | } 34 | 35 | func (p Publisher) PublishMessage(msg interface{}) error { 36 | 37 | data, err := jsoniter.Marshal(msg) 38 | 39 | if err != nil { 40 | p.log.Error("Error in marshalling message to publish message") 41 | return err 42 | } 43 | 44 | typeName := reflect.TypeOf(msg).Elem().Name() 45 | snakeTypeName := strcase.ToSnake(typeName) 46 | 47 | ctx, span := p.jaegerTracer.Start(p.ctx, typeName) 48 | defer span.End() 49 | 50 | // Inject the context in the headers 51 | headers := otel.InjectAMQPHeaders(ctx) 52 | 53 | channel, err := p.conn.Channel() 54 | if err != nil { 55 | p.log.Error("Error in opening channel to consume message") 56 | return err 57 | } 58 | 59 | defer channel.Close() 60 | 61 | err = channel.ExchangeDeclare( 62 | snakeTypeName, // name 63 | p.cfg.Kind, // type 64 | true, // durable 65 | false, // auto-deleted 66 | false, // internal 67 | false, // no-wait 68 | nil, // arguments 69 | ) 70 | 71 | if err != nil { 72 | p.log.Error("Error in declaring exchange to publish message") 73 | return err 74 | } 75 | 76 | correlationId := "" 77 | 78 | if ctx.Value(echo.HeaderXCorrelationID) != nil { 79 | correlationId = ctx.Value(echo.HeaderXCorrelationID).(string) 80 | } 81 | 82 | publishingMsg := amqp.Publishing{ 83 | Body: data, 84 | ContentType: "application/json", 85 | DeliveryMode: amqp.Persistent, 86 | MessageId: uuid.NewV4().String(), 87 | Timestamp: time.Now(), 88 | CorrelationId: correlationId, 89 | Headers: headers, 90 | } 91 | 92 | err = channel.Publish(snakeTypeName, snakeTypeName, false, false, publishingMsg) 93 | 94 | if err != nil { 95 | p.log.Error("Error in publishing message") 96 | return err 97 | } 98 | 99 | publishedMessages = append(publishedMessages, snakeTypeName) 100 | 101 | h, err := jsoniter.Marshal(headers) 102 | 103 | if err != nil { 104 | p.log.Error("Error in marshalling headers to publish message") 105 | return err 106 | } 107 | 108 | p.log.Infof("Published message: %s", publishingMsg.Body) 109 | span.SetAttributes(attribute.Key("message-id").String(publishingMsg.MessageId)) 110 | span.SetAttributes(attribute.Key("correlation-id").String(publishingMsg.CorrelationId)) 111 | span.SetAttributes(attribute.Key("exchange").String(snakeTypeName)) 112 | span.SetAttributes(attribute.Key("kind").String(p.cfg.Kind)) 113 | span.SetAttributes(attribute.Key("content-type").String("application/json")) 114 | span.SetAttributes(attribute.Key("timestamp").String(publishingMsg.Timestamp.String())) 115 | span.SetAttributes(attribute.Key("body").String(string(publishingMsg.Body))) 116 | span.SetAttributes(attribute.Key("headers").String(string(h))) 117 | 118 | return nil 119 | } 120 | 121 | func (p Publisher) IsPublished(msg interface{}) bool { 122 | 123 | typeName := reflect.TypeOf(msg).Name() 124 | snakeTypeName := strcase.ToSnake(typeName) 125 | isPublished := linq.From(publishedMessages).Contains(snakeTypeName) 126 | 127 | return isPublished 128 | } 129 | 130 | func NewPublisher(ctx context.Context, cfg *RabbitMQConfig, conn *amqp.Connection, log logger.ILogger, jaegerTracer trace.Tracer) IPublisher { 131 | return &Publisher{ctx: ctx, cfg: cfg, conn: conn, log: log, jaegerTracer: jaegerTracer} 132 | } 133 | -------------------------------------------------------------------------------- /internal/pkg/rabbitmq/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cenkalti/backoff/v4" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/streadway/amqp" 9 | "time" 10 | ) 11 | 12 | type RabbitMQConfig struct { 13 | Host string 14 | Port int 15 | User string 16 | Password string 17 | ExchangeName string 18 | Kind string 19 | } 20 | 21 | // Initialize new channel for rabbitmq 22 | func NewRabbitMQConn(cfg *RabbitMQConfig, ctx context.Context) (*amqp.Connection, error) { 23 | connAddr := fmt.Sprintf( 24 | "amqp://%s:%s@%s:%d/", 25 | cfg.User, 26 | cfg.Password, 27 | cfg.Host, 28 | cfg.Port, 29 | ) 30 | 31 | bo := backoff.NewExponentialBackOff() 32 | bo.MaxElapsedTime = 10 * time.Second // Maximum time to retry 33 | maxRetries := 5 // Number of retries (including the initial attempt) 34 | 35 | var conn *amqp.Connection 36 | var err error 37 | 38 | err = backoff.Retry(func() error { 39 | 40 | conn, err = amqp.Dial(connAddr) 41 | if err != nil { 42 | log.Errorf("Failed to connect to RabbitMQ: %v. Connection information: %s", err, connAddr) 43 | return err 44 | } 45 | 46 | return nil 47 | }, backoff.WithMaxRetries(bo, uint64(maxRetries-1))) 48 | 49 | log.Info("Connected to RabbitMQ") 50 | 51 | go func() { 52 | select { 53 | case <-ctx.Done(): 54 | err := conn.Close() 55 | if err != nil { 56 | log.Error("Failed to close RabbitMQ connection") 57 | } 58 | log.Info("RabbitMQ connection is closed") 59 | } 60 | }() 61 | 62 | return conn, err 63 | } 64 | -------------------------------------------------------------------------------- /internal/pkg/reflection/type_mappper/type_mapper_test.go: -------------------------------------------------------------------------------- 1 | package typemapper 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestTypes(t *testing.T) { 10 | s := Test{A: 10} 11 | s2 := &Test{A: 10} 12 | 13 | q2 := TypeByName("*typeMapper.Test") 14 | q22 := InstanceByTypeName("*typeMapper.Test").(*Test) 15 | q222 := InstancePointerByTypeName("*typeMapper.Test").(*Test) 16 | 17 | q := TypeByName("typeMapper.Test") 18 | q1 := InstanceByTypeName("typeMapper.Test").(Test) 19 | q11 := InstancePointerByTypeName("typeMapper.Test").(*Test) 20 | 21 | r := GenericInstanceByTypeName[*Test]("*typeMapper.Test") 22 | r2 := GenericInstanceByTypeName[Test]("typeMapper.Test") 23 | 24 | typeName := GetTypeName(s) 25 | typeName2 := GetTypeName(s2) 26 | assert.Equal(t, "typeMapper.Test", typeName) 27 | assert.Equal(t, "*typeMapper.Test", typeName2) 28 | 29 | q1.A = 100 30 | q22.A = 100 31 | 32 | fmt.Println(q.String()) 33 | fmt.Println(q1) 34 | fmt.Println(q11) 35 | fmt.Println(q2.String()) 36 | fmt.Println(q22) 37 | fmt.Println(q222) 38 | fmt.Println(q22.A) 39 | fmt.Println(q1.A) 40 | fmt.Println(r) 41 | fmt.Println(r2) 42 | 43 | assert.NotNil(t, q) 44 | assert.NotNil(t, q1) 45 | assert.NotNil(t, q11) 46 | assert.NotNil(t, q2) 47 | assert.NotNil(t, q22) 48 | assert.NotNil(t, q222) 49 | assert.NotNil(t, r) 50 | assert.NotNil(t, r2) 51 | assert.NotZero(t, q1.A) 52 | assert.NotZero(t, q22.A) 53 | } 54 | 55 | type Test struct { 56 | A int 57 | } 58 | -------------------------------------------------------------------------------- /internal/pkg/reflection/type_mappper/unsafe_types.go: -------------------------------------------------------------------------------- 1 | package typemapper 2 | 3 | import "unsafe" 4 | 5 | //go:linkname typelinks2 reflect.typelinks 6 | func typelinks2() (sections []unsafe.Pointer, offset [][]int32) 7 | 8 | //go:linkname resolveTypeOff reflect.resolveTypeOff 9 | func resolveTypeOff(rtype unsafe.Pointer, off int32) unsafe.Pointer 10 | 11 | type emptyInterface struct { 12 | typ unsafe.Pointer 13 | data unsafe.Pointer 14 | } 15 | -------------------------------------------------------------------------------- /internal/pkg/reflection/type_registry/type_registry.go: -------------------------------------------------------------------------------- 1 | package typeregistry 2 | 3 | // Ref:https://stackoverflow.com/questions/23030884/is-there-a-way-to-create-an-instance-of-a-struct-from-a-string 4 | import "reflect" 5 | 6 | var typeRegistry = make(map[string]reflect.Type) 7 | 8 | func registerType(typedNil interface{}) { 9 | t := reflect.TypeOf(typedNil).Elem() 10 | typeRegistry[t.PkgPath()+"."+t.Name()] = t 11 | } 12 | 13 | type MyString string 14 | type myString string 15 | 16 | func init() { 17 | registerType((*MyString)(nil)) 18 | registerType((*myString)(nil)) 19 | } 20 | 21 | func makeInstance(name string) interface{} { 22 | return reflect.New(typeRegistry[name]).Elem().Interface() 23 | } 24 | -------------------------------------------------------------------------------- /internal/pkg/test/container/postgres_container/postgres_container.go: -------------------------------------------------------------------------------- 1 | package postgrescontainer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cenkalti/backoff/v4" 7 | "github.com/docker/go-connections/nat" 8 | gormpgsql "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/gorm_pgsql" 9 | "github.com/pkg/errors" 10 | "github.com/testcontainers/testcontainers-go" 11 | "github.com/testcontainers/testcontainers-go/wait" 12 | "gorm.io/gorm" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | type PostgresContainerOptions struct { 18 | Database string 19 | Host string 20 | Port nat.Port 21 | HostPort int 22 | UserName string 23 | Password string 24 | ImageName string 25 | Name string 26 | Tag string 27 | Timeout time.Duration 28 | } 29 | 30 | func Start(ctx context.Context, t *testing.T) (*gorm.DB, error) { 31 | 32 | defaultPostgresOptions, err := getDefaultPostgresTestContainers() 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | req := getContainerRequest(defaultPostgresOptions) 38 | 39 | postgresContainer, err := testcontainers.GenericContainer( 40 | ctx, 41 | testcontainers.GenericContainerRequest{ 42 | ContainerRequest: req, 43 | Started: true, 44 | }) 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | // Clean up the container after the test is complete 51 | t.Cleanup(func() { 52 | if err := postgresContainer.Terminate(ctx); err != nil { 53 | t.Fatalf("failed to terminate container: %s", err) 54 | } 55 | }) 56 | 57 | var gormDB *gorm.DB 58 | var gormConfig *gormpgsql.GormPostgresConfig 59 | 60 | bo := backoff.NewExponentialBackOff() 61 | bo.MaxElapsedTime = 10 * time.Second // Maximum time to retry 62 | maxRetries := 5 // Number of retries (including the initial attempt) 63 | 64 | err = backoff.Retry(func() error { 65 | 66 | host, err := postgresContainer.Host(ctx) 67 | if err != nil { 68 | 69 | return errors.Errorf("failed to get container host: %v", err) 70 | } 71 | 72 | realPort, err := postgresContainer.MappedPort(ctx, defaultPostgresOptions.Port) 73 | 74 | if err != nil { 75 | return errors.Errorf("failed to get exposed container port: %v", err) 76 | } 77 | 78 | containerPort := realPort.Int() 79 | 80 | gormConfig = &gormpgsql.GormPostgresConfig{ 81 | Port: containerPort, 82 | Host: host, 83 | DBName: defaultPostgresOptions.Database, 84 | User: defaultPostgresOptions.UserName, 85 | Password: defaultPostgresOptions.Password, 86 | SSLMode: false, 87 | } 88 | gormDB, err = gormpgsql.NewGorm(gormConfig) 89 | if err != nil { 90 | return err 91 | } 92 | return nil 93 | }, backoff.WithMaxRetries(bo, uint64(maxRetries-1))) 94 | 95 | if err != nil { 96 | return nil, errors.Errorf("failed to create connection for postgres after retries: %v", err) 97 | } 98 | 99 | return gormDB, nil 100 | } 101 | 102 | func getContainerRequest(opts *PostgresContainerOptions) testcontainers.ContainerRequest { 103 | 104 | containerReq := testcontainers.ContainerRequest{ 105 | Image: fmt.Sprintf("%s:%s", opts.ImageName, opts.Tag), 106 | ExposedPorts: []string{"5432/tcp"}, 107 | WaitingFor: wait.ForListeningPort("5432/tcp"), 108 | Env: map[string]string{ 109 | "POSTGRES_DB": opts.Database, 110 | "POSTGRES_PASSWORD": opts.Password, 111 | "POSTGRES_USER": opts.UserName, 112 | }, 113 | } 114 | 115 | return containerReq 116 | } 117 | 118 | func getDefaultPostgresTestContainers() (*PostgresContainerOptions, error) { 119 | port, err := nat.NewPort("", "5432") 120 | if err != nil { 121 | return nil, fmt.Errorf("failed to build port: %v", err) 122 | } 123 | 124 | return &PostgresContainerOptions{ 125 | Database: "test_db", 126 | Port: port, 127 | Host: "localhost", 128 | UserName: "testcontainers", 129 | Password: "testcontainers", 130 | Tag: "latest", 131 | ImageName: "postgres", 132 | Name: "postgresql-testcontainer", 133 | Timeout: 5 * time.Minute, 134 | }, nil 135 | } 136 | -------------------------------------------------------------------------------- /internal/pkg/test/container/postgres_container/postgres_container_test.go: -------------------------------------------------------------------------------- 1 | package postgrescontainer 2 | 3 | import ( 4 | "context" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/stretchr/testify/require" 7 | "testing" 8 | ) 9 | 10 | func Test_Gorm_Container(t *testing.T) { 11 | gorm, err := Start(context.Background(), t) 12 | require.NoError(t, err) 13 | 14 | assert.NotNil(t, gorm) 15 | } 16 | -------------------------------------------------------------------------------- /internal/pkg/test/container/rabbitmq_container/rabbitmq_container_test.go: -------------------------------------------------------------------------------- 1 | package rabbitmqcontainer 2 | 3 | import ( 4 | "context" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/stretchr/testify/require" 7 | "testing" 8 | ) 9 | 10 | func Test_RabbitMQ_Container(t *testing.T) { 11 | rabbitmqConn, err := Start(context.Background(), t) 12 | require.NoError(t, err) 13 | 14 | assert.NotNil(t, rabbitmqConn) 15 | } 16 | -------------------------------------------------------------------------------- /internal/pkg/utils/password.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | // Hash user password with bcrypt 6 | func HashPassword(password string) (string, error) { 7 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 8 | if err != nil { 9 | return *new(string), err 10 | } 11 | password = string(hashedPassword) 12 | return password, err 13 | } 14 | 15 | // Compare user password and payload 16 | func ComparePasswords(hashedPassword string, password string) (bool, error) { 17 | err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 18 | if err != nil { 19 | return false, err 20 | } 21 | return true, err 22 | } 23 | -------------------------------------------------------------------------------- /internal/pkg/utils/workers_runner.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "context" 4 | 5 | type Worker interface { 6 | Start(ctx context.Context) chan error 7 | Stop(ctx context.Context) error 8 | } 9 | 10 | type WorkersRunner struct { 11 | workers []Worker 12 | errChan chan error 13 | } 14 | 15 | func NewWorkersRunner(workers []Worker) *WorkersRunner { 16 | return &WorkersRunner{workers: workers, errChan: make(chan error)} 17 | } 18 | 19 | func (r *WorkersRunner) Start(ctx context.Context) chan error { 20 | if r.workers == nil || len(r.workers) == 0 { 21 | return nil 22 | } 23 | 24 | for _, w := range r.workers { 25 | err := w.Start(ctx) 26 | go func() { 27 | for { 28 | select { 29 | case e := <-err: 30 | r.errChan <- e 31 | return 32 | case <-ctx.Done(): 33 | stopErr := r.Stop(ctx) 34 | if stopErr != nil { 35 | r.errChan <- stopErr 36 | return 37 | } 38 | return 39 | } 40 | } 41 | }() 42 | } 43 | 44 | return r.errChan 45 | } 46 | 47 | func (r *WorkersRunner) Stop(ctx context.Context) error { 48 | if r.workers == nil || len(r.workers) == 0 { 49 | return nil 50 | } 51 | 52 | for _, w := range r.workers { 53 | err := w.Stop(ctx) 54 | if err != nil { 55 | return err 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/services/identity_service/.env: -------------------------------------------------------------------------------- 1 | APP_ENV=development -------------------------------------------------------------------------------- /internal/services/identity_service/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-playground/validator" 5 | gormpgsql "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/gorm_pgsql" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http" 8 | echoserver "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/server" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http_client" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/oauth2" 12 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel" 13 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 14 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/config" 15 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/configurations" 16 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/data/repositories" 17 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/data/seeds" 18 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/mappings" 19 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/models" 20 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/server" 21 | "go.uber.org/fx" 22 | "gorm.io/gorm" 23 | ) 24 | 25 | // @securityDefinitions.apikey ApiKeyAuth 26 | // @in header 27 | // @name Authorization 28 | func main() { 29 | fx.New( 30 | fx.Options( 31 | fx.Provide( 32 | config.InitConfig, 33 | logger.InitLogger, 34 | http.NewContext, 35 | echoserver.NewEchoServer, 36 | grpc.NewGrpcServer, 37 | gormpgsql.NewGorm, 38 | otel.TracerProvider, 39 | httpclient.NewHttpClient, 40 | repositories.NewPostgresUserRepository, 41 | rabbitmq.NewRabbitMQConn, 42 | rabbitmq.NewPublisher, 43 | validator.New, 44 | ), 45 | fx.Invoke(server.RunServers), 46 | fx.Invoke(configurations.ConfigMiddlewares), 47 | fx.Invoke(configurations.ConfigSwagger), 48 | fx.Invoke(func(gorm *gorm.DB) error { 49 | err := gormpgsql.Migrate(gorm, &models.User{}) 50 | if err != nil { 51 | return err 52 | } 53 | return seeds.DataSeeder(gorm) 54 | }), 55 | fx.Invoke(mappings.ConfigureMappings), 56 | fx.Invoke(configurations.ConfigEndpoints), 57 | fx.Invoke(configurations.ConfigUsersMediator), 58 | fx.Invoke(oauth2.RunOauthServer), 59 | ), 60 | ).Run() 61 | } 62 | -------------------------------------------------------------------------------- /internal/services/identity_service/config/config.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "serviceName": "identity_service", 3 | "deliveryType": "http", 4 | "context": { 5 | "timeout": 20 6 | }, 7 | "rabbitMq": { 8 | "user": "guest", 9 | "password": "guest", 10 | "host": "localhost", 11 | "port": 5672, 12 | "exchangeName": "identity", 13 | "kind" : "topic" 14 | }, 15 | "echo": { 16 | "port": ":5002", 17 | "development": true, 18 | "timeout": 30, 19 | "basePath": "/api/v1", 20 | "host": "http://localhost", 21 | "debugHeaders": true, 22 | "httpClientDebug": true, 23 | "debugErrorsResponse": true, 24 | "ignoreLogUrls": [ 25 | "metrics" 26 | ] 27 | }, 28 | "grpc": { 29 | "port": ":6600", 30 | "host": "localhost", 31 | "development": true 32 | }, 33 | "logger": { 34 | "level": "debug" 35 | }, 36 | "jaeger": { 37 | "server": "http://localhost:14268", 38 | "serviceName":"identity_service", 39 | "tracerName": "identity_tracer" 40 | }, 41 | "gormPostgres": { 42 | "host": "localhost", 43 | "port": 5432, 44 | "user": "postgres", 45 | "password": "postgres", 46 | "dbName": "identity_service", 47 | "sslMode": false 48 | } 49 | } -------------------------------------------------------------------------------- /internal/services/identity_service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | gormpgsql "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/gorm_pgsql" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 8 | echoserver "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/server" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 12 | "os" 13 | "path/filepath" 14 | "runtime" 15 | "strings" 16 | 17 | "github.com/pkg/errors" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | var configPath string 22 | 23 | func init() { 24 | flag.StringVar(&configPath, "config", "", "products write microservice config path") 25 | } 26 | 27 | type Config struct { 28 | ServiceName string `mapstructure:"serviceName"` 29 | Logger *logger.LoggerConfig `mapstructure:"logger"` 30 | Rabbitmq *rabbitmq.RabbitMQConfig `mapstructure:"rabbitmq"` 31 | Echo *echoserver.EchoConfig `mapstructure:"echo"` 32 | Grpc *grpc.GrpcConfig `mapstructure:"grpc"` 33 | GormPostgres *gormpgsql.GormPostgresConfig `mapstructure:"gormPostgres"` 34 | Jaeger *otel.JaegerConfig `mapstructure:"jaeger"` 35 | } 36 | 37 | type Context struct { 38 | Timeout int `mapstructure:"timeout"` 39 | } 40 | 41 | func InitConfig() (*Config, *logger.LoggerConfig, *otel.JaegerConfig, *gormpgsql.GormPostgresConfig, 42 | *grpc.GrpcConfig, *echoserver.EchoConfig, *rabbitmq.RabbitMQConfig, error) { 43 | 44 | env := os.Getenv("APP_ENV") 45 | if env == "" { 46 | env = "development" 47 | } 48 | 49 | if configPath == "" { 50 | configPathFromEnv := os.Getenv("CONFIG_PATH") 51 | if configPathFromEnv != "" { 52 | configPath = configPathFromEnv 53 | } else { 54 | //https://stackoverflow.com/questions/31873396/is-it-possible-to-get-the-current-root-of-package-structure-as-a-string-in-golan 55 | //https://stackoverflow.com/questions/18537257/how-to-get-the-directory-of-the-currently-running-file 56 | d, err := dirname() 57 | if err != nil { 58 | return nil, nil, nil, nil, nil, nil, nil, err 59 | } 60 | 61 | configPath = d 62 | } 63 | } 64 | 65 | cfg := &Config{} 66 | 67 | viper.SetConfigName(fmt.Sprintf("config.%s", env)) 68 | viper.AddConfigPath(configPath) 69 | viper.SetConfigType("json") 70 | 71 | if err := viper.ReadInConfig(); err != nil { 72 | return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "viper.ReadInConfig") 73 | } 74 | 75 | if err := viper.Unmarshal(cfg); err != nil { 76 | return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "viper.Unmarshal") 77 | } 78 | 79 | return cfg, cfg.Logger, cfg.Jaeger, cfg.GormPostgres, cfg.Grpc, cfg.Echo, cfg.Rabbitmq, nil 80 | } 81 | 82 | func GetMicroserviceName(serviceName string) string { 83 | return fmt.Sprintf("%s", strings.ToUpper(serviceName)) 84 | } 85 | 86 | func filename() (string, error) { 87 | _, filename, _, ok := runtime.Caller(0) 88 | if !ok { 89 | return "", errors.New("unable to get the current filename") 90 | } 91 | return filename, nil 92 | } 93 | 94 | func dirname() (string, error) { 95 | filename, err := filename() 96 | if err != nil { 97 | return "", err 98 | } 99 | return filepath.Dir(filename), nil 100 | } 101 | -------------------------------------------------------------------------------- /internal/services/identity_service/docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs GENERATED BY SWAG; DO NOT EDIT 2 | // This file was generated by swaggo/swag 3 | package docs 4 | 5 | import "github.com/swaggo/swag" 6 | 7 | const docTemplate = `{ 8 | "schemes": {{ marshal .Schemes }}, 9 | "swagger": "2.0", 10 | "info": { 11 | "description": "{{escape .Description}}", 12 | "title": "{{.Title}}", 13 | "contact": {}, 14 | "version": "{{.Version}}" 15 | }, 16 | "host": "{{.Host}}", 17 | "basePath": "{{.BasePath}}", 18 | "paths": { 19 | "/api/v1/users": { 20 | "post": { 21 | "security": [ 22 | { 23 | "ApiKeyAuth": [] 24 | } 25 | ], 26 | "description": "Create new user", 27 | "consumes": [ 28 | "application/json" 29 | ], 30 | "produces": [ 31 | "application/json" 32 | ], 33 | "tags": [ 34 | "Users" 35 | ], 36 | "summary": "Register user", 37 | "parameters": [ 38 | { 39 | "description": "User data", 40 | "name": "RegisterUserRequestDto", 41 | "in": "body", 42 | "required": true, 43 | "schema": { 44 | "$ref": "#/definitions/dtos.RegisterUserRequestDto" 45 | } 46 | } 47 | ], 48 | "responses": { 49 | "201": { 50 | "description": "Created", 51 | "schema": { 52 | "$ref": "#/definitions/dtos.RegisterUserResponseDto" 53 | } 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "definitions": { 60 | "dtos.RegisterUserRequestDto": { 61 | "type": "object", 62 | "properties": { 63 | "email": { 64 | "type": "string" 65 | }, 66 | "firstName": { 67 | "type": "string" 68 | }, 69 | "lastName": { 70 | "type": "string" 71 | }, 72 | "password": { 73 | "type": "string" 74 | }, 75 | "userName": { 76 | "type": "string" 77 | } 78 | } 79 | }, 80 | "dtos.RegisterUserResponseDto": { 81 | "type": "object", 82 | "properties": { 83 | "email": { 84 | "type": "string" 85 | }, 86 | "firstName": { 87 | "type": "string" 88 | }, 89 | "lastName": { 90 | "type": "string" 91 | }, 92 | "password": { 93 | "type": "string" 94 | }, 95 | "userId": { 96 | "type": "string" 97 | }, 98 | "userName": { 99 | "type": "string" 100 | } 101 | } 102 | } 103 | }, 104 | "securityDefinitions": { 105 | "ApiKeyAuth": { 106 | "type": "apiKey", 107 | "name": "Authorization", 108 | "in": "header" 109 | } 110 | } 111 | }` 112 | 113 | // SwaggerInfo holds exported Swagger Info so clients can modify it 114 | var SwaggerInfo = &swag.Spec{ 115 | Version: "", 116 | Host: "", 117 | BasePath: "", 118 | Schemes: []string{}, 119 | Title: "", 120 | Description: "", 121 | InfoInstanceName: "swagger", 122 | SwaggerTemplate: docTemplate, 123 | } 124 | 125 | func init() { 126 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 127 | } 128 | -------------------------------------------------------------------------------- /internal/services/identity_service/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "contact": {} 5 | }, 6 | "paths": { 7 | "/api/v1/users": { 8 | "post": { 9 | "security": [ 10 | { 11 | "ApiKeyAuth": [] 12 | } 13 | ], 14 | "description": "Create new user", 15 | "consumes": [ 16 | "application/json" 17 | ], 18 | "produces": [ 19 | "application/json" 20 | ], 21 | "tags": [ 22 | "Users" 23 | ], 24 | "summary": "Register user", 25 | "parameters": [ 26 | { 27 | "description": "User data", 28 | "name": "RegisterUserRequestDto", 29 | "in": "body", 30 | "required": true, 31 | "schema": { 32 | "$ref": "#/definitions/dtos.RegisterUserRequestDto" 33 | } 34 | } 35 | ], 36 | "responses": { 37 | "201": { 38 | "description": "Created", 39 | "schema": { 40 | "$ref": "#/definitions/dtos.RegisterUserResponseDto" 41 | } 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "definitions": { 48 | "dtos.RegisterUserRequestDto": { 49 | "type": "object", 50 | "properties": { 51 | "email": { 52 | "type": "string" 53 | }, 54 | "firstName": { 55 | "type": "string" 56 | }, 57 | "lastName": { 58 | "type": "string" 59 | }, 60 | "password": { 61 | "type": "string" 62 | }, 63 | "userName": { 64 | "type": "string" 65 | } 66 | } 67 | }, 68 | "dtos.RegisterUserResponseDto": { 69 | "type": "object", 70 | "properties": { 71 | "email": { 72 | "type": "string" 73 | }, 74 | "firstName": { 75 | "type": "string" 76 | }, 77 | "lastName": { 78 | "type": "string" 79 | }, 80 | "password": { 81 | "type": "string" 82 | }, 83 | "userId": { 84 | "type": "string" 85 | }, 86 | "userName": { 87 | "type": "string" 88 | } 89 | } 90 | } 91 | }, 92 | "securityDefinitions": { 93 | "ApiKeyAuth": { 94 | "type": "apiKey", 95 | "name": "Authorization", 96 | "in": "header" 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /internal/services/identity_service/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | dtos.RegisterUserRequestDto: 3 | properties: 4 | email: 5 | type: string 6 | firstName: 7 | type: string 8 | lastName: 9 | type: string 10 | password: 11 | type: string 12 | userName: 13 | type: string 14 | type: object 15 | dtos.RegisterUserResponseDto: 16 | properties: 17 | email: 18 | type: string 19 | firstName: 20 | type: string 21 | lastName: 22 | type: string 23 | password: 24 | type: string 25 | userId: 26 | type: string 27 | userName: 28 | type: string 29 | type: object 30 | info: 31 | contact: {} 32 | paths: 33 | /api/v1/users: 34 | post: 35 | consumes: 36 | - application/json 37 | description: Create new user 38 | parameters: 39 | - description: User data 40 | in: body 41 | name: RegisterUserRequestDto 42 | required: true 43 | schema: 44 | $ref: '#/definitions/dtos.RegisterUserRequestDto' 45 | produces: 46 | - application/json 47 | responses: 48 | "201": 49 | description: Created 50 | schema: 51 | $ref: '#/definitions/dtos.RegisterUserResponseDto' 52 | security: 53 | - ApiKeyAuth: [] 54 | summary: Register user 55 | tags: 56 | - Users 57 | securityDefinitions: 58 | ApiKeyAuth: 59 | in: header 60 | name: Authorization 61 | type: apiKey 62 | swagger: "2.0" 63 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/configurations/endpoint_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator" 6 | "github.com/labstack/echo/v4" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | endpointsv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/features/registering_user/v1/endpoints" 9 | ) 10 | 11 | func ConfigEndpoints(validator *validator.Validate, log logger.ILogger, echo *echo.Echo, ctx context.Context) { 12 | 13 | endpointsv1.MapRoute(validator, log, echo, ctx) 14 | } 15 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/configurations/grpc_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/config" 7 | identityservice "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/grpc_server/protos" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/grpc_server/services" 9 | ) 10 | 11 | func ConfigIdentityGrpcServer(cfg *config.Config, log logger.ILogger, grpcServer *grpc.GrpcServer) { 12 | 13 | identityGrpcService := services.NewIdentityGrpcServerService(cfg, log) 14 | 15 | identityservice.RegisterIdentityServiceServer(grpcServer.Grpc, identityGrpcService) 16 | } 17 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/configurations/mediator_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "context" 5 | "github.com/mehdihadeli/go-mediatr" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/data/contracts" 9 | registeringuserv1commands "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/features/registering_user/v1/commands" 10 | registeringuserv1dtos "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/features/registering_user/v1/dtos" 11 | ) 12 | 13 | func ConfigUsersMediator(log logger.ILogger, rabbitmqPublisher rabbitmq.IPublisher, 14 | userRepository contracts.UserRepository, ctx context.Context) error { 15 | 16 | //https://stackoverflow.com/questions/72034479/how-to-implement-generic-interfaces 17 | err := mediatr.RegisterRequestHandler[*registeringuserv1commands.RegisterUser, *registeringuserv1dtos.RegisterUserResponseDto](registeringuserv1commands.NewRegisterUserHandler(log, rabbitmqPublisher, userRepository, ctx)) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/configurations/middleware_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/labstack/echo/v4/middleware" 6 | echomiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/middleware" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel" 8 | otelmiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel/middleware" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/constants" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/middlewares" 11 | "strings" 12 | ) 13 | 14 | func ConfigMiddlewares(e *echo.Echo, jaegerCfg *otel.JaegerConfig) { 15 | 16 | e.HideBanner = false 17 | 18 | e.Use(middleware.Logger()) 19 | e.HTTPErrorHandler = middlewares.ProblemDetailsHandler 20 | e.Use(otelmiddleware.EchoTracerMiddleware(jaegerCfg.ServiceName)) 21 | 22 | e.Use(echomiddleware.CorrelationIdMiddleware) 23 | e.Use(middleware.RequestID()) 24 | e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 25 | Level: constants.GzipLevel, 26 | Skipper: func(c echo.Context) bool { 27 | return strings.Contains(c.Request().URL.Path, "swagger") 28 | }, 29 | })) 30 | 31 | e.Use(middleware.BodyLimit(constants.BodyLimit)) 32 | } 33 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/configurations/swagger_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/docs" 6 | echoSwagger "github.com/swaggo/echo-swagger" 7 | ) 8 | 9 | func ConfigSwagger(e *echo.Echo) { 10 | 11 | docs.SwaggerInfo.Version = "1.0" 12 | docs.SwaggerInfo.Title = "Identities Service Api" 13 | docs.SwaggerInfo.Description = "Identities Service Api" 14 | e.GET("/swagger/*", echoSwagger.WrapHandler) 15 | } 16 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | ConfigPath = "CONFIG_PATH" 5 | Json = "json" 6 | BodyLimit = "2M" 7 | GzipLevel = 5 8 | ) 9 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/data/contracts/user_repository.go: -------------------------------------------------------------------------------- 1 | package contracts 2 | 3 | import ( 4 | "context" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/models" 6 | ) 7 | 8 | type UserRepository interface { 9 | RegisterUser(ctx context.Context, user *models.User) (*models.User, error) 10 | } 11 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/data/repositories/pg_user_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "github.com/jackc/pgx/v4/pgxpool" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/config" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/data/contracts" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/models" 10 | "github.com/pkg/errors" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | type PostgresUserRepository struct { 15 | log logger.ILogger 16 | cfg *config.Config 17 | db *pgxpool.Pool 18 | gorm *gorm.DB 19 | } 20 | 21 | func NewPostgresUserRepository(log logger.ILogger, cfg *config.Config, gorm *gorm.DB) contracts.UserRepository { 22 | return PostgresUserRepository{log: log, cfg: cfg, gorm: gorm} 23 | } 24 | 25 | func (p PostgresUserRepository) RegisterUser(ctx context.Context, user *models.User) (*models.User, error) { 26 | 27 | if err := p.gorm.Create(&user).Error; err != nil { 28 | return nil, errors.Wrap(err, "error in the inserting user into the database.") 29 | } 30 | 31 | return user, nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/data/seeds/data_seeder.go: -------------------------------------------------------------------------------- 1 | package seeds 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/models" 6 | "github.com/pkg/errors" 7 | uuid "github.com/satori/go.uuid" 8 | "gorm.io/gorm" 9 | "time" 10 | ) 11 | 12 | func DataSeeder(gorm *gorm.DB) error { 13 | return seedUser(gorm) 14 | } 15 | 16 | func seedUser(gorm *gorm.DB) error { 17 | if (gorm.Find(&models.User{}).RowsAffected <= 0) { 18 | pass, err := utils.HashPassword("Admin@12345") 19 | if err != nil { 20 | return err 21 | } 22 | user := &models.User{UserId: uuid.NewV4(), UserName: "admin_user", FirstName: "admin", LastName: "admin", CreatedAt: time.Now(), Email: "admin@admin.com", Password: pass} 23 | 24 | if err := gorm.Create(user).Error; err != nil { 25 | return errors.Wrap(err, "error in the inserting user into the database.") 26 | } 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/features/registering_user/v1/commands/register_user.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type RegisterUser struct { 8 | FirstName string `json:"firstName" validate:"required"` 9 | LastName string `json:"lastName" validate:"required"` 10 | UserName string `json:"userName" validate:"required"` 11 | Email string `json:"email" validate:"required,email"` 12 | Password string `json:"password" validate:"required,min=4"` 13 | CreatedAt time.Time `validate:"required"` 14 | } 15 | 16 | func NewRegisterUser(firstName string, lastName string, userName string, email string, password string) *RegisterUser { 17 | return &RegisterUser{FirstName: firstName, LastName: lastName, UserName: userName, Email: email, Password: password, CreatedAt: time.Now()} 18 | } 19 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/features/registering_user/v1/commands/register_user_handler.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/mapper" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/data/contracts" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/features/registering_user/v1/dtos" 12 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/models" 13 | ) 14 | 15 | type RegisterUserHandler struct { 16 | log logger.ILogger 17 | rabbitmqPublisher rabbitmq.IPublisher 18 | userRepository contracts.UserRepository 19 | ctx context.Context 20 | } 21 | 22 | func NewRegisterUserHandler(log logger.ILogger, rabbitmqPublisher rabbitmq.IPublisher, 23 | userRepository contracts.UserRepository, ctx context.Context) *RegisterUserHandler { 24 | return &RegisterUserHandler{log: log, userRepository: userRepository, ctx: ctx, rabbitmqPublisher: rabbitmqPublisher} 25 | } 26 | 27 | func (c *RegisterUserHandler) Handle(ctx context.Context, command *RegisterUser) (*dtos.RegisterUserResponseDto, error) { 28 | 29 | pass, err := utils.HashPassword(command.Password) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | product := &models.User{ 35 | Email: command.Email, 36 | Password: pass, 37 | UserName: command.UserName, 38 | LastName: command.LastName, 39 | FirstName: command.FirstName, 40 | CreatedAt: command.CreatedAt, 41 | } 42 | 43 | registeredUser, err := c.userRepository.RegisterUser(ctx, product) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | response, err := mapper.Map[*dtos.RegisterUserResponseDto](registeredUser) 49 | if err != nil { 50 | return nil, err 51 | } 52 | bytes, _ := json.Marshal(response) 53 | 54 | c.log.Info("RegisterUserResponseDto", string(bytes)) 55 | 56 | return response, nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/features/registering_user/v1/dtos/register_user_request_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | type RegisterUserRequestDto struct { 4 | FirstName string `json:"firstName"` 5 | LastName string `json:"lastName"` 6 | UserName string `json:"userName"` 7 | Email string `json:"email"` 8 | Password string `json:"password"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/features/registering_user/v1/dtos/register_user_response_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type RegisterUserResponseDto struct { 6 | UserId uuid.UUID `json:"userId"` 7 | FirstName string `json:"firstName"` 8 | LastName string `json:"lastName"` 9 | UserName string `json:"userName"` 10 | Email string `json:"email"` 11 | Password string `json:"password"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/features/registering_user/v1/endpoints/register_user_endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator" 6 | "github.com/labstack/echo/v4" 7 | "github.com/mehdihadeli/go-mediatr" 8 | echomiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/middleware" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | commandsv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/features/registering_user/v1/commands" 11 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/features/registering_user/v1/dtos" 12 | "github.com/pkg/errors" 13 | "net/http" 14 | ) 15 | 16 | func MapRoute(validator *validator.Validate, log logger.ILogger, echo *echo.Echo, ctx context.Context) { 17 | group := echo.Group("/api/v1/users") 18 | group.POST("", createUser(validator, log, ctx), echomiddleware.ValidateBearerToken()) 19 | } 20 | 21 | // RegisterUser 22 | // @Tags Users 23 | // @Summary Register user 24 | // @Description Create new user 25 | // @Accept json 26 | // @Produce json 27 | // @Param RegisterUserRequestDto body dtos.RegisterUserRequestDto true "User data" 28 | // @Success 201 {object} dtos.RegisterUserResponseDto 29 | // @Security ApiKeyAuth 30 | // @Router /api/v1/users [post] 31 | func createUser(validator *validator.Validate, log logger.ILogger, ctx context.Context) echo.HandlerFunc { 32 | return func(c echo.Context) error { 33 | 34 | request := &dtosv1.RegisterUserRequestDto{} 35 | 36 | if err := c.Bind(request); err != nil { 37 | badRequestErr := errors.Wrap(err, "[registerUserEndpoint_handler.Bind] error in the binding request") 38 | log.Error(badRequestErr) 39 | return echo.NewHTTPError(http.StatusBadRequest, err) 40 | } 41 | 42 | command := commandsv1.NewRegisterUser(request.FirstName, request.LastName, request.UserName, request.Email, request.Password) 43 | 44 | if err := validator.StructCtx(ctx, command); err != nil { 45 | validationErr := errors.Wrap(err, "[registerUserEndpoint_handler.StructCtx] command validation failed") 46 | log.Error(validationErr) 47 | return echo.NewHTTPError(http.StatusBadRequest, err) 48 | } 49 | 50 | result, err := mediatr.Send[*commandsv1.RegisterUser, *dtosv1.RegisterUserResponseDto](ctx, command) 51 | 52 | if err != nil { 53 | log.Errorf("(RegisterUser.Handle) id: {%d}, err: {%v}", result.UserId, err) 54 | return echo.NewHTTPError(http.StatusBadRequest, err) 55 | } 56 | 57 | log.Infof("(user registered) id: {%d}", result.UserId) 58 | return c.JSON(http.StatusCreated, result) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/grpc_server/protos/identity_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package identity_service; 4 | 5 | option go_package = "./;identity_service"; 6 | 7 | service IdentityService { 8 | rpc GetUserById(GetUserByIdReq) returns (GetUserByIdRes); 9 | } 10 | 11 | message User { 12 | string UserId = 1; 13 | string Name = 2; 14 | } 15 | 16 | message GetUserByIdReq { 17 | string UserId = 1; 18 | } 19 | 20 | message GetUserByIdRes { 21 | User User = 1; 22 | } 23 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/grpc_server/services/identity_grpc_server_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/config" 7 | identity_service "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/grpc_server/protos" 8 | ) 9 | 10 | type IdentityGrpcServerService struct { 11 | cfg *config.Config 12 | log logger.ILogger 13 | } 14 | 15 | func NewIdentityGrpcServerService(cfg *config.Config, log logger.ILogger) *IdentityGrpcServerService { 16 | return &IdentityGrpcServerService{log: log, cfg: cfg} 17 | } 18 | 19 | func (i IdentityGrpcServerService) GetUserById(ctx context.Context, req *identity_service.GetUserByIdReq) (*identity_service.GetUserByIdRes, error) { 20 | 21 | var user = &identity_service.User{UserId: req.UserId} 22 | 23 | var result = &identity_service.GetUserByIdRes{ 24 | User: user, 25 | } 26 | 27 | return result, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/mappings/mapping_profile.go: -------------------------------------------------------------------------------- 1 | package mappings 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/mapper" 5 | registeringuserdtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/features/registering_user/v1/dtos" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/identity/models" 7 | ) 8 | 9 | func ConfigureMappings() error { 10 | err := mapper.CreateMap[*models.User, *registeringuserdtosv1.RegisterUserResponseDto]() 11 | if err != nil { 12 | return err 13 | } 14 | return err 15 | } 16 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/middlewares/problem_details_handler.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/meysamhadeli/problem-details" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func ProblemDetailsHandler(error error, c echo.Context) { 10 | if !c.Response().Committed { 11 | if _, err := problem.ResolveProblemDetails(c.Response(), c.Request(), error); err != nil { 12 | log.Error(err) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/services/identity_service/identity/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | uuid "github.com/satori/go.uuid" 5 | "time" 6 | ) 7 | 8 | // User model 9 | type User struct { 10 | UserId uuid.UUID `json:"userId" gorm:"primaryKey"` 11 | FirstName string `json:"firstName"` 12 | LastName string `json:"lastName"` 13 | UserName string `json:"userName"` 14 | Email string `json:"email"` 15 | Password string `json:"password"` 16 | CreatedAt time.Time `json:"createdAt"` 17 | UpdatedAt time.Time `json:"updatedAt"` 18 | } 19 | -------------------------------------------------------------------------------- /internal/services/identity_service/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "github.com/labstack/echo/v4" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 7 | echoserver "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/server" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/identity_service/config" 10 | "github.com/pkg/errors" 11 | "go.uber.org/fx" 12 | "net/http" 13 | ) 14 | 15 | func RunServers(lc fx.Lifecycle, log logger.ILogger, e *echo.Echo, grpcServer *grpc.GrpcServer, ctx context.Context, cfg *config.Config) error { 16 | 17 | lc.Append(fx.Hook{ 18 | OnStart: func(_ context.Context) error { 19 | go func() { 20 | if err := echoserver.RunHttpServer(ctx, e, log, cfg.Echo); !errors.Is(err, http.ErrServerClosed) { 21 | log.Fatalf("error running http server: %v", err) 22 | } 23 | }() 24 | 25 | go func() { 26 | if err := grpcServer.RunGrpcServer(ctx); !errors.Is(err, http.ErrServerClosed) { 27 | log.Fatalf("error running grpc server: %v", err) 28 | } 29 | }() 30 | 31 | e.GET("/", func(c echo.Context) error { 32 | return c.String(http.StatusOK, config.GetMicroserviceName(cfg.ServiceName)) 33 | }) 34 | 35 | return nil 36 | }, 37 | OnStop: func(_ context.Context) error { 38 | log.Infof("all servers shutdown gracefully...") 39 | return nil 40 | }, 41 | }) 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/services/inventory_service/.env: -------------------------------------------------------------------------------- 1 | APP_ENV=development -------------------------------------------------------------------------------- /internal/services/inventory_service/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-playground/validator" 5 | gormpgsql "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/gorm_pgsql" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http" 7 | echoserver "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/server" 8 | httpclient "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http_client" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 12 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/config" 13 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/configurations" 14 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/data" 15 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/data/repositories" 16 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/mappings" 17 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/models" 18 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/server" 19 | "go.uber.org/fx" 20 | "gorm.io/gorm" 21 | ) 22 | 23 | // @securityDefinitions.apikey ApiKeyAuth 24 | // @in header 25 | // @name Authorization 26 | func main() { 27 | fx.New( 28 | fx.Options( 29 | fx.Provide( 30 | config.InitConfig, 31 | logger.InitLogger, 32 | http.NewContext, 33 | echoserver.NewEchoServer, 34 | gormpgsql.NewGorm, 35 | otel.TracerProvider, 36 | httpclient.NewHttpClient, 37 | repositories.NewPostgresInventoryRepository, 38 | rabbitmq.NewRabbitMQConn, 39 | rabbitmq.NewPublisher, 40 | validator.New, 41 | ), 42 | fx.Invoke(server.RunServers), 43 | fx.Invoke(configurations.ConfigMiddlewares), 44 | fx.Invoke(configurations.ConfigSwagger), 45 | fx.Invoke(func(gorm *gorm.DB) error { 46 | 47 | err := gormpgsql.Migrate(gorm, &models.Inventory{}, &models.ProductItem{}) 48 | if err != nil { 49 | return err 50 | } 51 | return data.SeedData(gorm) 52 | }), 53 | fx.Invoke(mappings.ConfigureMappings), 54 | fx.Invoke(configurations.ConfigEndpoints), 55 | fx.Invoke(configurations.ConfigProductsMediator), 56 | fx.Invoke(configurations.ConfigConsumers), 57 | ), 58 | ).Run() 59 | } 60 | -------------------------------------------------------------------------------- /internal/services/inventory_service/config/config.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "serviceName": "inventory_service", 3 | "deliveryType": "http", 4 | "context": { 5 | "timeout": 20 6 | }, 7 | "rabbitMq": { 8 | "user": "guest", 9 | "password": "guest", 10 | "host": "localhost", 11 | "port": 5672, 12 | "exchangeName": "inventory", 13 | "kind" : "topic" 14 | }, 15 | "echo": { 16 | "port": ":5004", 17 | "development": true, 18 | "timeout": 30, 19 | "basePath": "/api/v1", 20 | "host": "http://localhost", 21 | "debugHeaders": true, 22 | "httpClientDebug": true, 23 | "debugErrorsResponse": true, 24 | "ignoreLogUrls": [ 25 | "metrics" 26 | ] 27 | }, 28 | "logger": { 29 | "level": "debug" 30 | }, 31 | "jaeger": { 32 | "server": "http://localhost:14268", 33 | "serviceName":"inventory_service", 34 | "tracerName": "inventory_tracer" 35 | }, 36 | "gormPostgres": { 37 | "host": "localhost", 38 | "port": 5432, 39 | "user": "postgres", 40 | "password": "postgres", 41 | "dbName": "inventory_service", 42 | "sslMode": false 43 | } 44 | } -------------------------------------------------------------------------------- /internal/services/inventory_service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | gormpgsql "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/gorm_pgsql" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 8 | echoserver "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/server" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 12 | "github.com/pkg/errors" 13 | "github.com/spf13/viper" 14 | "os" 15 | "path/filepath" 16 | "runtime" 17 | "strings" 18 | ) 19 | 20 | var configPath string 21 | 22 | type Config struct { 23 | ServiceName string `mapstructure:"serviceName"` 24 | Logger *logger.LoggerConfig `mapstructure:"logger"` 25 | Rabbitmq *rabbitmq.RabbitMQConfig `mapstructure:"rabbitmq"` 26 | Echo *echoserver.EchoConfig `mapstructure:"echo"` 27 | Grpc *grpc.GrpcConfig `mapstructure:"grpc"` 28 | GormPostgres *gormpgsql.GormPostgresConfig `mapstructure:"gormPostgres"` 29 | Jaeger *otel.JaegerConfig `mapstructure:"jaeger"` 30 | } 31 | 32 | func init() { 33 | flag.StringVar(&configPath, "config", "", "products write microservice config path") 34 | } 35 | 36 | func InitConfig() (*Config, *logger.LoggerConfig, *otel.JaegerConfig, *gormpgsql.GormPostgresConfig, 37 | *grpc.GrpcConfig, *echoserver.EchoConfig, *rabbitmq.RabbitMQConfig, error) { 38 | 39 | env := os.Getenv("APP_ENV") 40 | if env == "" { 41 | env = "development" 42 | } 43 | 44 | if configPath == "" { 45 | configPathFromEnv := os.Getenv("CONFIG_PATH") 46 | if configPathFromEnv != "" { 47 | configPath = configPathFromEnv 48 | } else { 49 | //https://stackoverflow.com/questions/31873396/is-it-possible-to-get-the-current-root-of-package-structure-as-a-string-in-golan 50 | //https://stackoverflow.com/questions/18537257/how-to-get-the-directory-of-the-currently-running-file 51 | d, err := dirname() 52 | if err != nil { 53 | return nil, nil, nil, nil, nil, nil, nil, err 54 | } 55 | 56 | configPath = d 57 | } 58 | } 59 | 60 | cfg := &Config{} 61 | 62 | viper.SetConfigName(fmt.Sprintf("config.%s", env)) 63 | viper.AddConfigPath(configPath) 64 | viper.SetConfigType("json") 65 | 66 | if err := viper.ReadInConfig(); err != nil { 67 | return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "viper.ReadInConfig") 68 | } 69 | 70 | if err := viper.Unmarshal(cfg); err != nil { 71 | return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "viper.Unmarshal") 72 | } 73 | 74 | return cfg, cfg.Logger, cfg.Jaeger, cfg.GormPostgres, cfg.Grpc, cfg.Echo, cfg.Rabbitmq, nil 75 | } 76 | 77 | func GetMicroserviceName(serviceName string) string { 78 | return fmt.Sprintf("%s", strings.ToUpper(serviceName)) 79 | } 80 | 81 | func filename() (string, error) { 82 | _, filename, _, ok := runtime.Caller(0) 83 | if !ok { 84 | return "", errors.New("unable to get the current filename") 85 | } 86 | return filename, nil 87 | } 88 | 89 | func dirname() (string, error) { 90 | filename, err := filename() 91 | if err != nil { 92 | return "", err 93 | } 94 | return filepath.Dir(filename), nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/services/inventory_service/docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs GENERATED BY SWAG; DO NOT EDIT 2 | // This file was generated by swaggo/swag 3 | package docs 4 | 5 | import "github.com/swaggo/swag" 6 | 7 | const docTemplate = `{ 8 | "schemes": {{ marshal .Schemes }}, 9 | "swagger": "2.0", 10 | "info": { 11 | "description": "{{escape .Description}}", 12 | "title": "{{.Title}}", 13 | "contact": {}, 14 | "version": "{{.Version}}" 15 | }, 16 | "host": "{{.Host}}", 17 | "basePath": "{{.BasePath}}", 18 | "paths": { 19 | "/api/v1/users": { 20 | "post": { 21 | "security": [ 22 | { 23 | "ApiKeyAuth": [] 24 | } 25 | ], 26 | "description": "Create new user", 27 | "consumes": [ 28 | "application/json" 29 | ], 30 | "produces": [ 31 | "application/json" 32 | ], 33 | "tags": [ 34 | "Users" 35 | ], 36 | "summary": "Register user", 37 | "parameters": [ 38 | { 39 | "description": "User data", 40 | "name": "RegisterUserRequestDto", 41 | "in": "body", 42 | "required": true, 43 | "schema": { 44 | "$ref": "#/definitions/dtos.RegisterUserRequestDto" 45 | } 46 | } 47 | ], 48 | "responses": { 49 | "201": { 50 | "description": "Created", 51 | "schema": { 52 | "$ref": "#/definitions/dtos.RegisterUserResponseDto" 53 | } 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "definitions": { 60 | "dtos.RegisterUserRequestDto": { 61 | "type": "object", 62 | "properties": { 63 | "email": { 64 | "type": "string" 65 | }, 66 | "firstName": { 67 | "type": "string" 68 | }, 69 | "lastName": { 70 | "type": "string" 71 | }, 72 | "password": { 73 | "type": "string" 74 | }, 75 | "userName": { 76 | "type": "string" 77 | } 78 | } 79 | }, 80 | "dtos.RegisterUserResponseDto": { 81 | "type": "object", 82 | "properties": { 83 | "email": { 84 | "type": "string" 85 | }, 86 | "firstName": { 87 | "type": "string" 88 | }, 89 | "lastName": { 90 | "type": "string" 91 | }, 92 | "password": { 93 | "type": "string" 94 | }, 95 | "userId": { 96 | "type": "string" 97 | }, 98 | "userName": { 99 | "type": "string" 100 | } 101 | } 102 | } 103 | }, 104 | "securityDefinitions": { 105 | "ApiKeyAuth": { 106 | "type": "apiKey", 107 | "name": "Authorization", 108 | "in": "header" 109 | } 110 | } 111 | }` 112 | 113 | // SwaggerInfo holds exported Swagger Info so clients can modify it 114 | var SwaggerInfo = &swag.Spec{ 115 | Version: "", 116 | Host: "", 117 | BasePath: "", 118 | Schemes: []string{}, 119 | Title: "", 120 | Description: "", 121 | InfoInstanceName: "swagger", 122 | SwaggerTemplate: docTemplate, 123 | } 124 | 125 | func init() { 126 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 127 | } 128 | -------------------------------------------------------------------------------- /internal/services/inventory_service/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "contact": {} 5 | }, 6 | "paths": { 7 | "/api/v1/users": { 8 | "post": { 9 | "security": [ 10 | { 11 | "ApiKeyAuth": [] 12 | } 13 | ], 14 | "description": "Create new user", 15 | "consumes": [ 16 | "application/json" 17 | ], 18 | "produces": [ 19 | "application/json" 20 | ], 21 | "tags": [ 22 | "Users" 23 | ], 24 | "summary": "Register user", 25 | "parameters": [ 26 | { 27 | "description": "User data", 28 | "name": "RegisterUserRequestDto", 29 | "in": "body", 30 | "required": true, 31 | "schema": { 32 | "$ref": "#/definitions/dtos.RegisterUserRequestDto" 33 | } 34 | } 35 | ], 36 | "responses": { 37 | "201": { 38 | "description": "Created", 39 | "schema": { 40 | "$ref": "#/definitions/dtos.RegisterUserResponseDto" 41 | } 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "definitions": { 48 | "dtos.RegisterUserRequestDto": { 49 | "type": "object", 50 | "properties": { 51 | "email": { 52 | "type": "string" 53 | }, 54 | "firstName": { 55 | "type": "string" 56 | }, 57 | "lastName": { 58 | "type": "string" 59 | }, 60 | "password": { 61 | "type": "string" 62 | }, 63 | "userName": { 64 | "type": "string" 65 | } 66 | } 67 | }, 68 | "dtos.RegisterUserResponseDto": { 69 | "type": "object", 70 | "properties": { 71 | "email": { 72 | "type": "string" 73 | }, 74 | "firstName": { 75 | "type": "string" 76 | }, 77 | "lastName": { 78 | "type": "string" 79 | }, 80 | "password": { 81 | "type": "string" 82 | }, 83 | "userId": { 84 | "type": "string" 85 | }, 86 | "userName": { 87 | "type": "string" 88 | } 89 | } 90 | } 91 | }, 92 | "securityDefinitions": { 93 | "ApiKeyAuth": { 94 | "type": "apiKey", 95 | "name": "Authorization", 96 | "in": "header" 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /internal/services/inventory_service/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | dtos.RegisterUserRequestDto: 3 | properties: 4 | email: 5 | type: string 6 | firstName: 7 | type: string 8 | lastName: 9 | type: string 10 | password: 11 | type: string 12 | userName: 13 | type: string 14 | type: object 15 | dtos.RegisterUserResponseDto: 16 | properties: 17 | email: 18 | type: string 19 | firstName: 20 | type: string 21 | lastName: 22 | type: string 23 | password: 24 | type: string 25 | userId: 26 | type: string 27 | userName: 28 | type: string 29 | type: object 30 | info: 31 | contact: {} 32 | paths: 33 | /api/v1/users: 34 | post: 35 | consumes: 36 | - application/json 37 | description: Create new user 38 | parameters: 39 | - description: User data 40 | in: body 41 | name: RegisterUserRequestDto 42 | required: true 43 | schema: 44 | $ref: '#/definitions/dtos.RegisterUserRequestDto' 45 | produces: 46 | - application/json 47 | responses: 48 | "201": 49 | description: Created 50 | schema: 51 | $ref: '#/definitions/dtos.RegisterUserResponseDto' 52 | security: 53 | - ApiKeyAuth: [] 54 | summary: Register user 55 | tags: 56 | - Users 57 | securityDefinitions: 58 | ApiKeyAuth: 59 | in: header 60 | name: Authorization 61 | type: apiKey 62 | swagger: "2.0" 63 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/configurations/consumrs_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "context" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/config" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/consumers/events" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/consumers/handlers" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/data/contracts" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/shared/delivery" 12 | "github.com/streadway/amqp" 13 | "go.opentelemetry.io/otel/trace" 14 | ) 15 | 16 | func ConfigConsumers( 17 | ctx context.Context, 18 | jaegerTracer trace.Tracer, 19 | log logger.ILogger, 20 | connRabbitmq *amqp.Connection, 21 | rabbitmqPublisher rabbitmq.IPublisher, 22 | inventoryRepository contracts.InventoryRepository, 23 | cfg *config.Config) error { 24 | 25 | inventoryDeliveryBase := delivery.InventoryDeliveryBase{ 26 | Log: log, 27 | Cfg: cfg, 28 | JaegerTracer: jaegerTracer, 29 | ConnRabbitmq: connRabbitmq, 30 | RabbitmqPublisher: rabbitmqPublisher, 31 | InventoryRepository: inventoryRepository, 32 | Ctx: ctx, 33 | } 34 | 35 | createProductConsumer := rabbitmq.NewConsumer[*delivery.InventoryDeliveryBase](ctx, cfg.Rabbitmq, connRabbitmq, log, jaegerTracer, handlers.HandleConsumeCreateProduct) 36 | 37 | go func() { 38 | err := createProductConsumer.ConsumeMessage(events.ProductCreated{}, &inventoryDeliveryBase) 39 | if err != nil { 40 | log.Error(err) 41 | } 42 | }() 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/configurations/endpoint_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator" 6 | "github.com/labstack/echo/v4" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | ) 9 | 10 | func ConfigEndpoints(validator *validator.Validate, log logger.ILogger, echo *echo.Echo, ctx context.Context) { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/configurations/mediator_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "context" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 7 | contracts "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/data/contracts" 8 | ) 9 | 10 | func ConfigProductsMediator(log logger.ILogger, rabbitmqPublisher rabbitmq.IPublisher, 11 | inventoryRepository contracts.InventoryRepository, ctx context.Context) error { 12 | 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/configurations/middleware_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/labstack/echo/v4/middleware" 6 | echomiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/middleware" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel" 8 | otelmiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel/middleware" 9 | constants "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/constans" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/middlewares" 11 | "strings" 12 | ) 13 | 14 | func ConfigMiddlewares(e *echo.Echo, jaegerCfg *otel.JaegerConfig) { 15 | 16 | e.HideBanner = false 17 | 18 | e.Use(middleware.Logger()) 19 | 20 | e.HTTPErrorHandler = middlewares.ProblemDetailsHandler 21 | e.Use(otelmiddleware.EchoTracerMiddleware(jaegerCfg.ServiceName)) 22 | 23 | e.Use(echomiddleware.CorrelationIdMiddleware) 24 | e.Use(middleware.RequestID()) 25 | e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 26 | Level: constants.GzipLevel, 27 | Skipper: func(c echo.Context) bool { 28 | return strings.Contains(c.Request().URL.Path, "swagger") 29 | }, 30 | })) 31 | 32 | e.Use(middleware.BodyLimit(constants.BodyLimit)) 33 | } 34 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/configurations/swagger_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | echoSwagger "github.com/swaggo/echo-swagger" 6 | ) 7 | 8 | func ConfigSwagger(e *echo.Echo) { 9 | 10 | //docs.SwaggerInfo.Version = "1.0" 11 | //docs.SwaggerInfo.Title = "Products Service Api" 12 | //docs.SwaggerInfo.Description = "Products Service Api" 13 | e.GET("/swagger/*", echoSwagger.WrapHandler) 14 | } 15 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/constans/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | ConfigPath = "CONFIG_PATH" 5 | Json = "json" 6 | BodyLimit = "2M" 7 | GzipLevel = 5 8 | Test = "test" 9 | ) 10 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/consumers/events/inventory_updated.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type InventoryUpdated struct { 6 | ProductId uuid.UUID 7 | InventoryId int64 8 | Count int32 9 | } 10 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/consumers/events/product_created.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type ProductCreated struct { 6 | ProductId uuid.UUID 7 | InventoryId int64 8 | Count int32 9 | } 10 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/consumers/handlers/consume_create_product_handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/mapper" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/consumers/events" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/models" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/shared/delivery" 9 | uuid "github.com/satori/go.uuid" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/streadway/amqp" 12 | ) 13 | 14 | func HandleConsumeCreateProduct(queue string, msg amqp.Delivery, inventoryDeliveryBase *delivery.InventoryDeliveryBase) error { 15 | 16 | log.Infof("Message received on queue: %s with message: %s", queue, string(msg.Body)) 17 | 18 | var productCreated events.ProductCreated 19 | 20 | err := json.Unmarshal(msg.Body, &productCreated) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | count := productCreated.Count 26 | 27 | productItem, _ := inventoryDeliveryBase.InventoryRepository.GetProductInInventories(inventoryDeliveryBase.Ctx, productCreated.ProductId) 28 | 29 | if productItem != nil { 30 | count = productItem.Count + count 31 | } 32 | 33 | p, err := inventoryDeliveryBase.InventoryRepository.AddProductItemToInventory(inventoryDeliveryBase.Ctx, &models.ProductItem{ 34 | Id: uuid.NewV4(), 35 | ProductId: productCreated.ProductId, 36 | Count: count, 37 | InventoryId: productCreated.InventoryId, 38 | }) 39 | 40 | evt, err := mapper.Map[*events.InventoryUpdated](p) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | err = inventoryDeliveryBase.RabbitmqPublisher.PublishMessage(evt) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/data/contracts/inventory_repository.go: -------------------------------------------------------------------------------- 1 | package contracts 2 | 3 | import ( 4 | "context" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/models" 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | type InventoryRepository interface { 10 | AddProductItemToInventory(ctx context.Context, inventory *models.ProductItem) (*models.ProductItem, error) 11 | GetProductInInventories(ctx context.Context, productId uuid.UUID) (*models.ProductItem, error) 12 | } 13 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/data/inventories_pg_seed.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/models" 5 | "github.com/pkg/errors" 6 | "gorm.io/gorm" 7 | "time" 8 | ) 9 | 10 | func SeedData(gorm *gorm.DB) error { 11 | 12 | var rowsAffected = gorm.First(&models.Inventory{}).RowsAffected 13 | 14 | if rowsAffected == 0 { 15 | err := gorm.CreateInBatches(inventoriesSeeds, len(inventoriesSeeds)).Error 16 | if err != nil { 17 | return errors.Wrap(err, "error in seed inventories database") 18 | } 19 | } 20 | 21 | return nil 22 | } 23 | 24 | var inventoriesSeeds = []*models.Inventory{ 25 | { 26 | Id: 1, 27 | Name: "food", 28 | Description: "some food inventories data", 29 | CreatedAt: time.Now(), 30 | }, 31 | { 32 | Id: 2, 33 | Name: "health", 34 | Description: "some health inventories data", 35 | CreatedAt: time.Now(), 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/data/repositories/pg_inventory_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/jackc/pgx/v4/pgxpool" 7 | gormpgsql "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/gorm_pgsql" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 9 | contracts "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/data/contracts" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/models" 11 | "github.com/pkg/errors" 12 | uuid "github.com/satori/go.uuid" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | type PostgresInventoryRepository struct { 17 | log logger.ILogger 18 | cfg *gormpgsql.GormPostgresConfig 19 | db *pgxpool.Pool 20 | gorm *gorm.DB 21 | } 22 | 23 | func NewPostgresInventoryRepository(log logger.ILogger, cfg *gormpgsql.GormPostgresConfig, gorm *gorm.DB) contracts.InventoryRepository { 24 | return &PostgresInventoryRepository{log: log, cfg: cfg, gorm: gorm} 25 | } 26 | 27 | func (p *PostgresInventoryRepository) AddProductItemToInventory(ctx context.Context, productItem *models.ProductItem) (*models.ProductItem, error) { 28 | 29 | if err := p.gorm.Create(&productItem).Error; err != nil { 30 | return nil, errors.Wrap(err, "error in the inserting product into the database.") 31 | } 32 | 33 | return productItem, nil 34 | } 35 | 36 | func (p *PostgresInventoryRepository) GetProductInInventories(ctx context.Context, uuid uuid.UUID) (*models.ProductItem, error) { 37 | var productItem models.ProductItem 38 | 39 | if err := p.gorm.First(&productItem, uuid).Error; err != nil { 40 | return nil, errors.Wrap(err, fmt.Sprintf("can't find the product item with id %s into the database.", uuid)) 41 | } 42 | 43 | return &productItem, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/dtos/inventory_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type InventoryDto struct { 8 | Id int64 `json:"id"` 9 | Name string `json:"name"` 10 | Description string `json:"description"` 11 | CreatedAt time.Time `json:"createdAt"` 12 | UpdatedAt time.Time `json:"updatedAt"` 13 | } 14 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/dtos/product_item_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type ProductItemDto struct { 6 | Id uuid.UUID `json:"id"` 7 | ProductId uuid.UUID `json:"productId"` 8 | Count int32 `json:"count"` 9 | InventoryId int64 `json:"inventoryId"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/mappings/mappings_profile.go: -------------------------------------------------------------------------------- 1 | package mappings 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/mapper" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/consumers/events" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/dtos" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/models" 8 | ) 9 | 10 | func ConfigureMappings() error { 11 | err := mapper.CreateMap[*models.Inventory, *dtos.InventoryDto]() 12 | if err != nil { 13 | return err 14 | } 15 | 16 | err = mapper.CreateMap[*models.ProductItem, *events.InventoryUpdated]() 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/middlewares/problem_details_handler.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/meysamhadeli/problem-details" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func ProblemDetailsHandler(error error, c echo.Context) { 10 | if !c.Response().Committed { 11 | if _, err := problem.ResolveProblemDetails(c.Response(), c.Request(), error); err != nil { 12 | log.Error(err) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/models/inventory.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Inventory struct { 8 | Id int64 `json:"id" gorm:"primaryKey"` 9 | Name string `json:"name"` 10 | Description string `json:"description"` 11 | CreatedAt time.Time `json:"createdAt"` 12 | UpdatedAt time.Time `json:"updatedAt"` 13 | ProductItems []ProductItem 14 | } 15 | -------------------------------------------------------------------------------- /internal/services/inventory_service/inventory/models/product_item.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | uuid "github.com/satori/go.uuid" 5 | "time" 6 | ) 7 | 8 | type ProductItem struct { 9 | Id uuid.UUID `json:"id" gorm:"primaryKey"` 10 | ProductId uuid.UUID `json:"productId"` 11 | Count int32 `json:"count"` 12 | CreatedAt time.Time `json:"createdAt"` 13 | UpdatedAt time.Time `json:"updatedAt"` 14 | InventoryId int64 15 | } 16 | -------------------------------------------------------------------------------- /internal/services/inventory_service/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "github.com/labstack/echo/v4" 6 | echoserver "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/server" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/config" 9 | "github.com/pkg/errors" 10 | "go.uber.org/fx" 11 | "net/http" 12 | ) 13 | 14 | func RunServers(lc fx.Lifecycle, log logger.ILogger, e *echo.Echo, ctx context.Context, cfg *config.Config) error { 15 | 16 | lc.Append(fx.Hook{ 17 | OnStart: func(_ context.Context) error { 18 | go func() { 19 | if err := echoserver.RunHttpServer(ctx, e, log, cfg.Echo); !errors.Is(err, http.ErrServerClosed) { 20 | log.Fatalf("error running http server: %v", err) 21 | } 22 | }() 23 | 24 | e.GET("/", func(c echo.Context) error { 25 | return c.String(http.StatusOK, config.GetMicroserviceName(cfg.ServiceName)) 26 | }) 27 | 28 | return nil 29 | }, 30 | OnStop: func(_ context.Context) error { 31 | log.Infof("all servers shutdown gracefully...") 32 | return nil 33 | }, 34 | }) 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/services/inventory_service/shared/delivery/inventory_delivery.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "context" 5 | "github.com/go-resty/resty/v2" 6 | "github.com/labstack/echo/v4" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/config" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/inventory_service/inventory/data/contracts" 12 | "github.com/streadway/amqp" 13 | "go.opentelemetry.io/otel/trace" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | type InventoryDeliveryBase struct { 18 | Log logger.ILogger 19 | Cfg *config.Config 20 | RabbitmqPublisher rabbitmq.IPublisher 21 | ConnRabbitmq *amqp.Connection 22 | HttpClient *resty.Client 23 | JaegerTracer trace.Tracer 24 | Gorm *gorm.DB 25 | Echo *echo.Echo 26 | GrpcClient grpc.GrpcClient 27 | InventoryRepository contracts.InventoryRepository 28 | Ctx context.Context 29 | } 30 | -------------------------------------------------------------------------------- /internal/services/product_service/.env: -------------------------------------------------------------------------------- 1 | APP_ENV=development -------------------------------------------------------------------------------- /internal/services/product_service/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-playground/validator" 5 | gormpgsql "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/gorm_pgsql" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http" 8 | echoserver "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/server" 9 | httpclient "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http_client" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel" 12 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 13 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/config" 14 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/configurations" 15 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/data/repositories" 16 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/mappings" 17 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/models" 18 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/server" 19 | "go.uber.org/fx" 20 | "gorm.io/gorm" 21 | ) 22 | 23 | // @securityDefinitions.apikey ApiKeyAuth 24 | // @in header 25 | // @name Authorization 26 | func main() { 27 | fx.New( 28 | fx.Options( 29 | fx.Provide( 30 | config.InitConfig, 31 | logger.InitLogger, 32 | http.NewContext, 33 | echoserver.NewEchoServer, 34 | grpc.NewGrpcClient, 35 | gormpgsql.NewGorm, 36 | otel.TracerProvider, 37 | httpclient.NewHttpClient, 38 | repositories.NewPostgresProductRepository, 39 | rabbitmq.NewRabbitMQConn, 40 | rabbitmq.NewPublisher, 41 | validator.New, 42 | ), 43 | fx.Invoke(server.RunServers), 44 | fx.Invoke(configurations.ConfigMiddlewares), 45 | fx.Invoke(configurations.ConfigSwagger), 46 | fx.Invoke(func(gorm *gorm.DB) error { 47 | return gormpgsql.Migrate(gorm, &models.Product{}) 48 | }), 49 | fx.Invoke(mappings.ConfigureMappings), 50 | fx.Invoke(configurations.ConfigEndpoints), 51 | fx.Invoke(configurations.ConfigProductsMediator), 52 | ), 53 | ).Run() 54 | } 55 | -------------------------------------------------------------------------------- /internal/services/product_service/config/config.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "serviceName": "product_service", 3 | "deliveryType": "http", 4 | "context": { 5 | "timeout": 20 6 | }, 7 | "rabbitMq": { 8 | "user": "guest", 9 | "password": "guest", 10 | "host": "localhost", 11 | "port": 5672, 12 | "exchangeName": "product", 13 | "kind" : "topic" 14 | }, 15 | "echo": { 16 | "port": ":5000", 17 | "development": true, 18 | "timeout": 30, 19 | "basePath": "/api/v1", 20 | "host": "http://localhost", 21 | "debugHeaders": true, 22 | "httpClientDebug": true, 23 | "debugErrorsResponse": true, 24 | "ignoreLogUrls": [ 25 | "metrics" 26 | ] 27 | }, 28 | "grpc": { 29 | "port": ":6600", 30 | "host": "localhost", 31 | "development": true 32 | }, 33 | "logger": { 34 | "level": "debug" 35 | }, 36 | "jaeger": { 37 | "server": "http://localhost:14268", 38 | "serviceName":"product_service", 39 | "tracerName": "product_tracer" 40 | }, 41 | "gormPostgres": { 42 | "host": "localhost", 43 | "port": 5432, 44 | "user": "postgres", 45 | "password": "postgres", 46 | "dbName": "product_service", 47 | "sslMode": false 48 | } 49 | } -------------------------------------------------------------------------------- /internal/services/product_service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | gormpgsql "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/gorm_pgsql" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 8 | echoserver "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/server" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 12 | "github.com/pkg/errors" 13 | "github.com/spf13/viper" 14 | "os" 15 | "path/filepath" 16 | "runtime" 17 | "strings" 18 | ) 19 | 20 | var configPath string 21 | 22 | type Config struct { 23 | ServiceName string `mapstructure:"serviceName"` 24 | Logger *logger.LoggerConfig `mapstructure:"logger"` 25 | Rabbitmq *rabbitmq.RabbitMQConfig `mapstructure:"rabbitmq"` 26 | Echo *echoserver.EchoConfig `mapstructure:"echo"` 27 | Grpc *grpc.GrpcConfig `mapstructure:"grpc"` 28 | GormPostgres *gormpgsql.GormPostgresConfig `mapstructure:"gormPostgres"` 29 | Jaeger *otel.JaegerConfig `mapstructure:"jaeger"` 30 | } 31 | 32 | func init() { 33 | flag.StringVar(&configPath, "config", "", "products write microservice config path") 34 | } 35 | 36 | func InitConfig() (*Config, *logger.LoggerConfig, *otel.JaegerConfig, *gormpgsql.GormPostgresConfig, 37 | *grpc.GrpcConfig, *echoserver.EchoConfig, *rabbitmq.RabbitMQConfig, error) { 38 | 39 | env := os.Getenv("APP_ENV") 40 | if env == "" { 41 | env = "development" 42 | } 43 | 44 | if configPath == "" { 45 | configPathFromEnv := os.Getenv("CONFIG_PATH") 46 | if configPathFromEnv != "" { 47 | configPath = configPathFromEnv 48 | } else { 49 | //https://stackoverflow.com/questions/31873396/is-it-possible-to-get-the-current-root-of-package-structure-as-a-string-in-golan 50 | //https://stackoverflow.com/questions/18537257/how-to-get-the-directory-of-the-currently-running-file 51 | d, err := dirname() 52 | if err != nil { 53 | return nil, nil, nil, nil, nil, nil, nil, err 54 | } 55 | 56 | configPath = d 57 | } 58 | } 59 | 60 | cfg := &Config{} 61 | 62 | viper.SetConfigName(fmt.Sprintf("config.%s", env)) 63 | viper.AddConfigPath(configPath) 64 | viper.SetConfigType("json") 65 | 66 | if err := viper.ReadInConfig(); err != nil { 67 | return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "viper.ReadInConfig") 68 | } 69 | 70 | if err := viper.Unmarshal(cfg); err != nil { 71 | return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "viper.Unmarshal") 72 | } 73 | 74 | return cfg, cfg.Logger, cfg.Jaeger, cfg.GormPostgres, cfg.Grpc, cfg.Echo, cfg.Rabbitmq, nil 75 | } 76 | 77 | func GetMicroserviceName(serviceName string) string { 78 | return fmt.Sprintf("%s", strings.ToUpper(serviceName)) 79 | } 80 | 81 | func filename() (string, error) { 82 | _, filename, _, ok := runtime.Caller(0) 83 | if !ok { 84 | return "", errors.New("unable to get the current filename") 85 | } 86 | return filename, nil 87 | } 88 | 89 | func dirname() (string, error) { 90 | filename, err := filename() 91 | if err != nil { 92 | return "", err 93 | } 94 | return filepath.Dir(filename), nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/services/product_service/config/config.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "serviceName": "product_service", 3 | "deliveryType": "http", 4 | "context": { 5 | "timeout": 20 6 | }, 7 | "rabbitMq": { 8 | "user": "guest", 9 | "password": "guest", 10 | "host": "localhost", 11 | "port": 5672, 12 | "exchangeName": "product", 13 | "kind" : "topic" 14 | }, 15 | "echo": { 16 | "port": ":5000", 17 | "development": true, 18 | "timeout": 30, 19 | "basePath": "/api/v1", 20 | "host": "http://localhost", 21 | "debugHeaders": true, 22 | "httpClientDebug": true, 23 | "debugErrorsResponse": true, 24 | "ignoreLogUrls": [ 25 | "metrics" 26 | ] 27 | }, 28 | "grpc": { 29 | "port": ":6600", 30 | "host": "localhost", 31 | "development": true 32 | }, 33 | "logger": { 34 | "level": "debug" 35 | }, 36 | "jaeger": { 37 | "server": "http://localhost:14268", 38 | "serviceName":"product_service", 39 | "tracerName": "product_tracer" 40 | }, 41 | "gormPostgres": { 42 | "host": "localhost", 43 | "port": 5432, 44 | "user": "postgres", 45 | "password": "postgres", 46 | "dbName": "product_service", 47 | "sslMode": false 48 | } 49 | } -------------------------------------------------------------------------------- /internal/services/product_service/product/configurations/endpoint_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator" 6 | "github.com/labstack/echo/v4" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | creating_product "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/endpoints" 9 | deleting_product "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/deleting_product/v1/endpoints" 10 | getting_product_by_id "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_product_by_id/v1/endpoints" 11 | getting_products "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_products/v1/endpoints" 12 | searching_product "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/searching_product/v1/endpoints" 13 | updating_product "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/updating_product/v1/endpoints" 14 | ) 15 | 16 | func ConfigEndpoints(validator *validator.Validate, log logger.ILogger, echo *echo.Echo, ctx context.Context) { 17 | 18 | creating_product.MapRoute(validator, log, echo, ctx) 19 | deleting_product.MapRoute(validator, log, echo, ctx) 20 | getting_product_by_id.MapRoute(validator, log, echo, ctx) 21 | getting_products.MapRoute(validator, log, echo, ctx) 22 | searching_product.MapRoute(validator, log, echo, ctx) 23 | updating_product.MapRoute(validator, log, echo, ctx) 24 | } 25 | -------------------------------------------------------------------------------- /internal/services/product_service/product/configurations/mediator_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "context" 5 | "github.com/mehdihadeli/go-mediatr" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/data/contracts" 10 | creatingproductv1commands "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/commands" 11 | creatingproductv1dtos "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/dtos" 12 | deletingproductv1commands "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/deleting_product/v1/commands" 13 | gettingproductbyidv1dtos "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_product_by_id/v1/dtos" 14 | gettingproductbyidv1queries "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_product_by_id/v1/queries" 15 | gettingproductsv1dtos "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_products/v1/dtos" 16 | gettingproductsv1queries "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_products/v1/queries" 17 | searchingproductv1dtos "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/searching_product/v1/dtos" 18 | searchingproductv1queries "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/searching_product/v1/queries" 19 | updatingproductv1commands "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/updating_product/v1/commands" 20 | updatingproductv1dtos "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/updating_product/v1/dtos" 21 | ) 22 | 23 | func ConfigProductsMediator(log logger.ILogger, rabbitmqPublisher rabbitmq.IPublisher, 24 | productRepository contracts.ProductRepository, ctx context.Context, grpcClient grpc.GrpcClient) error { 25 | 26 | //https://stackoverflow.com/questions/72034479/how-to-implement-generic-interfaces 27 | err := mediatr.RegisterRequestHandler[*creatingproductv1commands.CreateProduct, *creatingproductv1dtos.CreateProductResponseDto](creatingproductv1commands.NewCreateProductHandler(log, rabbitmqPublisher, productRepository, ctx, grpcClient)) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | err = mediatr.RegisterRequestHandler[*gettingproductsv1queries.GetProducts, *gettingproductsv1dtos.GetProductsResponseDto](gettingproductsv1queries.NewGetProductsHandler(log, rabbitmqPublisher, productRepository, ctx, grpcClient)) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | err = mediatr.RegisterRequestHandler[*searchingproductv1queries.SearchProducts, *searchingproductv1dtos.SearchProductsResponseDto](searchingproductv1queries.NewSearchProductsHandler(log, rabbitmqPublisher, productRepository, ctx, grpcClient)) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | err = mediatr.RegisterRequestHandler[*updatingproductv1commands.UpdateProduct, *updatingproductv1dtos.UpdateProductResponseDto](updatingproductv1commands.NewUpdateProductHandler(log, rabbitmqPublisher, productRepository, ctx, grpcClient)) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | err = mediatr.RegisterRequestHandler[*deletingproductv1commands.DeleteProduct, *mediatr.Unit](deletingproductv1commands.NewDeleteProductHandler(log, rabbitmqPublisher, productRepository, ctx, grpcClient)) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | err = mediatr.RegisterRequestHandler[*gettingproductbyidv1queries.GetProductById, *gettingproductbyidv1dtos.GetProductByIdResponseDto](gettingproductbyidv1queries.NewGetProductByIdHandler(log, rabbitmqPublisher, productRepository, ctx, grpcClient)) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/services/product_service/product/configurations/middleware_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/labstack/echo/v4/middleware" 6 | echomiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/middleware" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel" 8 | otelmiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/otel/middleware" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/constants" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/middlewares" 11 | "strings" 12 | ) 13 | 14 | func ConfigMiddlewares(e *echo.Echo, jaegerCfg *otel.JaegerConfig) { 15 | 16 | e.HideBanner = false 17 | 18 | e.Use(middleware.Logger()) 19 | 20 | e.HTTPErrorHandler = middlewares.ProblemDetailsHandler 21 | e.Use(otelmiddleware.EchoTracerMiddleware(jaegerCfg.ServiceName)) 22 | 23 | e.Use(echomiddleware.CorrelationIdMiddleware) 24 | e.Use(middleware.RequestID()) 25 | e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 26 | Level: constants.GzipLevel, 27 | Skipper: func(c echo.Context) bool { 28 | return strings.Contains(c.Request().URL.Path, "swagger") 29 | }, 30 | })) 31 | 32 | e.Use(middleware.BodyLimit(constants.BodyLimit)) 33 | } 34 | -------------------------------------------------------------------------------- /internal/services/product_service/product/configurations/swagger_configurations.go: -------------------------------------------------------------------------------- 1 | package configurations 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/docs" 6 | echoSwagger "github.com/swaggo/echo-swagger" 7 | ) 8 | 9 | func ConfigSwagger(e *echo.Echo) { 10 | 11 | docs.SwaggerInfo.Version = "1.0" 12 | docs.SwaggerInfo.Title = "Products Service Api" 13 | docs.SwaggerInfo.Description = "Products Service Api" 14 | e.GET("/swagger/*", echoSwagger.WrapHandler) 15 | } 16 | -------------------------------------------------------------------------------- /internal/services/product_service/product/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | ConfigPath = "CONFIG_PATH" 5 | Json = "json" 6 | BodyLimit = "2M" 7 | GzipLevel = 5 8 | Test = "test" 9 | ) 10 | -------------------------------------------------------------------------------- /internal/services/product_service/product/consumers/consume_create_product_handler.go: -------------------------------------------------------------------------------- 1 | package consumers 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/shared/delivery" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/streadway/amqp" 7 | ) 8 | 9 | func HandleConsumeCreateProduct(queue string, msg amqp.Delivery, productDeliveryBase *delivery.ProductDeliveryBase) error { 10 | 11 | log.Infof("Message received on queue: %s with message: %s", queue, string(msg.Body)) 12 | return nil 13 | } 14 | -------------------------------------------------------------------------------- /internal/services/product_service/product/data/contracts/product_repository.go: -------------------------------------------------------------------------------- 1 | package contracts 2 | 3 | import ( 4 | "context" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/models" 7 | 8 | uuid "github.com/satori/go.uuid" 9 | ) 10 | 11 | type ProductRepository interface { 12 | GetAllProducts(ctx context.Context, listQuery *utils.ListQuery) (*utils.ListResult[*models.Product], error) 13 | SearchProducts(ctx context.Context, searchText string, listQuery *utils.ListQuery) (*utils.ListResult[*models.Product], error) 14 | GetProductById(ctx context.Context, uuid uuid.UUID) (*models.Product, error) 15 | CreateProduct(ctx context.Context, product *models.Product) (*models.Product, error) 16 | UpdateProduct(ctx context.Context, product *models.Product) (*models.Product, error) 17 | DeleteProductByID(ctx context.Context, uuid uuid.UUID) error 18 | } 19 | -------------------------------------------------------------------------------- /internal/services/product_service/product/data/products_pg_seed.go: -------------------------------------------------------------------------------- 1 | package data 2 | -------------------------------------------------------------------------------- /internal/services/product_service/product/data/repositories/pg_product_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/jackc/pgx/v4/pgxpool" 7 | gormpgsql "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/gorm_pgsql" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/data/contracts" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/models" 12 | "github.com/pkg/errors" 13 | uuid "github.com/satori/go.uuid" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | type PostgresProductRepository struct { 18 | log logger.ILogger 19 | cfg *gormpgsql.GormPostgresConfig 20 | db *pgxpool.Pool 21 | gorm *gorm.DB 22 | } 23 | 24 | func NewPostgresProductRepository(log logger.ILogger, cfg *gormpgsql.GormPostgresConfig, gorm *gorm.DB) contracts.ProductRepository { 25 | return &PostgresProductRepository{log: log, cfg: cfg, gorm: gorm} 26 | } 27 | 28 | func (p *PostgresProductRepository) GetAllProducts(ctx context.Context, listQuery *utils.ListQuery) (*utils.ListResult[*models.Product], error) { 29 | 30 | result, err := gormpgsql.Paginate[*models.Product](ctx, listQuery, p.gorm) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return result, nil 35 | } 36 | 37 | func (p *PostgresProductRepository) SearchProducts(ctx context.Context, searchText string, listQuery *utils.ListQuery) (*utils.ListResult[*models.Product], error) { 38 | 39 | whereQuery := fmt.Sprintf("%s IN (?)", "Name") 40 | query := p.gorm.Where(whereQuery, searchText) 41 | 42 | result, err := gormpgsql.Paginate[*models.Product](ctx, listQuery, query) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return result, nil 47 | } 48 | 49 | func (p *PostgresProductRepository) GetProductById(ctx context.Context, uuid uuid.UUID) (*models.Product, error) { 50 | 51 | var product models.Product 52 | 53 | if err := p.gorm.First(&product, uuid).Error; err != nil { 54 | return nil, errors.Wrap(err, fmt.Sprintf("can't find the product with id %s into the database.", uuid)) 55 | } 56 | 57 | return &product, nil 58 | } 59 | 60 | func (p *PostgresProductRepository) CreateProduct(ctx context.Context, product *models.Product) (*models.Product, error) { 61 | 62 | if err := p.gorm.Create(&product).Error; err != nil { 63 | return nil, errors.Wrap(err, "error in the inserting product into the database.") 64 | } 65 | 66 | return product, nil 67 | } 68 | 69 | func (p *PostgresProductRepository) UpdateProduct(ctx context.Context, updateProduct *models.Product) (*models.Product, error) { 70 | 71 | if err := p.gorm.Save(updateProduct).Error; err != nil { 72 | return nil, errors.Wrap(err, fmt.Sprintf("error in updating product with id %s into the database.", updateProduct.ProductId)) 73 | } 74 | 75 | return updateProduct, nil 76 | } 77 | 78 | func (p *PostgresProductRepository) DeleteProductByID(ctx context.Context, uuid uuid.UUID) error { 79 | 80 | var product models.Product 81 | 82 | if err := p.gorm.First(&product, uuid).Error; err != nil { 83 | return errors.Wrap(err, fmt.Sprintf("can't find the product with id %s into the database.", uuid)) 84 | } 85 | 86 | if err := p.gorm.Delete(&product).Error; err != nil { 87 | return errors.Wrap(err, "error in the deleting product into the database.") 88 | } 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /internal/services/product_service/product/dtos/product_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import ( 4 | uuid "github.com/satori/go.uuid" 5 | "time" 6 | ) 7 | 8 | type ProductDto struct { 9 | ProductId uuid.UUID `json:"productId"` 10 | Name string `json:"name"` 11 | Description string `json:"description"` 12 | Price float64 `json:"price"` 13 | CreatedAt time.Time `json:"createdAt"` 14 | UpdatedAt time.Time `json:"updatedAt"` 15 | InventoryId int64 `json:"inventoryId"` 16 | Count int32 `json:"count"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/creating_product/v1/commands/create_product.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | uuid "github.com/satori/go.uuid" 5 | "time" 6 | ) 7 | 8 | type CreateProduct struct { 9 | ProductID uuid.UUID `validate:"required"` 10 | Name string `validate:"required,gte=0,lte=255"` 11 | Description string `validate:"required,gte=0,lte=5000"` 12 | Price float64 `validate:"required,gte=0"` 13 | InventoryId int64 `validate:"required,gt=0"` 14 | Count int32 `validate:"required,gt=0"` 15 | CreatedAt time.Time `validate:"required"` 16 | } 17 | 18 | func NewCreateProduct(name string, description string, price float64, inventoryId int64, count int32) *CreateProduct { 19 | return &CreateProduct{ProductID: uuid.NewV4(), Name: name, Description: description, 20 | Price: price, CreatedAt: time.Now(), InventoryId: inventoryId, Count: count} 21 | } 22 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/creating_product/v1/commands/create_product_handler.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/mapper" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/data/contracts" 11 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/dtos" 12 | eventsv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/events" 13 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/models" 14 | ) 15 | 16 | type CreateProductHandler struct { 17 | log logger.ILogger 18 | rabbitmqPublisher rabbitmq.IPublisher 19 | productRepository contracts.ProductRepository 20 | ctx context.Context 21 | grpcClient grpc.GrpcClient 22 | } 23 | 24 | func NewCreateProductHandler(log logger.ILogger, rabbitmqPublisher rabbitmq.IPublisher, 25 | productRepository contracts.ProductRepository, ctx context.Context, grpcClient grpc.GrpcClient) *CreateProductHandler { 26 | return &CreateProductHandler{log: log, productRepository: productRepository, ctx: ctx, rabbitmqPublisher: rabbitmqPublisher, grpcClient: grpcClient} 27 | } 28 | 29 | func (c *CreateProductHandler) Handle(ctx context.Context, command *CreateProduct) (*dtosv1.CreateProductResponseDto, error) { 30 | 31 | product := &models.Product{ 32 | ProductId: command.ProductID, 33 | Name: command.Name, 34 | Description: command.Description, 35 | Price: command.Price, 36 | InventoryId: command.InventoryId, 37 | Count: command.Count, 38 | CreatedAt: command.CreatedAt, 39 | } 40 | 41 | createdProduct, err := c.productRepository.CreateProduct(ctx, product) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | evt, err := mapper.Map[*eventsv1.ProductCreated](createdProduct) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | err = c.rabbitmqPublisher.PublishMessage(evt) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | response := &dtosv1.CreateProductResponseDto{ProductId: product.ProductId} 57 | bytes, _ := json.Marshal(response) 58 | 59 | c.log.Info("CreateProductResponseDto", string(bytes)) 60 | 61 | return response, nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/creating_product/v1/dtos/create_product_request_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | type CreateProductRequestDto struct { 4 | Name string `json:"name"` 5 | Description string `json:"description"` 6 | Price float64 `json:"price"` 7 | Count int32 `json:"count"` 8 | InventoryId int64 `json:"inventoryId"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/creating_product/v1/dtos/create_product_response_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type CreateProductResponseDto struct { 6 | ProductId uuid.UUID `json:"productId"` 7 | } 8 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/creating_product/v1/endpoints/create_product_endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator" 6 | "github.com/labstack/echo/v4" 7 | "github.com/mehdihadeli/go-mediatr" 8 | echomiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/middleware" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | commandsv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/commands" 11 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/dtos" 12 | "github.com/pkg/errors" 13 | "net/http" 14 | ) 15 | 16 | func MapRoute(validator *validator.Validate, log logger.ILogger, echo *echo.Echo, ctx context.Context) { 17 | group := echo.Group("/api/v1/products") 18 | group.POST("", createProduct(validator, log, ctx), echomiddleware.ValidateBearerToken()) 19 | } 20 | 21 | // CreateProduct 22 | // @Tags Products 23 | // @Summary Create product 24 | // @Description Create new product item 25 | // @Accept json 26 | // @Produce json 27 | // @Param CreateProductRequestDto body dtos.CreateProductRequestDto true "Product data" 28 | // @Success 201 {object} dtos.CreateProductResponseDto 29 | // @Security ApiKeyAuth 30 | // @Router /api/v1/products [post] 31 | func createProduct(validator *validator.Validate, log logger.ILogger, ctx context.Context) echo.HandlerFunc { 32 | return func(c echo.Context) error { 33 | 34 | request := &dtosv1.CreateProductRequestDto{} 35 | 36 | if err := c.Bind(request); err != nil { 37 | badRequestErr := errors.Wrap(err, "[createProductEndpoint_handler.Bind] error in the binding request") 38 | log.Error(badRequestErr) 39 | return echo.NewHTTPError(http.StatusBadRequest, err) 40 | } 41 | 42 | command := commandsv1.NewCreateProduct(request.Name, request.Description, request.Price, request.InventoryId, request.Count) 43 | 44 | if err := validator.StructCtx(ctx, command); err != nil { 45 | validationErr := errors.Wrap(err, "[createProductEndpoint_handler.StructCtx] command validation failed") 46 | log.Error(validationErr) 47 | return echo.NewHTTPError(http.StatusBadRequest, err) 48 | } 49 | 50 | result, err := mediatr.Send[*commandsv1.CreateProduct, *dtosv1.CreateProductResponseDto](ctx, command) 51 | 52 | if err != nil { 53 | log.Errorf("(CreateProduct.Handle) id: {%s}, err: {%v}", command.ProductID, err) 54 | return echo.NewHTTPError(http.StatusBadRequest, err) 55 | } 56 | 57 | log.Infof("(product created) id: {%s}", command.ProductID) 58 | return c.JSON(http.StatusCreated, result) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/creating_product/v1/events/product_created.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type ProductCreated struct { 6 | ProductId uuid.UUID 7 | InventoryId int64 8 | Count int32 9 | } 10 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/deleting_product/v1/commands/delete_product.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | uuid "github.com/satori/go.uuid" 5 | ) 6 | 7 | type DeleteProduct struct { 8 | ProductID uuid.UUID `validate:"required"` 9 | } 10 | 11 | func NewDeleteProduct(productID uuid.UUID) *DeleteProduct { 12 | return &DeleteProduct{ProductID: productID} 13 | } 14 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/deleting_product/v1/commands/delete_product_handler.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "github.com/mehdihadeli/go-mediatr" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/data/contracts" 10 | eventsv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/deleting_product/v1/events" 11 | ) 12 | 13 | type DeleteProductHandler struct { 14 | log logger.ILogger 15 | rabbitmqPublisher rabbitmq.IPublisher 16 | productRepository contracts.ProductRepository 17 | ctx context.Context 18 | grpcClient grpc.GrpcClient 19 | } 20 | 21 | func NewDeleteProductHandler(log logger.ILogger, rabbitmqPublisher rabbitmq.IPublisher, 22 | productRepository contracts.ProductRepository, ctx context.Context, grpcClient grpc.GrpcClient) *DeleteProductHandler { 23 | return &DeleteProductHandler{log: log, productRepository: productRepository, ctx: ctx, rabbitmqPublisher: rabbitmqPublisher, grpcClient: grpcClient} 24 | } 25 | 26 | func (c *DeleteProductHandler) Handle(ctx context.Context, command *DeleteProduct) (*mediatr.Unit, error) { 27 | 28 | if err := c.productRepository.DeleteProductByID(ctx, command.ProductID); err != nil { 29 | return nil, err 30 | } 31 | 32 | err := c.rabbitmqPublisher.PublishMessage(eventsv1.ProductDeleted{ 33 | ProductId: command.ProductID, 34 | }) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | c.log.Info("DeleteProduct successfully executed") 40 | 41 | return &mediatr.Unit{}, err 42 | } 43 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/deleting_product/v1/dtos/delete_product_request_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type DeleteProductRequestDto struct { 6 | ProductID uuid.UUID `param:"id" json:"-"` 7 | } 8 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/deleting_product/v1/endpoints/delete_product_endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator" 6 | "github.com/labstack/echo/v4" 7 | "github.com/mehdihadeli/go-mediatr" 8 | echomiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/middleware" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | commandsv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/deleting_product/v1/commands" 11 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/deleting_product/v1/dtos" 12 | "net/http" 13 | ) 14 | 15 | func MapRoute(validator *validator.Validate, log logger.ILogger, echo *echo.Echo, ctx context.Context) { 16 | group := echo.Group("/api/v1/products") 17 | group.DELETE("/:id", deleteProduct(validator, log, ctx), echomiddleware.ValidateBearerToken()) 18 | } 19 | 20 | // DeleteProduct 21 | // @Tags Products 22 | // @Summary Delete product 23 | // @Description Delete existing product 24 | // @Accept json 25 | // @Produce json 26 | // @Success 204 27 | // @Param id path string true "Product ID" 28 | // @Security ApiKeyAuth 29 | // @Router /api/v1/products/{id} [delete] 30 | func deleteProduct(validator *validator.Validate, log logger.ILogger, ctx context.Context) echo.HandlerFunc { 31 | return func(c echo.Context) error { 32 | 33 | request := &dtosv1.DeleteProductRequestDto{} 34 | if err := c.Bind(request); err != nil { 35 | log.Warn("Bind", err) 36 | return echo.NewHTTPError(http.StatusBadRequest, err) 37 | } 38 | 39 | command := commandsv1.NewDeleteProduct(request.ProductID) 40 | 41 | if err := validator.StructCtx(ctx, command); err != nil { 42 | log.Warn("validate", err) 43 | return echo.NewHTTPError(http.StatusBadRequest, err) 44 | } 45 | 46 | _, err := mediatr.Send[*commandsv1.DeleteProduct, *mediatr.Unit](ctx, command) 47 | 48 | if err != nil { 49 | log.Warn("DeleteProduct", err) 50 | return echo.NewHTTPError(http.StatusBadRequest, err) 51 | } 52 | 53 | return c.NoContent(http.StatusNoContent) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/deleting_product/v1/events/product_deleted.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type ProductDeleted struct { 6 | ProductId uuid.UUID 7 | } 8 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/getting_product_by_id/v1/dtos/get_product_by_id_request_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type GetProductByIdRequestDto struct { 6 | ProductId uuid.UUID `param:"id" json:"-"` 7 | } 8 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/getting_product_by_id/v1/dtos/get_product_by_id_response_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/dtos" 5 | ) 6 | 7 | type GetProductByIdResponseDto struct { 8 | Product *dtos.ProductDto `json:"product"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/getting_product_by_id/v1/endpoints/get_product_by_id_endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator" 6 | "github.com/labstack/echo/v4" 7 | "github.com/mehdihadeli/go-mediatr" 8 | echomiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/middleware" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_product_by_id/v1/dtos" 11 | queriesv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_product_by_id/v1/queries" 12 | "net/http" 13 | ) 14 | 15 | func MapRoute(validator *validator.Validate, log logger.ILogger, echo *echo.Echo, ctx context.Context) { 16 | group := echo.Group("/api/v1/products") 17 | group.GET("/:id", getProductByID(validator, log, ctx), echomiddleware.ValidateBearerToken()) 18 | } 19 | 20 | // GetProductByID 21 | // @Tags Products 22 | // @Summary Get product 23 | // @Description Get product by id 24 | // @Accept json 25 | // @Produce json 26 | // @Param id path string true "Product ID" 27 | // @Success 200 {object} dtos.GetProductByIdResponseDto 28 | // @Security ApiKeyAuth 29 | // @Router /api/v1/products/{id} [get] 30 | func getProductByID(validator *validator.Validate, log logger.ILogger, ctx context.Context) echo.HandlerFunc { 31 | return func(c echo.Context) error { 32 | 33 | request := &dtosv1.GetProductByIdRequestDto{} 34 | if err := c.Bind(request); err != nil { 35 | log.Warn("Bind", err) 36 | return echo.NewHTTPError(http.StatusBadRequest, err) 37 | } 38 | 39 | query := queriesv1.NewGetProductById(request.ProductId) 40 | 41 | if err := validator.StructCtx(ctx, query); err != nil { 42 | log.Warn("validate", err) 43 | return echo.NewHTTPError(http.StatusBadRequest, err) 44 | } 45 | 46 | queryResult, err := mediatr.Send[*queriesv1.GetProductById, *dtosv1.GetProductByIdResponseDto](ctx, query) 47 | 48 | if err != nil { 49 | log.Warn("GetProductById", err) 50 | return echo.NewHTTPError(http.StatusBadRequest, err) 51 | } 52 | 53 | return c.JSON(http.StatusOK, queryResult) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/getting_product_by_id/v1/queries/get_product_by_id.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | uuid "github.com/satori/go.uuid" 5 | ) 6 | 7 | type GetProductById struct { 8 | ProductID uuid.UUID `validate:"required"` 9 | } 10 | 11 | func NewGetProductById(productID uuid.UUID) *GetProductById { 12 | return &GetProductById{ProductID: productID} 13 | } 14 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/getting_product_by_id/v1/queries/get_product_by_id_handler.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/mapper" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/data/contracts" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/dtos" 12 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_product_by_id/v1/dtos" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type GetProductByIdHandler struct { 17 | log logger.ILogger 18 | rabbitmqPublisher rabbitmq.IPublisher 19 | productRepository contracts.ProductRepository 20 | ctx context.Context 21 | grpcClient grpc.GrpcClient 22 | } 23 | 24 | func NewGetProductByIdHandler(log logger.ILogger, rabbitmqPublisher rabbitmq.IPublisher, 25 | productRepository contracts.ProductRepository, ctx context.Context, grpcClient grpc.GrpcClient) *GetProductByIdHandler { 26 | return &GetProductByIdHandler{log: log, productRepository: productRepository, ctx: ctx, rabbitmqPublisher: rabbitmqPublisher, grpcClient: grpcClient} 27 | } 28 | 29 | func (q *GetProductByIdHandler) Handle(ctx context.Context, query *GetProductById) (*dtosv1.GetProductByIdResponseDto, error) { 30 | 31 | product, err := q.productRepository.GetProductById(ctx, query.ProductID) 32 | 33 | if err != nil { 34 | notFoundErr := errors.Wrap(err, fmt.Sprintf("product with id %s not found", query.ProductID)) 35 | return nil, notFoundErr 36 | } 37 | 38 | productDto, err := mapper.Map[*dtos.ProductDto](product) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return &dtosv1.GetProductByIdResponseDto{Product: productDto}, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/getting_products/v1/dtos/get_products_request_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 5 | ) 6 | 7 | type GetProductsRequestDto struct { 8 | *utils.ListQuery 9 | } 10 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/getting_products/v1/dtos/get_products_response_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/dtos" 6 | ) 7 | 8 | type GetProductsResponseDto struct { 9 | Products *utils.ListResult[*dtos.ProductDto] 10 | } 11 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/getting_products/v1/endpoints/get_products_endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator" 6 | "github.com/labstack/echo/v4" 7 | "github.com/mehdihadeli/go-mediatr" 8 | echomiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/middleware" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 11 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_products/v1/dtos" 12 | queriesv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_products/v1/queries" 13 | "net/http" 14 | ) 15 | 16 | func MapRoute(validator *validator.Validate, log logger.ILogger, echo *echo.Echo, ctx context.Context) { 17 | group := echo.Group("/api/v1/products") 18 | group.GET("", getAllProducts(validator, log, ctx), echomiddleware.ValidateBearerToken()) 19 | } 20 | 21 | // GetAllProducts 22 | // @Tags Products 23 | // @Summary Get all product 24 | // @Description Get all products 25 | // @Accept json 26 | // @Produce json 27 | // @Param GetProductsRequestDto query dtos.GetProductsRequestDto false "GetProductsRequestDto" 28 | // @Success 200 {object} dtos.GetProductsResponseDto 29 | // @Security ApiKeyAuth 30 | // @Router /api/v1/products [get] 31 | func getAllProducts(validator *validator.Validate, log logger.ILogger, ctx context.Context) echo.HandlerFunc { 32 | return func(c echo.Context) error { 33 | 34 | listQuery, err := utils.GetListQueryFromCtx(c) 35 | if err != nil { 36 | return echo.NewHTTPError(http.StatusBadRequest, err) 37 | } 38 | 39 | request := &dtosv1.GetProductsRequestDto{ListQuery: listQuery} 40 | if err := c.Bind(request); err != nil { 41 | log.Warn("Bind", err) 42 | return echo.NewHTTPError(http.StatusBadRequest, err) 43 | } 44 | 45 | query := queriesv1.NewGetProducts(request.ListQuery) 46 | 47 | queryResult, err := mediatr.Send[*queriesv1.GetProducts, *dtosv1.GetProductsResponseDto](ctx, query) 48 | 49 | if err != nil { 50 | log.Warnf("GetProducts", err) 51 | return echo.NewHTTPError(http.StatusBadRequest, err) 52 | } 53 | 54 | return c.JSON(http.StatusOK, queryResult) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/getting_products/v1/queries/get_products.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 5 | ) 6 | 7 | // Ref: https://golangbot.com/inheritance/ 8 | 9 | type GetProducts struct { 10 | *utils.ListQuery 11 | } 12 | 13 | func NewGetProducts(query *utils.ListQuery) *GetProducts { 14 | return &GetProducts{ListQuery: query} 15 | } 16 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/getting_products/v1/queries/get_products_handler.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/data/contracts" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/dtos" 11 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/getting_products/v1/dtos" 12 | ) 13 | 14 | type GetProductsHandler struct { 15 | log logger.ILogger 16 | rabbitmqPublisher rabbitmq.IPublisher 17 | productRepository contracts.ProductRepository 18 | ctx context.Context 19 | grpcClient grpc.GrpcClient 20 | } 21 | 22 | func NewGetProductsHandler(log logger.ILogger, rabbitmqPublisher rabbitmq.IPublisher, 23 | productRepository contracts.ProductRepository, ctx context.Context, grpcClient grpc.GrpcClient) *GetProductsHandler { 24 | return &GetProductsHandler{log: log, productRepository: productRepository, ctx: ctx, rabbitmqPublisher: rabbitmqPublisher, grpcClient: grpcClient} 25 | } 26 | 27 | func (c *GetProductsHandler) Handle(ctx context.Context, query *GetProducts) (*dtosv1.GetProductsResponseDto, error) { 28 | 29 | products, err := c.productRepository.GetAllProducts(ctx, query.ListQuery) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | listResultDto, err := utils.ListResultToListResultDto[*dtos.ProductDto](products) 35 | 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &dtosv1.GetProductsResponseDto{Products: listResultDto}, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/searching_product/v1/dtos/search_products_request_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 5 | ) 6 | 7 | type SearchProductsRequestDto struct { 8 | SearchText string `query:"search" json:"search"` 9 | *utils.ListQuery `json:"listQuery"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/searching_product/v1/dtos/search_products_response_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/dtos" 6 | ) 7 | 8 | type SearchProductsResponseDto struct { 9 | Products *utils.ListResult[*dtos.ProductDto] 10 | } 11 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/searching_product/v1/endpoints/search_products_endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator" 6 | "github.com/labstack/echo/v4" 7 | "github.com/mehdihadeli/go-mediatr" 8 | echomiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/middleware" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 11 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/searching_product/v1/dtos" 12 | "net/http" 13 | ) 14 | 15 | func MapRoute(validator *validator.Validate, log logger.ILogger, echo *echo.Echo, ctx context.Context) { 16 | group := echo.Group("/api/v1/products") 17 | group.GET("/search", searchProducts(validator, log, ctx), echomiddleware.ValidateBearerToken()) 18 | } 19 | 20 | // SearchProducts 21 | // @Tags Products 22 | // @Summary Search products 23 | // @Description Search products 24 | // @Accept json 25 | // @Produce json 26 | // @Param searchProductsRequestDto query dtos.SearchProductsRequestDto false "SearchProductsRequestDto" 27 | // @Success 200 {object} dtos.SearchProductsResponseDto 28 | // @Security ApiKeyAuth 29 | // @Router /api/v1/products/search [get] 30 | func searchProducts(validator *validator.Validate, log logger.ILogger, ctx context.Context) echo.HandlerFunc { 31 | return func(c echo.Context) error { 32 | 33 | listQuery, err := utils.GetListQueryFromCtx(c) 34 | 35 | if err != nil { 36 | return echo.NewHTTPError(http.StatusBadRequest, err) 37 | } 38 | 39 | request := &dtosv1.SearchProductsRequestDto{ListQuery: listQuery} 40 | 41 | // https://echo.labstack.com/guide/binding/ 42 | if err := c.Bind(request); err != nil { 43 | log.Warn("Bind", err) 44 | return echo.NewHTTPError(http.StatusBadRequest, err) 45 | } 46 | 47 | query := &dtosv1.SearchProductsRequestDto{SearchText: request.SearchText, ListQuery: request.ListQuery} 48 | 49 | if err := validator.StructCtx(ctx, query); err != nil { 50 | log.Errorf("(validate) err: {%v}", err) 51 | return echo.NewHTTPError(http.StatusBadRequest, err) 52 | } 53 | 54 | queryResult, err := mediatr.Send[*dtosv1.SearchProductsRequestDto, *dtosv1.SearchProductsResponseDto](ctx, query) 55 | 56 | if err != nil { 57 | log.Warn("SearchProducts", err) 58 | return echo.NewHTTPError(http.StatusBadRequest, err) 59 | } 60 | 61 | return c.JSON(http.StatusOK, queryResult) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/searching_product/v1/queries/search_products.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 5 | ) 6 | 7 | type SearchProducts struct { 8 | SearchText string `validate:"required"` 9 | *utils.ListQuery 10 | } 11 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/searching_product/v1/queries/search_products_handler.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "context" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/utils" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/data/contracts" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/dtos" 11 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/searching_product/v1/dtos" 12 | ) 13 | 14 | type SearchProductsHandler struct { 15 | log logger.ILogger 16 | rabbitmqPublisher rabbitmq.IPublisher 17 | productRepository contracts.ProductRepository 18 | ctx context.Context 19 | grpcClient grpc.GrpcClient 20 | } 21 | 22 | func NewSearchProductsHandler(log logger.ILogger, rabbitmqPublisher rabbitmq.IPublisher, 23 | productRepository contracts.ProductRepository, ctx context.Context, grpcClient grpc.GrpcClient) *SearchProductsHandler { 24 | return &SearchProductsHandler{log: log, productRepository: productRepository, ctx: ctx, rabbitmqPublisher: rabbitmqPublisher, grpcClient: grpcClient} 25 | } 26 | 27 | func (c *SearchProductsHandler) Handle(ctx context.Context, query *SearchProducts) (*dtosv1.SearchProductsResponseDto, error) { 28 | 29 | products, err := c.productRepository.SearchProducts(ctx, query.SearchText, query.ListQuery) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | listResultDto, err := utils.ListResultToListResultDto[*dtos.ProductDto](products) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return &dtosv1.SearchProductsResponseDto{Products: listResultDto}, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/updating_product/v1/commands/update_product.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | uuid "github.com/satori/go.uuid" 5 | "time" 6 | ) 7 | 8 | type UpdateProduct struct { 9 | ProductID uuid.UUID `validate:"required"` 10 | Name string `validate:"required,gte=0,lte=255"` 11 | Description string `validate:"required,gte=0,lte=5000"` 12 | Price float64 `validate:"required,gte=0"` 13 | UpdatedAt time.Time `validate:"required"` 14 | Count int32 `validate:"required,gt=0"` 15 | InventoryId int64 `validate:"required,gt=0"` 16 | } 17 | 18 | func NewUpdateProduct(productID uuid.UUID, name string, description string, price float64, inventoryId int64, count int32) *UpdateProduct { 19 | return &UpdateProduct{ProductID: productID, Name: name, Description: description, 20 | Price: price, UpdatedAt: time.Now(), InventoryId: inventoryId, Count: count} 21 | } 22 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/updating_product/v1/commands/update_product_handler.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/mapper" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/data/contracts" 12 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/updating_product/v1/dtos" 13 | eventsv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/updating_product/v1/events" 14 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/models" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | type UpdateProductHandler struct { 19 | log logger.ILogger 20 | rabbitmqPublisher rabbitmq.IPublisher 21 | productRepository contracts.ProductRepository 22 | ctx context.Context 23 | grpcClient grpc.GrpcClient 24 | } 25 | 26 | func NewUpdateProductHandler(log logger.ILogger, rabbitmqPublisher rabbitmq.IPublisher, 27 | productRepository contracts.ProductRepository, ctx context.Context, grpcClient grpc.GrpcClient) *UpdateProductHandler { 28 | return &UpdateProductHandler{log: log, productRepository: productRepository, ctx: ctx, rabbitmqPublisher: rabbitmqPublisher, grpcClient: grpcClient} 29 | } 30 | 31 | func (c *UpdateProductHandler) Handle(ctx context.Context, command *UpdateProduct) (*dtosv1.UpdateProductResponseDto, error) { 32 | 33 | //simple call grpcClient 34 | //identityGrpcClient := identity_service.NewIdentityServiceClient(c.grpcClient.GetGrpcConnection()) 35 | //user, err := identityGrpcClient.GetUserById(ctx, &identity_service.GetUserByIdReq{UserId: "1"}) 36 | //if err != nil { 37 | // return nil, err 38 | //} 39 | // 40 | //c.log.Infof("userId: %s", user.User.UserId) 41 | 42 | _, err := c.productRepository.GetProductById(ctx, command.ProductID) 43 | 44 | if err != nil { 45 | notFoundErr := errors.Wrap(err, fmt.Sprintf("product with id %s not found", command.ProductID)) 46 | return nil, notFoundErr 47 | } 48 | 49 | product := &models.Product{ProductId: command.ProductID, Name: command.Name, Description: command.Description, Price: command.Price, UpdatedAt: command.UpdatedAt, InventoryId: command.InventoryId, Count: command.Count} 50 | 51 | updatedProduct, err := c.productRepository.UpdateProduct(ctx, product) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | evt, err := mapper.Map[*eventsv1.ProductUpdated](updatedProduct) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | err = c.rabbitmqPublisher.PublishMessage(evt) 62 | 63 | response := &dtosv1.UpdateProductResponseDto{ProductId: product.ProductId} 64 | bytes, _ := json.Marshal(response) 65 | 66 | c.log.Info("UpdateProductResponseDto", string(bytes)) 67 | 68 | return response, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/updating_product/v1/dtos/update_product_request_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | // https://echo.labstack.com/guide/binding/ 6 | 7 | type UpdateProductRequestDto struct { 8 | ProductId uuid.UUID `json:"-" param:"id"` 9 | Name string `json:"name" validate:"required"` 10 | Description string `json:"description"` 11 | Price float64 `json:"price" validate:"required"` 12 | Count int32 `json:"count" validate:"required"` 13 | InventoryId int64 `json:"inventoryId" validate:"required"` 14 | } 15 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/updating_product/v1/dtos/update_product_response_dto.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type UpdateProductResponseDto struct { 6 | ProductId uuid.UUID `json:"productId"` 7 | } 8 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/updating_product/v1/endpoints/update_product_endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "context" 5 | "github.com/go-playground/validator" 6 | "github.com/labstack/echo/v4" 7 | "github.com/mehdihadeli/go-mediatr" 8 | echomiddleware "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/middleware" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 10 | commandsv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/updating_product/v1/commands" 11 | dtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/updating_product/v1/dtos" 12 | "github.com/pkg/errors" 13 | "net/http" 14 | ) 15 | 16 | func MapRoute(validator *validator.Validate, log logger.ILogger, echo *echo.Echo, ctx context.Context) { 17 | group := echo.Group("/api/v1/products") 18 | group.PUT("/:id", updateProduct(validator, log, ctx), echomiddleware.ValidateBearerToken()) 19 | } 20 | 21 | // UpdateProduct 22 | // @Tags Products 23 | // @Summary Update product 24 | // @Description Update existing product 25 | // @Accept json 26 | // @Produce json 27 | // @Param UpdateProductRequestDto body dtos.UpdateProductRequestDto true "Product data" 28 | // @Param id path string true "Product ID" 29 | // @Success 204 30 | // @Security ApiKeyAuth 31 | // @Router /api/v1/products/{id} [put] 32 | func updateProduct(validator *validator.Validate, log logger.ILogger, ctx context.Context) echo.HandlerFunc { 33 | return func(c echo.Context) error { 34 | 35 | request := &dtosv1.UpdateProductRequestDto{} 36 | if err := c.Bind(request); err != nil { 37 | badRequestErr := errors.Wrap(err, "[updateProductEndpoint_handler.Bind] error in the binding request") 38 | log.Error(badRequestErr) 39 | return echo.NewHTTPError(http.StatusBadRequest, err) 40 | } 41 | 42 | command := commandsv1.NewUpdateProduct(request.ProductId, request.Name, request.Description, request.Price, request.InventoryId, request.Count) 43 | 44 | if err := validator.StructCtx(ctx, command); err != nil { 45 | validationErr := errors.Wrap(err, "[updateProductEndpoint_handler.StructCtx] command validation failed") 46 | log.Error(validationErr) 47 | return echo.NewHTTPError(http.StatusBadRequest, err) 48 | } 49 | 50 | _, err := mediatr.Send[*commandsv1.UpdateProduct, *dtosv1.UpdateProductResponseDto](ctx, command) 51 | 52 | if err != nil { 53 | log.Warnf("UpdateProduct", err) 54 | return echo.NewHTTPError(http.StatusBadRequest, err) 55 | } 56 | 57 | log.Infof("(product updated) id: {%s}", request.ProductId) 58 | 59 | return c.NoContent(http.StatusNoContent) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/services/product_service/product/features/updating_product/v1/events/product_updated.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | type ProductUpdated struct { 6 | ProductId uuid.UUID 7 | InventoryId int64 8 | Count int32 9 | } 10 | -------------------------------------------------------------------------------- /internal/services/product_service/product/grpc_client/protos/identity_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package identity_service; 4 | 5 | option go_package = "./;identity_service"; 6 | 7 | service IdentityService { 8 | rpc GetUserById(GetUserByIdReq) returns (GetUserByIdRes); 9 | } 10 | 11 | message User { 12 | string UserId = 1; 13 | string Name = 2; 14 | } 15 | 16 | message GetUserByIdReq { 17 | string UserId = 1; 18 | } 19 | 20 | message GetUserByIdRes { 21 | User User = 1; 22 | } 23 | -------------------------------------------------------------------------------- /internal/services/product_service/product/mappings/manual_mappings.go: -------------------------------------------------------------------------------- 1 | package mappings 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/dtos" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/models" 6 | ) 7 | 8 | func ProductToProductResponseDto(product *models.Product) *dtos.ProductDto { 9 | return &dtos.ProductDto{ 10 | ProductId: product.ProductId, 11 | Name: product.Name, 12 | Description: product.Description, 13 | Price: product.Price, 14 | CreatedAt: product.CreatedAt, 15 | UpdatedAt: product.UpdatedAt, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/services/product_service/product/mappings/mapping_profile.go: -------------------------------------------------------------------------------- 1 | package mappings 2 | 3 | import ( 4 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/mapper" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/dtos" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/events" 7 | events2 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/updating_product/v1/events" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/models" 9 | ) 10 | 11 | func ConfigureMappings() error { 12 | err := mapper.CreateMap[*models.Product, *dtos.ProductDto]() 13 | if err != nil { 14 | return err 15 | } 16 | 17 | err = mapper.CreateMap[*models.Product, *events.ProductCreated]() 18 | if err != nil { 19 | return err 20 | } 21 | 22 | err = mapper.CreateMap[*models.Product, *events2.ProductUpdated]() 23 | if err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/services/product_service/product/middlewares/problem_details_handler.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/meysamhadeli/problem-details" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func ProblemDetailsHandler(error error, c echo.Context) { 10 | if !c.Response().Committed { 11 | if _, err := problem.ResolveProblemDetails(c.Response(), c.Request(), error); err != nil { 12 | log.Error(err) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/services/product_service/product/models/product.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | // Product model 10 | type Product struct { 11 | ProductId uuid.UUID `json:"productId" gorm:"primaryKey"` 12 | Name string `json:"name"` 13 | Description string `json:"description"` 14 | Price float64 `json:"price"` 15 | InventoryId int64 `json:"inventoryId"` 16 | Count int32 `json:"count"` 17 | CreatedAt time.Time `json:"createdAt"` 18 | UpdatedAt time.Time `json:"updatedAt"` 19 | } 20 | -------------------------------------------------------------------------------- /internal/services/product_service/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "github.com/labstack/echo/v4" 6 | echoserver "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http/echo/server" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/config" 9 | "github.com/pkg/errors" 10 | "go.uber.org/fx" 11 | "net/http" 12 | ) 13 | 14 | func RunServers(lc fx.Lifecycle, log logger.ILogger, e *echo.Echo, ctx context.Context, cfg *config.Config) error { 15 | 16 | lc.Append(fx.Hook{ 17 | OnStart: func(_ context.Context) error { 18 | go func() { 19 | if err := echoserver.RunHttpServer(ctx, e, log, cfg.Echo); !errors.Is(err, http.ErrServerClosed) { 20 | log.Fatalf("error running http server: %v", err) 21 | } 22 | }() 23 | 24 | e.GET("/", func(c echo.Context) error { 25 | return c.String(http.StatusOK, config.GetMicroserviceName(cfg.ServiceName)) 26 | }) 27 | 28 | return nil 29 | }, 30 | OnStop: func(_ context.Context) error { 31 | log.Infof("all servers shutdown gracefully...") 32 | return nil 33 | }, 34 | }) 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/services/product_service/shared/delivery/product_delivery.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "context" 5 | "github.com/go-resty/resty/v2" 6 | "github.com/labstack/echo/v4" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/config" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/data/contracts" 12 | "github.com/streadway/amqp" 13 | "go.opentelemetry.io/otel/trace" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | type ProductDeliveryBase struct { 18 | Log logger.ILogger 19 | Cfg *config.Config 20 | RabbitmqPublisher rabbitmq.IPublisher 21 | ConnRabbitmq *amqp.Connection 22 | HttpClient *resty.Client 23 | JaegerTracer trace.Tracer 24 | Gorm *gorm.DB 25 | Echo *echo.Echo 26 | GrpcClient grpc.GrpcClient 27 | ProductRepository contracts.ProductRepository 28 | Ctx context.Context 29 | } 30 | -------------------------------------------------------------------------------- /internal/services/product_service/shared/test_fixture/unit_test_fixture.go: -------------------------------------------------------------------------------- 1 | package test_fixture 2 | 3 | import ( 4 | "context" 5 | mocks3 "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/grpc/mocks" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/http" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | mocks2 "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq/mocks" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/config" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/constants" 11 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/mappings" 12 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/tests/unit_tests/mocks" 13 | "github.com/stretchr/testify/mock" 14 | "github.com/stretchr/testify/require" 15 | "go.uber.org/fx" 16 | "go.uber.org/fx/fxtest" 17 | "os" 18 | "testing" 19 | ) 20 | 21 | type UnitTestFixture struct { 22 | *testing.T 23 | Log logger.ILogger 24 | Cfg *config.Config 25 | Ctx context.Context 26 | RabbitmqPublisher *mocks2.IPublisher 27 | ProductRepository *mocks.ProductRepository 28 | GrpcClient *mocks3.GrpcClient 29 | } 30 | 31 | func NewUnitTestFixture(t *testing.T) *UnitTestFixture { 32 | 33 | err := os.Setenv("APP_ENV", constants.Test) 34 | 35 | if err != nil { 36 | require.FailNow(t, err.Error()) 37 | } 38 | 39 | unitTestFixture := &UnitTestFixture{T: t} 40 | 41 | app := fxtest.New(t, 42 | fx.Options( 43 | fx.Provide( 44 | config.InitConfig, 45 | logger.InitLogger, 46 | http.NewContext, 47 | ), 48 | fx.Invoke(func( 49 | ctx context.Context, 50 | log logger.ILogger, 51 | cfg *config.Config, 52 | ) { 53 | unitTestFixture.Log = log 54 | unitTestFixture.Cfg = cfg 55 | unitTestFixture.Ctx = ctx 56 | }), 57 | fx.Invoke(mappings.ConfigureMappings), 58 | ), 59 | ) 60 | 61 | //https://github.com/uber-go/fx/blob/master/app_test.go 62 | defer app.RequireStart().RequireStop() 63 | require.NoError(t, app.Err()) 64 | 65 | // create new mocks 66 | unitTestFixture.RabbitmqPublisher = &mocks2.IPublisher{} 67 | 68 | unitTestFixture.ProductRepository = &mocks.ProductRepository{} 69 | unitTestFixture.GrpcClient = &mocks3.GrpcClient{} 70 | 71 | unitTestFixture.RabbitmqPublisher.On("PublishMessage", mock.Anything, mock.Anything).Return(nil) 72 | 73 | return unitTestFixture 74 | } 75 | -------------------------------------------------------------------------------- /internal/services/product_service/tests/end_to_end_tests/features/creating_product/create_product_end_to_end_test.go: -------------------------------------------------------------------------------- 1 | package creating_product 2 | 3 | import ( 4 | "github.com/brianvoe/gofakeit/v6" 5 | "github.com/gavv/httpexpect/v2" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/dtos" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/shared/test_fixture" 8 | "go.uber.org/fx" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | ) 13 | 14 | type createProductEndToEndTests struct { 15 | *test_fixture.IntegrationTestFixture 16 | } 17 | 18 | func TestRunner(t *testing.T) { 19 | 20 | var endToEndTestFixture = test_fixture.NewIntegrationTestFixture(t, fx.Options()) 21 | 22 | //https://pkg.go.dev/testing@master#hdr-Subtests_and_Sub_benchmarks 23 | t.Run("A=create-product-end-to-end-tests", func(t *testing.T) { 24 | 25 | testFixture := &createProductEndToEndTests{endToEndTestFixture} 26 | testFixture.Test_Should_Return_Ok_Status_When_Create_New_Product_To_DB() 27 | }) 28 | } 29 | 30 | func (c *createProductEndToEndTests) Test_Should_Return_Ok_Status_When_Create_New_Product_To_DB() { 31 | 32 | tsrv := httptest.NewServer(c.Echo) 33 | defer tsrv.Close() 34 | 35 | e := httpexpect.Default(c.T, tsrv.URL) 36 | 37 | request := &dtos.CreateProductRequestDto{ 38 | Name: gofakeit.Name(), 39 | Description: gofakeit.AdjectiveDescriptive(), 40 | Price: gofakeit.Price(150, 6000), 41 | InventoryId: 1, 42 | Count: 1, 43 | } 44 | 45 | e.POST("/api/v1/products"). 46 | WithContext(c.Ctx). 47 | WithJSON(request). 48 | Expect(). 49 | Status(http.StatusCreated) 50 | } 51 | -------------------------------------------------------------------------------- /internal/services/product_service/tests/integration_tests/features/creating_product/create_product_integration_test.go: -------------------------------------------------------------------------------- 1 | package creating_product 2 | 3 | import ( 4 | "context" 5 | "github.com/brianvoe/gofakeit/v6" 6 | "github.com/mehdihadeli/go-mediatr" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/logger" 8 | "github.com/meysamhadeli/shop-golang-microservices/internal/pkg/rabbitmq" 9 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/config" 10 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/consumers" 11 | creatingproductcommandsv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/commands" 12 | creatingproductdtosv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/dtos" 13 | creatingproducteventsv1 "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/events" 14 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/shared/delivery" 15 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/shared/test_fixture" 16 | "github.com/streadway/amqp" 17 | "github.com/stretchr/testify/assert" 18 | "go.opentelemetry.io/otel/trace" 19 | "go.uber.org/fx" 20 | "testing" 21 | ) 22 | 23 | type createProductIntegrationTests struct { 24 | *test_fixture.IntegrationTestFixture 25 | } 26 | 27 | var consumer rabbitmq.IConsumer[*delivery.ProductDeliveryBase] 28 | 29 | func TestRunner(t *testing.T) { 30 | 31 | var integrationTestFixture = test_fixture.NewIntegrationTestFixture(t, fx.Options( 32 | fx.Invoke(func(ctx context.Context, jaegerTracer trace.Tracer, log logger.ILogger, connRabbitmq *amqp.Connection, cfg *config.Config) { 33 | consumer = rabbitmq.NewConsumer(ctx, cfg.Rabbitmq, connRabbitmq, log, jaegerTracer, consumers.HandleConsumeCreateProduct) 34 | err := consumer.ConsumeMessage(creatingproducteventsv1.ProductCreated{}, nil) 35 | if err != nil { 36 | assert.Error(t, err) 37 | } 38 | }))) 39 | 40 | //https://pkg.go.dev/testing@master#hdr-Subtests_and_Sub_benchmarks 41 | t.Run("A=create-product-integration-tests", func(t *testing.T) { 42 | 43 | testFixture := &createProductIntegrationTests{integrationTestFixture} 44 | testFixture.Test_Should_Create_New_Product_To_DB() 45 | }) 46 | } 47 | 48 | func (c *createProductIntegrationTests) Test_Should_Create_New_Product_To_DB() { 49 | 50 | command := creatingproductcommandsv1.NewCreateProduct(gofakeit.Name(), gofakeit.AdjectiveDescriptive(), gofakeit.Price(150, 6000), 1, 1) 51 | result, err := mediatr.Send[*creatingproductcommandsv1.CreateProduct, *creatingproductdtosv1.CreateProductResponseDto](c.Ctx, command) 52 | 53 | assert.NoError(c.T, err) 54 | assert.NotNil(c.T, result) 55 | assert.Equal(c.T, command.ProductID, result.ProductId) 56 | 57 | isPublished := c.RabbitmqPublisher.IsPublished(creatingproducteventsv1.ProductCreated{}) 58 | assert.Equal(c.T, true, isPublished) 59 | 60 | isConsumed := consumer.IsConsumed(creatingproducteventsv1.ProductCreated{}) 61 | assert.Equal(c.T, true, isConsumed) 62 | 63 | createdProduct, err := c.IntegrationTestFixture.ProductRepository.GetProductById(c.Ctx, result.ProductId) 64 | assert.NoError(c.T, err) 65 | assert.NotNil(c.T, createdProduct) 66 | } 67 | -------------------------------------------------------------------------------- /internal/services/product_service/tests/unit_tests/features/creating_product/create_product_handler_unit_test.go: -------------------------------------------------------------------------------- 1 | package creating_product 2 | 3 | import ( 4 | "github.com/brianvoe/gofakeit/v6" 5 | creatingproductv1commands "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/features/creating_product/v1/commands" 6 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/shared/test_fixture" 7 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/tests/unit_tests/test_data" 8 | uuid "github.com/satori/go.uuid" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | type createProductHandlerUnitTests struct { 16 | *test_fixture.UnitTestFixture 17 | createProductHandler *creatingproductv1commands.CreateProductHandler 18 | } 19 | 20 | func TestRunner(t *testing.T) { 21 | 22 | //https://pkg.go.dev/testing@master#hdr-Subtests_and_Sub_benchmarks 23 | t.Run("A=create-product-unit-tests", func(t *testing.T) { 24 | 25 | var unitTestFixture = test_fixture.NewUnitTestFixture(t) 26 | 27 | mockCreateProductHandler := creatingproductv1commands.NewCreateProductHandler(unitTestFixture.Log, unitTestFixture.RabbitmqPublisher, 28 | unitTestFixture.ProductRepository, unitTestFixture.Ctx, unitTestFixture.GrpcClient) 29 | 30 | testFixture := &createProductHandlerUnitTests{unitTestFixture, mockCreateProductHandler} 31 | testFixture.Test_Handle_Should_Create_New_Product_With_Valid_Data() 32 | }) 33 | } 34 | 35 | func (c *createProductHandlerUnitTests) SetupTest() { 36 | // create new mocks or clear mocks before executing 37 | c.createProductHandler = creatingproductv1commands.NewCreateProductHandler(c.Log, c.RabbitmqPublisher, c.ProductRepository, c.Ctx, c.GrpcClient) 38 | } 39 | 40 | func (c *createProductHandlerUnitTests) Test_Handle_Should_Create_New_Product_With_Valid_Data() { 41 | 42 | createProductCommand := &creatingproductv1commands.CreateProduct{ 43 | ProductID: uuid.NewV4(), 44 | Name: gofakeit.Name(), 45 | CreatedAt: time.Now(), 46 | Description: gofakeit.EmojiDescription(), 47 | Price: gofakeit.Price(100, 1000), 48 | InventoryId: 1, 49 | Count: 1, 50 | } 51 | 52 | product := test_data.Products[0] 53 | 54 | c.ProductRepository.On("CreateProduct", mock.Anything, mock.Anything). 55 | Once(). 56 | Return(product, nil) 57 | 58 | dto, err := c.createProductHandler.Handle(c.Ctx, createProductCommand) 59 | 60 | assert.NoError(c.T, err) 61 | 62 | c.ProductRepository.AssertNumberOfCalls(c.T, "CreateProduct", 1) 63 | c.RabbitmqPublisher.AssertNumberOfCalls(c.T, "PublishMessage", 1) 64 | assert.Equal(c.T, dto.ProductId, createProductCommand.ProductID) 65 | } 66 | -------------------------------------------------------------------------------- /internal/services/product_service/tests/unit_tests/mocks/IdentityServiceClient.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.16.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | grpc "google.golang.org/grpc" 9 | 10 | identity_service "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/grpc_client/protos" 11 | mock "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | // IdentityServiceClient is an autogenerated mock type for the IdentityServiceClient type 15 | type IdentityServiceClient struct { 16 | mock.Mock 17 | } 18 | 19 | // GetUserById provides a mock function with given fields: ctx, in, opts 20 | func (_m *IdentityServiceClient) GetUserById(ctx context.Context, in *identity_service.GetUserByIdReq, opts ...grpc.CallOption) (*identity_service.GetUserByIdRes, error) { 21 | _va := make([]interface{}, len(opts)) 22 | for _i := range opts { 23 | _va[_i] = opts[_i] 24 | } 25 | var _ca []interface{} 26 | _ca = append(_ca, ctx, in) 27 | _ca = append(_ca, _va...) 28 | ret := _m.Called(_ca...) 29 | 30 | var r0 *identity_service.GetUserByIdRes 31 | if rf, ok := ret.Get(0).(func(context.Context, *identity_service.GetUserByIdReq, ...grpc.CallOption) *identity_service.GetUserByIdRes); ok { 32 | r0 = rf(ctx, in, opts...) 33 | } else { 34 | if ret.Get(0) != nil { 35 | r0 = ret.Get(0).(*identity_service.GetUserByIdRes) 36 | } 37 | } 38 | 39 | var r1 error 40 | if rf, ok := ret.Get(1).(func(context.Context, *identity_service.GetUserByIdReq, ...grpc.CallOption) error); ok { 41 | r1 = rf(ctx, in, opts...) 42 | } else { 43 | r1 = ret.Error(1) 44 | } 45 | 46 | return r0, r1 47 | } 48 | 49 | type mockConstructorTestingTNewIdentityServiceClient interface { 50 | mock.TestingT 51 | Cleanup(func()) 52 | } 53 | 54 | // NewIdentityServiceClient creates a new instance of IdentityServiceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 55 | func NewIdentityServiceClient(t mockConstructorTestingTNewIdentityServiceClient) *IdentityServiceClient { 56 | mock := &IdentityServiceClient{} 57 | mock.Mock.Test(t) 58 | 59 | t.Cleanup(func() { mock.AssertExpectations(t) }) 60 | 61 | return mock 62 | } 63 | -------------------------------------------------------------------------------- /internal/services/product_service/tests/unit_tests/mocks/IdentityServiceServer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.16.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | identity_service "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/grpc_client/protos" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // IdentityServiceServer is an autogenerated mock type for the IdentityServiceServer type 13 | type IdentityServiceServer struct { 14 | mock.Mock 15 | } 16 | 17 | // GetUserById provides a mock function with given fields: _a0, _a1 18 | func (_m *IdentityServiceServer) GetUserById(_a0 context.Context, _a1 *identity_service.GetUserByIdReq) (*identity_service.GetUserByIdRes, error) { 19 | ret := _m.Called(_a0, _a1) 20 | 21 | var r0 *identity_service.GetUserByIdRes 22 | if rf, ok := ret.Get(0).(func(context.Context, *identity_service.GetUserByIdReq) *identity_service.GetUserByIdRes); ok { 23 | r0 = rf(_a0, _a1) 24 | } else { 25 | if ret.Get(0) != nil { 26 | r0 = ret.Get(0).(*identity_service.GetUserByIdRes) 27 | } 28 | } 29 | 30 | var r1 error 31 | if rf, ok := ret.Get(1).(func(context.Context, *identity_service.GetUserByIdReq) error); ok { 32 | r1 = rf(_a0, _a1) 33 | } else { 34 | r1 = ret.Error(1) 35 | } 36 | 37 | return r0, r1 38 | } 39 | 40 | type mockConstructorTestingTNewIdentityServiceServer interface { 41 | mock.TestingT 42 | Cleanup(func()) 43 | } 44 | 45 | // NewIdentityServiceServer creates a new instance of IdentityServiceServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 46 | func NewIdentityServiceServer(t mockConstructorTestingTNewIdentityServiceServer) *IdentityServiceServer { 47 | mock := &IdentityServiceServer{} 48 | mock.Mock.Test(t) 49 | 50 | t.Cleanup(func() { mock.AssertExpectations(t) }) 51 | 52 | return mock 53 | } 54 | -------------------------------------------------------------------------------- /internal/services/product_service/tests/unit_tests/mocks/UnsafeIdentityServiceServer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.16.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // UnsafeIdentityServiceServer is an autogenerated mock type for the UnsafeIdentityServiceServer type 8 | type UnsafeIdentityServiceServer struct { 9 | mock.Mock 10 | } 11 | 12 | // mustEmbedUnimplementedIdentityServiceServer provides a mock function with given fields: 13 | func (_m *UnsafeIdentityServiceServer) mustEmbedUnimplementedIdentityServiceServer() { 14 | _m.Called() 15 | } 16 | 17 | type mockConstructorTestingTNewUnsafeIdentityServiceServer interface { 18 | mock.TestingT 19 | Cleanup(func()) 20 | } 21 | 22 | // NewUnsafeIdentityServiceServer creates a new instance of UnsafeIdentityServiceServer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 23 | func NewUnsafeIdentityServiceServer(t mockConstructorTestingTNewUnsafeIdentityServiceServer) *UnsafeIdentityServiceServer { 24 | mock := &UnsafeIdentityServiceServer{} 25 | mock.Mock.Test(t) 26 | 27 | t.Cleanup(func() { mock.AssertExpectations(t) }) 28 | 29 | return mock 30 | } 31 | -------------------------------------------------------------------------------- /internal/services/product_service/tests/unit_tests/test_data/product.go: -------------------------------------------------------------------------------- 1 | package test_data 2 | 3 | import ( 4 | "github.com/brianvoe/gofakeit/v6" 5 | "github.com/meysamhadeli/shop-golang-microservices/internal/services/product_service/product/models" 6 | uuid "github.com/satori/go.uuid" 7 | "time" 8 | ) 9 | 10 | var Products = []*models.Product{ 11 | { 12 | ProductId: uuid.NewV4(), 13 | Name: gofakeit.Name(), 14 | CreatedAt: time.Now(), 15 | Description: gofakeit.AdjectiveDescriptive(), 16 | Price: gofakeit.Price(100, 1000), 17 | }, 18 | { 19 | ProductId: uuid.NewV4(), 20 | Name: gofakeit.Name(), 21 | CreatedAt: time.Now(), 22 | Description: gofakeit.AdjectiveDescriptive(), 23 | Price: gofakeit.Price(100, 1000), 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /shop.rest: -------------------------------------------------------------------------------- 1 | # https://github.com/Huachao/vscode-restclient 2 | @identity-api=http://localhost:5002 3 | @product-api=http://localhost:5000 4 | 5 | ################################# Identity API ################################# 6 | 7 | ### 8 | # @name ApiRoot_Identity 9 | GET {{identity-api}} 10 | ### 11 | 12 | 13 | ### 14 | # @name Token 15 | GET {{identity-api}}/connect/token?grant_type=password&client_id=clientId&client_secret=clientSecret&scope=all&username=admin_user&password=Admin@12345 16 | Content-Type: application/json 17 | accept: application/json 18 | ### 19 | 20 | 21 | ### 22 | # @name Validate-Token 23 | GET {{identity-api}}/validate-token 24 | Content-Type: application/json 25 | authorization: Bearer {{Token.response.body.access_token}} 26 | accept: application/json 27 | ### 28 | 29 | ################################# Product API ################################# 30 | 31 | ### 32 | # @name ApiRoot_Product 33 | GET {{product-api}} 34 | ### 35 | 36 | ### 37 | # @name Create 38 | Post {{product-api}}/api/v1/products 39 | accept: application/json 40 | Content-Type: application/json 41 | authorization: Bearer {{Token.response.body.access_token}} 42 | 43 | { 44 | "description": "test-desc", 45 | "name": "test-product", 46 | "price": 20, 47 | "inventoryId": 1, 48 | "count": 10 49 | } 50 | ### 51 | 52 | ### 53 | # @name Update 54 | Put {{product-api}}/api/v1/products/09d7ef0b-b1ba-4c4c-a44b-9541fc8719bb 55 | accept: application/json 56 | Content-Type: application/json 57 | authorization: Bearer {{Token.response.body.access_token}} 58 | 59 | { 60 | "description": "test-desc", 61 | "name": "test-product", 62 | "price": 50, 63 | "inventoryId": 1, 64 | "count": 10 65 | } 66 | ### 67 | 68 | --------------------------------------------------------------------------------