├── .air.http.toml ├── .env.example ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── http.go ├── kafka.go ├── root.go └── rsa_generator.go ├── docker-compose.yml ├── docker ├── .gitignore ├── api.Dockerfile └── db │ └── initdb │ └── postgres-init.sh ├── go.mod ├── go.sum ├── main.go ├── migrations ├── 20221023161513_create_items.down.sql └── 20221023161513_create_items.up.sql ├── src ├── applications │ ├── commands │ │ └── oauth │ │ │ ├── createTokenAuthCodeCommand.go │ │ │ ├── createTokenClientCredentialCommand.go │ │ │ └── createTokenCommandContract.go │ ├── dto │ │ ├── bank.go │ │ └── token.go │ ├── listeners │ │ ├── listener.go │ │ ├── sendEmailConfirmationListener.go │ │ └── sendVerificationEmailListener.go │ ├── provider.go │ ├── services │ │ ├── oauth │ │ │ ├── createTokenAuthCodeService.go │ │ │ ├── createTokenClientCredentialService.go │ │ │ └── createTokenServiceContract.go │ │ └── sendEmailService.go │ ├── usecases │ │ ├── bankUsecase.go │ │ ├── bank_usecase.go │ │ ├── oauthUsecase.go │ │ ├── tokenUsecase.go │ │ └── usecase.go │ └── wire_gen.go ├── bootstrap │ └── app.go ├── domain │ ├── entities │ │ ├── bank.go │ │ ├── jwt.go │ │ ├── oauthAccessToken.go │ │ ├── oauthAuthCode.go │ │ ├── oauthClient.go │ │ ├── token.go │ │ └── user.go │ ├── errors │ │ ├── dataInvalid.go │ │ ├── databaseCommandError.go │ │ ├── databaseQueryError.go │ │ ├── domain.go │ │ ├── eventDuplicateError.go │ │ ├── fieldsRequired.go │ │ ├── internalServerError.go │ │ ├── invalidHeader.go │ │ ├── oauth │ │ │ └── clientIdAndClientSecretNotFound.go │ │ ├── queryParamInvalid.go │ │ ├── recordFieldsNotFound.go │ │ └── recordNotFound.go │ ├── events │ │ ├── event.go │ │ ├── order_created.go │ │ └── user_registered.go │ ├── repositories │ │ ├── bankRepository.go │ │ ├── baseRepository.go │ │ ├── oauthAccessTokenRepository.go │ │ ├── oauthAuthCodeRepository.go │ │ └── oauthClientRepository.go │ └── valueObjects │ │ └── timestamp.go ├── factories │ ├── commands │ │ └── create_token.go │ ├── entities │ │ ├── oauthAccessToken.go │ │ └── token.go │ └── services │ │ └── create_token_service.go ├── infrastructure │ ├── config │ │ ├── app.go │ │ ├── database.go │ │ └── jwt.go │ ├── constants │ │ ├── common.go │ │ ├── database.go │ │ ├── error.go │ │ ├── errorCodes.go │ │ ├── event.go │ │ ├── listener.go │ │ ├── mongoDbCollection.go │ │ ├── oauth.go │ │ └── timeFormat.go │ ├── errors │ │ ├── problemdetails.go │ │ └── validation.go │ ├── external │ │ └── http │ │ │ └── httpClient.go │ ├── persistance │ │ └── database │ │ │ ├── database.go │ │ │ ├── gorm_repository.go │ │ │ ├── models │ │ │ ├── bank.go │ │ │ ├── oauthAccessToken.go │ │ │ ├── oauthAuthCode.go │ │ │ ├── oauthClient.go │ │ │ └── user.go │ │ │ ├── mysql │ │ │ ├── db.go │ │ │ └── repositories │ │ │ │ └── bankRepository.go │ │ │ ├── postgres │ │ │ ├── db.go │ │ │ └── repositories │ │ │ │ ├── bankRepository.go │ │ │ │ ├── oauthAccessTokenRepository.go │ │ │ │ └── oauthClientRepository.go │ │ │ ├── provider.go │ │ │ └── wire_gen.go │ └── utils │ │ ├── common.go │ │ ├── config.go │ │ ├── eventEmitter.go │ │ ├── gracefulShutdown.go │ │ ├── log.go │ │ ├── response.go │ │ ├── text.go │ │ └── workerpool.go └── interfaces │ └── rest │ ├── form_request │ ├── bankRequest.go │ ├── bank_request.go │ ├── oauth2Request.go │ └── request.go │ ├── handler.go │ ├── middleware │ └── timeout.go │ ├── provider.go │ ├── rest.go │ ├── routes │ ├── contracts │ │ └── resources.go │ ├── oauth2 │ │ ├── handler │ │ │ ├── authorize.go │ │ │ ├── client.go │ │ │ ├── index.go │ │ │ └── token.go │ │ └── index.go │ └── v1 │ │ └── simkah_app │ │ ├── handler │ │ └── bank.go │ │ └── index.go │ └── wire_gen.go ├── storage └── .gitignore └── tests ├── integration ├── database │ └── postgres │ │ ├── oauthClientRepository_test.go │ │ └── postgresSuite_test.go ├── factories │ └── oauthClientFactory.go └── interfaces │ └── .gitkeep ├── mocks └── repositories │ ├── oauthAccessTokenMock.go │ └── oauthClientRepositoryMock.go ├── test.go └── unit ├── applications ├── .gitkeep └── services │ └── oauth │ └── createTokenClientCredentialService_test.go └── domain ├── .gitkeep ├── bank_test.go └── main_test.go /.air.http.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | bin = "./tmp/main http" 7 | cmd = "go build -o ./tmp/main ." 8 | delay = 1000 9 | exclude_dir = ["assets", "tmp", "vendor", "testdata", "storage", "docker"] 10 | exclude_file = [] 11 | exclude_regex = ["_test.go"] 12 | exclude_unchanged = false 13 | follow_symlink = false 14 | full_bin = "" 15 | include_dir = [] 16 | include_ext = ["go", "tpl", "tmpl", "html"] 17 | kill_delay = "0s" 18 | log = "air.log" 19 | send_interrupt = false 20 | stop_on_error = true 21 | args_bin = ["golang-clean-architecture", "http"] 22 | 23 | 24 | [color] 25 | app = "" 26 | build = "yellow" 27 | main = "magenta" 28 | runner = "green" 29 | watcher = "cyan" 30 | 31 | [log] 32 | time = false 33 | 34 | [misc] 35 | clean_on_exit = false 36 | 37 | [screen] 38 | clear_on_rebuild = false 39 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # application config 2 | APP_ENV=LOCAL 3 | APP_NAME="Golang Clean Architecture" 4 | APP_KEY= 5 | APP_URL="http://localhost:8080" 6 | 7 | # Database Config 8 | DB_DRIVER= 9 | DB_HOST= 10 | DB_USERNAME= 11 | DB_PASSWORD= 12 | DB_NAME= 13 | DB_PORT= 14 | DB_SSL_MODE= 15 | DB_SCHEMA= 16 | 17 | # logging 18 | LOG_NAME=ms-simkah-api 19 | 20 | # http 21 | HTTP_PORT=8080 22 | HTTP_TIMEOUT=60 23 | 24 | # Redis 25 | REDIS_ADDRESS=localhost:6379 26 | REDIS_PASSWORD= 27 | REDIS_DB=0 28 | 29 | # Mongo Config 30 | MONGO_DSN= 31 | 32 | # in seconds 33 | GRACEFUL_SHUTDOWN_TIMEOUT=5 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Go workspace file 20 | go.work 21 | 22 | # Folders 23 | bin/ 24 | tmp/ 25 | vendor/ 26 | logs 27 | .idea 28 | _obj 29 | _test 30 | 31 | # Dependency directories (remove the comment below to include it) 32 | # vendor/ 33 | 34 | # Optional eslint cache 35 | .eslintcache 36 | 37 | # Editor spesific 38 | .DS_Store 39 | .editorconfig 40 | .vscode 41 | 42 | # Env files 43 | .env.local 44 | .env.development.local 45 | .env.test.local 46 | .env.production.local 47 | .env 48 | 49 | migrations 50 | *.pem 51 | *.pem 52 | *private.key 53 | *public.key -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ENV := $(PWD)/.env 2 | 3 | # Environment variables for project 4 | include $(ENV) 5 | 6 | migrate: 7 | docker compose --profile tools run migrate 8 | migrate-rollback: 9 | docker compose --profile tools run migrate-rollback 10 | 11 | migrate-both: 12 | docker compose --profile tools run migrate 13 | docker compose --profile tools run migrate-db-test 14 | 15 | migrate-rollback-both: 16 | docker compose --profile tools run migrate-rollback-db-test 17 | docker compose --profile tools run migrate-rollback -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | ### Domain-Driven Design (DDD) Folder Structure & Wiki 4 | 5 | This project follows the principles of Domain-Driven Design (DDD) and uses the following folder structure to organize the codebase: 6 | ```` 7 | root 8 | |-- cmd 9 | | |-- http.go 10 | | |-- kafka.go 11 | | |-- root.go 12 | |-- docker 13 | | |-- db 14 | | |-- api.Dockerfile 15 | |-- migrations 16 | | |-- create_table_xyz.up.go 17 | | |-- create_table_xyz.down.go 18 | |-- src 19 | | |-- applications 20 | | | |-- commands 21 | | | |-- dto 22 | | | |-- listeners 23 | | | |-- services 24 | | | |-- usecases 25 | | | |-- provider.go 26 | | | |-- wire_gen.go 27 | | |-- bootstrap 28 | | | |-- app.go 29 | | |-- domain 30 | | | |-- entities 31 | | | | |-- entity.go 32 | | | |-- errors 33 | | | | | internalServerError.go. 34 | | | | | badRequest.go. 35 | | | |-- events 36 | | | | | orderCreated.go. 37 | | | | | userRegistered.go. 38 | | | |-- repositories 39 | | | | |-- userRepository.go 40 | | | | |-- orderRepository.go 41 | | | |-- valueObjects 42 | | | | |-- email.go 43 | | | | |-- mooney.go 44 | | | | |-- timestamps.go 45 | |-- factories 46 | | |-- commands 47 | | |-- entities 48 | | |-- services 49 | |-- infrastructure 50 | | |-- config 51 | | |-- constants 52 | | |-- errors 53 | | |-- external 54 | | |-- messaging 55 | | |-- persistence 56 | | | |-- database.go 57 | | |-- messaging 58 | | | |-- message_queue.go 59 | | |-- utils 60 | | | |-- error.go 61 | |-- interfaces 62 | | |-- rest 63 | | |-- grpc 64 | | |-- kafka 65 | | |-- graphql 66 | |-- main.go 67 | |-- storage 68 | | |-- logs 69 | |-- tests 70 | | |-- integration 71 | | | |-- database 72 | | | |-- interfaces 73 | | | |-- controller 74 | | |-- mocks 75 | | |-- unit 76 | | | |--applications 77 | | | |--domain 78 | 79 | ```` 80 | 81 | - The domain folder contains all the business logic of the application. It is divided into two subfolders: model and services. 82 | 83 | - The domain folder contains all the entities, value objects, events, repositories, errors that make up the application's domain model. 84 | - Entities are objects that have an identity and encapsulate the business rules of the application. 85 | - > In Domain-Driven Design (DDD), entities can have business logic, as they represent business concepts or objects and it makes sense that they have their own behavior and logic. Entities are responsible for maintaining the state of the domain and encapsulating the data and methods that are specific to that business concept. 86 | - > Entities can have methods that implement business rules, validation, or other business-specific logic. For example, an Order entity may have methods for calculating the total cost of the order, checking for discounts, or applying taxes. 87 | - > However, it's important to note that entities should only contain logic that is directly related to the state of the entity. Business logic that is not directly related to the state of the entity should be handled by services. This helps to separate the concerns of the entities and services, making the system more maintainable and flexible. 88 | - > So, in short, entities can have business logic, but it should be logic that is directly related to the state of the entity and should not contain any logic that is not directly related to the state of the entity, that should be handled by services. 89 | - Value objects are objects that don't have an identity and are used to represent simple values like money or date. 90 | - The services folder contains domain services that are used to perform complex business logic that doesn't belong to any specific entity or value object. 91 | - >**Services** are used to encapsulate business logic that does not fit into an entity or value object. 92 | > 93 | > Services are typically stateless and used for tasks that don't have a lifecycle, like calculating a value or providing information. 94 | > 95 | > Services are usually stateless, and their methods are usually more procedural than object-oriented. They can also be used to encapsulate cross-cutting concerns, like logging or security. Services can be used by multiple usecases. 96 | - The infrastructure folder contains all the technical logic of the application. It is divided into three subfolders: persistence, messaging, and external. 97 | - > The **persistence** folder contains the code responsible for storing and retrieving data from a database. 98 | - > The **messaging** folder contains the code responsible for sending and receiving messages through message queues. 99 | - > The **external** folder contains the code responsible for communicating with external systems, such as APIs. 100 | - The **application** folder contains the code responsible for orchestrating the usecases of the application. 101 | 102 | - The **usecase** folder contains the code for the usecases of the application, which are use to orchestrate the services and entities to accomplish specific tasks. 103 | - > **Use cases** represent the business logic that handles a specific business goal or objective. 104 | > 105 | > These are the interactions between the application and the user. They are used to handle the input from the external services, translate it to the domain model, and handle the output that is sent to external services. 106 | > 107 | > They are also responsible for enforcing the business rules and orchestrating the interaction between entities and value objects. 108 | - > In Domain-Driven Design (DDD), it is generally recommended that use cases (also known as application services) do not access entities directly. Instead, use cases should interact with entities through services. 109 | > 110 | > The idea behind this is to separate the concerns of the `use cases` and `entities`, so that changes to the use cases do not affect the entities, and vice versa. By having services as a layer between the use cases and entities, it becomes easier to make changes to the system without affecting the other parts of the system. 111 | > 112 | > Services provide a way to encapsulate complex logic and behavior that is not specific to any particular entity. They are responsible for implementing the use cases or business processes of the system and can use one or more entities to perform their operations. Services can also interact with other services to coordinate the overall behavior of the system. 113 | > 114 | > Additionally, use case should not have knowledge about how the entities are stored or retrieved, so it's better to have services handle this type of operation, so the use case can focus on the business process and the service take care of the technical details. 115 | > 116 | > It's worth noting that there are some exceptions, for example, if the use case is a simple CRUD operation, it can be fine to access the entities directly, but in general, it's a good practice to have services as a layer between the use cases and entities. 117 | 118 | - The difference between service and usecase 119 | - > **Use cases** provide a high-level description of a specific business capability that a user wants to achieve. They outline the steps that the system needs to take to complete the task and define the problem that the system is trying to solve. 120 | - > **Services**, on the other hand, are responsible for implementing the steps outlined in the use case and coordinating the activities between domain entities, aggregates, and infrastructure components. They can make use of domain entities and aggregates, perform specific tasks, and access infrastructure components as needed to complete the use case. 121 | - > So, in summary, use cases provide a high-level definition of a business capability, while services implement the steps needed to complete the use case. 122 | - The cmd/root.go file is the entry point of the application. 123 | 124 | This folder structure allows for a clear separation of concerns and makes it easy to understand the different responsibilities of each part of the codebase. 125 | 126 | ### Dependency Injection 127 | - This project uses dependency injection to manage the dependencies between different parts of the codebase. 128 | This makes it easy to swap out the implementation of different interfaces with mocks or different implementations during testing and makes it easier to change the implementation of a specific feature. 129 | 130 | ### Database Setup 131 | 132 | #### Docker 133 | 134 | - create migration file 135 | ```bash 136 | docker compose --profile tools run create-migration 137 | ``` 138 | 139 |

140 | Because we've mounted the /tmp/migrations folder in the container to the local project folder 141 | /migrations, we can see two new files appear in migrations folder. 142 | One for our up migration, and one for down 143 |

144 | 145 | - run migration 146 | ```bash 147 | docker compose --profile tools run migrate 148 | ``` 149 | 150 | #### Manual Installation 151 | 152 | Install migrate CLI tools to manage database versioning 153 | 154 | Linux: 155 | 156 | ```bash 157 | curl -L https://packagecloud.io/golang-migrate/migrate/gpgkey | apt-key add - 158 | echo "deb https://packagecloud.io/golang-migrate/migrate/ubuntu/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/migrate.list 159 | apt-get update 160 | apt-get install -y migrate 161 | ``` 162 | 163 | Mac: 164 | 165 | ```bash 166 | brew install golang-migrate 167 | ``` 168 | 169 | Run migration script 170 | 171 | 172 | ```bash 173 | migrate -database "postgresql://username:password@localhost:5432/test?sslmode=disable" -path migrations/ up 174 | -------------------------------------------------------------------------------- /cmd/http.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/google/gops/agent" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/applications" 9 | "github.com/hendrorahmat/golang-clean-architecture/src/bootstrap" 10 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database" 11 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/utils" 12 | "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest" 13 | "github.com/spf13/cobra" 14 | "net/http" 15 | "time" 16 | ) 17 | 18 | var httpCommand = &cobra.Command{ 19 | Use: "http", 20 | Short: "Run HTTP Api", 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | ctx := context.Background() 23 | application := bootstrap.Boot() 24 | 25 | repositories := database.InjectRepository(application.GetActiveConnection().DB(), application.GetLogger()) 26 | usecases := applications.InjectUsecase(repositories, application.GetLogger()) 27 | controllers := rest.InjectHandler(usecases, application.GetLogger()) 28 | 29 | router := rest.NewRoute(ctx, controllers, application.GetConfig()) 30 | srv := &http.Server{ 31 | Addr: ":" + application.GetConfig().Http.Port, 32 | Handler: router, 33 | } 34 | 35 | go func() { 36 | if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { 37 | application.GetLogger().Warnf("listen %s\n", err) 38 | } 39 | }() 40 | 41 | if err := agent.Listen(agent.Options{}); err != nil { 42 | application.GetLogger().Info("Gops Error") 43 | application.GetLogger().Fatal(err) 44 | } 45 | application.GetLogger().Info("Server listen ", application.GetConfig().Http.Port) 46 | timeout := time.Duration(application.GetConfig().App.GracefulShutdownTimeout) * time.Second 47 | 48 | operations := map[string]utils.GracefulOperation{} 49 | 50 | for c, connection := range application.GetConnections().Connection { 51 | operations["database-"+c] = func(ctx context.Context) error { 52 | return connection.SqlDB().Close() 53 | } 54 | } 55 | 56 | operations["http-server"] = func(ctx context.Context) error { 57 | return srv.Shutdown(ctx) 58 | } 59 | 60 | wait := utils.GracefulShutdown(ctx, application.GetLogger(), timeout, operations) 61 | <-wait 62 | fmt.Println("Closed!") 63 | 64 | return nil 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /cmd/kafka.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/config" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/utils" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var rootCommand = &cobra.Command{} 10 | var err error 11 | 12 | func Execute() error { 13 | utils.LoadEnv() 14 | conf := config.Make() 15 | rootCommand.Use = utils.ToKebabCase(conf.App.Name) 16 | rootCommand.AddCommand(httpCommand) 17 | rootCommand.AddCommand(rsaGenerator) 18 | return rootCommand.Execute() 19 | } 20 | -------------------------------------------------------------------------------- /cmd/rsa_generator.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "fmt" 9 | "github.com/spf13/cobra" 10 | "os" 11 | ) 12 | 13 | var rsaGenerator = &cobra.Command{ 14 | Use: "generate:credentials {name}", 15 | Args: cobra.MinimumNArgs(1), 16 | Short: "Generate public secret key", 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | // generate key 19 | privatekey, err := rsa.GenerateKey(rand.Reader, 2048) 20 | if err != nil { 21 | fmt.Printf("Cannot generate RSA key\n") 22 | os.Exit(1) 23 | } 24 | publickey := &privatekey.PublicKey 25 | 26 | // dump private key to file 27 | var privateKeyBytes []byte = x509.MarshalPKCS1PrivateKey(privatekey) 28 | privateKeyBlock := &pem.Block{ 29 | Type: "RSA PRIVATE KEY", 30 | Bytes: privateKeyBytes, 31 | } 32 | privatePem, err := os.Create("storage/" + args[0] + "-" + "private.key") 33 | if err != nil { 34 | fmt.Printf("error when create private.pem: %s \n", err) 35 | os.Exit(1) 36 | } 37 | err = pem.Encode(privatePem, privateKeyBlock) 38 | if err != nil { 39 | fmt.Printf("error when encode private pem: %s \n", err) 40 | os.Exit(1) 41 | } 42 | 43 | // dump public key to file 44 | publicKeyBytes, err := x509.MarshalPKIXPublicKey(publickey) 45 | if err != nil { 46 | fmt.Printf("error when dumping publickey: %s \n", err) 47 | os.Exit(1) 48 | } 49 | publicKeyBlock := &pem.Block{ 50 | Type: "PUBLIC KEY", 51 | Bytes: publicKeyBytes, 52 | } 53 | publicPem, err := os.Create("storage/" + args[0] + "-" + "public.key") 54 | if err != nil { 55 | fmt.Printf("error when create public.pem: %s \n", err) 56 | os.Exit(1) 57 | } 58 | err = pem.Encode(publicPem, publicKeyBlock) 59 | if err != nil { 60 | fmt.Printf("error when encode public pem: %s \n", err) 61 | os.Exit(1) 62 | } 63 | return nil 64 | }, 65 | } 66 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | app: 5 | build: 6 | dockerfile: docker/api.Dockerfile 7 | context: . 8 | target: dev 9 | volumes: 10 | - .:/opt/app/api 11 | env_file: ./.env 12 | ports: 13 | - "${HTTP_PORT}:${HTTP_PORT}" 14 | links: 15 | - db 16 | db: 17 | image: postgres:15-alpine 18 | volumes: 19 | - ./docker/db/initdb/postgres-init.sh:/docker-entrypoint-initdb.d/postgres-init.sh 20 | - ./docker/db/postgres/data:/var/lib/postgresql/data 21 | ports: 22 | - "15432:5432" 23 | environment: 24 | - POSTGRES_USER=${DB_USERNAME} # from .env 25 | - POSTGRES_PASSWORD=${DB_PASSWORD} # from .env 26 | - POSTGRES_DB=${DB_NAME} 27 | - POSTGRES_HOST_AUTH_METHOD=trust 28 | - POSTGRES_SCHEMA=${DB_SCHEMA} 29 | - APP_ENV=${APP_ENV} 30 | 31 | migrate: &basemigrate 32 | profiles: [ "tools" ] 33 | image: migrate/migrate 34 | entrypoint: "migrate -database 'postgres://${DB_USERNAME}@db/${DB_NAME}?sslmode=disable&search_path=${DB_SCHEMA}' -path /tmp/migrations" 35 | command: up 36 | links: 37 | - db 38 | volumes: 39 | - ./migrations:/tmp/migrations 40 | 41 | migrate-db-test: 42 | profiles: [ "tools" ] 43 | image: migrate/migrate 44 | entrypoint: "migrate -database 'postgres://${DB_USERNAME}@db/${DB_NAME}_test?sslmode=disable&search_path=${DB_SCHEMA}_test' -path /tmp/migrations" 45 | command: up 46 | links: 47 | - db 48 | volumes: 49 | - ./migrations:/tmp/migrations 50 | 51 | migrate-rollback: &basemigrate 52 | profiles: [ "tools" ] 53 | image: migrate/migrate 54 | entrypoint: "migrate -database 'postgres://${DB_USERNAME}@db/${DB_NAME}?sslmode=disable&search_path=${DB_SCHEMA}' -path /tmp/migrations" 55 | command: down 56 | links: 57 | - db 58 | volumes: 59 | - ./migrations:/tmp/migrations 60 | 61 | migrate-rollback-db-test: 62 | profiles: [ "tools" ] 63 | image: migrate/migrate 64 | entrypoint: "migrate -database 'postgres://${DB_USERNAME}@db/${DB_NAME}_test?sslmode=disable&search_path=${DB_SCHEMA}_test' -path /tmp/migrations" 65 | command: down 66 | links: 67 | - db 68 | volumes: 69 | - ./migrations:/tmp/migrations 70 | 71 | create-migration: 72 | <<: *basemigrate 73 | entrypoint: migrate create -dir /tmp/migrations -ext sql 74 | command: "" 75 | -------------------------------------------------------------------------------- /docker/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | 3 | postgres 4 | -------------------------------------------------------------------------------- /docker/api.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19.1 AS base 2 | FROM base as dev 3 | RUN curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s -- -b $(go env GOPATH)/bin 4 | WORKDIR /opt/app/api 5 | 6 | CMD ["air", "-c", ".air.http.toml"] 7 | 8 | FROM base as built 9 | 10 | WORKDIR /app/api 11 | COPY . . 12 | ENV CGO_ENABLED=0 13 | ENV GO111MODULE=on 14 | 15 | RUN go get -d -v ./... 16 | RUN go build -o api-server ./*.go 17 | 18 | FROM busybox 19 | WORKDIR /app 20 | EXPOSE ${HTTP_PORT} 21 | COPY --from=built /app/api/ /app/ 22 | CMD ["./api-server", "http"] -------------------------------------------------------------------------------- /docker/db/initdb/postgres-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | export PGPASSWORD=$POSTGRES_PASSWORD; 5 | env=${APP_ENV:-production} 6 | 7 | echo "$env" 8 | 9 | if [ "$env" != "local" ]; then 10 | exit 1 11 | fi 12 | 13 | echo "Creating database ${POSTGRES_DB} and test db ....." 14 | 15 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 16 | CREATE SCHEMA IF NOT EXISTS ${POSTGRES_SCHEMA}; 17 | SET search_path To "${POSTGRES_SCHEMA}"; 18 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 19 | CREATE DATABASE "${POSTGRES_DB}_test"; 20 | EOSQL 21 | 22 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "${POSTGRES_DB}_test" <<-EOSQL 23 | CREATE SCHEMA IF NOT EXISTS "${POSTGRES_SCHEMA}_test"; 24 | SET search_path To "${POSTGRES_SCHEMA}_test"; 25 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 26 | EOSQL -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hendrorahmat/golang-clean-architecture 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/brianvoe/gofakeit/v6 v6.20.1 7 | github.com/gin-gonic/gin v1.8.1 8 | github.com/go-ozzo/ozzo-validation/v4 v4.3.0 9 | github.com/go-testfixtures/testfixtures/v3 v3.8.1 10 | github.com/gofrs/uuid v4.3.1+incompatible 11 | github.com/golang-jwt/jwt/v4 v4.4.3 12 | github.com/golang-migrate/migrate/v4 v4.15.2 13 | github.com/google/gops v0.3.25 14 | github.com/google/wire v0.5.0 15 | github.com/joho/godotenv v1.4.0 16 | github.com/lib/pq v1.10.7 17 | github.com/mattn/go-colorable v0.1.12 18 | github.com/sirupsen/logrus v1.8.1 19 | github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d 20 | github.com/spf13/cobra v1.5.0 21 | github.com/stretchr/testify v1.8.1 22 | gorm.io/driver/mysql v1.3.6 23 | gorm.io/driver/postgres v1.3.9 24 | gorm.io/gorm v1.24.3 25 | ) 26 | 27 | require ( 28 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/gin-contrib/sse v0.1.0 // indirect 31 | github.com/go-playground/locales v0.14.0 // indirect 32 | github.com/go-playground/universal-translator v0.18.0 // indirect 33 | github.com/go-playground/validator/v10 v10.10.0 // indirect 34 | github.com/go-sql-driver/mysql v1.6.0 // indirect 35 | github.com/goccy/go-json v0.9.7 // indirect 36 | github.com/google/go-cmp v0.5.9 // indirect 37 | github.com/hashicorp/errwrap v1.1.0 // indirect 38 | github.com/hashicorp/go-multierror v1.1.1 // indirect 39 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 40 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 41 | github.com/jackc/pgconn v1.13.0 // indirect 42 | github.com/jackc/pgio v1.0.0 // indirect 43 | github.com/jackc/pgpassfile v1.0.0 // indirect 44 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 45 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 46 | github.com/jackc/pgtype v1.12.0 // indirect 47 | github.com/jackc/pgx/v4 v4.17.2 // indirect 48 | github.com/jinzhu/inflection v1.0.0 // indirect 49 | github.com/jinzhu/now v1.1.5 // indirect 50 | github.com/json-iterator/go v1.1.12 // indirect 51 | github.com/leodido/go-urn v1.2.1 // indirect 52 | github.com/mattn/go-isatty v0.0.14 // indirect 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 54 | github.com/modern-go/reflect2 v1.0.2 // indirect 55 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 56 | github.com/pmezard/go-difflib v1.0.0 // indirect 57 | github.com/spf13/pflag v1.0.5 // indirect 58 | github.com/stretchr/objx v0.5.0 // indirect 59 | github.com/ugorji/go/codec v1.2.7 // indirect 60 | go.uber.org/atomic v1.7.0 // indirect 61 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect 62 | golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect 63 | golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 // indirect 64 | golang.org/x/text v0.7.0 // indirect 65 | google.golang.org/protobuf v1.28.0 // indirect 66 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 67 | gopkg.in/yaml.v2 v2.4.0 // indirect 68 | gopkg.in/yaml.v3 v3.0.1 // indirect 69 | ) 70 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 4 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 5 | github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= 6 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= 7 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 8 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 9 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 10 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 11 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 13 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 14 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/denisenkom/go-mssqldb v0.12.2 h1:1OcPn5GBIobjWNd+8yjfHNIaFX14B1pWI3F9HZy5KXw= 19 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 20 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 21 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 22 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 23 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 24 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 25 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 26 | github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= 27 | github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= 28 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 29 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 30 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 31 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 32 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 33 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 34 | github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= 35 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 36 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 37 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 38 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 39 | github.com/go-testfixtures/testfixtures/v3 v3.8.1 h1:uonwvepqRvSgddcrReZQhojTlWlmOlHkYAb9ZaOMWgU= 40 | github.com/go-testfixtures/testfixtures/v3 v3.8.1/go.mod h1:Kdu7YeMC0KRXVHdaQ91Vmx3pcjoTF63h4f1qTJDdXLA= 41 | github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= 42 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 43 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 44 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 45 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 46 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 47 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 48 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 49 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 52 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 53 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 54 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 55 | github.com/google/gops v0.3.25 h1:Pf6uw+cO6pDhc7HJ71NiG0x8dyQTeQcmg3HQFF39qVw= 56 | github.com/google/gops v0.3.25/go.mod h1:8A7ebAm0id9K3H0uOggeRVGxszSvnlURun9mg3GdYDw= 57 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 58 | github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 59 | github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= 60 | github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= 61 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 62 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 63 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 64 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 65 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 66 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 67 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 68 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 69 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 70 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 71 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 72 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 73 | github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= 74 | github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= 75 | github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= 76 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 77 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 78 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 79 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 80 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 81 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 82 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 83 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 84 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 85 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 86 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 87 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 88 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 89 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 90 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 91 | github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 92 | github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= 93 | github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 94 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 95 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 96 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 97 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 98 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 99 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 100 | github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 101 | github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= 102 | github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 103 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 104 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 105 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 106 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 107 | github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= 108 | github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= 109 | github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= 110 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 111 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 112 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 113 | github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 114 | github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 115 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 116 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 117 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 118 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 119 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 120 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 121 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 122 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 123 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 124 | github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19/go.mod h1:hY+WOq6m2FpbvyrI93sMaypsttvaIL5nhVR92dTMUcQ= 125 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 126 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 127 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 128 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 129 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 130 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 131 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 132 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 133 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 134 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 135 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 136 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 137 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 138 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 139 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 140 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 141 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 142 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 143 | github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= 144 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 145 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 146 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 147 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 148 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 149 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 150 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 151 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 152 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 153 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 154 | github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= 155 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 156 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 157 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 158 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 159 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= 160 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 161 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 162 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 163 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 164 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 165 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 166 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 167 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 168 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 169 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 170 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 171 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 172 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 173 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 174 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 175 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 176 | github.com/shirou/gopsutil/v3 v3.22.4/go.mod h1:D01hZJ4pVHPpCTZ3m3T2+wDF2YAGfd+H4ifUguaQzHM= 177 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 178 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 179 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 180 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 181 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 182 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 183 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 184 | github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d h1:4660u5vJtsyrn3QwJNfESwCws+TM1CMhRn123xjVyQ8= 185 | github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d/go.mod h1:ZLVe3VfhAuMYLYWliGEydMBoRnfib8EFSqkBYu1ck9E= 186 | github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= 187 | github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= 188 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 189 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 190 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 191 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 192 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 193 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 194 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 195 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 196 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 197 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 198 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 199 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 200 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 201 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 202 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 203 | github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= 204 | github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= 205 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 206 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 207 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 208 | github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= 209 | github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 210 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 211 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 212 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 213 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 214 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 215 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 216 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 217 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 218 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 219 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 220 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 221 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 222 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 223 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 224 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 225 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 226 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 227 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 228 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 229 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 230 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 231 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 232 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 233 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= 234 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 235 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 236 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 237 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 238 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 239 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 240 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 241 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 242 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 243 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 244 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 245 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 246 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 247 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 248 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 249 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 250 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 251 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 252 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 253 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 255 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 257 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 258 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 259 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 260 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 261 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 262 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 263 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 264 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 265 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 266 | golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 h1:OK7RB6t2WQX54srQQYSXMW8dF5C6/8+oA/s5QBmmto4= 267 | golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 268 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 269 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 270 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 271 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 272 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 273 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 274 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 275 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 276 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 277 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 278 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 279 | golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 280 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 281 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 282 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 283 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 284 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 285 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 286 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 287 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 288 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 289 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 290 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 291 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 292 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 293 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 294 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 295 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 296 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 297 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 298 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 299 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 300 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 301 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 302 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 303 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 304 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 305 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 306 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 307 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 308 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 309 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 310 | gorm.io/driver/mysql v1.3.6 h1:BhX1Y/RyALb+T9bZ3t07wLnPZBukt+IRkMn8UZSNbGM= 311 | gorm.io/driver/mysql v1.3.6/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= 312 | gorm.io/driver/postgres v1.3.9 h1:lWGiVt5CijhQAg0PWB7Od1RNcBw/jS4d2cAScBcSDXg= 313 | gorm.io/driver/postgres v1.3.9/go.mod h1:qw/FeqjxmYqW5dBcYNBsnhQULIApQdk7YuuDPktVi1U= 314 | gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 315 | gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE= 316 | gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 317 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 318 | rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= 319 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/hendrorahmat/golang-clean-architecture/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | 8 | //blocking := make(chan bool) 9 | //worker := utils.NewWorkerPool() 10 | //const numTasks = 10 11 | //type Job struct { 12 | // id int 13 | // name string 14 | //} 15 | //for i := 0; i < numTasks; i++ { 16 | // result := fmt.Sprintf("Data ke %d ", i) 17 | // job := Job{name: "job " + result, id: i} 18 | // 19 | // worker.AddTask(func(ctx context.Context) (any, error) { 20 | // return job, nil 21 | // }) 22 | //} 23 | //ctx, cancel := context.WithTimeout(context.Background(), time.Second*411) 24 | //defer cancel() 25 | //worker.Run(ctx) 26 | //fmt.Println(worker.GetResults()) 27 | //<-blocking 28 | } 29 | -------------------------------------------------------------------------------- /migrations/20221023161513_create_items.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE items; 2 | DROP EXTENSION pgcrypto; -------------------------------------------------------------------------------- /migrations/20221023161513_create_items.up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; 2 | 3 | CREATE TABLE items( 4 | id uuid DEFAULT public.gen_random_uuid() NOT NULL, 5 | name character varying NOT NULL 6 | ) -------------------------------------------------------------------------------- /src/applications/commands/oauth/createTokenAuthCodeCommand.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | type CreateTokenAuthCodeCommand struct { 4 | ClientId string 5 | ClientSecret string 6 | Scopes []string 7 | RedirectUri string 8 | } 9 | 10 | func NewCreateTokenAuthCodeCommand( 11 | clientId string, 12 | clientSecret string, 13 | scopes []string, 14 | redirectUri string, 15 | ) IIssueTokenCommand { 16 | return &CreateTokenAuthCodeCommand{ClientId: clientId, 17 | ClientSecret: clientSecret, 18 | Scopes: scopes, 19 | RedirectUri: redirectUri, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/applications/commands/oauth/createTokenClientCredentialCommand.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | type CreateTokenClientCredentialCommand struct { 4 | ClientId string `fake:"{uuid}"` 5 | ClientSecret string `fake:"{lettern:40}"` 6 | Scopes []string `fakesize:"2"` 7 | } 8 | 9 | func NewCreateTokenClientCredentialCommand(clientId, clientSecret string, scopes []string) IIssueTokenCommand { 10 | return &CreateTokenClientCredentialCommand{ 11 | ClientId: clientId, 12 | ClientSecret: clientSecret, 13 | Scopes: scopes, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/applications/commands/oauth/createTokenCommandContract.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | type IIssueTokenCommand interface{} 4 | -------------------------------------------------------------------------------- /src/applications/dto/bank.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type BankList struct { 4 | Page int 5 | PerPage int 6 | Keyword string 7 | } 8 | -------------------------------------------------------------------------------- /src/applications/dto/token.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type IssueToken struct { 4 | GrantType string 5 | ClientId string 6 | ClientSecret string 7 | Scope []string 8 | RedirectUri string 9 | } 10 | 11 | func NewIssueToken(grantType string, clientId string, clientSecret string, scope []string) *IssueToken { 12 | return &IssueToken{GrantType: grantType, ClientId: clientId, ClientSecret: clientSecret, Scope: scope} 13 | } 14 | -------------------------------------------------------------------------------- /src/applications/listeners/listener.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | import ( 4 | domain_errors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 5 | domain_events "github.com/hendrorahmat/golang-clean-architecture/src/domain/events" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 7 | "sync" 8 | ) 9 | 10 | type IListener interface { 11 | ShouldHandleAsync() bool 12 | Handle(event domain_events.IEvent) domain_errors.DomainError 13 | } 14 | 15 | type ListenerName string 16 | 17 | var listenerObjectOnce sync.Once 18 | var ListenerObject map[ListenerName]IListener 19 | 20 | func init() { 21 | listenerObjectOnce.Do(func() { 22 | ListenerObject = map[ListenerName]IListener{ 23 | constants.SendEmailListener: &SendEmailConfirmationListener{}, 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/applications/listeners/sendEmailConfirmationListener.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/services" 6 | domain_errors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/events" 8 | ) 9 | 10 | type SendEmailConfirmationListener struct{} 11 | 12 | func (*SendEmailConfirmationListener) ShouldHandleAsync() bool { 13 | return false 14 | } 15 | 16 | func (*SendEmailConfirmationListener) Handle(event domain_events.IEvent) domain_errors.DomainError { 17 | objectEvent := event.(*domain_events.OrderCreated) 18 | objectEvent.GetName() 19 | fmt.Println(objectEvent.Order) 20 | emailService := services.NewSendEmailService("halo", 21 | []string{""}, 22 | []string{""}, 23 | []string{""}, 24 | "", 25 | "", 26 | []string{""}, 27 | ) 28 | emailService.Send() 29 | return nil 30 | } 31 | 32 | var _ IListener = &SendEmailConfirmationListener{} 33 | -------------------------------------------------------------------------------- /src/applications/listeners/sendVerificationEmailListener.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | type SendVerificationEmailListener struct{} 4 | -------------------------------------------------------------------------------- /src/applications/provider.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package applications 5 | 6 | import ( 7 | "github.com/google/wire" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/usecases" 9 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | //var OauthUsecaseSet = wire.NewSet(wire.Struct(new(usecases.OauthUsecase), "*")) 14 | 15 | func ProvideOauthUsecase(repository *database.Repository, logger *logrus.Logger) *usecases.OauthUsecase { 16 | return &usecases.OauthUsecase{ 17 | OauthClientRepository: repository.OauthClientRepository, 18 | OauthAccessTokenRepository: repository.OauthAccessTokenRepository, 19 | Logger: logger, 20 | } 21 | } 22 | 23 | var ( 24 | ProviderUsecaseSet wire.ProviderSet = wire.NewSet( 25 | ProvideOauthUsecase, 26 | wire.Struct(new(usecases.Usecase), "*"), 27 | wire.Bind(new(usecases.IOauthUsecase), new(*usecases.OauthUsecase)), 28 | ) 29 | ) 30 | 31 | func InjectUsecase(repository *database.Repository, logger *logrus.Logger, defaultJoins ...string) *usecases.Usecase { 32 | panic(wire.Build(ProviderUsecaseSet)) 33 | } 34 | -------------------------------------------------------------------------------- /src/applications/services/oauth/createTokenAuthCodeService.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/commands/oauth" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 6 | ) 7 | 8 | type CreateTokenAuthCodeService struct { 9 | command *oauth.CreateTokenAuthCodeCommand 10 | } 11 | 12 | func NewCreateTokenAuthCodeService( 13 | command *oauth.CreateTokenClientCredentialCommand, 14 | oauthClientRepository repositories.IOauthClientRepository, 15 | oauthAccessTokenRepository repositories.IOauthAccessTokenRepository, 16 | ) ICreateTokenService { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /src/applications/services/oauth/createTokenClientCredentialService.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | import ( 4 | "context" 5 | "github.com/gofrs/uuid" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/commands/oauth" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 9 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 10 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/config" 11 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 12 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database/models" 13 | "time" 14 | ) 15 | 16 | type CreateTokenClientCredentialService struct { 17 | command *oauth.CreateTokenClientCredentialCommand 18 | oauthClientRepository repositories.IOauthClientRepository 19 | oauthAccessTokenRepository repositories.IOauthAccessTokenRepository 20 | } 21 | 22 | func (c *CreateTokenClientCredentialService) Handle(ctx context.Context) (*entities.Token, errors.DomainError) { 23 | _, err := c.oauthClientRepository.FindByClientIdAndClientSecret(ctx, c.command.ClientId, c.command.ClientSecret) 24 | if err != nil { 25 | return nil, err 26 | } 27 | var audiences []uuid.UUID 28 | audiences = append(audiences, uuid.FromStringOrNil(c.command.ClientId)) 29 | 30 | oauthAccessTokenModel := new(models.OauthAccessToken) 31 | oauthAccessTokenEntity, err := entities.CreateGrantTypeClientCredentials( 32 | uuid.FromStringOrNil(c.command.ClientId), 33 | c.command.Scopes, 34 | time.Now().Add(config.LoginExpirationDuration), 35 | ) 36 | 37 | oauthAccessTokenModel = models.CreateModelFromEntityOauthAccessToken(*oauthAccessTokenEntity) 38 | err = c.oauthAccessTokenRepository.Create(ctx, oauthAccessTokenModel) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | oauthAccessTokenEntity, err = oauthAccessTokenModel.ToEntity() 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | var subject = "" 49 | if oauthAccessTokenEntity.UserId() != nil { 50 | subject = oauthAccessTokenEntity.UserId().String() 51 | } 52 | 53 | jwtClaim := entities.NewJwtClaim(oauthAccessTokenEntity.Id(), audiences, subject, oauthAccessTokenEntity.Scopes()) 54 | 55 | token, err := jwtClaim.Generate() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | var tokenEntity *entities.Token 61 | tokenEntity, err = entities.NewToken(jwtClaim.GetJwtId(), constants.TokenTypeBearer, jwtClaim.GetExpiresAt(), *token, nil) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return tokenEntity, nil 66 | } 67 | 68 | func NewCreateTokenClientCredentialService( 69 | command *oauth.CreateTokenClientCredentialCommand, 70 | oauthClientRepository repositories.IOauthClientRepository, 71 | oauthAccessTokenRepository repositories.IOauthAccessTokenRepository, 72 | ) ICreateTokenService { 73 | return &CreateTokenClientCredentialService{ 74 | command: command, 75 | oauthAccessTokenRepository: oauthAccessTokenRepository, 76 | oauthClientRepository: oauthClientRepository, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/applications/services/oauth/createTokenServiceContract.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 6 | domainErrors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | ) 8 | 9 | type ICreateTokenService interface { 10 | Handle(ctx context.Context) (*entities.Token, domainErrors.DomainError) 11 | } 12 | -------------------------------------------------------------------------------- /src/applications/services/sendEmailService.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import domain_errors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 4 | 5 | type ISentEmailService interface { 6 | Send() domain_errors.DomainError 7 | GetBcc() []string 8 | GetSubject() string 9 | GetCc() []string 10 | GetMessage() string 11 | GetAttachments() []string 12 | } 13 | 14 | type SendEmailService struct { 15 | from string 16 | to []string 17 | cc []string 18 | bcc []string 19 | subject string 20 | messages string 21 | attachments []string 22 | } 23 | 24 | func (s *SendEmailService) GetSubject() string { 25 | return s.subject 26 | } 27 | 28 | func (s *SendEmailService) GetCc() []string { 29 | return s.cc 30 | } 31 | 32 | func (s *SendEmailService) GetMessage() string { 33 | return s.messages 34 | } 35 | 36 | func (s *SendEmailService) GetAttachments() []string { 37 | return s.attachments 38 | } 39 | 40 | func (s *SendEmailService) Send() domain_errors.DomainError { 41 | //TODO implement me 42 | panic("implement me") 43 | } 44 | 45 | func (s *SendEmailService) GetBcc() []string { 46 | return s.bcc 47 | } 48 | 49 | func NewSendEmailService(from string, 50 | to []string, 51 | cc []string, 52 | bcc []string, 53 | subject string, 54 | messages string, 55 | attachments []string, 56 | ) ISentEmailService { 57 | return &SendEmailService{from: from, 58 | to: to, 59 | cc: cc, 60 | bcc: bcc, 61 | subject: subject, 62 | messages: messages, 63 | attachments: attachments, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/applications/usecases/bankUsecase.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 7 | ) 8 | 9 | type IBankUsecase interface { 10 | GetListBank(ctx context.Context) ([]entities.Bank, error) 11 | } 12 | 13 | type BankUsecase struct { 14 | Repository repositories.IBankRepository 15 | } 16 | 17 | func (b *BankUsecase) GetListBank(ctx context.Context) ([]entities.Bank, error) { 18 | return b.Repository.GetBankList(ctx, nil) 19 | } 20 | -------------------------------------------------------------------------------- /src/applications/usecases/bank_usecase.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domains/entities" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domains/repositories" 7 | ) 8 | 9 | type IBankUsecase interface { 10 | GetListBank(ctx context.Context) ([]entities.Bank, error) 11 | } 12 | 13 | type BankUsecase struct { 14 | Repository repositories.IBankRepository 15 | } 16 | 17 | func (b *BankUsecase) GetListBank(ctx context.Context) ([]entities.Bank, error) { 18 | return b.Repository.GetBankList(ctx, nil) 19 | } 20 | -------------------------------------------------------------------------------- /src/applications/usecases/oauthUsecase.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/dto" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 7 | domainErrors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 9 | factoryCommand "github.com/hendrorahmat/golang-clean-architecture/src/factories/commands" 10 | "github.com/hendrorahmat/golang-clean-architecture/src/factories/services" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type IOauthUsecase interface { 15 | IssueToken(ctx *gin.Context, token *dto.IssueToken) (*entities.Token, domainErrors.DomainError) 16 | } 17 | 18 | type OauthUsecase struct { 19 | OauthClientRepository repositories.IOauthClientRepository 20 | OauthAccessTokenRepository repositories.IOauthAccessTokenRepository 21 | Logger *logrus.Logger 22 | } 23 | 24 | func (o *OauthUsecase) IssueToken(ctx *gin.Context, dtoIssueToken *dto.IssueToken) (*entities.Token, domainErrors.DomainError) { 25 | commandFactory, err := factoryCommand.CreateTokenCommandFactory(dtoIssueToken) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | service, err := services.CreateTokenServiceFactory(commandFactory, o.OauthClientRepository, o.OauthAccessTokenRepository) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | tokenEntity, err := service.Handle(ctx) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return tokenEntity, nil 41 | } 42 | 43 | var _ IOauthUsecase = &OauthUsecase{} 44 | -------------------------------------------------------------------------------- /src/applications/usecases/tokenUsecase.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | -------------------------------------------------------------------------------- /src/applications/usecases/usecase.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | type Usecase struct { 4 | OauthUsecase IOauthUsecase 5 | } 6 | -------------------------------------------------------------------------------- /src/applications/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package applications 8 | 9 | import ( 10 | "github.com/google/wire" 11 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/usecases" 12 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // Injectors from provider.go: 17 | 18 | func InjectUsecase(repository *database.Repository, logger *logrus.Logger, defaultJoins ...string) *usecases.Usecase { 19 | oauthUsecase := ProvideOauthUsecase(repository, logger) 20 | usecase := &usecases.Usecase{ 21 | OauthUsecase: oauthUsecase, 22 | } 23 | return usecase 24 | } 25 | 26 | // provider.go: 27 | 28 | func ProvideOauthUsecase(repository *database.Repository, logger *logrus.Logger) *usecases.OauthUsecase { 29 | return &usecases.OauthUsecase{ 30 | OauthClientRepository: repository.OauthClientRepository, 31 | OauthAccessTokenRepository: repository.OauthAccessTokenRepository, 32 | Logger: logger, 33 | } 34 | } 35 | 36 | var ( 37 | ProviderUsecaseSet wire.ProviderSet = wire.NewSet( 38 | ProvideOauthUsecase, wire.Struct(new(usecases.Usecase), "*"), wire.Bind(new(usecases.IOauthUsecase), new(*usecases.OauthUsecase)), 39 | ) 40 | ) 41 | -------------------------------------------------------------------------------- /src/bootstrap/app.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/config" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/utils" 8 | "github.com/sirupsen/logrus" 9 | "runtime" 10 | ) 11 | 12 | type App struct { 13 | databases *database.Connections 14 | config *config.Config 15 | logger *logrus.Logger 16 | } 17 | 18 | func (a *App) GetActiveConnection() database.IDB { 19 | return a.databases.Connection[constants.ActiveConnectionDb] 20 | } 21 | 22 | func (a *App) SetActiveConnectionDB(connectionName string) { 23 | a.databases.Connection[constants.ActiveConnectionDb] = a.databases.Connection[connectionName] 24 | } 25 | 26 | func (a *App) GetConnections() *database.Connections { 27 | return a.databases 28 | } 29 | 30 | func (a *App) GetConnection(name string) database.IDB { 31 | return a.databases.Connection[name] 32 | } 33 | 34 | func (a *App) GetLogger() *logrus.Logger { 35 | return a.logger 36 | } 37 | 38 | func (a *App) GetRepository() *database.Repository { 39 | return database.InjectRepository(a.GetActiveConnection().DB(), a.logger) 40 | } 41 | 42 | func (a *App) GetRepositoryCustomConnection(connectionName string) *database.Repository { 43 | if _, ok := a.databases.Connection[connectionName]; !ok { 44 | a.logger.Fatalf(constants.ConnectionNotEstablished) 45 | panic(constants.ConnectionNotEstablished) 46 | } 47 | 48 | return database.InjectRepository(a.GetConnection(connectionName).DB(), a.logger) 49 | } 50 | 51 | func (a *App) GetConfig() *config.Config { 52 | return a.config 53 | } 54 | 55 | type IApp interface { 56 | GetRepository() *database.Repository 57 | GetRepositoryCustomConnection(connectionName string) *database.Repository 58 | GetConfig() *config.Config 59 | GetLogger() *logrus.Logger 60 | GetConnections() *database.Connections 61 | GetConnection(name string) database.IDB 62 | SetActiveConnectionDB(connectionName string) 63 | GetActiveConnection() database.IDB 64 | } 65 | 66 | func Boot() IApp { 67 | utils.LoadEnv() 68 | conf := config.Make() 69 | m := make(map[string]interface{}) 70 | m["env"] = conf.App.Environment 71 | m["total-goroutine"] = runtime.NumGoroutine() 72 | m["service"] = utils.ToKebabCase(conf.App.Name) 73 | 74 | isProduction := false 75 | 76 | if conf.App.Environment == constants.PRODUCTION { 77 | isProduction = true 78 | } 79 | 80 | logger := utils.NewLogInstance( 81 | utils.LogName(conf.Log.Name), 82 | utils.IsProduction(isProduction), 83 | utils.LogAdditionalFields(m), 84 | utils.LogEnvironment(utils.GetEnvWithDefaultValue("APP_ENV", "local")), 85 | ) 86 | 87 | db := database.MakeDatabase(conf.Database, logger) 88 | app := &App{ 89 | config: conf, 90 | logger: logger, 91 | databases: db, 92 | } 93 | return app 94 | } 95 | -------------------------------------------------------------------------------- /src/domain/entities/bank.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type IBankListDto interface { 9 | Validate() error 10 | } 11 | type Bank struct { 12 | Name string 13 | CreatedAt time.Time 14 | UpdatedAt time.Time 15 | } 16 | 17 | func (b *Bank) Validate() error { 18 | panic("implement me") 19 | } 20 | 21 | func NewBankListDTO() IBankListDto { 22 | return &Bank{} 23 | } 24 | 25 | func MakeBankEntity( 26 | name string, 27 | createdAt time.Time, 28 | updatedAt time.Time, 29 | ) (*Bank, error) { 30 | return &Bank{ 31 | Name: name, 32 | CreatedAt: createdAt, 33 | UpdatedAt: updatedAt, 34 | }, nil 35 | } 36 | 37 | func (b *Bank) MarshalJSON() ([]byte, error) { 38 | respon, err := json.Marshal(struct { 39 | Name string 40 | CreatedAt string 41 | UpdatedAt string 42 | }{ 43 | Name: b.Name, 44 | CreatedAt: b.CreatedAt.String(), 45 | UpdatedAt: b.UpdatedAt.String(), 46 | }) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return respon, nil 51 | } 52 | -------------------------------------------------------------------------------- /src/domain/entities/jwt.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/gofrs/uuid" 5 | "github.com/golang-jwt/jwt/v4" 6 | domainErrors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/config" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/utils" 9 | "time" 10 | ) 11 | 12 | type JWTClaims struct { 13 | registeredClaims jwt.RegisteredClaims 14 | scopes []string `json:"scopes" json:"scopes,omitempty"` 15 | } 16 | 17 | func NewJwtClaim(id *uuid.UUID, audiences []uuid.UUID, subject string, scopes []string) *JWTClaims { 18 | config.MakeJwtConfig() 19 | claims := jwt.ClaimStrings{} 20 | 21 | for _, audience := range audiences { 22 | claims = append(claims, audience.String()) 23 | } 24 | return &JWTClaims{ 25 | registeredClaims: jwt.RegisteredClaims{ 26 | Issuer: "", 27 | Subject: subject, 28 | Audience: claims, 29 | ExpiresAt: jwt.NewNumericDate(time.Now().UTC().Add(config.LoginExpirationDuration)), 30 | NotBefore: jwt.NewNumericDate(time.Now().UTC()), 31 | IssuedAt: jwt.NewNumericDate(time.Now().UTC()), 32 | ID: id.String(), 33 | }, 34 | scopes: scopes, 35 | } 36 | } 37 | 38 | func (jwtClaim *JWTClaims) GetJwtId() uuid.UUID { 39 | return uuid.FromStringOrNil(jwtClaim.registeredClaims.ID) 40 | } 41 | 42 | func (jwtClaim *JWTClaims) GetExpiresAt() time.Time { 43 | return jwtClaim.registeredClaims.ExpiresAt.Time 44 | } 45 | 46 | func (jwtClaim *JWTClaims) SetId(id uuid.UUID) *JWTClaims { 47 | jwtClaim.registeredClaims.ID = id.String() 48 | return jwtClaim 49 | } 50 | 51 | func (jwtClaim *JWTClaims) Generate() (*string, domainErrors.DomainError) { 52 | if jwtClaim.registeredClaims.ID == "" || &jwtClaim.registeredClaims.ID == nil { 53 | return nil, domainErrors.ThrowFieldsRequired("jwt id") 54 | } 55 | 56 | if config.JwtSignatureKey == nil || len(config.JwtSignatureKey) <= 0 { 57 | return nil, domainErrors.ThrowFieldsRequired("public_key", "private_key") 58 | } 59 | 60 | privateKeyFromPEM, err := jwt.ParseRSAPrivateKeyFromPEM(config.JwtSignatureKey) 61 | if err != nil { 62 | return nil, domainErrors.ThrowInternalServerError(err.Error()) 63 | } 64 | 65 | token := jwt.New(jwt.SigningMethodRS256) 66 | claims := token.Claims.(jwt.MapClaims) 67 | if jwtClaim.registeredClaims.Subject != "" { 68 | claims["sub"] = jwtClaim.registeredClaims.Subject 69 | } 70 | 71 | claims["iss"] = utils.GetEnvWithDefaultValue("APP_URL", "http://localhost:8000") 72 | claims["aud"] = jwtClaim.registeredClaims.Audience 73 | claims["exp"] = jwtClaim.registeredClaims.ExpiresAt 74 | claims["nbf"] = jwtClaim.registeredClaims.NotBefore 75 | claims["iat"] = jwtClaim.registeredClaims.IssuedAt 76 | claims["jti"] = jwtClaim.registeredClaims.ID 77 | claims["scopes"] = jwtClaim.scopes 78 | 79 | signedToken, err := token.SignedString(privateKeyFromPEM) 80 | if err != nil { 81 | return nil, domainErrors.ThrowInternalServerError(err.Error()) 82 | } 83 | 84 | return &signedToken, nil 85 | } 86 | -------------------------------------------------------------------------------- /src/domain/entities/oauthAccessToken.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/gofrs/uuid" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 7 | "time" 8 | ) 9 | 10 | type OauthAccessToken struct { 11 | id uuid.UUID `json:"id"` 12 | userId *uuid.UUID `json:"user_id"` 13 | grantType string `json:"grant_type"` 14 | clientId uuid.UUID `json:"client_id"` 15 | scopes []string `json:"scopes"` 16 | createdAt time.Time `json:"created_at"` 17 | updatedAt time.Time `json:"updated_at"` 18 | deletedAt *time.Time `json:"updated_at"` 19 | expiresAt time.Time `json:"expires_at"` 20 | } 21 | 22 | func NewOauthAccessTokenEntity( 23 | id uuid.UUID, 24 | userId *uuid.UUID, 25 | grantType string, 26 | clientId uuid.UUID, 27 | scopes []string, 28 | expiresAt time.Time, 29 | createdAt time.Time, 30 | updatedAt time.Time, 31 | deletedAt *time.Time, 32 | ) (*OauthAccessToken, errors.DomainError) { 33 | if grantType == "" { 34 | return nil, errors.ThrowFieldsRequired("grant_type") 35 | } 36 | 37 | if clientId.IsNil() { 38 | return nil, errors.ThrowFieldsRequired("client_id") 39 | } 40 | 41 | if expiresAt.IsZero() { 42 | return nil, errors.ThrowFieldsRequired("expires_at") 43 | } 44 | 45 | return &OauthAccessToken{ 46 | id: id, 47 | userId: userId, 48 | grantType: grantType, 49 | clientId: clientId, 50 | scopes: scopes, 51 | createdAt: createdAt, 52 | updatedAt: updatedAt, 53 | deletedAt: deletedAt, 54 | expiresAt: expiresAt, 55 | }, nil 56 | } 57 | 58 | func (oauth *OauthAccessToken) ClientId() uuid.UUID { 59 | return oauth.clientId 60 | } 61 | 62 | func CreateOauthAccessToken( 63 | userId *uuid.UUID, 64 | clientId uuid.UUID, 65 | grantType string, 66 | scopes []string, 67 | revokedAt *time.Time, 68 | expiresAt time.Time, 69 | ) (*OauthAccessToken, errors.DomainError) { 70 | if grantType == "" { 71 | return nil, errors.ThrowFieldsRequired("grant_type") 72 | } 73 | 74 | if clientId.IsNil() { 75 | return nil, errors.ThrowFieldsRequired("client_id") 76 | } 77 | 78 | if expiresAt.IsZero() { 79 | return nil, errors.ThrowFieldsRequired("expires_at") 80 | } 81 | 82 | return &OauthAccessToken{ 83 | userId: userId, 84 | grantType: grantType, 85 | clientId: clientId, 86 | scopes: scopes, 87 | createdAt: time.Now(), 88 | updatedAt: time.Now(), 89 | deletedAt: revokedAt, 90 | expiresAt: expiresAt, 91 | }, nil 92 | } 93 | 94 | func CreateGrantTypeClientCredentials( 95 | clientId uuid.UUID, 96 | scopes []string, 97 | expiresAt time.Time, 98 | ) (*OauthAccessToken, errors.DomainError) { 99 | data, err := CreateOauthAccessToken(nil, clientId, constants.ClientCredentialsGrantType, scopes, nil, expiresAt) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return data, nil 104 | } 105 | 106 | func (oauth *OauthAccessToken) Id() *uuid.UUID { 107 | return &oauth.id 108 | } 109 | 110 | func (oauth *OauthAccessToken) UserId() *uuid.UUID { 111 | return oauth.userId 112 | } 113 | 114 | func (oauth *OauthAccessToken) GrantType() string { 115 | return oauth.grantType 116 | } 117 | 118 | func (oauth *OauthAccessToken) Scopes() []string { 119 | return oauth.scopes 120 | } 121 | 122 | func (oauth *OauthAccessToken) CreatedAt() time.Time { 123 | return oauth.createdAt 124 | } 125 | 126 | func (oauth *OauthAccessToken) UpdatedAt() time.Time { 127 | return oauth.updatedAt 128 | } 129 | 130 | func (oauth *OauthAccessToken) RevokedAt() *time.Time { 131 | return oauth.deletedAt 132 | } 133 | 134 | func (oauth *OauthAccessToken) ExpiresAt() time.Time { 135 | return oauth.expiresAt 136 | } 137 | -------------------------------------------------------------------------------- /src/domain/entities/oauthAuthCode.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/gofrs/uuid" 5 | domainErrors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/valueObjects" 7 | "time" 8 | ) 9 | 10 | type OauthAuthCode struct { 11 | id uuid.UUID `json:"id"` 12 | userId uuid.UUID `json:"user_id"` 13 | clientId uuid.UUID `json:"client_id"` 14 | scopes []string `json:"scopes"` 15 | expiresAt time.Time `json:"expires_at"` 16 | timestamps valueObjects.TimestampValueObject 17 | } 18 | 19 | func NewOauthAuthCode( 20 | id uuid.UUID, 21 | userId uuid.UUID, 22 | clientId uuid.UUID, 23 | scopes []string, 24 | expiresAt time.Time, 25 | timestamps valueObjects.TimestampValueObject, 26 | ) *OauthAuthCode { 27 | return &OauthAuthCode{ 28 | id: id, 29 | userId: userId, 30 | clientId: clientId, 31 | scopes: scopes, 32 | expiresAt: expiresAt, 33 | timestamps: timestamps, 34 | } 35 | } 36 | 37 | func CreateOauthCode( 38 | userId uuid.UUID, 39 | clientId uuid.UUID, 40 | scopes []string, 41 | expiresAt time.Time, 42 | ) (*OauthAuthCode, domainErrors.DomainError) { 43 | if userId.IsNil() { 44 | return nil, domainErrors.ThrowFieldsRequired("user_id") 45 | } 46 | 47 | if clientId.IsNil() { 48 | return nil, domainErrors.ThrowFieldsRequired("client_id") 49 | } 50 | 51 | if expiresAt.IsZero() { 52 | return nil, domainErrors.ThrowFieldsRequired("expires_at") 53 | } 54 | 55 | timestamps := valueObjects.NewTimestampValueObject(time.Now(), time.Now(), nil) 56 | return &OauthAuthCode{ 57 | userId: userId, 58 | clientId: clientId, 59 | scopes: scopes, 60 | expiresAt: expiresAt, 61 | timestamps: timestamps, 62 | }, nil 63 | } 64 | -------------------------------------------------------------------------------- /src/domain/entities/oauthClient.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/gofrs/uuid" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 6 | "time" 7 | ) 8 | 9 | type OauthClient struct { 10 | ID uuid.UUID `json:"id"` 11 | Name string `json:"name"` 12 | EnabledGrantType []string `json:"enabled_grant_type"` 13 | Secret string `json:"secret"` 14 | Redirect string `json:"redirect"` 15 | CreatedAt *time.Time `json:"created_at"` 16 | UpdatedAt *time.Time `json:"updated_at"` 17 | DeletedAt *time.Time `json:"deleted_at"` 18 | } 19 | 20 | func MakeOauthClientEntity( 21 | ID uuid.UUID, 22 | name string, 23 | enabledGrantType []string, 24 | secret string, 25 | redirect string, 26 | createdAt *time.Time, 27 | updatedAt *time.Time, 28 | deletedAt *time.Time, 29 | ) (*OauthClient, errors.DomainError) { 30 | return &OauthClient{ 31 | ID: ID, 32 | Name: name, 33 | EnabledGrantType: enabledGrantType, 34 | Secret: secret, 35 | Redirect: redirect, 36 | CreatedAt: createdAt, 37 | UpdatedAt: updatedAt, 38 | DeletedAt: deletedAt, 39 | }, nil 40 | } 41 | -------------------------------------------------------------------------------- /src/domain/entities/token.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gofrs/uuid" 6 | domainErrors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | "time" 8 | ) 9 | 10 | type Token struct { 11 | id uuid.UUID 12 | tokenType string `json:"token_type"` 13 | expiresIn time.Time `json:"expires_in"` 14 | accessToken string `json:"access_token"` 15 | refreshToken *string `json:"refresh_token,omitempty"` 16 | } 17 | 18 | func (token *Token) Id() uuid.UUID { 19 | return token.id 20 | } 21 | 22 | func (token *Token) TokenType() string { 23 | return token.tokenType 24 | } 25 | 26 | func (token *Token) ExpiresIn() time.Time { 27 | return token.expiresIn 28 | } 29 | 30 | func (token *Token) AccessToken() string { 31 | return token.accessToken 32 | } 33 | 34 | func (token *Token) RefreshToken() *string { 35 | return token.refreshToken 36 | } 37 | 38 | func NewToken( 39 | id uuid.UUID, 40 | tokenType string, 41 | expiresIn time.Time, 42 | accessToken string, 43 | refreshToken *string, 44 | ) (*Token, domainErrors.DomainError) { 45 | return &Token{ 46 | id: id, 47 | tokenType: tokenType, 48 | expiresIn: expiresIn, 49 | accessToken: accessToken, 50 | refreshToken: refreshToken, 51 | }, nil 52 | } 53 | 54 | func (token *Token) MarshalJSON() ([]byte, error) { 55 | response, err := json.Marshal(struct { 56 | TokenType string `json:"token_type"` 57 | ExpiresIn int64 `json:"expires_in"` 58 | AccessToken string `json:"access_token"` 59 | RefreshToken *string `json:"refresh_token,omitempty"` 60 | }{ 61 | TokenType: token.tokenType, 62 | ExpiresIn: token.expiresIn.Unix(), 63 | AccessToken: token.accessToken, 64 | RefreshToken: token.refreshToken, 65 | }) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return response, nil 70 | } 71 | -------------------------------------------------------------------------------- /src/domain/entities/user.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import "time" 4 | 5 | type User struct { 6 | ID string 7 | Username string 8 | Password string 9 | CreatedAt time.Time 10 | UpdatedAt time.Time 11 | DeletedAt time.Time 12 | } 13 | 14 | func MakeUserEntity( 15 | id, 16 | username, 17 | password string, 18 | createdAt, 19 | updatedAt, 20 | deletedAt time.Time, 21 | ) (*User, error) { 22 | return &User{ 23 | ID: id, 24 | Username: username, 25 | Password: password, 26 | CreatedAt: createdAt, 27 | UpdatedAt: updatedAt, 28 | DeletedAt: deletedAt, 29 | }, nil 30 | } 31 | -------------------------------------------------------------------------------- /src/domain/errors/dataInvalid.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | ) 7 | 8 | type InvalidData struct { 9 | message string 10 | } 11 | 12 | func (*InvalidData) GetStatusCode() int { 13 | return http.StatusBadRequest 14 | } 15 | 16 | func (d *InvalidData) Error() string { 17 | return d.message 18 | } 19 | 20 | func (d *InvalidData) GetTitle() string { 21 | return constants.DataInvalid 22 | } 23 | 24 | func (d *InvalidData) GetCode() uint { 25 | return constants.DataPayloadInvalidCode 26 | } 27 | 28 | func ThrowInvalidData(msg string) DomainError { 29 | return &InvalidData{message: msg} 30 | } 31 | -------------------------------------------------------------------------------- /src/domain/errors/databaseCommandError.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | ) 7 | 8 | type DatabaseCommandError struct { 9 | message string 10 | } 11 | 12 | func (*DatabaseCommandError) GetStatusCode() int { 13 | return http.StatusInternalServerError 14 | } 15 | 16 | func (d *DatabaseCommandError) Error() string { 17 | return d.message 18 | } 19 | 20 | func (d *DatabaseCommandError) GetTitle() string { 21 | return constants.DatabaseCommandError 22 | } 23 | 24 | func (d *DatabaseCommandError) GetCode() uint { 25 | return 0_01_002_001 26 | } 27 | 28 | func ThrowDatabaseCommandError(message string) DomainError { 29 | return &DatabaseCommandError{message: message} 30 | } 31 | -------------------------------------------------------------------------------- /src/domain/errors/databaseQueryError.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | ) 7 | 8 | type DatabaseQueryError struct { 9 | message string 10 | } 11 | 12 | func (*DatabaseQueryError) GetStatusCode() int { 13 | return http.StatusInternalServerError 14 | } 15 | 16 | func (d *DatabaseQueryError) Error() string { 17 | return d.message 18 | } 19 | 20 | func (d *DatabaseQueryError) GetTitle() string { 21 | return constants.DatabaseQueryError 22 | } 23 | 24 | func (d *DatabaseQueryError) GetCode() uint { 25 | return constants.DBQueryErrorCode 26 | } 27 | 28 | func ThrowDatabaseQueryError(message string) DomainError { 29 | return &DatabaseQueryError{message: message} 30 | } 31 | -------------------------------------------------------------------------------- /src/domain/errors/domain.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | type DomainError interface { 4 | Error() string 5 | GetTitle() string 6 | GetCode() uint 7 | GetStatusCode() int 8 | } 9 | -------------------------------------------------------------------------------- /src/domain/errors/eventDuplicateError.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | type EventDuplicateError struct { 10 | eventName string 11 | } 12 | 13 | func (e *EventDuplicateError) Error() string { 14 | return strings.Replace(constants.ErrorEventNameAlreadyAdded, "{name}", e.eventName, 1) 15 | } 16 | 17 | func (e *EventDuplicateError) GetTitle() string { 18 | return constants.ErrorEventAlreadyAdded 19 | } 20 | 21 | func (e *EventDuplicateError) GetCode() uint { 22 | return constants.InvalidHeaderCode 23 | } 24 | 25 | func (e *EventDuplicateError) GetStatusCode() int { 26 | return http.StatusInternalServerError 27 | } 28 | 29 | func ThrowEventDuplicateError(eventName string) DomainError { 30 | return &EventDuplicateError{eventName: eventName} 31 | } 32 | -------------------------------------------------------------------------------- /src/domain/errors/fieldsRequired.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // FieldsRequired should only be used when one of the fields is empty 10 | type FieldsRequired struct { 11 | fields string 12 | } 13 | 14 | func (f *FieldsRequired) Error() string { 15 | return strings.Replace(constants.ErrorFieldNotFound, "{fields}", f.fields, 1) 16 | } 17 | 18 | func (f *FieldsRequired) GetTitle() string { 19 | return constants.DataInvalid 20 | } 21 | 22 | func (f *FieldsRequired) GetCode() uint { 23 | return constants.FieldsRequiredCode 24 | } 25 | 26 | func (f *FieldsRequired) GetStatusCode() int { 27 | return http.StatusBadRequest 28 | } 29 | 30 | func ThrowFieldsRequired(fields ...string) DomainError { 31 | return &FieldsRequired{fields: strings.Join(fields, ",")} 32 | } 33 | -------------------------------------------------------------------------------- /src/domain/errors/internalServerError.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | ) 7 | 8 | type InternalServerError struct { 9 | message string 10 | } 11 | 12 | func (*InternalServerError) GetStatusCode() int { 13 | return http.StatusInternalServerError 14 | } 15 | 16 | func (i *InternalServerError) Error() string { 17 | return i.message 18 | } 19 | 20 | func (i *InternalServerError) GetTitle() string { 21 | return constants.InternalServerError 22 | } 23 | 24 | func (i *InternalServerError) GetCode() uint { 25 | return constants.InternalCodeError 26 | } 27 | 28 | func ThrowInternalServerError(msg string) DomainError { 29 | return &InternalServerError{message: msg} 30 | } 31 | -------------------------------------------------------------------------------- /src/domain/errors/invalidHeader.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | ) 7 | 8 | type InvalidHeader struct { 9 | message string 10 | } 11 | 12 | func (*InvalidHeader) GetStatusCode() int { 13 | return http.StatusBadRequest 14 | } 15 | 16 | func (i *InvalidHeader) Error() string { 17 | return i.message 18 | } 19 | 20 | func (i *InvalidHeader) GetTitle() string { 21 | return constants.HeaderInvalid 22 | } 23 | 24 | func (i *InvalidHeader) GetCode() uint { 25 | return constants.InvalidHeaderCode 26 | } 27 | 28 | func ThrowInvalidHeader(message string) DomainError { 29 | return &InvalidHeader{message: message} 30 | } 31 | -------------------------------------------------------------------------------- /src/domain/errors/oauth/clientIdAndClientSecretNotFound.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 6 | "net/http" 7 | ) 8 | 9 | type ClientIdAndSecretNotFoundError struct { 10 | clientId string 11 | clientSecret string 12 | } 13 | 14 | func (*ClientIdAndSecretNotFoundError) GetStatusCode() int { 15 | return http.StatusNotFound 16 | } 17 | 18 | func (c *ClientIdAndSecretNotFoundError) GetCode() uint { 19 | //TODO implement me 20 | panic("implement me") 21 | } 22 | 23 | func (c *ClientIdAndSecretNotFoundError) GetTitle() string { 24 | //TODO implement me 25 | panic("implement me") 26 | } 27 | 28 | func (c *ClientIdAndSecretNotFoundError) Error() string { 29 | return constants.ClientIdAndSecretNotFound 30 | } 31 | 32 | func ThrowClientIdAndSecretNotFound(clientId, clientSecret string) errors.DomainError { 33 | return &ClientIdAndSecretNotFoundError{ 34 | clientId: clientId, 35 | clientSecret: clientSecret, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/domain/errors/queryParamInvalid.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | ) 7 | 8 | type QueryParamInvalid struct { 9 | fields string 10 | } 11 | 12 | func (*QueryParamInvalid) GetStatusCode() int { 13 | return http.StatusBadRequest 14 | } 15 | 16 | func (q *QueryParamInvalid) Error() string { 17 | return q.fields 18 | } 19 | 20 | func (q *QueryParamInvalid) GetTitle() string { 21 | return constants.QueryParamInvalid 22 | } 23 | 24 | func (q *QueryParamInvalid) GetCode() uint { 25 | return constants.QueryParamDataInvalidCode 26 | } 27 | 28 | func ThrowQueryParamInvalid(fields string) DomainError { 29 | return &QueryParamInvalid{fields: fields} 30 | } 31 | -------------------------------------------------------------------------------- /src/domain/errors/recordFieldsNotFound.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // RecordWithFieldsNotFound should only be used where get data from db and return nil 10 | type RecordWithFieldsNotFound struct { 11 | fields string 12 | } 13 | 14 | func (f *RecordWithFieldsNotFound) Error() string { 15 | return strings.Replace(constants.RecordFieldsNotFound, "{fields}", f.fields, 1) 16 | } 17 | 18 | func (f *RecordWithFieldsNotFound) GetTitle() string { 19 | return constants.DataInvalid 20 | } 21 | 22 | func (f *RecordWithFieldsNotFound) GetCode() uint { 23 | return constants.DBQueryNotFoundCode 24 | } 25 | 26 | func (f *RecordWithFieldsNotFound) GetStatusCode() int { 27 | return http.StatusNotFound 28 | } 29 | 30 | func ThrowRecordFieldsNotFound(fields ...string) DomainError { 31 | dataFields := strings.Join(fields, ",") 32 | return &RecordWithFieldsNotFound{fields: dataFields} 33 | } 34 | -------------------------------------------------------------------------------- /src/domain/errors/recordNotFound.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | ) 7 | 8 | type RecordNotFoundError struct{} 9 | 10 | func (*RecordNotFoundError) GetStatusCode() int { 11 | return http.StatusNotFound 12 | } 13 | 14 | func (r *RecordNotFoundError) GetCode() uint { 15 | return constants.DBQueryNotFoundCode 16 | } 17 | 18 | func (r *RecordNotFoundError) Error() string { 19 | return constants.RecordNotFound 20 | } 21 | 22 | func (r *RecordNotFoundError) GetTitle() string { 23 | return constants.DataInvalid 24 | } 25 | 26 | func ThrowRecordNotFoundError() DomainError { 27 | return &RecordNotFoundError{} 28 | } 29 | -------------------------------------------------------------------------------- /src/domain/events/event.go: -------------------------------------------------------------------------------- 1 | package domain_events 2 | 3 | type IEvent interface { 4 | GetName() string 5 | GetListenersName() []string 6 | } 7 | -------------------------------------------------------------------------------- /src/domain/events/order_created.go: -------------------------------------------------------------------------------- 1 | package domain_events 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | ) 6 | 7 | type OrderCreated struct { 8 | Order string 9 | } 10 | 11 | func (c *OrderCreated) GetListenersName() []string { 12 | return []string{ 13 | constants.SendEmailListener, 14 | } 15 | } 16 | 17 | func (*OrderCreated) GetName() string { 18 | return constants.OrderCreatedEvent 19 | } 20 | 21 | func OrderCreatedEvent(order string) IEvent { 22 | return &OrderCreated{Order: order} 23 | } 24 | 25 | var _ IEvent = &OrderCreated{} 26 | -------------------------------------------------------------------------------- /src/domain/events/user_registered.go: -------------------------------------------------------------------------------- 1 | package domain_events 2 | -------------------------------------------------------------------------------- /src/domain/repositories/bankRepository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 6 | ) 7 | 8 | type BankRepositoryFilter struct { 9 | Keyword string 10 | page int64 11 | perPage uint16 12 | } 13 | 14 | type IBankRepository interface { 15 | GetBankList(ctx context.Context, filter *BankRepositoryFilter) ([]entities.Bank, error) 16 | } 17 | -------------------------------------------------------------------------------- /src/domain/repositories/baseRepository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // IRepository is a generic DB handler that cares about default error handling 10 | type IRepository interface { 11 | FindAll(ctx context.Context, target interface{}, preloads ...string) errors.DomainError 12 | FindBatch(ctx context.Context, target interface{}, limit, offset int, preloads ...string) errors.DomainError 13 | 14 | FindWhere(ctx context.Context, target interface{}, condition string, preloads ...string) errors.DomainError 15 | FindWhereBatch(ctx context.Context, target interface{}, condition string, limit, offset int, preloads ...string) errors.DomainError 16 | 17 | FindByField(ctx context.Context, target interface{}, field string, value interface{}, preloads ...string) errors.DomainError 18 | FindByFields(ctx context.Context, target interface{}, filters map[string]interface{}, preloads ...string) errors.DomainError 19 | FindByFieldBatch(ctx context.Context, target interface{}, field string, value interface{}, limit, offset int, preloads ...string) errors.DomainError 20 | FindByFieldsBatch(ctx context.Context, target interface{}, filters map[string]interface{}, limit, offset int, preloads ...string) errors.DomainError 21 | 22 | FindOneByField(ctx context.Context, target interface{}, field string, value interface{}, preloads ...string) errors.DomainError 23 | FindOneByFields(ctx context.Context, target interface{}, filters map[string]interface{}, preloads ...string) errors.DomainError 24 | 25 | // FindOneByID assumes you have a PK column "id" which is a UUID. If this is not the case just ignore the method 26 | // and add a custom struct with this IRepository embedded. 27 | FindOneByID(ctx context.Context, target interface{}, id string, preloads ...string) errors.DomainError 28 | 29 | Create(ctx context.Context, target interface{}) errors.DomainError 30 | Save(ctx context.Context, target interface{}) errors.DomainError 31 | Delete(ctx context.Context, target interface{}) errors.DomainError 32 | 33 | DBGorm() *gorm.DB 34 | DBWithPreloads(preloads []string) *gorm.DB 35 | HandleQueryError(res *gorm.DB) errors.DomainError 36 | HandleCommandError(res *gorm.DB) errors.DomainError 37 | HandleOneError(res *gorm.DB) errors.DomainError 38 | } 39 | 40 | // ITransactionRepository extends IRepository with modifier functions that accept a transaction 41 | type ITransactionRepository interface { 42 | IRepository 43 | CreateTx(ctx context.Context, target interface{}, tx *gorm.DB) errors.DomainError 44 | SaveTx(ctx context.Context, target interface{}, tx *gorm.DB) errors.DomainError 45 | DeleteTx(ctx context.Context, target interface{}, tx *gorm.DB) errors.DomainError 46 | } 47 | -------------------------------------------------------------------------------- /src/domain/repositories/oauthAccessTokenRepository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | type IOauthAccessTokenRepository interface { 4 | IRepository 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/repositories/oauthAuthCodeRepository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | type IOauthAuthCodeRepository interface { 4 | FindByCode(code string) 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/repositories/oauthClientRepository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 6 | errors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | ) 8 | 9 | type IOauthClientRepository interface { 10 | ITransactionRepository 11 | FindByClientIdAndClientSecret(ctx context.Context, clientId, clientSecret string) (*entities.OauthClient, errors.DomainError) 12 | } 13 | -------------------------------------------------------------------------------- /src/domain/valueObjects/timestamp.go: -------------------------------------------------------------------------------- 1 | package valueObjects 2 | 3 | import "time" 4 | 5 | type TimestampValueObject struct { 6 | createdAt time.Time 7 | updatedAt time.Time 8 | deletedAt *time.Time 9 | } 10 | 11 | func (t *TimestampValueObject) CreatedAt() time.Time { 12 | return t.createdAt 13 | } 14 | 15 | func (t *TimestampValueObject) UpdatedAt() time.Time { 16 | return t.updatedAt 17 | } 18 | 19 | func (t *TimestampValueObject) DeletedAt() *time.Time { 20 | return t.deletedAt 21 | } 22 | 23 | func NewTimestampValueObject(createdAt time.Time, updatedAt time.Time, deletedAt *time.Time) TimestampValueObject { 24 | return TimestampValueObject{createdAt: createdAt, updatedAt: updatedAt, deletedAt: deletedAt} 25 | } 26 | 27 | func (t *TimestampValueObject) IsDeleted() bool { 28 | return t.deletedAt != nil 29 | } 30 | -------------------------------------------------------------------------------- /src/factories/commands/create_token.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/commands/oauth" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/dto" 6 | domainErrors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 8 | ) 9 | 10 | func CreateTokenCommandFactory(dto *dto.IssueToken) (oauth.IIssueTokenCommand, domainErrors.DomainError) { 11 | switch dto.GrantType { 12 | case constants.ClientCredentialsGrantType: 13 | return oauth.NewCreateTokenClientCredentialCommand(dto.ClientId, dto.ClientSecret, dto.Scope), nil 14 | default: 15 | return nil, domainErrors.ThrowInvalidData("Grant type invalid.") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/factories/entities/oauthAccessToken.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/gofrs/uuid" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/dto" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 7 | domainErrors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/config" 9 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 10 | "time" 11 | ) 12 | 13 | func OauthAccessTokenFactory(token *dto.IssueToken) (*entities.OauthAccessToken, domainErrors.DomainError) { 14 | switch token.GrantType { 15 | case constants.ClientCredentialsGrantType: 16 | oauthAccessTokenEntity, err := entities.CreateGrantTypeClientCredentials( 17 | uuid.FromStringOrNil(token.ClientId), 18 | token.Scope, 19 | time.Now().Add(config.LoginExpirationDuration), 20 | ) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return oauthAccessTokenEntity, nil 25 | default: 26 | return nil, domainErrors.ThrowRecordFieldsNotFound("grant_type") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/factories/entities/token.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | //import ( 4 | // "errors" 5 | // "github.com/gin-gonic/gin" 6 | // "github.com/hendrorahmat/golang-clean-architecture/src/applications/dto" 7 | // "github.com/hendrorahmat/golang-clean-architecture/src/domain/model/aggregates" 8 | // "github.com/hendrorahmat/golang-clean-architecture/src/domain/model/aggregates/contracts" 9 | // "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 10 | // "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 11 | //) 12 | // 13 | //func TokenFactory(data *dto.IssueToken, clientRepository repositories.IOauthClientRepository, ctx *gin.Context) (contracts.IIssuedToken, error) { 14 | // switch data.GrantType { 15 | // case constants.ClientCredentialsGrantType: 16 | // result, _ := aggregates.NewTokenClientCredentials() 17 | // return &result, nil 18 | // default: 19 | // return nil, errors.New("grant type not found") 20 | // } 21 | //} 22 | -------------------------------------------------------------------------------- /src/factories/services/create_token_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/commands/oauth" 5 | oauthService "github.com/hendrorahmat/golang-clean-architecture/src/applications/services/oauth" 6 | domainErrors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 9 | ) 10 | 11 | func CreateTokenServiceFactory( 12 | command oauth.IIssueTokenCommand, 13 | oauthClientRepository repositories.IOauthClientRepository, 14 | oauthAccessTokenRepository repositories.IOauthAccessTokenRepository, 15 | ) (oauthService.ICreateTokenService, domainErrors.DomainError) { 16 | switch commandType := command.(type) { 17 | case *oauth.CreateTokenClientCredentialCommand: 18 | service := oauthService.NewCreateTokenClientCredentialService(commandType, oauthClientRepository, oauthAccessTokenRepository) 19 | return service, nil 20 | default: 21 | return nil, domainErrors.ThrowInternalServerError(constants.ErrorCommandTypeNotFound) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/infrastructure/config/app.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "github.com/sirupsen/logrus" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | type AppConf struct { 13 | Environment string 14 | Name string 15 | Key string 16 | GracefulShutdownTimeout int 17 | } 18 | 19 | type HttpConf struct { 20 | Port string 21 | XRequestID string 22 | Timeout int 23 | } 24 | 25 | type MongoDbConf struct { 26 | Dsn string 27 | } 28 | 29 | type RedisConf struct { 30 | Address string 31 | Password string 32 | Db int 33 | } 34 | 35 | type LogConf struct { 36 | Name string 37 | Logger *logrus.Logger 38 | } 39 | 40 | // Config ... 41 | type Config struct { 42 | App AppConf 43 | MongoDb MongoDbConf 44 | Redis RedisConf 45 | Http HttpConf 46 | Log LogConf 47 | Database Databases 48 | } 49 | 50 | var appConfigOnce sync.Once 51 | var appConfig *Config 52 | 53 | // Make builds a appConfig value based on .env file. 54 | func Make() *Config { 55 | appConfigOnce.Do(func() { 56 | gracefulShutdownTimeout, err := strconv.Atoi(os.Getenv("GRACEFUL_SHUTDOWN_TIMEOUT")) 57 | app := AppConf{ 58 | Environment: strings.ToLower(os.Getenv("APP_ENV")), 59 | Name: os.Getenv("APP_NAME"), 60 | Key: os.Getenv("APP_KEY"), 61 | GracefulShutdownTimeout: gracefulShutdownTimeout, 62 | } 63 | 64 | mongodb := MongoDbConf{ 65 | Dsn: os.Getenv("MONGO_DSN"), 66 | } 67 | 68 | http := HttpConf{ 69 | Port: os.Getenv("HTTP_PORT"), 70 | XRequestID: os.Getenv("HTTP_REQUEST_ID"), 71 | } 72 | 73 | log := LogConf{ 74 | Name: os.Getenv("LOG_NAME"), 75 | Logger: logrus.New(), 76 | } 77 | 78 | if app.Key == "" { 79 | logrus.Fatalf("Please generate random string and set to APP_KEY .env variable") 80 | panic("Please generate random string and set to APP_KEY") 81 | } 82 | 83 | db, err := strconv.Atoi(os.Getenv("REDIS_DB")) 84 | redis := RedisConf{ 85 | Address: os.Getenv("REDIS_ADDRESS"), 86 | Password: os.Getenv("REDIS_PASSWORD"), 87 | Db: db, 88 | } 89 | 90 | // set default env to local 91 | if app.Environment == "" { 92 | app.Environment = "local" 93 | } 94 | 95 | // set default port for HTTP 96 | if http.Port == "" { 97 | http.Port = string(constants.DefaultPort) 98 | } 99 | 100 | httpTimeout, err := strconv.Atoi(os.Getenv("HTTP_TIMEOUT")) 101 | if err == nil { 102 | http.Timeout = httpTimeout 103 | } 104 | 105 | if os.Getenv("DB_DRIVER") == "" { 106 | panic(constants.DbDriverNotFound) 107 | } 108 | 109 | MakeDatabaseConfig() 110 | 111 | appConfig = &Config{ 112 | App: app, 113 | MongoDb: mongodb, 114 | Http: http, 115 | Redis: redis, 116 | Log: log, 117 | Database: DBConfig, 118 | } 119 | }) 120 | 121 | return appConfig 122 | } 123 | -------------------------------------------------------------------------------- /src/infrastructure/config/database.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/utils" 6 | ) 7 | 8 | type BasicDBConf struct { 9 | Host string 10 | Username string 11 | Password string 12 | Name string 13 | Port string 14 | } 15 | 16 | type MYSQLConf struct { 17 | Charset string 18 | ParseTime bool 19 | Timezone string 20 | } 21 | 22 | type PostgresConf struct { 23 | Schema string 24 | } 25 | 26 | type DBDriver uint 27 | 28 | type DatabaseConfig struct { 29 | ConnectionName string 30 | SkipCreateConnection bool 31 | Driver DBDriver 32 | BasicDBConf 33 | SSLMode string 34 | PostgresConf 35 | MYSQLConf 36 | MaxOpenConn int 37 | MaxIdleConn int 38 | MaxIdleTimeConnSeconds int64 39 | MaxLifeTimeConnSeconds int64 40 | } 41 | 42 | var DBConfig Databases 43 | 44 | func MakeDatabaseConfig() { 45 | config := make(Databases) 46 | config = Databases{ 47 | constants.DefaultConnectionDB: { 48 | Driver: constants.POSTGRES, 49 | SkipCreateConnection: false, 50 | BasicDBConf: BasicDBConf{ 51 | Host: utils.GetEnv("DB_HOST"), 52 | Username: utils.GetEnv("DB_USERNAME"), 53 | Password: utils.GetEnv("DB_PASSWORD"), 54 | Name: utils.GetEnv("DB_NAME"), 55 | Port: utils.GetEnv("DB_PORT"), 56 | }, 57 | SSLMode: "", 58 | PostgresConf: PostgresConf{ 59 | Schema: utils.GetEnvWithDefaultValue("DB_SCHEMA", "public"), 60 | }, 61 | MaxOpenConn: 0, 62 | MaxIdleConn: 0, 63 | MaxIdleTimeConnSeconds: 0, 64 | MaxLifeTimeConnSeconds: 0, 65 | }, 66 | "mysql": { 67 | SkipCreateConnection: true, 68 | Driver: constants.MYSQL, 69 | BasicDBConf: BasicDBConf{ 70 | Host: utils.GetEnv("DB_HOST_2"), 71 | Username: utils.GetEnv("DB_USERNAME_2"), 72 | Password: utils.GetEnv("DB_PASSWORD_2"), 73 | Name: utils.GetEnv("DB_NAME_2"), 74 | Port: utils.GetEnv("DB_PORT_2"), 75 | }, 76 | MYSQLConf: MYSQLConf{ 77 | Charset: utils.GetEnvWithDefaultValue("DB_CHARSET_2", "utf8"), 78 | ParseTime: true, 79 | Timezone: utils.GetEnvWithDefaultValue("DB_TIMEZONE_2", "Local"), 80 | }, 81 | SSLMode: "", 82 | MaxOpenConn: 0, 83 | MaxIdleConn: 0, 84 | MaxIdleTimeConnSeconds: 0, 85 | MaxLifeTimeConnSeconds: 0, 86 | }, 87 | } 88 | DBConfig = config 89 | } 90 | 91 | type Databases map[ConnectionDBName]DatabaseConfig 92 | 93 | type ConnectionDBName string 94 | -------------------------------------------------------------------------------- /src/infrastructure/config/jwt.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/golang-jwt/jwt/v4" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/utils" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | const OneDay = 24 * time.Hour 12 | const LoginExpirationDuration = OneDay 13 | 14 | var JwtSigningMethod = jwt.SigningMethodRS256 15 | 16 | var JwtSignatureKey []byte 17 | var jwtConfigOnce sync.Once 18 | 19 | func MakeJwtConfig() { 20 | jwtConfigOnce.Do(func() { 21 | privateKey, err := utils.GetOauthPrivateKeyFile() 22 | if err != nil { 23 | fmt.Println("Error read private key") 24 | panic(err) 25 | return 26 | } 27 | JwtSignatureKey = privateKey 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/infrastructure/constants/common.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const PRODUCTION = "production" 4 | const STAGING = "staging" 5 | const LOCAL = "local" 6 | 7 | const DefaultPort = 8080 8 | const ActiveConnectionDb = "active" 9 | const DefaultConnectionDB = "default" 10 | 11 | const TotalWorkerMax = 4 12 | const ValidationError = "Validation error." 13 | -------------------------------------------------------------------------------- /src/infrastructure/constants/database.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | MYSQL = iota 5 | POSTGRES 6 | ) 7 | 8 | const PostgresDriverName = "postgres" 9 | -------------------------------------------------------------------------------- /src/infrastructure/constants/error.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const DbDriverNotFound = "DBGorm Driver Not Found!" 4 | const ConnectionNotEstablished = "Connection not Established!" 5 | const ClientIdAndSecretNotFound = "Client id and secret not found." 6 | const RecordNotFound = "Record Not Found." 7 | const RecordFieldsNotFound = "{fields} not found." 8 | const DatabaseCommandError = "Database Command Error." 9 | const DatabaseQueryError = "Database Query Error." 10 | const DataInvalid = "Invalid Data." 11 | const InternalServerError = "Internal Server Error." 12 | const QueryParamInvalid = "Invalid Query param." 13 | const HeaderInvalid = "Invalid Header." 14 | const ErrorEventAlreadyAdded = "Event Duplicate." 15 | const ErrorEventNameAlreadyAdded = "Event {name} already added." 16 | const ErrorCannotConvertModelToEntity = "Cannot convert model {model} to entity {entity}" 17 | const ErrorFieldNotFound = "Field {fields} should not be empty." 18 | const ErrorCommandTypeNotFound = "Command type not found." 19 | -------------------------------------------------------------------------------- /src/infrastructure/constants/errorCodes.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // urgency_service_error-type_unique-id 4 | /** 5 | urgency = p0, p1, p2, p3 -> smaller is more urgent 6 | service_id = 01, 02 ,... 7 | error_type = 8 | General Validation Payload / business logic Error (001), 9 | DBGorm Error (002), 10 | Third party error (003), 11 | Internal Server error (004), 12 | {resource_name} = (005++)... 13 | unique_id = 001 14 | */ 15 | 16 | /* Category Infrastructures */ 17 | const ( 18 | DBQueryNotFoundCode uint = 3_01_001_004 19 | DBQueryErrorCode uint = 0_01_002_001 20 | InternalCodeError uint = 0_01_004_001 21 | ) 22 | 23 | /* Category Request Param */ 24 | const ( 25 | InvalidHeaderXDeviceTypeCode uint = 3_01_001_003 26 | InvalidHeaderCode uint = 3_01_001_003 27 | QueryParamDataInvalidCode uint = 3_01_001_002 28 | FieldMissingCode uint = 3_01_001_005 29 | ) 30 | 31 | /* Category Business Logic */ 32 | const ( 33 | DataPayloadInvalidCode uint = 3_01_001_001 34 | FieldsRequiredCode uint = 3_01_001_002 35 | ) 36 | -------------------------------------------------------------------------------- /src/infrastructure/constants/event.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const OrderCreatedEvent = "order_created" 4 | -------------------------------------------------------------------------------- /src/infrastructure/constants/listener.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const SendEmailListener = "send_email_listener" 4 | -------------------------------------------------------------------------------- /src/infrastructure/constants/mongoDbCollection.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // database name... 5 | MongoDatabase string = "ms-buyer-location" 6 | 7 | // List of all collections... 8 | CollectionLocation string = "locations" 9 | ) 10 | -------------------------------------------------------------------------------- /src/infrastructure/constants/oauth.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const AuthorizationGrantType = "authorization_code" 4 | const ClientCredentialsGrantType = "client_credentials" 5 | const RefreshTokenGrantType = "refresh_token" 6 | const DeviceCodeGrantType = "device_code" 7 | const TokenTypeBearer = "Bearer" 8 | -------------------------------------------------------------------------------- /src/infrastructure/constants/timeFormat.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ISODateTimeFormat string = "2006-01-02T15:04:05Z" 4 | const WesternIndonesiaTimeFormat string = "2006-01-02T15:04:05Z07:00" 5 | const DateOnlyFormat string = "2006-01-02" 6 | const SQLTimestampFormat string = "2006-01-02 15:04:05" 7 | -------------------------------------------------------------------------------- /src/infrastructure/errors/problemdetails.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "encoding/xml" 5 | domain_errors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 6 | ) 7 | 8 | type ProblemDetails struct { 9 | XMLName xml.Name `json:"-" xml:"urn:ietf:rfc:7807 problem"` 10 | // Type is a URI reference [RFC3986] that identifies the 11 | // problem type. This specification encourages that, when 12 | // dereferenced, it provide human-readable documentation for the 13 | // problem type (e.g., using HTML [W3C.REC-html5-20141028]). When 14 | // this member is not present, its value is assumed to be 15 | // "about:blank". 16 | Type string `json:"type" xml:"type"` 17 | // Title is a short, human-readable summary of the problem 18 | // type. It SHOULD NOT change from occurrence to occurrence of the 19 | // problem, except for purposes of localization (e.g., using 20 | // proactive content negotiation; see [RFC7231], Section 3.4). 21 | Title string `json:"title" xml:"title"` 22 | // Status is the HTTP status code ([RFC7231], Section 6) 23 | // generated by the origin server for this occurrence of the problem. 24 | Status int `json:"status,omitempty" xml:"status,omitempty"` 25 | // Detail is a human-readable explanation specific to this 26 | // occurrence of the problem. 27 | // If present, it ought to focus on helping the client 28 | // correct the problem, rather than giving debugging information. 29 | Detail string `json:"detail,omitempty" xml:"detail,omitempty"` 30 | // Instance is a URI reference that identifies the specific 31 | // occurrence of the problem. It may or may not yield further 32 | // information if dereferenced. 33 | Instance string `json:"instance,omitempty" xml:"instance,omitempty"` 34 | 35 | InvalidParams ValidationErrors `json:"invalid_params,omitempty" xml:"invalid_params,omitempty"` 36 | Code uint `json:"code"` 37 | } 38 | 39 | func NewProblemDetails(statusCode int, commonError any, problemType, instance string) *ProblemDetails { 40 | if problemType == "" { 41 | problemType = "about:blank" 42 | } 43 | 44 | switch errorType := commonError.(type) { 45 | case *ValidationError: 46 | return &ProblemDetails{ 47 | Type: problemType, 48 | Title: errorType.Message, 49 | Status: statusCode, 50 | Instance: instance, 51 | InvalidParams: errorType.ValidationErrors, 52 | Code: errorType.ErrorCode, 53 | } 54 | case domain_errors.DomainError: 55 | return &ProblemDetails{ 56 | Type: problemType, 57 | Title: errorType.GetTitle(), 58 | Status: statusCode, 59 | Detail: errorType.Error(), 60 | Instance: instance, 61 | InvalidParams: nil, 62 | Code: errorType.GetCode(), 63 | } 64 | default: 65 | panic("Error type not found!") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/infrastructure/errors/validation.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 5 | "net/http" 6 | ) 7 | 8 | type Errorlists []string 9 | type ValidationErrors map[string]Errorlists 10 | 11 | type ValidationError struct { 12 | Message string `json:"message"` 13 | ValidationErrors ValidationErrors `json:"errors"` 14 | ErrorCode uint `json:"code"` 15 | StatusCode int `json:"-"` 16 | } 17 | 18 | func NewError(errCode uint) *ValidationError { 19 | return &ValidationError{ 20 | Message: constants.ValidationError, 21 | ErrorCode: errCode, 22 | StatusCode: http.StatusUnprocessableEntity, 23 | ValidationErrors: make(ValidationErrors), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/infrastructure/external/http/httpClient.go: -------------------------------------------------------------------------------- 1 | package http 2 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/config" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database/mysql" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database/postgres" 9 | "github.com/sirupsen/logrus" 10 | "gorm.io/gorm" 11 | "sync" 12 | ) 13 | 14 | type IDB interface { 15 | SqlDB() *sql.DB 16 | DB() *gorm.DB 17 | GetDsn() string 18 | } 19 | 20 | type Connection map[string]IDB 21 | 22 | type Connections struct { 23 | Connection 24 | } 25 | 26 | var connections *Connections 27 | var dbConnOnce sync.Once 28 | 29 | func MakeDatabase(databases config.Databases, log *logrus.Logger) *Connections { 30 | dbConnOnce.Do(func() { 31 | listConnections := Connection{} 32 | var dbConnection IDB 33 | for name, databaseConf := range databases { 34 | if databaseConf.SkipCreateConnection { 35 | continue 36 | } 37 | 38 | databaseConf.ConnectionName = string(name) 39 | 40 | switch databaseConf.Driver { 41 | case constants.POSTGRES: 42 | pgCon := postgres.NewPostgresDB() 43 | dbConnection = pgCon.NewConnection(databaseConf, log) 44 | case constants.MYSQL: 45 | mysqlCon := mysql.NewMysqlDB() 46 | dbConnection = mysqlCon.NewConnection(databaseConf, log) 47 | } 48 | 49 | listConnections[string(name)] = dbConnection 50 | } 51 | 52 | listConnections[constants.ActiveConnectionDb] = listConnections[constants.DefaultConnectionDB] 53 | 54 | connections = &Connections{ 55 | listConnections, 56 | } 57 | }) 58 | 59 | return connections 60 | } 61 | func (c *Connections) GetConnection(connectionName string) *gorm.DB { 62 | return c.Connection[connectionName].DB() 63 | } 64 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/gorm_repository.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | log "github.com/sirupsen/logrus" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | func (r *gormRepository) DBGorm() *gorm.DB { 12 | return r.DBWithPreloads(nil) 13 | } 14 | 15 | type gormRepository struct { 16 | logger *log.Logger 17 | db *gorm.DB 18 | defaultJoins []string 19 | } 20 | 21 | func (r *gormRepository) FindAll(ctx context.Context, target interface{}, preloads ...string) errors.DomainError { 22 | r.logger.Debugf("Executing GetAll on %T", target) 23 | //res := r.db.WithContext(ctx).Find(target) 24 | res := r.DBWithPreloads(preloads).WithContext(ctx). 25 | Unscoped(). 26 | Find(target) 27 | 28 | return r.HandleQueryError(res) 29 | } 30 | 31 | func (r *gormRepository) FindBatch(ctx context.Context, target interface{}, limit, offset int, preloads ...string) errors.DomainError { 32 | r.logger.Debugf("Executing GetBatch on %T", target) 33 | 34 | res := r.DBWithPreloads(preloads).WithContext(ctx). 35 | Unscoped(). 36 | Limit(limit). 37 | Offset(offset). 38 | Find(target) 39 | 40 | return r.HandleQueryError(res) 41 | } 42 | 43 | func (r *gormRepository) FindWhere(ctx context.Context, target interface{}, condition string, preloads ...string) errors.DomainError { 44 | r.logger.Debugf("Executing GetWhere on %T with %v ", target, condition) 45 | 46 | res := r.DBWithPreloads(preloads).WithContext(ctx). 47 | Where(condition). 48 | Find(target) 49 | 50 | return r.HandleQueryError(res) 51 | } 52 | 53 | func (r *gormRepository) FindWhereBatch(ctx context.Context, target interface{}, condition string, limit, offset int, preloads ...string) errors.DomainError { 54 | r.logger.Debugf("Executing GetWhere on %T with %v ", target, condition) 55 | 56 | res := r.DBWithPreloads(preloads).WithContext(ctx). 57 | Where(condition). 58 | Limit(limit). 59 | Offset(offset). 60 | Find(target) 61 | 62 | return r.HandleQueryError(res) 63 | } 64 | 65 | func (r *gormRepository) FindByField(ctx context.Context, target interface{}, field string, value interface{}, preloads ...string) errors.DomainError { 66 | r.logger.Debugf("Executing GetByField on %T with %v = %v", target, field, value) 67 | 68 | res := r.DBWithPreloads(preloads).WithContext(ctx). 69 | Where(fmt.Sprintf("%v = ?", field), value). 70 | Find(target) 71 | 72 | return r.HandleQueryError(res) 73 | } 74 | 75 | func (r *gormRepository) FindByFields(ctx context.Context, target interface{}, filters map[string]interface{}, preloads ...string) errors.DomainError { 76 | r.logger.Debugf("Executing GetByField on %T with filters = %+v", target, filters) 77 | 78 | db := r.DBWithPreloads(preloads).WithContext(ctx) 79 | for field, value := range filters { 80 | db = db.Where(fmt.Sprintf("%v = ?", field), value) 81 | } 82 | 83 | res := db.Find(target) 84 | 85 | return r.HandleQueryError(res) 86 | } 87 | 88 | func (r *gormRepository) FindByFieldBatch(ctx context.Context, target interface{}, field string, value interface{}, limit, offset int, preloads ...string) errors.DomainError { 89 | r.logger.Debugf("Executing GetByField on %T with %v = %v", target, field, value) 90 | 91 | res := r.DBWithPreloads(preloads).WithContext(ctx). 92 | Where(fmt.Sprintf("%v = ?", field), value). 93 | Limit(limit). 94 | Offset(offset). 95 | Find(target) 96 | 97 | return r.HandleQueryError(res) 98 | } 99 | 100 | func (r *gormRepository) FindByFieldsBatch(ctx context.Context, target interface{}, filters map[string]interface{}, limit, offset int, preloads ...string) errors.DomainError { 101 | r.logger.Debugf("Executing GetByField on %T with filters = %+v", target, filters) 102 | 103 | db := r.DBWithPreloads(preloads).WithContext(ctx) 104 | for field, value := range filters { 105 | db = db.Where(fmt.Sprintf("%v = ?", field), value) 106 | } 107 | 108 | res := db. 109 | Limit(limit). 110 | Offset(offset). 111 | Find(target) 112 | 113 | return r.HandleQueryError(res) 114 | } 115 | 116 | func (r *gormRepository) FindOneByField(ctx context.Context, target interface{}, field string, value interface{}, preloads ...string) errors.DomainError { 117 | r.logger.Debugf("Executing GetOneByField on %T with %v = %v", target, field, value) 118 | 119 | res := r.DBWithPreloads(preloads). 120 | WithContext(ctx). 121 | Where(fmt.Sprintf("%v = ?", field), value). 122 | First(target) 123 | 124 | return r.HandleOneError(res) 125 | } 126 | 127 | func (r *gormRepository) FindOneByFields(ctx context.Context, target interface{}, filters map[string]interface{}, preloads ...string) errors.DomainError { 128 | r.logger.Debugf("Executing FindOneByField on %T with filters = %+v", target, filters) 129 | 130 | db := r.DBWithPreloads(preloads).WithContext(ctx) 131 | for field, value := range filters { 132 | db = db.Where(fmt.Sprintf("%v = ?", field), value) 133 | } 134 | 135 | res := db.First(target) 136 | return r.HandleOneError(res) 137 | } 138 | 139 | func (r *gormRepository) FindOneByID(ctx context.Context, target interface{}, id string, preloads ...string) errors.DomainError { 140 | r.logger.Debugf("Executing GetOneByID on %T with id %v", target, id) 141 | 142 | res := r.DBWithPreloads(preloads).WithContext(ctx). 143 | Where("id = ?", id). 144 | First(target) 145 | 146 | return r.HandleOneError(res) 147 | } 148 | 149 | func (r *gormRepository) Create(ctx context.Context, target interface{}) errors.DomainError { 150 | r.logger.Debugf("Executing Create on %T", target) 151 | 152 | res := r.db.WithContext(ctx).Create(target) 153 | return r.HandleCommandError(res) 154 | } 155 | 156 | func (r *gormRepository) CreateTx(ctx context.Context, target interface{}, tx *gorm.DB) errors.DomainError { 157 | r.logger.Debugf("Executing Create on %T", target) 158 | 159 | res := tx.WithContext(ctx).Create(target) 160 | return r.HandleCommandError(res) 161 | } 162 | 163 | func (r *gormRepository) Save(ctx context.Context, target interface{}) errors.DomainError { 164 | r.logger.Debugf("Executing Save on %T", target) 165 | 166 | res := r.db.WithContext(ctx).Save(target) 167 | return r.HandleCommandError(res) 168 | } 169 | 170 | func (r *gormRepository) SaveTx(ctx context.Context, target interface{}, tx *gorm.DB) errors.DomainError { 171 | r.logger.Debugf("Executing Save on %T", target) 172 | 173 | res := tx.WithContext(ctx).Save(target) 174 | return r.HandleCommandError(res) 175 | } 176 | 177 | func (r *gormRepository) Delete(ctx context.Context, target interface{}) errors.DomainError { 178 | r.logger.Debugf("Executing Delete on %T", target) 179 | 180 | res := r.db.WithContext(ctx).Delete(target) 181 | return r.HandleCommandError(res) 182 | } 183 | 184 | func (r *gormRepository) DeleteTx(ctx context.Context, target interface{}, tx *gorm.DB) errors.DomainError { 185 | r.logger.Debugf("Executing Delete on %T", target) 186 | 187 | res := tx.WithContext(ctx).Delete(target) 188 | return r.HandleCommandError(res) 189 | } 190 | 191 | func (r *gormRepository) HandleQueryError(res *gorm.DB) errors.DomainError { 192 | if res.Error != nil && res.Error != gorm.ErrRecordNotFound { 193 | err := fmt.Errorf("error: %w", res.Error) 194 | r.logger.Error(err) 195 | return errors.ThrowDatabaseQueryError(err.Error()) 196 | } 197 | return nil 198 | } 199 | 200 | func (r *gormRepository) HandleCommandError(res *gorm.DB) errors.DomainError { 201 | if res.Error != nil && res.Error != gorm.ErrRecordNotFound { 202 | err := fmt.Errorf("error: %w", res.Error) 203 | r.logger.Error(err) 204 | return errors.ThrowDatabaseQueryError(err.Error()) 205 | } 206 | return nil 207 | } 208 | 209 | func (r *gormRepository) HandleOneError(res *gorm.DB) errors.DomainError { 210 | if err := r.HandleQueryError(res); err != nil { 211 | return err 212 | } 213 | 214 | if res.RowsAffected < 1 { 215 | return errors.ThrowRecordNotFoundError() 216 | } 217 | 218 | return nil 219 | } 220 | 221 | func (r *gormRepository) DBWithPreloads(preloads []string) *gorm.DB { 222 | dbConn := r.db 223 | 224 | for _, join := range r.defaultJoins { 225 | dbConn = dbConn.Joins(join) 226 | } 227 | 228 | for _, preload := range preloads { 229 | dbConn = dbConn.Preload(preload) 230 | } 231 | 232 | return dbConn 233 | } 234 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/models/bank.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 5 | "time" 6 | ) 7 | 8 | type Bank struct { 9 | ID uint `gorm:"primaryKey"` 10 | Name string 11 | DisplayName string 12 | CreatedAt time.Time 13 | UpdatedAt time.Time 14 | } 15 | 16 | func (b Bank) ToEntity() (*entities.Bank, error) { 17 | entity, err := entities.MakeBankEntity(b.Name, b.CreatedAt, b.UpdatedAt) 18 | if err != nil { 19 | return &entities.Bank{}, err 20 | } 21 | return entity, nil 22 | } 23 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/models/oauthAccessToken.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/gofrs/uuid" 5 | domainModelEntities "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | "github.com/lib/pq" 8 | "gorm.io/gorm" 9 | "reflect" 10 | "time" 11 | ) 12 | 13 | type OauthAccessToken struct { 14 | ID uuid.UUID `json:"id" gorm:"type:uuid;default:uuid_generate_v4();primaryKey"` 15 | ClientId uuid.UUID `json:"client_id" gorm:"type:uuid;primaryKey;column:client_id"` 16 | UserId *uuid.UUID `json:"user_id" gorm:"type:uuid;column:user_id"` 17 | GrantType string `json:"grant_type" gorm:"column:grant_type"` 18 | Scopes pq.StringArray `json:"scopes" gorm:"type:varchar(100)[];column:scopes"` 19 | CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` 20 | UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` 21 | ExpiresAt time.Time `json:"expires_at" gorm:"column:expires_at"` 22 | DeletedAt *time.Time `json:"revoked_at" gorm:"column:deleted_at"` 23 | } 24 | 25 | func (o *OauthAccessToken) BeforeCreate(db *gorm.DB) (err error) { 26 | for { 27 | // UUID version 4 28 | o.ID, _ = uuid.NewV4() 29 | var model OauthAccessToken 30 | db.Raw("SELECT id where id = ?", o.ID).Scan(&model) 31 | if reflect.ValueOf(model).IsZero() { 32 | break 33 | } 34 | } 35 | return 36 | } 37 | 38 | func CreateModelFromEntityOauthAccessToken(entity domainModelEntities.OauthAccessToken) *OauthAccessToken { 39 | oauthAccessTokenModel := &OauthAccessToken{} 40 | oauthAccessTokenModel.ClientId = entity.ClientId() 41 | oauthAccessTokenModel.UserId = entity.UserId() 42 | oauthAccessTokenModel.Scopes = entity.Scopes() 43 | oauthAccessTokenModel.GrantType = entity.GrantType() 44 | oauthAccessTokenModel.DeletedAt = entity.RevokedAt() 45 | oauthAccessTokenModel.CreatedAt = entity.CreatedAt() 46 | oauthAccessTokenModel.UpdatedAt = entity.UpdatedAt() 47 | oauthAccessTokenModel.ExpiresAt = entity.ExpiresAt() 48 | return oauthAccessTokenModel 49 | } 50 | 51 | func (o *OauthAccessToken) ToEntity() (*domainModelEntities.OauthAccessToken, errors.DomainError) { 52 | entity, err := domainModelEntities.NewOauthAccessTokenEntity( 53 | o.ID, 54 | o.UserId, 55 | o.GrantType, 56 | o.ClientId, 57 | o.Scopes, 58 | o.ExpiresAt, 59 | o.CreatedAt, 60 | o.UpdatedAt, 61 | o.DeletedAt, 62 | ) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return entity, nil 67 | } 68 | 69 | //func (oauth *OauthAccessToken) BeforeCreate(tx *gorm.DBGorm) (err error) { 70 | // field := tx.Statement.Schema.LookUpField("ID") 71 | // fmt.Println(field) 72 | //} 73 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/models/oauthAuthCode.go: -------------------------------------------------------------------------------- 1 | package models 2 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/models/oauthClient.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/gofrs/uuid" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | "github.com/lib/pq" 8 | "gorm.io/gorm" 9 | "time" 10 | ) 11 | 12 | type OauthClient struct { 13 | ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;column:id;default:uuid_generate_v4()"` 14 | Name string `json:"name" gorm:"column:name"` 15 | EnabledGrantType pq.StringArray `json:"enabled_grant_type" gorm:"type:varchar(100)[];column:enabled_grant_type"` 16 | Secret string `json:"secret" gorm:"column:secret"` 17 | Redirect string `json:"redirect" gorm:"column:redirect"` 18 | CreatedAt *time.Time `json:"created_at" gorm:"column:created_at"` 19 | UpdatedAt *time.Time `json:"updated_at" gorm:"column:updated_at"` 20 | DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"column:deleted_at"` 21 | } 22 | 23 | func (oauthClient *OauthClient) ToEntity() (*entities.OauthClient, errors.DomainError) { 24 | entity, err := entities.MakeOauthClientEntity( 25 | oauthClient.ID, 26 | oauthClient.Name, 27 | oauthClient.EnabledGrantType, 28 | oauthClient.Secret, 29 | oauthClient.Redirect, 30 | oauthClient.CreatedAt, 31 | oauthClient.UpdatedAt, 32 | &oauthClient.DeletedAt.Time, 33 | ) 34 | return entity, err 35 | } 36 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 5 | "time" 6 | ) 7 | 8 | type User struct { 9 | ID string `json:"id" gorm:"column:id,primaryKey"` 10 | Username string `json:"username" gorm:"column:username"` 11 | Password string `json:"password" gorm:"column:password"` 12 | CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` 13 | UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` 14 | DeletedAt time.Time `json:"deleted_at" gorm:"column:deleted_at"` 15 | } 16 | 17 | func (m *User) TableName() string { 18 | return "users" 19 | } 20 | 21 | func (m *User) ToEntity() (*entities.User, error) { 22 | entity, err := entities.MakeUserEntity(m.ID, m.Username, m.Password, m.CreatedAt, m.UpdatedAt, m.DeletedAt) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return entity, nil 27 | } 28 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/mysql/db.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/config" 7 | "github.com/sirupsen/logrus" 8 | "gorm.io/driver/mysql" 9 | "gorm.io/gorm" 10 | gormLog "gorm.io/gorm/logger" 11 | "os" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | type mysqlDB struct { 17 | database *gorm.DB 18 | sqlDb *sql.DB 19 | dsn string 20 | } 21 | 22 | func (p *mysqlDB) GetDsn() string { 23 | return p.dsn 24 | } 25 | 26 | type IMysqlDB interface { 27 | SqlDB() *sql.DB 28 | DB() *gorm.DB 29 | GetDsn() string 30 | } 31 | 32 | func (p *mysqlDB) SqlDB() *sql.DB { 33 | return p.sqlDb 34 | } 35 | 36 | func (p *mysqlDB) DB() *gorm.DB { 37 | return p.database 38 | } 39 | func NewMysqlDB() *mysqlDB { 40 | return &mysqlDB{} 41 | } 42 | 43 | func (m *mysqlDB) NewConnection(config config.DatabaseConfig, log *logrus.Logger) IMysqlDB { 44 | logger := log 45 | logger.Info(fmt.Sprintf("Creating connection %s ...", config.ConnectionName)) 46 | dsn := fmt.Sprintf( 47 | "%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=%t&loc=%s", 48 | config.Username, 49 | config.Password, 50 | config.Host, 51 | config.Port, 52 | config.Name, 53 | config.MYSQLConf.Charset, 54 | config.MYSQLConf.ParseTime, 55 | config.MYSQLConf.Timezone, 56 | ) 57 | 58 | if config.Password == "" { 59 | dsn = fmt.Sprintf( 60 | "%s:@tcp(%s:%s)/%s?charset=%s&parseTime=%t&loc=%s", 61 | config.Username, 62 | config.Host, 63 | config.Port, 64 | config.Name, 65 | config.MYSQLConf.Charset, 66 | config.MYSQLConf.ParseTime, 67 | config.MYSQLConf.Timezone, 68 | ) 69 | } 70 | 71 | dBMaxOpenConn, err := strconv.Atoi(os.Getenv("DB_MAX_OPEN_CONN")) 72 | if err == nil { 73 | config.MaxOpenConn = dBMaxOpenConn 74 | } else { 75 | config.MaxOpenConn = 100 76 | } 77 | 78 | dBMaxIdleConn, err := strconv.Atoi(os.Getenv("DB_MAX_IDLE_CONN")) 79 | if err == nil { 80 | config.MaxIdleConn = dBMaxIdleConn 81 | } else { 82 | config.MaxIdleConn = 10 83 | } 84 | 85 | dBMaxIdleTimeConnSeconds, err := strconv.Atoi(os.Getenv("DB_MAX_IDLE_TIME_CONN_SECONDS")) 86 | if err == nil { 87 | config.MaxIdleTimeConnSeconds = int64(dBMaxIdleTimeConnSeconds) 88 | } 89 | 90 | dBMaxLifeTimeConnSeconds, err := strconv.Atoi(os.Getenv("DB_MAX_LIFE_TIME_CONN_SECONDS")) 91 | if err == nil { 92 | config.MaxLifeTimeConnSeconds = int64(dBMaxLifeTimeConnSeconds) 93 | } else { 94 | config.MaxLifeTimeConnSeconds = time.Hour.Milliseconds() 95 | } 96 | 97 | logger.Info("Connecting to Mysql database...") 98 | db, err := gorm.Open(mysql.New(mysql.Config{ 99 | DSN: dsn, 100 | DefaultStringSize: 255, 101 | }), &gorm.Config{ 102 | Logger: gormLog.Default.LogMode(gormLog.Warn), 103 | }) 104 | 105 | if err != nil { 106 | logger.Fatalf("connect database err: %s", err) 107 | panic("Failed to connect to database!") 108 | } 109 | 110 | m.dsn = dsn 111 | sqlDB, err := db.DB() 112 | sqlDB.SetMaxIdleConns(config.MaxIdleConn) 113 | sqlDB.SetConnMaxLifetime(time.Duration(config.MaxLifeTimeConnSeconds)) 114 | sqlDB.SetConnMaxIdleTime(time.Duration(config.MaxIdleTimeConnSeconds)) 115 | sqlDB.SetMaxOpenConns(config.MaxOpenConn) 116 | 117 | if err != nil { 118 | logger.Fatalf("database err: %s", err) 119 | panic("database Error!") 120 | } 121 | logger.Info("Success Connect MySql database") 122 | sqlDB.Ping() 123 | m.sqlDb = sqlDB 124 | m.database = db 125 | 126 | return m 127 | } 128 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/mysql/repositories/bankRepository.go: -------------------------------------------------------------------------------- 1 | package repositories_mysql 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database/models" 8 | ) 9 | 10 | type MysqlBankRepository struct { 11 | repositories.ITransactionRepository 12 | } 13 | 14 | func (g *MysqlBankRepository) GetBankList(ctx context.Context, filter *repositories.BankRepositoryFilter) ([]entities.Bank, error) { 15 | var bankEntities []entities.Bank 16 | 17 | var bankList []models.Bank 18 | 19 | g.FindAll(ctx, &bankList) 20 | 21 | for _, bank := range bankList { 22 | bankEntity, _ := bank.ToEntity() 23 | bankEntities = append(bankEntities, *bankEntity) 24 | } 25 | return bankEntities, nil 26 | } 27 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/postgres/db.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/config" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/utils" 8 | "github.com/sirupsen/logrus" 9 | "gorm.io/driver/postgres" 10 | "gorm.io/gorm" 11 | gormLog "gorm.io/gorm/logger" 12 | schema2 "gorm.io/gorm/schema" 13 | "os" 14 | "strconv" 15 | "time" 16 | ) 17 | 18 | type connectionPostgresDB struct { 19 | database *gorm.DB 20 | sqlDb *sql.DB 21 | dsn string 22 | } 23 | 24 | func (p *connectionPostgresDB) GetDsn() string { 25 | return p.dsn 26 | } 27 | 28 | type IPostgresDB interface { 29 | SqlDB() *sql.DB 30 | DB() *gorm.DB 31 | GetDsn() string 32 | } 33 | 34 | func NewPostgresDB() *connectionPostgresDB { 35 | return &connectionPostgresDB{} 36 | } 37 | 38 | func (p *connectionPostgresDB) NewConnection(config config.DatabaseConfig, log *logrus.Logger) IPostgresDB { 39 | logger := log 40 | logger.Info(fmt.Sprintf("Creating Connection %s ...", config.ConnectionName)) 41 | //dsn := fmt.Sprintf( 42 | // "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", 43 | // config.Host, 44 | // config.Username, 45 | // config.Password, 46 | // config.Name, 47 | // config.Port, 48 | //) 49 | 50 | if config.Password == "" { 51 | //dsn = fmt.Sprintf( 52 | // "host=%s user=%s dbname=%s port=%s sslmode=disable", 53 | // config.Host, 54 | // config.Username, 55 | // config.Name, 56 | // config.Port, 57 | //) 58 | } 59 | 60 | dBMaxOpenConn, err := strconv.Atoi(os.Getenv("DB_MAX_OPEN_CONN")) 61 | if err == nil { 62 | config.MaxOpenConn = dBMaxOpenConn 63 | } else { 64 | config.MaxOpenConn = 100 65 | } 66 | 67 | dBMaxIdleConn, err := strconv.Atoi(os.Getenv("DB_MAX_IDLE_CONN")) 68 | if err == nil { 69 | config.MaxIdleConn = dBMaxIdleConn 70 | } else { 71 | config.MaxIdleConn = 10 72 | } 73 | 74 | dBMaxIdleTimeConnSeconds, err := strconv.Atoi(os.Getenv("DB_MAX_IDLE_TIME_CONN_SECONDS")) 75 | if err == nil { 76 | config.MaxIdleTimeConnSeconds = int64(dBMaxIdleTimeConnSeconds) 77 | } 78 | 79 | dBMaxLifeTimeConnSeconds, err := strconv.Atoi(os.Getenv("DB_MAX_LIFE_TIME_CONN_SECONDS")) 80 | if err == nil { 81 | config.MaxLifeTimeConnSeconds = int64(dBMaxLifeTimeConnSeconds) 82 | } else { 83 | config.MaxLifeTimeConnSeconds = time.Hour.Milliseconds() 84 | } 85 | 86 | schemaDb := utils.GetEnvWithDefaultValue("DB_SCHEMA", "public") 87 | namingStrategy := schema2.NamingStrategy{ 88 | TablePrefix: schemaDb + ".", 89 | SingularTable: false, 90 | } 91 | p.dsn = fmt.Sprintf( 92 | "postgres://%s@%s:%s/%s?sslmode=disable&search_path=%s", 93 | config.Username, 94 | config.Host, 95 | config.Port, 96 | config.Name, 97 | config.Schema, 98 | ) 99 | db, err := gorm.Open(postgres.New(postgres.Config{ 100 | DSN: p.dsn, 101 | }), &gorm.Config{ 102 | Logger: gormLog.Default.LogMode(gormLog.Warn), 103 | NamingStrategy: namingStrategy, 104 | }) 105 | 106 | logger.Info("Connecting to Postgres database...") 107 | if err != nil { 108 | logger.Fatalf("connect database err: %s", err) 109 | panic("Failed to connect to database!") 110 | } 111 | 112 | sqlDB, err := db.DB() 113 | sqlDB.SetMaxIdleConns(config.MaxIdleConn) 114 | sqlDB.SetConnMaxLifetime(time.Duration(config.MaxLifeTimeConnSeconds)) 115 | sqlDB.SetConnMaxIdleTime(time.Duration(config.MaxIdleTimeConnSeconds)) 116 | sqlDB.SetMaxOpenConns(config.MaxOpenConn) 117 | 118 | if err != nil { 119 | logger.Fatalf("database err: %s", err) 120 | panic("database Error!") 121 | } 122 | logger.Info(fmt.Sprintf("Database %s Connected Successfully!", config.Name)) 123 | err = sqlDB.Ping() 124 | if err != nil { 125 | return nil 126 | } 127 | p.sqlDb = sqlDB 128 | p.database = db 129 | 130 | return p 131 | } 132 | 133 | func (p *connectionPostgresDB) SqlDB() *sql.DB { 134 | return p.sqlDb 135 | } 136 | 137 | func (p *connectionPostgresDB) DB() *gorm.DB { 138 | return p.database 139 | } 140 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/postgres/repositories/bankRepository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database/models" 8 | ) 9 | 10 | type PostgresBankRepository struct { 11 | repositories.ITransactionRepository 12 | } 13 | 14 | func (g *PostgresBankRepository) GetBankList(ctx context.Context, filter *repositories.BankRepositoryFilter) ([]entities.Bank, error) { 15 | var bankEntities = make([]entities.Bank, 0) 16 | 17 | var bankList []models.Bank 18 | 19 | g.FindAll(ctx, &bankList) 20 | 21 | for _, bank := range bankList { 22 | bankEntity, _ := bank.ToEntity() 23 | bankEntities = append(bankEntities, *bankEntity) 24 | } 25 | return bankEntities, nil 26 | } 27 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/postgres/repositories/oauthAccessTokenRepository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 5 | ) 6 | 7 | type OauthAccessTokenRepository struct { 8 | repositories.ITransactionRepository 9 | } 10 | 11 | var _ repositories.IOauthAccessTokenRepository = &OauthAccessTokenRepository{} 12 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/postgres/repositories/oauthClientRepository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 6 | domainErrors "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | domainErrorsOauth "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors/oauth" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 9 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database/models" 10 | ) 11 | 12 | type OauthClientRepository struct { 13 | repositories.ITransactionRepository 14 | } 15 | 16 | func (db *OauthClientRepository) FindByClientIdAndClientSecret( 17 | ctx context.Context, 18 | clientId, 19 | clientSecret string, 20 | ) (*entities.OauthClient, domainErrors.DomainError) { 21 | var oauthClientModel = new(models.OauthClient) 22 | err := db.FindOneByFields(ctx, oauthClientModel, map[string]interface{}{ 23 | "id": clientId, 24 | "secret": clientSecret, 25 | }) 26 | 27 | if err != nil { 28 | switch err.(type) { 29 | case *domainErrors.RecordNotFoundError: 30 | return nil, domainErrorsOauth.ThrowClientIdAndSecretNotFound(clientId, clientSecret) 31 | default: 32 | return nil, err 33 | } 34 | } 35 | entity, err := oauthClientModel.ToEntity() 36 | if err != nil { 37 | return nil, err 38 | } 39 | return entity, nil 40 | } 41 | 42 | var _ repositories.IOauthClientRepository = &OauthClientRepository{} 43 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/provider.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package database 5 | 6 | import ( 7 | "github.com/google/wire" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 9 | repositories_postgres2 "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database/postgres/repositories" 10 | "github.com/sirupsen/logrus" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | type Repository struct { 15 | TransactionRepository repositories.ITransactionRepository 16 | BaseRepository repositories.IRepository 17 | BankRepository repositories.IBankRepository 18 | OauthClientRepository repositories.IOauthClientRepository 19 | OauthAccessTokenRepository repositories.IOauthAccessTokenRepository 20 | } 21 | 22 | func ProvideDatabaseGorm(db *gorm.DB, logger *logrus.Logger, defaultJoins ...string) *gormRepository { 23 | return &gormRepository{ 24 | defaultJoins: defaultJoins, 25 | logger: logger, 26 | db: db, 27 | } 28 | } 29 | 30 | // var GormBankRepositoryPostgresSet = wire.NewSet(new(*repositories2.PostgresBankRepository)) 31 | var GormBankRepositorySet = wire.NewSet(wire.Struct(new(repositories_postgres2.PostgresBankRepository), "*")) 32 | var GormOauthClientRepositorySet = wire.NewSet(wire.Struct(new(repositories_postgres2.OauthClientRepository), "*")) 33 | var GormOauthAccessTokenRepositorySet = wire.NewSet(wire.Struct(new(repositories_postgres2.OauthAccessTokenRepository), "*")) 34 | 35 | var ( 36 | ProviderRepositorySet wire.ProviderSet = wire.NewSet( 37 | ProvideDatabaseGorm, 38 | //GormBankRepositoryPostgresSet, 39 | GormBankRepositorySet, 40 | GormOauthClientRepositorySet, 41 | GormOauthAccessTokenRepositorySet, 42 | wire.Struct(new(Repository), "*"), 43 | wire.Bind(new(repositories.ITransactionRepository), new(*gormRepository)), 44 | wire.Bind(new(repositories.IRepository), new(*gormRepository)), 45 | wire.Bind(new(repositories.IBankRepository), new(*repositories_postgres2.PostgresBankRepository)), 46 | wire.Bind(new(repositories.IOauthClientRepository), new(*repositories_postgres2.OauthClientRepository)), 47 | wire.Bind(new(repositories.IOauthAccessTokenRepository), new(*repositories_postgres2.OauthAccessTokenRepository)), 48 | //wire.Bind(new(repositories.IBankRepository), new(*repositories3.PostgresBankRepository)), 49 | ) 50 | ) 51 | 52 | func InjectRepository(db *gorm.DB, logger *logrus.Logger, defaultJoins ...string) *Repository { 53 | panic(wire.Build(ProviderRepositorySet)) 54 | } 55 | -------------------------------------------------------------------------------- /src/infrastructure/persistance/database/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package database 8 | 9 | import ( 10 | "github.com/google/wire" 11 | repositories2 "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 12 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database/postgres/repositories" 13 | "github.com/sirupsen/logrus" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | // Injectors from provider.go: 18 | 19 | func InjectRepository(db *gorm.DB, logger *logrus.Logger, defaultJoins ...string) *Repository { 20 | databaseGormRepository := ProvideDatabaseGorm(db, logger, defaultJoins...) 21 | postgresBankRepository := &repositories.PostgresBankRepository{ 22 | ITransactionRepository: databaseGormRepository, 23 | } 24 | postgresOauthClientRepository := &repositories.OauthClientRepository{ 25 | ITransactionRepository: databaseGormRepository, 26 | } 27 | postgresOauthAccessTokenRepository := &repositories.OauthAccessTokenRepository{ 28 | ITransactionRepository: databaseGormRepository, 29 | } 30 | repository := &Repository{ 31 | TransactionRepository: databaseGormRepository, 32 | BaseRepository: databaseGormRepository, 33 | BankRepository: postgresBankRepository, 34 | OauthClientRepository: postgresOauthClientRepository, 35 | OauthAccessTokenRepository: postgresOauthAccessTokenRepository, 36 | } 37 | return repository 38 | } 39 | 40 | // provider.go: 41 | 42 | type Repository struct { 43 | TransactionRepository repositories2.ITransactionRepository 44 | BaseRepository repositories2.IRepository 45 | BankRepository repositories2.IBankRepository 46 | OauthClientRepository repositories2.IOauthClientRepository 47 | OauthAccessTokenRepository repositories2.IOauthAccessTokenRepository 48 | } 49 | 50 | func ProvideDatabaseGorm(db *gorm.DB, logger *logrus.Logger, defaultJoins ...string) *gormRepository { 51 | return &gormRepository{ 52 | defaultJoins: defaultJoins, 53 | logger: logger, 54 | db: db, 55 | } 56 | } 57 | 58 | // var GormBankRepositoryPostgresSet = wire.NewSet(new(*repositories2.PostgresBankRepository)) 59 | var GormBankRepositorySet = wire.NewSet(wire.Struct(new(repositories.PostgresBankRepository), "*")) 60 | 61 | var GormOauthClientRepositorySet = wire.NewSet(wire.Struct(new(repositories.OauthClientRepository), "*")) 62 | 63 | var GormOauthAccessTokenRepositorySet = wire.NewSet(wire.Struct(new(repositories.OauthAccessTokenRepository), "*")) 64 | 65 | var ( 66 | ProviderRepositorySet wire.ProviderSet = wire.NewSet( 67 | ProvideDatabaseGorm, 68 | 69 | GormBankRepositorySet, 70 | GormOauthClientRepositorySet, 71 | GormOauthAccessTokenRepositorySet, wire.Struct(new(Repository), "*"), wire.Bind(new(repositories2.ITransactionRepository), new(*gormRepository)), wire.Bind(new(repositories2.IRepository), new(*gormRepository)), wire.Bind(new(repositories2.IBankRepository), new(*repositories.PostgresBankRepository)), wire.Bind(new(repositories2.IOauthClientRepository), new(*repositories.OauthClientRepository)), wire.Bind(new(repositories2.IOauthAccessTokenRepository), new(*repositories.OauthAccessTokenRepository)), 72 | ) 73 | ) 74 | -------------------------------------------------------------------------------- /src/infrastructure/utils/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/joho/godotenv" 5 | "math/rand" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | func GetRootPath() string { 11 | path, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() 12 | if err == nil { 13 | return strings.TrimSuffix(string(path), "\n") 14 | } 15 | return "" 16 | } 17 | 18 | func LoadEnv() error { 19 | rootPath := GetRootPath() 20 | var err error 21 | if rootPath == "" { 22 | rootPath += ".env" 23 | err = godotenv.Load(".env") 24 | } else { 25 | rootPath = rootPath + "/" + ".env" 26 | err = godotenv.Load(rootPath) 27 | } 28 | 29 | if err != nil { 30 | return err 31 | } 32 | return nil 33 | } 34 | 35 | const alphabet = "abcdefghijklmnopqrstuvwxyz" 36 | 37 | func RandomString(n int) string { 38 | var sb strings.Builder 39 | k := len(alphabet) 40 | 41 | for i := 0; i < n; i++ { 42 | c := alphabet[rand.Intn(k)] 43 | sb.WriteByte(c) 44 | } 45 | 46 | return sb.String() 47 | } 48 | -------------------------------------------------------------------------------- /src/infrastructure/utils/config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "os" 4 | 5 | func GetEnv(name string) string { 6 | return os.Getenv(name) 7 | } 8 | 9 | func GetEnvWithDefaultValue(name string, defaultValue string) string { 10 | if os.Getenv(name) == "" { 11 | return defaultValue 12 | } 13 | return os.Getenv(name) 14 | } 15 | 16 | func GetOauthPrivateKeyFile() ([]byte, error) { 17 | rootPath := GetRootPath() 18 | path := rootPath + "/storage/oauth-private.key" 19 | privateKey, err := os.ReadFile(path) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return privateKey, nil 24 | } 25 | 26 | func GetOauthPublicKeyFile() ([]byte, error) { 27 | rootPath := GetRootPath() 28 | path := rootPath + "/storage/oauth-public.key" 29 | publicKey, err := os.ReadFile(path) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return publicKey, nil 34 | } 35 | -------------------------------------------------------------------------------- /src/infrastructure/utils/eventEmitter.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/listeners" 6 | domain_events "github.com/hendrorahmat/golang-clean-architecture/src/domain/events" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func DispatchEvent(logger *logrus.Logger, events ...domain_events.IEvent) { 11 | for _, event := range events { 12 | logger.Info(fmt.Sprintf("Event name %s", event.GetName())) 13 | listenersName := event.GetListenersName() 14 | 15 | for _, listenerName := range listenersName { 16 | logger.Info(fmt.Sprintf("Listener name %s", listenerName)) 17 | 18 | listener, ok := listeners.ListenerObject[listeners.ListenerName(listenerName)] 19 | if !ok { 20 | logger.Info(fmt.Sprintf("Listener %s not found", listenerName)) 21 | continue 22 | } 23 | 24 | if listener.ShouldHandleAsync() { 25 | logger.Info("Handle Async") 26 | go listener.Handle(event) 27 | } else { 28 | logger.Info("Handle Sync") 29 | listener.Handle(event) 30 | } 31 | 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/infrastructure/utils/gracefulShutdown.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "github.com/sirupsen/logrus" 6 | "os" 7 | "os/signal" 8 | "sync" 9 | "syscall" 10 | "time" 11 | ) 12 | 13 | type GracefulOperation func(ctx context.Context) error 14 | 15 | func GracefulShutdown(ctx context.Context, log *logrus.Logger, timeout time.Duration, ops map[string]GracefulOperation) <-chan struct{} { 16 | wait := make(chan struct{}) 17 | go func() { 18 | s := make(chan os.Signal, 1) 19 | signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 20 | data := <-s 21 | /** 22 | program in below will never run if <-s not return a value because assigning value as channel will run 23 | syncronizely 24 | */ 25 | 26 | log.Warnf("Signal %s pid : %d", data.String(), data.Signal) 27 | timeoutFunc := time.AfterFunc(timeout, func() { 28 | log.Warnf("timeout %d ms has been elapsed, force exit", timeout.Milliseconds()) 29 | os.Exit(0) 30 | }) 31 | 32 | defer timeoutFunc.Stop() 33 | var wg sync.WaitGroup 34 | 35 | for key, op := range ops { 36 | wg.Add(1) 37 | innerOp := op 38 | innerKey := key 39 | go func() { 40 | defer wg.Done() 41 | log.Printf("cleaning up: %s", innerKey) 42 | if err := innerOp(ctx); err != nil { 43 | log.Fatalf("%s: clean up failed: %s", innerKey, err.Error()) 44 | return 45 | } 46 | log.Printf("%s was shutdown gracefully", innerKey) 47 | }() 48 | } 49 | wg.Wait() 50 | close(wait) 51 | }() 52 | return wait 53 | } 54 | -------------------------------------------------------------------------------- /src/infrastructure/utils/log.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/mattn/go-colorable" 5 | "github.com/sirupsen/logrus" 6 | "github.com/snowzach/rotatefilehook" 7 | "path" 8 | "runtime" 9 | "strings" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | const Default = "default" 15 | 16 | type DefaultFieldHook struct { 17 | fields map[string]interface{} 18 | } 19 | 20 | func (h *DefaultFieldHook) Levels() []logrus.Level { 21 | return logrus.AllLevels 22 | } 23 | 24 | func (h *DefaultFieldHook) Fire(e *logrus.Entry) error { 25 | for i, v := range h.fields { 26 | e.Data[i] = v 27 | } 28 | return nil 29 | } 30 | 31 | type LogConfig struct { 32 | IsProduction bool 33 | Environment string 34 | LogFileName string 35 | Fields map[string]interface{} 36 | } 37 | 38 | type LogOption func(*LogConfig) 39 | 40 | func IsProduction(isProd bool) LogOption { 41 | return func(o *LogConfig) { 42 | o.IsProduction = isProd 43 | } 44 | } 45 | 46 | func LogEnvironment(env string) LogOption { 47 | return func(logConfig *LogConfig) { 48 | logConfig.Environment = env 49 | } 50 | } 51 | 52 | func LogName(logname string) LogOption { 53 | return func(o *LogConfig) { 54 | o.LogFileName = logname 55 | } 56 | } 57 | 58 | func LogAdditionalFields(fields map[string]interface{}) LogOption { 59 | return func(o *LogConfig) { 60 | o.Fields = fields 61 | } 62 | } 63 | 64 | var logOnce sync.Once 65 | var logger *logrus.Logger 66 | 67 | // NewLogInstance ... 68 | func NewLogInstance(logOptions ...LogOption) *logrus.Logger { 69 | logOnce.Do(func() { 70 | rootPath := GetRootPath() 71 | var level logrus.Level 72 | logger = logrus.New() 73 | 74 | logConfig := &LogConfig{} 75 | logConfig.LogFileName = Default 76 | 77 | for _, logOption := range logOptions { 78 | logOption(logConfig) 79 | } 80 | 81 | //if it is production will output warn and error level 82 | if logConfig.IsProduction { 83 | level = logrus.WarnLevel 84 | } else { 85 | level = logrus.TraceLevel 86 | } 87 | 88 | logger.SetLevel(level) 89 | logger.SetOutput(colorable.NewColorableStdout()) 90 | if logConfig.IsProduction { 91 | logger.SetFormatter(&logrus.JSONFormatter{ 92 | TimestampFormat: time.RFC3339, 93 | PrettyPrint: true, 94 | CallerPrettyfier: func(f *runtime.Frame) (string, string) { 95 | s := strings.Split(f.Function, ".") 96 | funcName := s[len(s)-1] 97 | _, filename := path.Split(f.File) 98 | return funcName, filename 99 | }, 100 | }) 101 | } else { 102 | logger.SetFormatter(&logrus.TextFormatter{ 103 | TimestampFormat: time.RFC3339, 104 | CallerPrettyfier: func(f *runtime.Frame) (string, string) { 105 | s := strings.Split(f.Function, ".") 106 | funcname := s[len(s)-1] 107 | _, filename := path.Split(f.File) 108 | return funcname, filename 109 | }, 110 | }) 111 | } 112 | 113 | if !logConfig.IsProduction { 114 | dt := time.Now().UTC() 115 | rotateFileHook, err := rotatefilehook.NewRotateFileHook(rotatefilehook.RotateFileConfig{ 116 | Filename: rootPath + "/storage/logs/" + dt.Format("20060102") + "-log-" + logConfig.Environment + "-" + logConfig.LogFileName, 117 | MaxSize: 50, // megabytes 118 | MaxBackups: 3, 119 | MaxAge: 28, //days 120 | Level: level, 121 | Formatter: &logrus.JSONFormatter{ 122 | TimestampFormat: time.RFC3339, 123 | CallerPrettyfier: func(f *runtime.Frame) (string, string) { 124 | s := strings.Split(f.Function, ".") 125 | funcname := s[len(s)-1] 126 | _, filename := path.Split(f.File) 127 | return funcname, filename 128 | }, 129 | }, 130 | }) 131 | 132 | if err != nil { 133 | logger.Fatalf("Failed to initialize file rotate hook: %v", err) 134 | } 135 | 136 | logger.AddHook(rotateFileHook) 137 | } 138 | logger.AddHook(&DefaultFieldHook{logConfig.Fields}) 139 | }) 140 | return logger 141 | } 142 | -------------------------------------------------------------------------------- /src/infrastructure/utils/response.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type IResponse[TData, ITMeta any] interface { 4 | MakeMetaData(message string, data map[string]ITMeta, page *int, perPage *int, totalPage *int) 5 | BuildResponse() *response[TData, ITMeta] 6 | } 7 | 8 | type Meta[T any] struct { 9 | Message string `json:"message,omitempty"` 10 | Additional map[string]T `json:"additional,omitempty"` 11 | Page *int `json:"page,omitempty"` 12 | PerPage *int `json:"per_page,omitempty"` 13 | TotalPage *int `json:"total_page,omitempty"` 14 | } 15 | 16 | type response[TData, TMeta any] struct { 17 | Data TData `json:"data"` 18 | Meta *Meta[TMeta] `json:"meta,omitempty"` 19 | } 20 | 21 | func (r *response[TData, TMeta]) BuildResponse() *response[TData, TMeta] { 22 | return r 23 | } 24 | 25 | func (r *response[TData, ITMeta]) MakeMetaData(message string, data map[string]ITMeta, page *int, perPage *int, totalPage *int) { 26 | r.Meta = &Meta[ITMeta]{ 27 | Message: message, 28 | Page: page, 29 | PerPage: perPage, 30 | TotalPage: totalPage, 31 | Additional: data, 32 | } 33 | } 34 | 35 | func NewResponse[T, TMeta any](data T) IResponse[T, TMeta] { 36 | return &response[T, TMeta]{ 37 | Data: data, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/infrastructure/utils/text.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") 9 | var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") 10 | var space = regexp.MustCompile(`\s+`) 11 | var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9 ]+`) 12 | 13 | func clearString(str string) string { 14 | return nonAlphanumericRegex.ReplaceAllString(str, "") 15 | } 16 | 17 | func ToSnakeCase(str string) string { 18 | snake := clearString(str) 19 | snake = matchFirstCap.ReplaceAllString(snake, "${1}_${2}") 20 | snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") 21 | snake = space.ReplaceAllString(snake, "") 22 | return strings.ToLower(snake) 23 | } 24 | 25 | func ToKebabCase(str string) string { 26 | snake := clearString(str) 27 | snake = space.ReplaceAllString(snake, "-") 28 | 29 | return strings.ToLower(snake) 30 | } 31 | -------------------------------------------------------------------------------- /src/infrastructure/utils/workerpool.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type task func(ctx context.Context) (any, error) 12 | type result struct { 13 | data any 14 | error error 15 | } 16 | type WorkerPool struct { 17 | tasks []task 18 | results []result 19 | jobs chan task 20 | wg *sync.WaitGroup 21 | ctx context.Context 22 | TotalSuccessJob int 23 | workersCount int 24 | } 25 | 26 | var resultPool = sync.Pool{New: func() any { 27 | return new(result) 28 | }} 29 | 30 | func (wa *WorkerPool) WaitWorker() { 31 | wa.wg.Wait() 32 | } 33 | 34 | func worker(jobs <-chan task, wa *WorkerPool) <-chan result { 35 | wa.wg.Add(wa.workersCount) 36 | chanOut := make(chan result) 37 | var rp *result 38 | for i := 0; i < wa.workersCount; i++ { 39 | go func(workerId int) { 40 | fmt.Printf("Worker id %d started \n", workerId) 41 | for job := range jobs { 42 | select { 43 | case <-wa.ctx.Done(): 44 | rp.data = nil 45 | rp.error = wa.ctx.Err() 46 | resultPool.Put(rp) 47 | fmt.Println("Break") 48 | chanOut <- *rp 49 | break 50 | default: 51 | fmt.Printf("Worker id %d processing \n", workerId) 52 | rp = resultPool.Get().(*result) 53 | data, err := job(wa.ctx) 54 | fmt.Printf("Worker id %d finished \n", workerId) 55 | rp.data = data 56 | rp.error = err 57 | resultPool.Put(rp) 58 | chanOut <- *rp 59 | } 60 | } 61 | fmt.Printf("Worker id %d closed \n", workerId) 62 | wa.wg.Done() 63 | }(i) 64 | } 65 | 66 | go func() { 67 | fmt.Println("Wait group") 68 | wa.WaitWorker() 69 | fmt.Println("Wait group done") 70 | close(chanOut) 71 | }() 72 | return chanOut 73 | } 74 | 75 | func (wa *WorkerPool) dispatchJob() <-chan task { 76 | wa.jobs = make(chan task) 77 | go func() { 78 | for i, t := range wa.tasks { 79 | time.Sleep(1 * time.Second) 80 | select { 81 | case <-wa.ctx.Done(): 82 | fmt.Println("Job dispatch canceled") 83 | break 84 | default: 85 | i++ 86 | fmt.Printf("dispatching job %d ... \n", i) 87 | wa.jobs <- t 88 | } 89 | } 90 | fmt.Println("Job dispatched successfully") 91 | close(wa.jobs) 92 | fmt.Println("Job channel was closed") 93 | }() 94 | fmt.Println("return jobs") 95 | return wa.jobs 96 | } 97 | 98 | func (wa *WorkerPool) Run(ctx context.Context) { 99 | wa.ctx = ctx 100 | done := make(chan int) 101 | go func() { 102 | jobs := wa.dispatchJob() 103 | w := worker(jobs, wa) 104 | counterSuccess := 0 105 | for data := range w { 106 | 107 | counterSuccess++ 108 | wa.results = append(wa.results, data) 109 | fmt.Println("Total processed job ", counterSuccess) 110 | } 111 | done <- counterSuccess 112 | }() 113 | 114 | select { 115 | case <-wa.ctx.Done(): 116 | fmt.Println("Proccess Stopped ", ctx.Err()) 117 | case totalSuccess := <-done: 118 | close(done) 119 | wa.TotalSuccessJob = totalSuccess 120 | fmt.Println("Total Keseluruhan ", totalSuccess) 121 | } 122 | } 123 | 124 | func (wa *WorkerPool) AddTask(task task) { 125 | wa.tasks = append(wa.tasks, task) 126 | } 127 | 128 | func (wa *WorkerPool) SetWorkerNumber(total int) { 129 | wa.workersCount = total 130 | } 131 | 132 | func (wa *WorkerPool) GetTotalSuccessJob() int { 133 | return wa.TotalSuccessJob 134 | } 135 | func (wa *WorkerPool) GetResults() []result { 136 | return wa.results 137 | } 138 | 139 | type IWorkerPool interface { 140 | Run(ctx context.Context) 141 | WaitWorker() 142 | GetResults() []result 143 | AddTask(task task) 144 | SetWorkerNumber(total int) 145 | GetTotalSuccessJob() int 146 | } 147 | 148 | func NewWorkerPool() IWorkerPool { 149 | return &WorkerPool{wg: new(sync.WaitGroup), workersCount: constants.TotalWorkerMax} 150 | } 151 | -------------------------------------------------------------------------------- /src/interfaces/rest/form_request/bankRequest.go: -------------------------------------------------------------------------------- 1 | package form_request 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | validation "github.com/go-ozzo/ozzo-validation/v4" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/errors" 8 | ) 9 | 10 | type BankRequest struct { 11 | Page int `form:"page" json:"page"` 12 | } 13 | 14 | func (b BankRequest) Validate(ctx *gin.Context) *errors.ValidationError { 15 | request := NewRequestValidation() 16 | request.AddParam("page", &b.Page, validation.Required, validation.Min(1)) 17 | request.SetCustomCode(constants.QueryParamDataInvalidCode) 18 | return request.Validate(ctx) 19 | } 20 | -------------------------------------------------------------------------------- /src/interfaces/rest/form_request/bank_request.go: -------------------------------------------------------------------------------- 1 | package form_request 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | validation "github.com/go-ozzo/ozzo-validation/v4" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructures/errors" 7 | ) 8 | 9 | type BankRequest struct { 10 | Page int `form:"page" json:"page"` 11 | } 12 | 13 | func (b BankRequest) Validate(ctx *gin.Context) *errors.GeneralError { 14 | request := NewRequestValidation() 15 | request.AddParam("page", &b.Page, validation.Required, validation.Min(1)) 16 | request.SetCustomCode(errors.QueryParamDataInvalid) 17 | return request.Validate(ctx) 18 | } 19 | -------------------------------------------------------------------------------- /src/interfaces/rest/form_request/oauth2Request.go: -------------------------------------------------------------------------------- 1 | package form_request 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | validation "github.com/go-ozzo/ozzo-validation/v4" 6 | "github.com/go-ozzo/ozzo-validation/v4/is" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/errors" 9 | ) 10 | 11 | type Oauth2Request struct { 12 | ClientId string `form:"client_id" json:"client_id"` 13 | ClientSecret string `form:"client_secret" json:"client_secret"` 14 | GrantType string `form:"grant_type" json:"grant_type"` 15 | RedirectUri string `form:"redirect_uri" json:"redirect_uri"` 16 | Scope string `form:"scope" json:"scope"` 17 | Code string `form:"code" json:"code"` 18 | State string `form:"state" json:"state"` 19 | } 20 | 21 | func (oauthRequest *Oauth2Request) Validate(ctx *gin.Context) *errors.ValidationError { 22 | request := NewRequestValidation() 23 | request.AddParam("client_id", &oauthRequest.ClientId, validation.Required, validation.Length(1, 36), is.UUID) 24 | request.AddParam("client_secret", &oauthRequest.ClientSecret, validation.When( 25 | oauthRequest.GrantType == constants.ClientCredentialsGrantType, 26 | validation.Required, 27 | )) 28 | request.AddParam("grant_type", &oauthRequest.GrantType, 29 | validation.Required, 30 | validation.Length(1, len(constants.AuthorizationGrantType)), 31 | validation.In( 32 | constants.AuthorizationGrantType, 33 | constants.ClientCredentialsGrantType, 34 | constants.RefreshTokenGrantType, 35 | constants.DeviceCodeGrantType, 36 | ), 37 | ) 38 | request.AddParam("redirect_uri", &oauthRequest.RedirectUri, validation.When( 39 | oauthRequest.GrantType == constants.AuthorizationGrantType, 40 | validation.Required, 41 | is.URL, 42 | )) 43 | request.AddParam("code", &oauthRequest.Code, validation.When( 44 | oauthRequest.GrantType != "" && 45 | &oauthRequest.GrantType != nil && 46 | oauthRequest.GrantType == constants.AuthorizationGrantType, 47 | validation.Required, 48 | )) 49 | request.AddParam("scope", &oauthRequest.Scope, validation.Required) 50 | 51 | return request.Validate(ctx) 52 | } 53 | -------------------------------------------------------------------------------- /src/interfaces/rest/form_request/request.go: -------------------------------------------------------------------------------- 1 | package form_request 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | validation "github.com/go-ozzo/ozzo-validation/v4" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/errors" 9 | ) 10 | 11 | type paramValidation struct { 12 | name string 13 | value any 14 | rules []validation.Rule 15 | } 16 | 17 | type requestValidation struct { 18 | Params []paramValidation 19 | Code *uint 20 | StatusCode int 21 | } 22 | 23 | func NewRequestValidation() *requestValidation { 24 | return &requestValidation{} 25 | } 26 | 27 | func (rv *requestValidation) AddParam(name string, value any, rules ...validation.Rule) { 28 | param := paramValidation{ 29 | name: name, 30 | value: value, 31 | rules: rules, 32 | } 33 | rv.Params = append(rv.Params, param) 34 | } 35 | 36 | func (rv *requestValidation) Validate(ctx *gin.Context) *errors.ValidationError { 37 | var customErr *errors.ValidationError 38 | 39 | if rv.Code == nil { 40 | customErr = errors.NewError(constants.DataPayloadInvalidCode) 41 | } else { 42 | customErr = errors.NewError(*rv.Code) 43 | } 44 | 45 | customErr.ValidationErrors = map[string]errors.Errorlists{} 46 | for _, param := range rv.Params { 47 | errorLists := rv.ValidateParam(¶m.value, param.rules...) 48 | if len(errorLists) > 0 { 49 | customErr.ValidationErrors[param.name] = errorLists 50 | } 51 | } 52 | 53 | if len(customErr.ValidationErrors) <= 0 { 54 | return nil 55 | } 56 | 57 | ctx.Header("Content-Type", "application/problem+json") 58 | return customErr 59 | } 60 | 61 | func (rv *requestValidation) ValidateParam(value any, rules ...validation.Rule) errors.Errorlists { 62 | var errorLists []string 63 | for _, rule := range rules { 64 | errorParam := validation.Validate(&value, 65 | rule, 66 | ) 67 | 68 | if errorParam != nil { 69 | 70 | errorLists = append(errorLists, fmt.Sprintf("%s", errorParam)) 71 | } 72 | } 73 | 74 | if len(errorLists) <= 0 { 75 | return nil 76 | } 77 | 78 | return errorLists 79 | } 80 | 81 | func (rv *requestValidation) SetCustomCode(code uint) { 82 | rv.Code = &code 83 | } 84 | 85 | func (rv *requestValidation) SetStatusCode(statusCode int) { 86 | rv.StatusCode = statusCode 87 | } 88 | -------------------------------------------------------------------------------- /src/interfaces/rest/handler.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/routes/oauth2/handler" 4 | 5 | type Handler struct { 6 | oauth2_handler.Oauth2Handler 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/rest/middleware/timeout.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "github.com/gin-gonic/gin" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/config" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func TimeoutHandler(config config.HttpConf) func(c *gin.Context) { 12 | timeout := time.Duration(config.Timeout) * time.Second 13 | responseBodyTimeout := gin.H{ 14 | "code": http.StatusRequestTimeout, 15 | "message": "request timeout, response is sent from middleware", 16 | } 17 | 18 | return func(c *gin.Context) { 19 | ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) 20 | 21 | defer func() { 22 | if ctx.Err() == context.DeadlineExceeded { 23 | c.JSON(http.StatusRequestTimeout, responseBodyTimeout) 24 | c.Abort() 25 | } 26 | 27 | cancel() 28 | }() 29 | c.Request = c.Request.WithContext(ctx) 30 | c.Next() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/interfaces/rest/provider.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package rest 5 | 6 | import ( 7 | "github.com/google/wire" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/usecases" 9 | handler2 "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/routes/oauth2/handler" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func ProvideOauthClientHandler(u *usecases.Usecase, logger *logrus.Logger) *handler2.OauthClientHandler { 14 | return &handler2.OauthClientHandler{ 15 | Usecase: u.OauthUsecase, 16 | Logger: logger, 17 | } 18 | } 19 | 20 | func ProvideOauthTokenHandler(u *usecases.Usecase, logger *logrus.Logger) *handler2.OauthTokenHandler { 21 | return &handler2.OauthTokenHandler{ 22 | Usecase: u.OauthUsecase, 23 | Logger: logger, 24 | } 25 | } 26 | 27 | var ( 28 | ProviderHandlerSet wire.ProviderSet = wire.NewSet( 29 | ProvideOauthClientHandler, 30 | ProvideOauthTokenHandler, 31 | wire.Struct(new(Handler), "*"), 32 | wire.Struct(new(handler2.Oauth2Handler), "*"), 33 | wire.Bind(new(handler2.IOauthClientHandler), new(*handler2.OauthClientHandler)), 34 | wire.Bind(new(handler2.IOauthTokenHandler), new(*handler2.OauthTokenHandler)), 35 | ) 36 | ) 37 | 38 | func InjectHandler(usecases *usecases.Usecase, logger *logrus.Logger, defaultJoins ...string) *Handler { 39 | panic(wire.Build(ProviderHandlerSet)) 40 | } 41 | -------------------------------------------------------------------------------- /src/interfaces/rest/rest.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "context" 5 | "github.com/gin-gonic/gin" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/config" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/middleware" 9 | "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/routes/oauth2" 10 | ) 11 | 12 | func NewRoute( 13 | ctx context.Context, 14 | handler *Handler, 15 | config *config.Config, 16 | ) *gin.Engine { 17 | 18 | if config.App.Environment == constants.PRODUCTION { 19 | gin.SetMode(gin.ReleaseMode) 20 | } 21 | 22 | router := gin.Default() 23 | router.Use(func(ginContext *gin.Context) { 24 | ginContext.Request = ginContext.Request.WithContext(ctx) 25 | ginContext.Next() 26 | }) 27 | router.Use(middleware.TimeoutHandler(config.Http)) 28 | 29 | router.GET("/health", HealthGET) 30 | 31 | oauth2Group := router.Group("/oauth2") 32 | { 33 | oauth2.RouteOauth2Client(oauth2Group, handler.Oauth2Handler) 34 | } 35 | return router 36 | } 37 | 38 | func HealthGET(c *gin.Context) { 39 | c.JSON(200, gin.H{ 40 | "status": "UP", 41 | "service_name": "sdf", 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /src/interfaces/rest/routes/contracts/resources.go: -------------------------------------------------------------------------------- 1 | package contracts 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type IResourcesHandler interface { 6 | Index(ctx *gin.Context) 7 | Store(ctx *gin.Context) 8 | Update(ctx *gin.Context) 9 | Delete(ctx *gin.Context) 10 | Show(ctx *gin.Context) 11 | } 12 | -------------------------------------------------------------------------------- /src/interfaces/rest/routes/oauth2/handler/authorize.go: -------------------------------------------------------------------------------- 1 | package oauth2_handler 2 | 3 | type IOauthAuthorize interface { 4 | } 5 | -------------------------------------------------------------------------------- /src/interfaces/rest/routes/oauth2/handler/client.go: -------------------------------------------------------------------------------- 1 | package oauth2_handler 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/usecases" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/routes/contracts" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type IOauthClientHandler interface { 11 | contracts.IResourcesHandler 12 | } 13 | 14 | type OauthClientHandler struct { 15 | Usecase usecases.IOauthUsecase 16 | Logger *logrus.Logger 17 | } 18 | 19 | func (o *OauthClientHandler) Update(ctx *gin.Context) { 20 | //TODO implement me 21 | panic("implement me") 22 | } 23 | 24 | func (o *OauthClientHandler) Delete(ctx *gin.Context) { 25 | //TODO implement me 26 | panic("implement me") 27 | } 28 | 29 | func (o *OauthClientHandler) Show(ctx *gin.Context) { 30 | //TODO implement me 31 | panic("implement me") 32 | } 33 | 34 | func (o *OauthClientHandler) Index(ctx *gin.Context) { 35 | //TODO implement me 36 | panic("implement me") 37 | } 38 | 39 | func (o *OauthClientHandler) Store(ctx *gin.Context) { 40 | //TODO implement me 41 | panic("implement me") 42 | } 43 | 44 | var _ IOauthClientHandler = &OauthClientHandler{} 45 | -------------------------------------------------------------------------------- /src/interfaces/rest/routes/oauth2/handler/index.go: -------------------------------------------------------------------------------- 1 | package oauth2_handler 2 | 3 | type Oauth2Handler struct { 4 | OauthClientHandler IOauthClientHandler 5 | OauthTokenHandler IOauthTokenHandler 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/rest/routes/oauth2/handler/token.go: -------------------------------------------------------------------------------- 1 | package oauth2_handler 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/dto" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/usecases" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/errors" 9 | "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/form_request" 10 | "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/routes/contracts" 11 | "github.com/sirupsen/logrus" 12 | "net/http" 13 | "strings" 14 | ) 15 | 16 | type IOauthTokenHandler interface { 17 | contracts.IResourcesHandler 18 | } 19 | 20 | type OauthTokenHandler struct { 21 | Usecase usecases.IOauthUsecase 22 | Logger *logrus.Logger 23 | } 24 | 25 | func (o OauthTokenHandler) Index(ctx *gin.Context) { 26 | //TODO implement me 27 | panic("implement me") 28 | } 29 | 30 | func (o OauthTokenHandler) Store(ctx *gin.Context) { 31 | var request form_request.Oauth2Request 32 | err := ctx.ShouldBind(&request) 33 | if err != nil { 34 | ctx.JSON(500, err) 35 | return 36 | } 37 | errV := request.Validate(ctx) 38 | if errV != nil { 39 | pd := errors.NewProblemDetails(422, errV, "", "") 40 | messages, _ := json.Marshal(pd) 41 | o.Logger.Error(string(messages)) 42 | ctx.JSON(422, pd) 43 | return 44 | } 45 | scopes := strings.Split(request.Scope, ",") 46 | issueTokenDto := dto.NewIssueToken(request.GrantType, request.ClientId, request.ClientSecret, scopes) 47 | 48 | tokenEntity, errUsecase := o.Usecase.IssueToken(ctx, issueTokenDto) 49 | if errUsecase != nil { 50 | pd := errors.NewProblemDetails(errUsecase.GetStatusCode(), errUsecase, "", "") 51 | messages, _ := json.Marshal(pd) 52 | o.Logger.Warnf(string(messages)) 53 | ctx.JSON(errUsecase.GetStatusCode(), pd) 54 | return 55 | } 56 | ctx.Header("cache-control", "no-store, private") 57 | ctx.Header("connection", "keep-alive") 58 | ctx.JSON(http.StatusOK, tokenEntity) 59 | } 60 | 61 | func (o OauthTokenHandler) Update(ctx *gin.Context) { 62 | //TODO implement me 63 | panic("implement me") 64 | } 65 | 66 | func (o OauthTokenHandler) Delete(ctx *gin.Context) { 67 | //TODO implement me 68 | panic("implement me") 69 | } 70 | 71 | func (o OauthTokenHandler) Show(ctx *gin.Context) { 72 | //TODO implement me 73 | panic("implement me") 74 | } 75 | 76 | var _ IOauthClientHandler = OauthTokenHandler{} 77 | -------------------------------------------------------------------------------- /src/interfaces/rest/routes/oauth2/index.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/routes/oauth2/handler" 6 | ) 7 | 8 | func RouteOauth2Client(routeGroup *gin.RouterGroup, handler oauth2_handler.Oauth2Handler) { 9 | routeGroup.GET("/client", handler.OauthClientHandler.Index) 10 | routeGroup.POST("/client", handler.OauthClientHandler.Store) 11 | routeGroup.POST("/token", handler.OauthTokenHandler.Store) 12 | } 13 | -------------------------------------------------------------------------------- /src/interfaces/rest/routes/v1/simkah_app/handler/bank.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/usecases" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/errors" 9 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/utils" 10 | "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/form_request" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type IBankHandler interface { 15 | Index(ctx *gin.Context) 16 | } 17 | 18 | type BankHandler struct { 19 | Usecase usecases.IBankUsecase 20 | Logger *logrus.Logger 21 | } 22 | 23 | func (b *BankHandler) Index(ctx *gin.Context) { 24 | var bankRequest form_request.BankRequest 25 | ctx.ShouldBind(&bankRequest) 26 | ctx.Handler() 27 | errV := bankRequest.Validate(ctx) 28 | 29 | if errV != nil { 30 | messages, _ := json.Marshal(errV) 31 | 32 | b.Logger.Warnf(string(messages)) 33 | ctx.JSON(422, errors.NewProblemDetails(422, errV, "", "")) 34 | return 35 | } 36 | 37 | listBank, err := b.Usecase.GetListBank(ctx.Request.Context()) 38 | if err != nil { 39 | return 40 | } 41 | 42 | resp := utils.NewResponse[[]entities.Bank, string](listBank) 43 | resp.MakeMetaData("halo", nil, nil, nil, nil) 44 | ctx.JSON(200, resp) 45 | } 46 | -------------------------------------------------------------------------------- /src/interfaces/rest/routes/v1/simkah_app/index.go: -------------------------------------------------------------------------------- 1 | package simkah_app 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/routes/v1/simkah_app/handler" 6 | ) 7 | 8 | func RouteSimkahAppV1(routeGroup *gin.RouterGroup, handler handler.IBankHandler) { 9 | //app.SetActiveConnectionDB("mysql") 10 | routeGroup.GET("/banks", handler.Index) 11 | } 12 | -------------------------------------------------------------------------------- /src/interfaces/rest/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package rest 8 | 9 | import ( 10 | "github.com/google/wire" 11 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/usecases" 12 | "github.com/hendrorahmat/golang-clean-architecture/src/interfaces/rest/routes/oauth2/handler" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // Injectors from provider.go: 17 | 18 | func InjectHandler(usecases2 *usecases.Usecase, logger *logrus.Logger, defaultJoins ...string) *Handler { 19 | oauthClientHandler := ProvideOauthClientHandler(usecases2, logger) 20 | oauthTokenHandler := ProvideOauthTokenHandler(usecases2, logger) 21 | oauth2Handler := oauth2_handler.Oauth2Handler{ 22 | OauthClientHandler: oauthClientHandler, 23 | OauthTokenHandler: oauthTokenHandler, 24 | } 25 | handler := &Handler{ 26 | Oauth2Handler: oauth2Handler, 27 | } 28 | return handler 29 | } 30 | 31 | // provider.go: 32 | 33 | func ProvideOauthClientHandler(u *usecases.Usecase, logger *logrus.Logger) *oauth2_handler.OauthClientHandler { 34 | return &oauth2_handler.OauthClientHandler{ 35 | Usecase: u.OauthUsecase, 36 | Logger: logger, 37 | } 38 | } 39 | 40 | func ProvideOauthTokenHandler(u *usecases.Usecase, logger *logrus.Logger) *oauth2_handler.OauthTokenHandler { 41 | return &oauth2_handler.OauthTokenHandler{ 42 | Usecase: u.OauthUsecase, 43 | Logger: logger, 44 | } 45 | } 46 | 47 | var ( 48 | ProviderHandlerSet wire.ProviderSet = wire.NewSet( 49 | ProvideOauthClientHandler, 50 | ProvideOauthTokenHandler, wire.Struct(new(Handler), "*"), wire.Struct(new(oauth2_handler.Oauth2Handler), "*"), wire.Bind(new(oauth2_handler.IOauthClientHandler), new(*oauth2_handler.OauthClientHandler)), wire.Bind(new(oauth2_handler.IOauthTokenHandler), new(*oauth2_handler.OauthTokenHandler)), 51 | ) 52 | ) 53 | -------------------------------------------------------------------------------- /storage/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | 3 | 4 | *.cert 5 | *.crt 6 | *.key 7 | -------------------------------------------------------------------------------- /tests/integration/database/postgres/oauthClientRepository_test.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "github.com/brianvoe/gofakeit/v6" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors/oauth" 7 | "github.com/hendrorahmat/golang-clean-architecture/tests/integration/factories" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | type OauthClientRepository struct { 14 | db *gorm.DB 15 | suite.Suite 16 | } 17 | 18 | func (p *PostgresDatabaseSuiteTest) TestOauthClientRepository_FindByClientIdAndClientSecret() { 19 | ctx := context.Background() 20 | model := factories.NewOauthClientFactory() 21 | repository := p.app.GetRepository().OauthClientRepository 22 | //gormDb := database.ProvideDatabaseGorm(p.db.DB(), p.app.GetLogger()) 23 | 24 | err := repository.Create(ctx, &model) 25 | p.Assert().NoError(err) 26 | data, err := repository.FindByClientIdAndClientSecret(ctx, model.ID.String(), model.Secret) 27 | p.Assert().NoError(err) 28 | 29 | p.Assert().Equal(model.ID, data.ID) 30 | } 31 | 32 | func (p *PostgresDatabaseSuiteTest) TestOauthClientRepository_FindByClientIdAndClientSecret_ShouldReturnDomainNotFound() { 33 | repository := p.app.GetRepository().OauthClientRepository 34 | ctx := context.Background() 35 | clientId, clientSecret := gofakeit.UUID(), gofakeit.LetterN(40) 36 | data, err := repository.FindByClientIdAndClientSecret(ctx, clientId, clientSecret) 37 | assert.Nil(p.T(), data) 38 | assert.Equal(p.T(), err, oauth.ThrowClientIdAndSecretNotFound(clientId, clientSecret)) 39 | } 40 | -------------------------------------------------------------------------------- /tests/integration/database/postgres/postgresSuite_test.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "github.com/golang-migrate/migrate/v4" 5 | "github.com/golang-migrate/migrate/v4/database/postgres" 6 | _ "github.com/golang-migrate/migrate/v4/source/file" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/bootstrap" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 9 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database" 10 | _ "github.com/lib/pq" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/suite" 13 | "testing" 14 | ) 15 | 16 | const MigrationsPath = "file://../../../../migrations" 17 | 18 | type PostgresDatabaseSuiteTest struct { 19 | db database.IDB 20 | app bootstrap.IApp 21 | suite.Suite 22 | } 23 | 24 | func (pgDb *PostgresDatabaseSuiteTest) SetupSuite() { 25 | app := bootstrap.Boot() 26 | pgDb.db = app.GetActiveConnection() 27 | pgDb.app = app 28 | } 29 | 30 | func (pgDb *PostgresDatabaseSuiteTest) SetupTest() { 31 | driver, err := postgres.WithInstance(pgDb.db.SqlDB(), &postgres.Config{}) 32 | m, err := migrate.NewWithDatabaseInstance(MigrationsPath, constants.PostgresDriverName, driver) 33 | assert.NoError(pgDb.T(), err) 34 | 35 | err = m.Up() 36 | if err != nil && err == migrate.ErrNoChange { 37 | return 38 | } else if err != nil { 39 | panic(err) 40 | } 41 | } 42 | 43 | func (pgDb *PostgresDatabaseSuiteTest) TearDownTest() { 44 | driver, err := postgres.WithInstance(pgDb.db.SqlDB(), &postgres.Config{}) 45 | assert.NoError(pgDb.T(), err) 46 | m, err := migrate.NewWithDatabaseInstance(MigrationsPath, constants.PostgresDriverName, driver) 47 | assert.NoError(pgDb.T(), err) 48 | assert.NoError(pgDb.T(), m.Down()) 49 | } 50 | 51 | func TestPostgresRepositoryTestSuite(t *testing.T) { 52 | suite.Run(t, &PostgresDatabaseSuiteTest{}) 53 | } 54 | -------------------------------------------------------------------------------- /tests/integration/factories/oauthClientFactory.go: -------------------------------------------------------------------------------- 1 | package factories 2 | 3 | import ( 4 | "github.com/brianvoe/gofakeit/v6" 5 | "github.com/gofrs/uuid" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database/models" 8 | "gorm.io/gorm" 9 | "time" 10 | ) 11 | 12 | func NewOauthClientFactory() models.OauthClient { 13 | timeNow := time.Now() 14 | 15 | return models.OauthClient{ 16 | ID: uuid.FromStringOrNil(gofakeit.UUID()), 17 | Name: gofakeit.Username(), 18 | EnabledGrantType: []string{constants.ClientCredentialsGrantType}, 19 | Secret: gofakeit.Password(true, true, true, true, true, 2), 20 | Redirect: gofakeit.URL(), 21 | CreatedAt: &timeNow, 22 | UpdatedAt: &timeNow, 23 | DeletedAt: gorm.DeletedAt{}, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/integration/interfaces/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrorahmat/golang-clean-architecture/4bd82ba65c80c31b86bf9c62025981e0cf34c44f/tests/integration/interfaces/.gitkeep -------------------------------------------------------------------------------- /tests/mocks/repositories/oauthAccessTokenMock.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | type OauthAccessTokenMock struct { 11 | mock.Mock 12 | repositories.ITransactionRepository 13 | } 14 | 15 | func (mockRepository *OauthAccessTokenMock) Create(ctx context.Context, oauthAccessTokenModel interface{}) errors.DomainError { 16 | args := mockRepository.Called(ctx, oauthAccessTokenModel) 17 | if args[0] == nil { 18 | return nil 19 | } 20 | return args[0].(errors.DomainError) 21 | } 22 | -------------------------------------------------------------------------------- /tests/mocks/repositories/oauthClientRepositoryMock.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/repositories" 8 | "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | type OauthClientRepositoryMock struct { 12 | mock.Mock 13 | repositories.IOauthClientRepository 14 | } 15 | 16 | func (o *OauthClientRepositoryMock) FindByClientIdAndClientSecret(ctx context.Context, clientId, clientSecret string) (*entities.OauthClient, errors.DomainError) { 17 | args := o.Called(ctx, clientId, clientSecret) 18 | var expectedError errors.DomainError 19 | var expectedData *entities.OauthClient 20 | if args[1] == nil { 21 | expectedError = nil 22 | } else { 23 | expectedError = args[1].(errors.DomainError) 24 | } 25 | 26 | if args[0] == nil { 27 | expectedData = nil 28 | } else { 29 | expectedData = args[0].(*entities.OauthClient) 30 | } 31 | return expectedData, expectedError 32 | } 33 | -------------------------------------------------------------------------------- /tests/test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-testfixtures/testfixtures/v3" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/bootstrap" 7 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/utils" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | var fixtures *testfixtures.Loader 14 | 15 | func FixturesLoad(m *testing.M) { 16 | application := bootstrap.Boot() 17 | rootPath := utils.GetRootPath() 18 | var dialect = "" 19 | if application.GetConfig().Database[constants.DefaultConnectionDB].Driver == constants.POSTGRES { 20 | dialect = "postgresql" 21 | } 22 | 23 | var err error 24 | fixtures, err = testfixtures.New( 25 | testfixtures.Template(), 26 | testfixtures.Database(application.GetActiveConnection().SqlDB()), // You database connection 27 | testfixtures.Dialect(dialect), // Available: "postgresql", "timescaledb", "mysql", "mariadb", "sqlite" and "sqlserver" 28 | testfixtures.Directory(rootPath+"/testdata/fixtures"), // The directory containing the YAML files 29 | testfixtures.UseDropConstraint(), 30 | ) 31 | if err != nil { 32 | application.GetLogger().Fatal(err) 33 | } 34 | os.Exit(m.Run()) 35 | } 36 | 37 | func PrepareTestDatabase() { 38 | if err := fixtures.Load(); err != nil { 39 | fmt.Println("err") 40 | fmt.Println(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/applications/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrorahmat/golang-clean-architecture/4bd82ba65c80c31b86bf9c62025981e0cf34c44f/tests/unit/applications/.gitkeep -------------------------------------------------------------------------------- /tests/unit/applications/services/oauth/createTokenClientCredentialService_test.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | import ( 4 | "context" 5 | "github.com/brianvoe/gofakeit/v6" 6 | "github.com/hendrorahmat/golang-clean-architecture/src/applications/commands/oauth" 7 | oauthService "github.com/hendrorahmat/golang-clean-architecture/src/applications/services/oauth" 8 | "github.com/hendrorahmat/golang-clean-architecture/src/domain/entities" 9 | domainErrorsOauth "github.com/hendrorahmat/golang-clean-architecture/src/domain/errors/oauth" 10 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/constants" 11 | "github.com/hendrorahmat/golang-clean-architecture/src/infrastructure/persistance/database/models" 12 | repositoriesMock "github.com/hendrorahmat/golang-clean-architecture/tests/mocks/repositories" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/mock" 15 | "testing" 16 | "time" 17 | ) 18 | 19 | func TestCreateToken(t *testing.T) { 20 | command := &oauth.CreateTokenClientCredentialCommand{} 21 | oauthClientRepository := new(repositoriesMock.OauthClientRepositoryMock) 22 | oauthAccessTokenRepository := new(repositoriesMock.OauthAccessTokenMock) 23 | entityOauthClient := &entities.OauthClient{} 24 | oauthAccessTokenModel := &models.OauthAccessToken{} 25 | 26 | gofakeit.Struct(command) 27 | gofakeit.Struct(entityOauthClient) 28 | gofakeit.Struct(oauthAccessTokenModel) 29 | 30 | oauthClientRepository. 31 | On("FindByClientIdAndClientSecret", context.Background(), command.ClientId, command.ClientSecret). 32 | Return( 33 | entityOauthClient, 34 | nil, 35 | ) 36 | oauthAccessTokenRepository. 37 | On("Create", context.Background(), mock.AnythingOfType("*models.OauthAccessToken")). 38 | Run(func(args mock.Arguments) { 39 | arg := args.Get(1).(*models.OauthAccessToken) 40 | arg.ID = oauthAccessTokenModel.ID 41 | arg.CreatedAt = oauthAccessTokenModel.CreatedAt 42 | arg.UpdatedAt = oauthAccessTokenModel.UpdatedAt 43 | arg.UserId = oauthAccessTokenModel.UserId 44 | arg.GrantType = oauthAccessTokenModel.GrantType 45 | arg.Scopes = oauthAccessTokenModel.Scopes 46 | arg.ExpiresAt = oauthAccessTokenModel.ExpiresAt 47 | arg.DeletedAt = oauthAccessTokenModel.DeletedAt 48 | arg.ClientId = oauthAccessTokenModel.ClientId 49 | }). 50 | Return(nil) 51 | service := oauthService.NewCreateTokenClientCredentialService(command, oauthClientRepository, oauthAccessTokenRepository) 52 | token, domainError := service.Handle(context.Background()) 53 | oneDay := time.Now().UTC().Add(24 * time.Hour).Format(constants.SQLTimestampFormat) 54 | 55 | assert.Nil(t, domainError) 56 | assert.NotEmpty(t, token) 57 | assert.Equal(t, token.ExpiresIn().Format(constants.SQLTimestampFormat), oneDay) 58 | assert.Equal(t, token.TokenType(), constants.TokenTypeBearer) 59 | oauthClientRepository.AssertExpectations(t) 60 | oauthAccessTokenRepository.AssertExpectations(t) 61 | } 62 | 63 | func TestShouldReturnDomainErrorWhenClientIdAndSecreteNotFound(t *testing.T) { 64 | command := &oauth.CreateTokenClientCredentialCommand{} 65 | oauthClientRepository := new(repositoriesMock.OauthClientRepositoryMock) 66 | entityOauthClient := &entities.OauthClient{} 67 | oauthAccessTokenRepository := new(repositoriesMock.OauthAccessTokenMock) 68 | 69 | gofakeit.Struct(command) 70 | gofakeit.Struct(entityOauthClient) 71 | 72 | expectedError := domainErrorsOauth.ThrowClientIdAndSecretNotFound(command.ClientId, command.ClientSecret) 73 | oauthClientRepository. 74 | On("FindByClientIdAndClientSecret", context.Background(), command.ClientId, command.ClientSecret). 75 | Return( 76 | nil, 77 | expectedError, 78 | ) 79 | service := oauthService.NewCreateTokenClientCredentialService(command, oauthClientRepository, oauthAccessTokenRepository) 80 | data, domainError := service.Handle(context.Background()) 81 | 82 | assert.Equal(t, expectedError, domainError) 83 | assert.Nil(t, data) 84 | oauthClientRepository.AssertExpectations(t) 85 | } 86 | -------------------------------------------------------------------------------- /tests/unit/domain/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrorahmat/golang-clean-architecture/4bd82ba65c80c31b86bf9c62025981e0cf34c44f/tests/unit/domain/.gitkeep -------------------------------------------------------------------------------- /tests/unit/domain/bank_test.go: -------------------------------------------------------------------------------- 1 | package domain_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hendrorahmat/golang-clean-architecture/tests" 6 | "testing" 7 | ) 8 | 9 | func TestX(t *testing.T) { 10 | t.Parallel() 11 | tests.PrepareTestDatabase() 12 | fmt.Println("hay") 13 | // Your test here ... 14 | } 15 | 16 | func TestZ(t *testing.T) { 17 | tests.PrepareTestDatabase() 18 | fmt.Println("hay Z") 19 | // Your test here ... 20 | } 21 | -------------------------------------------------------------------------------- /tests/unit/domain/main_test.go: -------------------------------------------------------------------------------- 1 | package domain_test 2 | 3 | import ( 4 | "github.com/go-testfixtures/testfixtures/v3" 5 | "github.com/hendrorahmat/golang-clean-architecture/tests" 6 | "testing" 7 | ) 8 | 9 | var fixtures *testfixtures.Loader 10 | 11 | func TestMain(m *testing.M) { 12 | tests.FixturesLoad(m) 13 | } 14 | --------------------------------------------------------------------------------