diff --git a/chapter-D.4-clean-architecture-golang/config/.env b/chapter-D.4-clean-architecture-golang/config/.env new file mode 100644 index 0000000..65054c4 --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/config/.env @@ -0,0 +1,10 @@ +DATABASE_HOST=127.0.0.1 +DATABASE_PORT=5432 +DATABASE_USERNAME=postgres +DATABASE_PASSWORD=root +DATABASE_NAME=testing + +APP_NAME=clean-architeture-example +APP_HOST=127.0.0.1 +APP_PORT=9090 +APP_VERSIONAPI=v1 \ No newline at end of file diff --git a/chapter-D.4-clean-architecture-golang/config/database.go b/chapter-D.4-clean-architecture-golang/config/database.go new file mode 100644 index 0000000..11186a2 --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/config/database.go @@ -0,0 +1,40 @@ +package config + +import ( + "time" + + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type DatabaseConfig struct { + Host string + Port string + Username string + Password string + DBname string +} + +func NewDatabaseConfig(config DatabaseConfig) (db *gorm.DB, err error) { + stringConnection := "host=" + config.Host + " user=" + config.Username + " password=" + config.Password + " dbname=" + config.DBname + " port=" + config.Port + " TimeZone=UTC" + db, err = gorm.Open(postgres.Open(stringConnection), &gorm.Config{}) + if err != nil { + return + } + + sqlDB, err := db.DB() + if err != nil { + return + } + + // SetConnMaxIdleTime: berfungsi untuk menetapkan jumlah maksimum waktu koneksi secara idle(tidak berjalan) + sqlDB.SetConnMaxIdleTime(1 * time.Minute) + // SetConnMaxLifetime: berfungsi menentukan maksimum waktu dapat digunakan kembali + sqlDB.SetConnMaxLifetime(10 * time.Minute) + // SetMaxIdleConns: berfungsi untuk menentukan jumlah connection tidak dijalankan(idle) + sqlDB.SetMaxIdleConns(20) + // SetMaxOpenConns: berfungsi untuk menetapkan jumlah open koneksi + sqlDB.SetMaxOpenConns(5) + + return +} diff --git a/chapter-D.4-clean-architecture-golang/controllers/product_controller.go b/chapter-D.4-clean-architecture-golang/controllers/product_controller.go new file mode 100644 index 0000000..3841151 --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/controllers/product_controller.go @@ -0,0 +1,168 @@ +package controllers + +import ( + "clean-architecture-golang-example/entities" + "encoding/json" + "log" + "net/http" + "strconv" +) + +type ProductController struct { + versionApi string + logger *log.Logger + http *http.ServeMux + usecase *entities.ProductUsecase +} + +func NewProductController(versionApi string, logger *log.Logger, http *http.ServeMux, usecase *entities.ProductUsecase) *ProductController { + controller := &ProductController{versionApi, logger, http, usecase} + controller.Route() + + return controller +} + +func (Controller *ProductController) Gets(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method == http.MethodGet { + id, _ := strconv.Atoi(r.URL.Query().Get("id")) + controller := *Controller.usecase + + if id == 0 { + result, err := controller.Gets() + for _, each := range result.Data { + Controller.logger.Println("[SUCCESS] ID : ", each.ID) + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Controller.logger.Println("[ERROR]:" + err.Error()) + } else { + w.WriteHeader(http.StatusOK) + Controller.logger.Println("[SUCCESS]: Gets product is success") + } + + json.NewEncoder(w).Encode(result) + } else { + result, err := controller.GetOne(id) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Controller.logger.Println("[ERROR]:" + err.Error()) + } else { + w.WriteHeader(http.StatusOK) + Controller.logger.Println("[SUCCESS]: Get product by id is success") + } + + json.NewEncoder(w).Encode(result) + } + } else { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("Method not allowed")) + Controller.logger.Println("[ERROR]: method not allowed") + } +} + +func (Controller *ProductController) Create(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method == http.MethodPost { + var data entities.CreateProductRequest + + controller := *Controller.usecase + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + err := dec.Decode(&data) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Controller.logger.Println("[ERROR]:" + err.Error()) + } + + result, err := controller.Create(data) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Controller.logger.Println("[ERROR]:" + err.Error()) + } else { + w.WriteHeader(http.StatusCreated) + Controller.logger.Println("[SUCCESS]: Create product is success") + } + json.NewEncoder(w).Encode(result) + } else { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("Method not allowed")) + Controller.logger.Println("[ERROR]: method not allowed") + } +} + +func (Controller *ProductController) Update(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method == http.MethodPut { + var data entities.UpdateProductRequest + + controller := *Controller.usecase + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + err := dec.Decode(&data) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Controller.logger.Println("[ERROR]:" + err.Error()) + } + + result, err := controller.Update(data) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Controller.logger.Println("[ERROR]:" + err.Error()) + } else { + w.WriteHeader(http.StatusOK) + Controller.logger.Println("[SUCCESS] Update product is success") + + } + json.NewEncoder(w).Encode(result) + } else { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("Method not allowed")) + Controller.logger.Println("[ERROR]: method not allowed") + } +} + +func (Controller *ProductController) Delete(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method == http.MethodDelete { + var data entities.DeleteProductRequest + + controller := *Controller.usecase + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + err := dec.Decode(&data) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Controller.logger.Println("[ERROR]:" + err.Error()) + } + + result, err := controller.DeleteByID(data.ID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Controller.logger.Println("[ERROR]:" + err.Error()) + } else { + w.WriteHeader(http.StatusOK) + Controller.logger.Println("[SUCCESS] Delete product is success") + + } + json.NewEncoder(w).Encode(result) + } else { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("Method not allowed")) + Controller.logger.Println("[ERROR]: method not allowed") + } +} + +func (Controller *ProductController) Route() { + Controller.http.HandleFunc(Controller.versionApi+"/product/gets", Controller.Gets) + Controller.http.HandleFunc(Controller.versionApi+"/product/create", Controller.Create) + Controller.http.HandleFunc(Controller.versionApi+"/product/update", Controller.Update) + Controller.http.HandleFunc(Controller.versionApi+"/product/delete", Controller.Delete) +} diff --git a/chapter-D.4-clean-architecture-golang/entities/product_request.go b/chapter-D.4-clean-architecture-golang/entities/product_request.go new file mode 100644 index 0000000..cc2d3b6 --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/entities/product_request.go @@ -0,0 +1,18 @@ +package entities + +type CreateProductRequest struct { + Name string `json:"name" validate:"required"` + Price float64 `json:"price" validate:"required"` + Weight float64 `json:"weight" validate:"required"` +} + +type UpdateProductRequest struct { + ID int `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Price float64 `json:"price" validate:"required"` + Weight float64 `json:"weight" validate:"required"` +} + +type DeleteProductRequest struct { + ID int `json:"id" validate:"required"` +} diff --git a/chapter-D.4-clean-architecture-golang/entities/product_response.go b/chapter-D.4-clean-architecture-golang/entities/product_response.go new file mode 100644 index 0000000..a25839d --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/entities/product_response.go @@ -0,0 +1,9 @@ +package entities + +// Struct request and response product +type ProductResponseJSON struct { + Data []Products `json:"data"` + Count int64 `json:"count"` + Success bool `json:"success"` + Message string `json:"message"` +} diff --git a/chapter-D.4-clean-architecture-golang/entities/products.go b/chapter-D.4-clean-architecture-golang/entities/products.go new file mode 100644 index 0000000..1c30b8f --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/entities/products.go @@ -0,0 +1,30 @@ +package entities + +import "gorm.io/gorm" + +type Products struct { + gorm.Model + ID int `gorm:"column:id"` + Name string `gorm:"column:name"` + Price float64 `gorm:"column:price"` + Weight float64 `gorm:"column:weight"` +} + +// Membuat model interface untuk repository product +type ProductRepository interface { + GetByID(id int) (Products, error) + Gets() ([]Products, error) + Create(product *Products) error + Update(product *Products) error + DeleteByID(id int) (Products, error) + Count() (int64, error) +} + +// Membuat model interface untuk usecase product +type ProductUsecase interface { + GetOne(id int) (ProductResponseJSON, error) + Gets() (ProductResponseJSON, error) + Create(product CreateProductRequest) (ProductResponseJSON, error) + Update(product UpdateProductRequest) (ProductResponseJSON, error) + DeleteByID(id int) (ProductResponseJSON, error) +} diff --git a/chapter-D.4-clean-architecture-golang/go.mod b/chapter-D.4-clean-architecture-golang/go.mod new file mode 100644 index 0000000..f26cadc --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/go.mod @@ -0,0 +1,25 @@ +module clean-architecture-golang-example + +go 1.18 + +require ( + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.15.4 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.3.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + golang.org/x/crypto v0.8.0 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect + gorm.io/driver/mysql v1.5.1 // indirect + gorm.io/driver/postgres v1.5.2 // indirect + gorm.io/gorm v1.25.4 // indirect +) diff --git a/chapter-D.4-clean-architecture-golang/go.sum b/chapter-D.4-clean-architecture-golang/go.sum new file mode 100644 index 0000000..9fe22d3 --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/go.sum @@ -0,0 +1,61 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs= +github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= +gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= +gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= +gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= +gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= +gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= diff --git a/chapter-D.4-clean-architecture-golang/main.go b/chapter-D.4-clean-architecture-golang/main.go new file mode 100644 index 0000000..7d73338 --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "clean-architecture-golang-example/config" + "clean-architecture-golang-example/controllers" + "clean-architecture-golang-example/repositories" + "clean-architecture-golang-example/usecases" + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "time" + + "github.com/go-playground/validator/v10" + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load("config/.env") + if err != nil { + panic(fmt.Errorf("Error Environment : %s", err)) + } + + appConfig := struct { + name string + host string + port string + versionapi string + }{ + name: os.Getenv("APP_NAME"), + host: os.Getenv("APP_HOST"), + port: os.Getenv("APP_PORT"), + versionapi: "/api/" + os.Getenv("APP_VERSIONAPI"), + } + + envDBConfig := config.DatabaseConfig{ + Host: os.Getenv("DATABASE_HOST"), + Port: os.Getenv("DATABASE_PORT"), + Username: os.Getenv("DATABASE_USERNAME"), + Password: os.Getenv("DATABASE_PASSWORD"), + DBname: os.Getenv("DATABASE_Name"), + } + + Logger := log.New(os.Stdout, appConfig.name+" || ", log.LstdFlags) + validate := validator.New() + + db, err := config.NewDatabaseConfig(envDBConfig) + if err != nil { + panic(fmt.Errorf("Error Connection : %s", err)) + } + + serveMux := http.NewServeMux() + + productRepository := repositories.NewProductRepositories(db, true) + productUsecase := usecases.NewProductUsecase(&productRepository, validate) + controllers.NewProductController(appConfig.versionapi, Logger, serveMux, &productUsecase) + + s := &http.Server{ + Addr: ":" + appConfig.port, + Handler: serveMux, + IdleTimeout: 120 * time.Second, // max time for connection using TCP keep-alive + ReadTimeout: 5 * time.Second, // max time to read request from client + WriteTimeout: 5 * time.Second, // max time to write response to client + } + + go func() { + Logger.Printf("Starting server on port :%s\n", appConfig.port) + err := s.ListenAndServe() + if err != nil { + Logger.Fatal(err) + } + }() + + // get signal when trap signal interupt and grafefully shutdown server + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt) + signal.Notify(sigChan, os.Kill) + + // block until signal is received + sig := <-sigChan + Logger.Println("Received terminate, Graceful shutdown: ", sig) + + // waiting max 30 second for current operation complete + tContext, _ := context.WithTimeout(context.Background(), 30*time.Second) + s.Shutdown(tContext) +} diff --git a/chapter-D.4-clean-architecture-golang/repositories/product_repository.go b/chapter-D.4-clean-architecture-golang/repositories/product_repository.go new file mode 100644 index 0000000..6f3d8cb --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/repositories/product_repository.go @@ -0,0 +1,156 @@ +package repositories + +import ( + "clean-architecture-golang-example/entities" + "errors" + "log" + + "gorm.io/gorm" +) + +// ProductRepositores: repository untuk model product +type ProductRepositories struct { + database *gorm.DB +} + +// NewProductRepositories: Injeksi repository product model +func NewProductRepositories(conn *gorm.DB, isMigrate bool) entities.ProductRepository { + if isMigrate { + err := conn.AutoMigrate(entities.Products{}) + if err != nil { + log.Fatal("Migration Error:", err) + } + } + return &ProductRepositories{conn} +} + +// Create: digunakan untuk membuat insert data ke model product. +func (p *ProductRepositories) Create(product *entities.Products) error { + var err error + var tx *gorm.DB = p.database.Begin() + + query := tx.Model(entities.Products{}).Create(product) + err = query.Error + if err != nil { + tx.Rollback() + return err + } + + query = tx.Commit() + err = query.Error + if err != nil { + tx.Rollback() + return err + } + + return err +} + +// Count: digunakan untuk menghitung jumlah data product yang tersimpan. +func (p *ProductRepositories) Count() (int64, error) { + var count int64 + var err error + var tx *gorm.DB = p.database.Begin() + + query := tx.Model(entities.Products{}).Select("*").Count(&count) + err = query.Error + if err != nil { + return count, err + } + + query = tx.Commit() + err = query.Error + if err != nil { + return count, err + } + + return count, err +} + +// DelteByID: digunakan untuk menghapus data product dengan id yang dipilih. +func (p *ProductRepositories) DeleteByID(id int) (entities.Products, error) { + var product entities.Products + var err error + + queryFind := p.database.Model(entities.Products{}).Where("id = ?", id).Find(&product) + err = queryFind.Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return product, errors.New("ID is not found") + } + return product, err + } + + queryDelete := queryFind.Delete(&product) + err = queryDelete.Error + if err != nil { + return product, err + } + + return product, err +} + +// GetByID: digunakan untuk menampilkan data product yang sesuai dengan id yang dipilih. +func (p *ProductRepositories) GetByID(id int) (entities.Products, error) { + var result entities.Products + var err error + var tx *gorm.DB = p.database.Begin() + + query := tx.Model(&entities.Products{}).Where("id = ?", id).Where("deleted_at IS NULL").First(&result) + err = query.Error + if err != nil { + return result, err + } + + query = tx.Commit() + err = query.Error + if err != nil { + return result, err + } + + return result, err +} + +// Gets: digunakan untuk menampilkan semua data product. +func (p *ProductRepositories) Gets() ([]entities.Products, error) { + var results []entities.Products + var err error + var tx *gorm.DB = p.database.Begin() + + query := tx.Model(&entities.Products{}).Select("*").Where("deleted_at IS NULL").Find(&results) + err = query.Error + if err != nil { + return results, err + } + + query = tx.Commit() + err = query.Error + if err != nil { + return results, err + } + + return results, err +} + +// Update: digunakan untuk update data product. +func (p *ProductRepositories) Update(product *entities.Products) error { + var err error + var tx *gorm.DB = p.database.Begin() + + queryFind := tx.Model(entities.Products{}).Where("id = ?", product.ID).Updates(&product) + err = queryFind.Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("ID is not found") + } + return err + } + + queryFind = tx.Commit() + err = queryFind.Error + if err != nil { + return err + } + + return err +} diff --git a/chapter-D.4-clean-architecture-golang/usecases/product_usecase.go b/chapter-D.4-clean-architecture-golang/usecases/product_usecase.go new file mode 100644 index 0000000..8602c2b --- /dev/null +++ b/chapter-D.4-clean-architecture-golang/usecases/product_usecase.go @@ -0,0 +1,193 @@ +package usecases + +import ( + "clean-architecture-golang-example/entities" + "errors" + + "github.com/go-playground/validator/v10" +) + +// ProductUsecase: sebagai orchestrator bisnis proses product. +type ProductUsecase struct { + repository *entities.ProductRepository + valid *validator.Validate +} + +// NewProductUsecase: injeksi dari repository ke usecase +func NewProductUsecase(repository *entities.ProductRepository, valid *validator.Validate) entities.ProductUsecase { + return &ProductUsecase{repository, valid} +} + +// Create: digunakan untuk insert product ke repository. +func (usecase *ProductUsecase) Create(product entities.CreateProductRequest) (entities.ProductResponseJSON, error) { + var err error + var result entities.ProductResponseJSON + repo := *usecase.repository + err = usecase.valid.Struct(product) + if err != nil { + result = entities.ProductResponseJSON{ + Data: []entities.Products{}, + Count: 0, + Success: false, + Message: "Error validation:" + err.Error(), + } + return result, err + } + + count, err := repo.Count() + if err != nil { + result = entities.ProductResponseJSON{ + Data: []entities.Products{}, + Count: 0, + Success: false, + Message: "Error Internal Server:" + err.Error(), + } + return result, err + } + + var data = entities.Products{ + ID: int(count) + 1, + Name: product.Name, + Price: product.Price, + Weight: product.Weight, + } + + if err = repo.Create(&data); err != nil { + result = entities.ProductResponseJSON{ + Data: []entities.Products{}, + Count: 0, + Success: false, + Message: "Error Internal Server:" + err.Error(), + } + return result, err + } + + result = entities.ProductResponseJSON{ + Data: []entities.Products{data}, + Count: 1, + Success: true, + Message: "Create product success", + } + + return result, err +} + +// DeleteByID: digunakan untuk hapus product dengan id ke repository. +func (usecase *ProductUsecase) DeleteByID(id int) (entities.ProductResponseJSON, error) { + var result entities.ProductResponseJSON + if id == 0 { + return result, errors.New("ID must be not empty") + } + repo := *usecase.repository + data, err := repo.DeleteByID(id) + if err != nil { + result = entities.ProductResponseJSON{ + Data: []entities.Products{}, + Count: 0, + Success: false, + Message: "Error Internal Server:" + err.Error(), + } + return result, err + } + + result = entities.ProductResponseJSON{ + Data: []entities.Products{data}, + Count: 1, + Success: true, + Message: "Delete product success", + } + + return result, nil +} + +// GetOne: digunakan untuk mengambil data product dengan id yang sudah dipilih +func (usecase *ProductUsecase) GetOne(id int) (entities.ProductResponseJSON, error) { + var result entities.ProductResponseJSON + if id == 0 { + result = entities.ProductResponseJSON{ + Data: []entities.Products{}, + Count: 0, + Success: false, + Message: "Error Internal Server: ID must be not empty", + } + return result, errors.New("ID must be not empty") + } + repo := *usecase.repository + + data, err := repo.GetByID(id) + if err != nil { + result = entities.ProductResponseJSON{ + Data: []entities.Products{}, + Count: 0, + Success: false, + Message: "Error Internal Server: " + err.Error(), + } + return result, err + } + + result = entities.ProductResponseJSON{ + Data: []entities.Products{ + data, + }, + Count: 1, + Success: true, + } + + return result, nil +} + +// Gets: digunakan untuk menampilkan semua data product +func (usecase *ProductUsecase) Gets() (entities.ProductResponseJSON, error) { + repo := *usecase.repository + data, err := repo.Gets() + if err != nil { + return entities.ProductResponseJSON{}, err + } + + count, err := repo.Count() + if err != nil { + return entities.ProductResponseJSON{}, err + } + + result := entities.ProductResponseJSON{ + Data: data, + Count: count, + Success: true, + } + + return result, nil +} + +// Update: digunakan untuk mengubah data dengan id yang sudah dipilih +func (usecase *ProductUsecase) Update(product entities.UpdateProductRequest) (entities.ProductResponseJSON, error) { + err := usecase.valid.Struct(product) + var result entities.ProductResponseJSON + if err != nil { + return result, err + } + repo := *usecase.repository + var data = entities.Products{ + ID: product.ID, + Name: product.Name, + Price: product.Price, + Weight: product.Weight, + } + + if err = repo.Update(&data); err != nil { + return result, err + } + + count, err := repo.Count() + if err != nil { + return entities.ProductResponseJSON{}, err + } + + result = entities.ProductResponseJSON{ + Data: []entities.Products{data}, + Count: count, + Success: true, + Message: "Update product success", + } + + return result, nil +}