├── .env.example ├── .gitignore ├── .idea ├── .gitignore ├── misc.xml ├── modules.xml ├── task-one.iml └── vcs.xml ├── README.md ├── category ├── category_controller.go ├── category_controller_test.go ├── category_repository.go ├── category_route.go ├── category_service.go ├── dto │ ├── create_category_dto.go │ └── update_category_dto.go ├── model │ └── category_model.go └── response │ └── category_response.go ├── configs ├── database │ └── database.go ├── mail │ └── mail.go ├── redis │ └── redis.go └── route │ └── routes.go ├── docker-compose.yaml ├── exception ├── error_handler.go └── not_found_error.go ├── go.mod ├── go.sum ├── helpers ├── get_env.go ├── json.go ├── model.go ├── panic_handler.go ├── response.go └── tx.go ├── main.go └── product ├── dto ├── create_product_dto.go └── update_product_dto.go ├── model └── product_model.go ├── product_controller.go ├── product_controller_test.go ├── product_repository.go ├── product_routes.go ├── product_service.go └── response └── product_response.go /.env.example: -------------------------------------------------------------------------------- 1 | #Database 2 | DB_DRIVER="postgres" 3 | DB_URI="postgres://fzrsahi:123@localhost:5434/task_one_dikti?sslmode=disable" 4 | DB_TEST_URI="postgres://fzrsahi:123@localhost:5435/task_one_dikti_dev?sslmode=disable" 5 | 6 | #Port 7 | PORT="localhost:3001" 8 | 9 | #Redis 10 | REDIS_HOST="localhost:6379" 11 | REDIS_PASSWORD="" 12 | REDIS_DB=0 13 | 14 | #Mail 15 | CONFIG_SMTP_HOST="smtp.gmail.com" 16 | CONFIG_SMTP_PORT=587 17 | CONFIG_SENDER_NAME= 18 | CONFIG_AUTH_EMAIL= 19 | CONFIG_AUTH_PASSWORD= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | /data* 3 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/task-one.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task One Dikti - Internship 2 | 3 | #### TODO : 4 | 5 | - [x] Install and configure GO 6 | - [x] Run Go App 7 | - [x] Understanding syntax, data structure, interface 8 | - [x] Create Rest API 9 | - [x] Implement Postgres For Database 10 | - [x] CRUD Category 11 | - [x] Category Unit Test 12 | - [x] CRUD Product 13 | - [x] Product Unit Test 14 | - [x] Implement Redis 15 | - [x] Implement Goroutine 16 | - [x] Add Notification By Email 17 | - [ ] Implement JWT for Auth -------------------------------------------------------------------------------- /category/category_controller.go: -------------------------------------------------------------------------------- 1 | package category 2 | 3 | import ( 4 | "github.com/julienschmidt/httprouter" 5 | "net/http" 6 | "strconv" 7 | "task-one/category/dto" 8 | "task-one/helpers" 9 | ) 10 | 11 | type CategoryController interface { 12 | Create(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 13 | Update(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 14 | Delete(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 15 | FindById(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 16 | FindAll(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 17 | } 18 | 19 | type CategoryControllerImpl struct { 20 | Service CategoryService 21 | } 22 | 23 | func NewCategoryController(categoryService CategoryService) CategoryController { 24 | return &CategoryControllerImpl{Service: categoryService} 25 | } 26 | 27 | func (controller *CategoryControllerImpl) Create(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 28 | categoryRequest := &dto.CategoryCreateDto{} 29 | helpers.ReadFromRequestBody(request, categoryRequest) 30 | 31 | data := controller.Service.Create(request.Context(), categoryRequest) 32 | result := helpers.ApiResponse{ 33 | StatusCode: 201, 34 | Data: data, 35 | } 36 | 37 | helpers.WriteToResponse(writer, result, 201) 38 | 39 | } 40 | 41 | func (controller *CategoryControllerImpl) Update(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 42 | categoryUpdateRequest := &dto.CategoryUpdateDto{} 43 | helpers.ReadFromRequestBody(request, categoryUpdateRequest) 44 | 45 | categoryId := params.ByName("id") 46 | res, err := strconv.Atoi(categoryId) 47 | helpers.PanicIfError(err) 48 | 49 | categoryUpdateRequest.Id = res 50 | 51 | data := controller.Service.Update(request.Context(), categoryUpdateRequest) 52 | result := helpers.ApiResponse{ 53 | StatusCode: 201, 54 | Data: data, 55 | } 56 | helpers.WriteToResponse(writer, result, 201) 57 | } 58 | 59 | func (controller *CategoryControllerImpl) Delete(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 60 | categoryId := params.ByName("id") 61 | res, err := strconv.Atoi(categoryId) 62 | helpers.PanicIfError(err) 63 | 64 | controller.Service.Delete(request.Context(), res) 65 | result := helpers.ApiResponse{ 66 | StatusCode: 200, 67 | Data: nil, 68 | } 69 | helpers.WriteToResponse(writer, result, 200) 70 | 71 | } 72 | 73 | func (controller *CategoryControllerImpl) FindById(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 74 | categoryId := params.ByName("id") 75 | res, err := strconv.Atoi(categoryId) 76 | helpers.PanicIfError(err) 77 | 78 | data := controller.Service.FindById(request.Context(), res) 79 | result := helpers.ApiResponse{ 80 | StatusCode: 200, 81 | Data: data, 82 | } 83 | helpers.WriteToResponse(writer, result, 200) 84 | 85 | } 86 | 87 | func (controller *CategoryControllerImpl) FindAll(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 88 | categoryResponses := controller.Service.FindAll(request.Context()) 89 | result := helpers.ApiResponse{ 90 | StatusCode: 200, 91 | Data: categoryResponses, 92 | } 93 | helpers.WriteToResponse(writer, result, 200) 94 | } 95 | -------------------------------------------------------------------------------- /category/category_controller_test.go: -------------------------------------------------------------------------------- 1 | package category 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "github.com/go-playground/assert/v2" 8 | "github.com/julienschmidt/httprouter" 9 | _ "github.com/lib/pq" 10 | "io/ioutil" 11 | "net/http" 12 | "net/http/httptest" 13 | "strconv" 14 | "strings" 15 | "task-one/category/model" 16 | "task-one/configs/database" 17 | "task-one/exception" 18 | "testing" 19 | ) 20 | 21 | func setupRouter(db *sql.DB) http.Handler { 22 | router := httprouter.New() 23 | router.PanicHandler = exception.ErrorHandler 24 | RegisterRoute(router, db) 25 | 26 | return router 27 | 28 | } 29 | 30 | func truncateCategory(db *sql.DB) { 31 | db.Exec("TRUNCATE category") 32 | } 33 | 34 | func TestMain(m *testing.M) { 35 | m.Run() 36 | } 37 | 38 | func TestGetListCategory(t *testing.T) { 39 | db := database.ConnectToDbTest() 40 | router := setupRouter(db) 41 | 42 | req := httptest.NewRequest("GET", "http://localhost:3001/categories", nil) 43 | req.Header.Add("Content-Type", "application/json") 44 | 45 | recorder := httptest.NewRecorder() 46 | 47 | router.ServeHTTP(recorder, req) 48 | res := recorder.Result() 49 | assert.Equal(t, 200, res.StatusCode) 50 | } 51 | 52 | func TestCreateCategory(t *testing.T) { 53 | db := database.ConnectToDbTest() 54 | router := setupRouter(db) 55 | truncateCategory(db) 56 | 57 | reqBody := strings.NewReader(`{"name" : "Website"}`) 58 | req := httptest.NewRequest("POST", "http://localhost:3001/categories", reqBody) 59 | req.Header.Add("Content-Type", "application/json") 60 | 61 | recorder := httptest.NewRecorder() 62 | router.ServeHTTP(recorder, req) 63 | 64 | res := recorder.Result() 65 | 66 | body, _ := ioutil.ReadAll(res.Body) 67 | var responseBody map[string]interface{} 68 | json.Unmarshal(body, &responseBody) 69 | 70 | assert.Equal(t, 201, res.StatusCode) 71 | assert.Equal(t, "Website", responseBody["data"].(map[string]interface{})["name"]) 72 | 73 | } 74 | 75 | func TestUpdateCategory(t *testing.T) { 76 | db := database.ConnectToDbTest() 77 | truncateCategory(db) 78 | router := setupRouter(db) 79 | 80 | tx, _ := db.Begin() 81 | 82 | repository := NewCategoryRepository() 83 | category := repository.Save(context.Background(), tx, model.Category{ 84 | Name: "Handphone", 85 | }) 86 | tx.Commit() 87 | 88 | t.Run("Test Update Category Success", func(t *testing.T) { 89 | reqBody := strings.NewReader(`{"name" : "Not Handphone"}`) 90 | req := httptest.NewRequest("PATCH", "http://localhost:3001/categories/"+strconv.Itoa(category.Id), reqBody) 91 | req.Header.Add("Content-Type", "application/json") 92 | recorder := httptest.NewRecorder() 93 | router.ServeHTTP(recorder, req) 94 | 95 | res := recorder.Result() 96 | 97 | body, _ := ioutil.ReadAll(res.Body) 98 | var responseBody map[string]interface{} 99 | json.Unmarshal(body, &responseBody) 100 | 101 | assert.Equal(t, 201, res.StatusCode) 102 | assert.Equal(t, "Not Handphone", responseBody["data"].(map[string]interface{})["name"]) 103 | 104 | }) 105 | 106 | t.Run("Test Update Category Failed", func(t *testing.T) { 107 | reqBody := strings.NewReader(`{"name" : "Not Handphone"}`) 108 | req := httptest.NewRequest("PATCH", "http://localhost:3001/categories/"+strconv.Itoa(404), reqBody) 109 | req.Header.Add("Content-Type", "application/json") 110 | recorder := httptest.NewRecorder() 111 | router.ServeHTTP(recorder, req) 112 | 113 | res := recorder.Result() 114 | body, _ := ioutil.ReadAll(res.Body) 115 | var responseBody map[string]interface{} 116 | json.Unmarshal(body, &responseBody) 117 | 118 | assert.Equal(t, 404, res.StatusCode) 119 | }) 120 | 121 | } 122 | 123 | func TestDeleteCategory(t *testing.T) { 124 | db := database.ConnectToDbTest() 125 | truncateCategory(db) 126 | router := setupRouter(db) 127 | 128 | tx, _ := db.Begin() 129 | 130 | repository := NewCategoryRepository() 131 | category := repository.Save(context.Background(), tx, model.Category{ 132 | Name: "Delete", 133 | }) 134 | tx.Commit() 135 | 136 | t.Run("Test Delete Category Success", func(t *testing.T) { 137 | req := httptest.NewRequest("DELETE", "http://localhost:3001/categories/"+strconv.Itoa(category.Id), nil) 138 | req.Header.Add("Content-Type", "application/json") 139 | recorder := httptest.NewRecorder() 140 | router.ServeHTTP(recorder, req) 141 | 142 | res := recorder.Result() 143 | 144 | body, _ := ioutil.ReadAll(res.Body) 145 | var responseBody map[string]interface{} 146 | json.Unmarshal(body, &responseBody) 147 | 148 | assert.Equal(t, 200, res.StatusCode) 149 | 150 | }) 151 | 152 | t.Run("Test Update Category Failed", func(t *testing.T) { 153 | req := httptest.NewRequest("DELETE", "http://localhost:3001/categories/"+strconv.Itoa(404), nil) 154 | req.Header.Add("Content-Type", "application/json") 155 | recorder := httptest.NewRecorder() 156 | router.ServeHTTP(recorder, req) 157 | 158 | res := recorder.Result() 159 | body, _ := ioutil.ReadAll(res.Body) 160 | var responseBody map[string]interface{} 161 | json.Unmarshal(body, &responseBody) 162 | 163 | assert.Equal(t, 404, res.StatusCode) 164 | }) 165 | 166 | } 167 | 168 | func TestGetCategoryById(t *testing.T) { 169 | db := database.ConnectToDbTest() 170 | truncateCategory(db) 171 | router := setupRouter(db) 172 | 173 | tx, _ := db.Begin() 174 | 175 | repository := NewCategoryRepository() 176 | category := repository.Save(context.Background(), tx, model.Category{ 177 | Name: "Delete", 178 | }) 179 | tx.Commit() 180 | 181 | t.Run("Test Delete Category Success", func(t *testing.T) { 182 | req := httptest.NewRequest("GET", "http://localhost:3001/categories/"+strconv.Itoa(category.Id), nil) 183 | req.Header.Add("Content-Type", "application/json") 184 | recorder := httptest.NewRecorder() 185 | router.ServeHTTP(recorder, req) 186 | 187 | res := recorder.Result() 188 | 189 | body, _ := ioutil.ReadAll(res.Body) 190 | var responseBody map[string]interface{} 191 | json.Unmarshal(body, &responseBody) 192 | 193 | assert.Equal(t, 200, res.StatusCode) 194 | 195 | }) 196 | 197 | t.Run("Test Update Category Failed", func(t *testing.T) { 198 | req := httptest.NewRequest("GET", "http://localhost:3001/categories/"+strconv.Itoa(404), nil) 199 | req.Header.Add("Content-Type", "application/json") 200 | recorder := httptest.NewRecorder() 201 | router.ServeHTTP(recorder, req) 202 | 203 | res := recorder.Result() 204 | body, _ := ioutil.ReadAll(res.Body) 205 | var responseBody map[string]interface{} 206 | json.Unmarshal(body, &responseBody) 207 | 208 | assert.Equal(t, 404, res.StatusCode) 209 | }) 210 | } 211 | -------------------------------------------------------------------------------- /category/category_repository.go: -------------------------------------------------------------------------------- 1 | package category 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "task-one/category/model" 8 | "task-one/helpers" 9 | ) 10 | 11 | type CategoryRepository interface { 12 | Save(ctx context.Context, tx *sql.Tx, category model.Category) model.Category 13 | Update(ctx context.Context, tx *sql.Tx, category model.Category) model.Category 14 | Delete(ctx context.Context, tx *sql.Tx, categoryId int) 15 | FindAll(ctx context.Context, tx *sql.Tx) []model.Category 16 | FindById(ctx context.Context, tx *sql.Tx, categoryId int) (model.Category, error) 17 | } 18 | 19 | type CategoryRepositoryImpl struct { 20 | } 21 | 22 | func NewCategoryRepository() CategoryRepository { 23 | return &CategoryRepositoryImpl{} 24 | } 25 | 26 | func (repository *CategoryRepositoryImpl) Save(ctx context.Context, tx *sql.Tx, category model.Category) model.Category { 27 | query := "INSERT INTO category(name) values ($1) RETURNING id" 28 | row := tx.QueryRowContext(ctx, query, category.Name) 29 | err := row.Scan(&category.Id) 30 | helpers.PanicIfError(err) 31 | 32 | return category 33 | } 34 | 35 | func (repository *CategoryRepositoryImpl) Update(ctx context.Context, tx *sql.Tx, category model.Category) model.Category { 36 | query := "UPDATE category set name = $1 where id = $2" 37 | _, err := tx.ExecContext(ctx, query, category.Name, category.Id) 38 | helpers.PanicIfError(err) 39 | 40 | return category 41 | 42 | } 43 | 44 | func (repository *CategoryRepositoryImpl) Delete(ctx context.Context, tx *sql.Tx, categoryId int) { 45 | query := "DELETE FROM category where id = $1" 46 | _, err := tx.ExecContext(ctx, query, categoryId) 47 | helpers.PanicIfError(err) 48 | } 49 | 50 | func (repository *CategoryRepositoryImpl) FindAll(ctx context.Context, tx *sql.Tx) []model.Category { 51 | query := "SELECT id,name FROM category" 52 | rows, err := tx.QueryContext(ctx, query) 53 | helpers.PanicIfError(err) 54 | defer rows.Close() 55 | var categories []model.Category 56 | 57 | for rows.Next() { 58 | category := model.Category{} 59 | err := rows.Scan(&category.Id, &category.Name) 60 | helpers.PanicIfError(err) 61 | 62 | categories = append(categories, category) 63 | 64 | } 65 | 66 | return categories 67 | } 68 | 69 | func (repository *CategoryRepositoryImpl) FindById(ctx context.Context, tx *sql.Tx, categoryId int) (model.Category, error) { 70 | SQL := "SELECT id, name FROM category WHERE id = $1" 71 | rows, err := tx.QueryContext(ctx, SQL, categoryId) 72 | helpers.PanicIfError(err) 73 | defer rows.Close() 74 | 75 | category := model.Category{} 76 | if rows.Next() { 77 | err := rows.Scan(&category.Id, &category.Name) 78 | helpers.PanicIfError(err) 79 | return category, nil 80 | } else { 81 | return category, errors.New("category Not Found") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /category/category_route.go: -------------------------------------------------------------------------------- 1 | package category 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/julienschmidt/httprouter" 6 | ) 7 | 8 | func RegisterRoute(router *httprouter.Router, db *sql.DB) { 9 | 10 | categoryRepository := NewCategoryRepository() 11 | categoryService := NewCategoryService(categoryRepository, db) 12 | categoryController := NewCategoryController(categoryService) 13 | 14 | router.POST("/categories", categoryController.Create) 15 | router.GET("/categories", categoryController.FindAll) 16 | router.GET("/categories/:id", categoryController.FindById) 17 | router.DELETE("/categories/:id", categoryController.Delete) 18 | router.PATCH("/categories/:id", categoryController.Update) 19 | } 20 | -------------------------------------------------------------------------------- /category/category_service.go: -------------------------------------------------------------------------------- 1 | package category 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "task-one/category/dto" 7 | "task-one/category/model" 8 | "task-one/category/response" 9 | "task-one/exception" 10 | "task-one/helpers" 11 | ) 12 | 13 | type CategoryService interface { 14 | Create(ctx context.Context, request *dto.CategoryCreateDto) response.CategoryResponse 15 | Update(ctx context.Context, request *dto.CategoryUpdateDto) response.CategoryResponse 16 | Delete(ctx context.Context, categoryId int) 17 | FindById(ctx context.Context, categoryId int) response.CategoryResponse 18 | FindAll(ctx context.Context) []response.CategoryResponse 19 | } 20 | 21 | type CategoryServiceImpl struct { 22 | Repository CategoryRepository 23 | DB *sql.DB 24 | } 25 | 26 | func NewCategoryService(repository CategoryRepository, DB *sql.DB) CategoryService { 27 | return &CategoryServiceImpl{ 28 | Repository: repository, 29 | DB: DB, 30 | } 31 | } 32 | 33 | func (service *CategoryServiceImpl) Create(ctx context.Context, request *dto.CategoryCreateDto) response.CategoryResponse { 34 | 35 | tx, err := service.DB.Begin() 36 | helpers.PanicIfError(err) 37 | 38 | defer helpers.CommitOrRollback(tx) 39 | 40 | category := model.Category{ 41 | Name: request.Name, 42 | } 43 | 44 | category = service.Repository.Save(ctx, tx, category) 45 | return helpers.ToCategoryResponse(category) 46 | 47 | } 48 | 49 | func (service *CategoryServiceImpl) Update(ctx context.Context, request *dto.CategoryUpdateDto) response.CategoryResponse { 50 | 51 | tx, err := service.DB.Begin() 52 | helpers.PanicIfError(err) 53 | defer helpers.CommitOrRollback(tx) 54 | 55 | category, err := service.Repository.FindById(ctx, tx, request.Id) 56 | if err != nil { 57 | panic(exception.NewNotFoundError(err.Error())) 58 | } 59 | category.Name = request.Name 60 | category = service.Repository.Update(ctx, tx, category) 61 | 62 | return helpers.ToCategoryResponse(category) 63 | } 64 | 65 | func (service *CategoryServiceImpl) Delete(ctx context.Context, categoryId int) { 66 | tx, err := service.DB.Begin() 67 | helpers.PanicIfError(err) 68 | defer helpers.CommitOrRollback(tx) 69 | 70 | category, err := service.Repository.FindById(ctx, tx, categoryId) 71 | if err != nil { 72 | panic(exception.NewNotFoundError(err.Error())) 73 | } 74 | 75 | service.Repository.Delete(ctx, tx, category.Id) 76 | } 77 | 78 | func (service *CategoryServiceImpl) FindById(ctx context.Context, categoryId int) response.CategoryResponse { 79 | tx, err := service.DB.Begin() 80 | helpers.PanicIfError(err) 81 | defer helpers.CommitOrRollback(tx) 82 | 83 | category, err := service.Repository.FindById(ctx, tx, categoryId) 84 | if err != nil { 85 | panic(exception.NewNotFoundError(err.Error())) 86 | } 87 | 88 | return helpers.ToCategoryResponse(category) 89 | } 90 | 91 | func (service *CategoryServiceImpl) FindAll(ctx context.Context) []response.CategoryResponse { 92 | tx, err := service.DB.Begin() 93 | helpers.PanicIfError(err) 94 | defer helpers.CommitOrRollback(tx) 95 | 96 | categories := service.Repository.FindAll(ctx, tx) 97 | 98 | return helpers.ToCategoryResponses(categories) 99 | } 100 | -------------------------------------------------------------------------------- /category/dto/create_category_dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type CategoryCreateDto struct { 4 | Name string `json:"name" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /category/dto/update_category_dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type CategoryUpdateDto struct { 4 | Id int 5 | Name string `json:"name" validate:"required"` 6 | } 7 | -------------------------------------------------------------------------------- /category/model/category_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Category struct { 4 | Id int `json:"id"` 5 | Name string `json:"name"` 6 | } 7 | -------------------------------------------------------------------------------- /category/response/category_response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | type CategoryResponse struct { 4 | Id int `json:"id"` 5 | Name string `json:"name"` 6 | } 7 | -------------------------------------------------------------------------------- /configs/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "task-one/helpers" 7 | "time" 8 | ) 9 | 10 | func ConnectToDb() *sql.DB { 11 | env := helpers.GetConfig() 12 | 13 | db, err := sql.Open(env.DB.Connection, env.DB.URI) 14 | helpers.PanicIfError(err) 15 | 16 | db.SetMaxIdleConns(5) 17 | db.SetMaxOpenConns(20) 18 | db.SetConnMaxLifetime(60 * time.Minute) 19 | 20 | log.Println("Database Connected..") 21 | 22 | return db 23 | 24 | } 25 | 26 | func ConnectToDbTest() *sql.DB { 27 | 28 | env := helpers.GetConfig() 29 | 30 | db, err := sql.Open(env.DB.Connection, env.DB.Test_URI) 31 | helpers.PanicIfError(err) 32 | 33 | db.SetMaxIdleConns(5) 34 | db.SetMaxOpenConns(20) 35 | db.SetConnMaxLifetime(60 * time.Minute) 36 | 37 | return db 38 | } 39 | -------------------------------------------------------------------------------- /configs/mail/mail.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "fmt" 5 | "net/smtp" 6 | "strings" 7 | "task-one/helpers" 8 | ) 9 | 10 | type Mailer interface { 11 | SendMail(to []string, cc []string, subject, message string) error 12 | } 13 | 14 | type SMTPMailer struct { 15 | } 16 | 17 | func (g *SMTPMailer) SendMail(to []string, cc []string, subject, message string) error { 18 | env := helpers.GetConfig() 19 | body := "From: " + env.Mail.SenderName + "\n" + 20 | "To: " + strings.Join(to, ",") + "\n" + 21 | "Cc: " + strings.Join(cc, ",") + "\n" + 22 | "Subject: " + subject + "\n\n" + 23 | message 24 | 25 | auth := smtp.PlainAuth("", env.Mail.AuthEmail, env.Mail.AuthPassword, env.Mail.SmtpHost) 26 | smtpAddr := fmt.Sprintf("%s:%d", env.Mail.SmtpHost, env.Mail.SmtpPort) 27 | 28 | err := smtp.SendMail(smtpAddr, auth, env.Mail.AuthEmail, append(to, cc...), []byte(body)) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | 35 | } 36 | -------------------------------------------------------------------------------- /configs/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/go-redis/redis/v8" 7 | "log" 8 | "task-one/helpers" 9 | "time" 10 | ) 11 | 12 | type Redis interface { 13 | Set(ctx context.Context, key string, value interface{}) error 14 | Get(ctx context.Context, key string) (string, error) 15 | } 16 | 17 | type RedisClient struct { 18 | rdb *redis.Client 19 | } 20 | 21 | func InitRedis() *RedisClient { 22 | env := helpers.GetConfig() 23 | client := redis.NewClient(&redis.Options{ 24 | Addr: env.Redis.Host, 25 | Password: env.Redis.Password, 26 | DB: env.Redis.Db, 27 | }) 28 | 29 | res := client.Ping(context.Background()) 30 | log.Println(res) 31 | 32 | return &RedisClient{rdb: client} 33 | } 34 | 35 | func (r *RedisClient) Set(ctx context.Context, key string, value interface{}) error { 36 | data, err := json.Marshal(value) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | err = r.rdb.Set(ctx, key, data, 10*time.Minute).Err() 42 | return err 43 | 44 | } 45 | 46 | func (r *RedisClient) Get(ctx context.Context, key string) (string, error) { 47 | val, err := r.rdb.Get(ctx, key).Result() 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | return val, err 53 | 54 | } 55 | -------------------------------------------------------------------------------- /configs/route/routes.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "github.com/julienschmidt/httprouter" 5 | "task-one/category" 6 | "task-one/configs/database" 7 | "task-one/exception" 8 | "task-one/product" 9 | ) 10 | 11 | func NewRouter() *httprouter.Router { 12 | var Router *httprouter.Router = httprouter.New() 13 | 14 | db := database.ConnectToDb() 15 | category.RegisterRoute(Router, db) 16 | product.RegisterRoute(Router, db) 17 | 18 | Router.PanicHandler = exception.ErrorHandler 19 | return Router 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | db: 4 | image: postgres:alpine3.18 5 | ports: 6 | - "5434:5432" 7 | volumes: 8 | - ./data:/var/lib/postgresql/data 9 | environment: 10 | POSTGRES_USER: fzrsahi 11 | POSTGRES_PASSWORD: 123 12 | POSTGRES_DB: task_one_dikti 13 | 14 | dev: 15 | image: postgres:alpine3.18 16 | ports: 17 | - "5435:5432" 18 | volumes: 19 | - ./data_dev:/var/lib/postgresql/data 20 | environment: 21 | POSTGRES_USER: fzrsahi 22 | POSTGRES_PASSWORD: 123 23 | POSTGRES_DB: task_one_dikti_dev 24 | -------------------------------------------------------------------------------- /exception/error_handler.go: -------------------------------------------------------------------------------- 1 | package exception 2 | 3 | import ( 4 | "net/http" 5 | "task-one/helpers" 6 | ) 7 | 8 | func ErrorHandler(writer http.ResponseWriter, request *http.Request, err interface{}) { 9 | 10 | if notFoundError(writer, request, err) { 11 | return 12 | } 13 | 14 | internalServerError(writer, request, err) 15 | 16 | } 17 | 18 | func notFoundError(writer http.ResponseWriter, request *http.Request, err interface{}) bool { 19 | exception, ok := err.(NotFoundError) 20 | if ok { 21 | writer.Header().Set("Content-Type", "application/json") 22 | 23 | apiResponse := helpers.ApiResponse{ 24 | StatusCode: http.StatusNotFound, 25 | Data: exception.Error, 26 | } 27 | 28 | helpers.WriteToResponse(writer, apiResponse, http.StatusNotFound) 29 | return true 30 | } else { 31 | return false 32 | } 33 | } 34 | 35 | func internalServerError(writer http.ResponseWriter, request *http.Request, err interface{}) { 36 | writer.Header().Set("Content-Type", "application/json") 37 | 38 | apiResponse := helpers.ApiResponse{ 39 | StatusCode: http.StatusInternalServerError, 40 | Data: nil, 41 | } 42 | 43 | panic(err) 44 | helpers.WriteToResponse(writer, apiResponse, http.StatusInternalServerError) 45 | } 46 | -------------------------------------------------------------------------------- /exception/not_found_error.go: -------------------------------------------------------------------------------- 1 | package exception 2 | 3 | type NotFoundError struct { 4 | Error string 5 | } 6 | 7 | func NewNotFoundError(error string) NotFoundError { 8 | return NotFoundError{Error: error} 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module task-one 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 7 | github.com/go-playground/assert/v2 v2.2.0 8 | github.com/go-playground/validator/v10 v10.18.0 9 | github.com/go-redis/redis/v8 v8.11.5 10 | github.com/joho/godotenv v1.5.1 11 | github.com/julienschmidt/httprouter v1.3.0 12 | github.com/lib/pq v1.10.9 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 2 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 3 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 4 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 6 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 8 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 15 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 16 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 17 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 18 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 19 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 20 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 21 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 22 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 23 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 24 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 25 | github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= 26 | github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= 27 | github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 28 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 29 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 30 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 31 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 32 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 33 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 34 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 35 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 36 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 37 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 38 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 39 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 40 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 41 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 42 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 43 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 44 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 46 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 47 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 48 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 49 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 50 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 51 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 52 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 53 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 54 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 55 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 56 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 57 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 58 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 59 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 60 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 61 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 62 | github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 63 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 64 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 65 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 66 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 67 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/redis/go-redis v6.15.9+incompatible h1:F+tnlesQSl3h9V8DdmtcYFdvkHLhbb7AgcLW6UJxnC4= 70 | github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= 71 | github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= 72 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 73 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 74 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 75 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 76 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 77 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 78 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 79 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 80 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 81 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 82 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 83 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 84 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 85 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 86 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 87 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 88 | golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= 89 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 90 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 91 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 92 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 93 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 94 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 95 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 96 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 97 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 98 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 99 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 100 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 101 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 102 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 103 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 104 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 105 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 106 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 111 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 131 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 132 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 133 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 134 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 135 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 136 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 137 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 138 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 139 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 140 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 141 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 142 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 143 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 144 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 145 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 146 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 147 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 148 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 149 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 150 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 151 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 152 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 153 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 154 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 155 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 156 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 157 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 158 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 159 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 160 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 161 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 162 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 163 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 164 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 165 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 166 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 167 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 168 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 169 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 170 | gopkg.in/go-playground/validator.v7 v7.0.0-20150924115013-792d6f2ec751 h1:2vR/4ZdD/Ss6xolxNuNkiggbct+8/2j/XAgJUiMIYOE= 171 | gopkg.in/go-playground/validator.v7 v7.0.0-20150924115013-792d6f2ec751/go.mod h1:+M6OypEWTMOHYJ4jRmY4YcEVAIuGvKjAApWJw1xuXgg= 172 | gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= 173 | gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 174 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 175 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 177 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 178 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 179 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 180 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 181 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 182 | -------------------------------------------------------------------------------- /helpers/get_env.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "github.com/joho/godotenv" 5 | "log" 6 | "os" 7 | "regexp" 8 | "strconv" 9 | ) 10 | 11 | const projectDirName = "task-one" // change to relevant project name 12 | 13 | func loadEnv() { 14 | projectName := regexp.MustCompile(`^(.*` + projectDirName + `)`) 15 | currentWorkDirectory, _ := os.Getwd() 16 | rootPath := projectName.Find([]byte(currentWorkDirectory)) 17 | 18 | err := godotenv.Load(string(rootPath) + `/.env`) 19 | 20 | if err != nil { 21 | log.Fatalf("Error loading .env file") 22 | } 23 | } 24 | 25 | type DBConfig struct { 26 | Connection string 27 | URI string 28 | Test_URI string 29 | } 30 | 31 | type RedisConfig struct { 32 | Host string 33 | Password string 34 | Db int 35 | } 36 | 37 | type AppConfig struct { 38 | Port string 39 | } 40 | 41 | type MailConfig struct { 42 | SmtpHost string 43 | SmtpPort int 44 | SenderName string 45 | AuthEmail string 46 | AuthPassword string 47 | } 48 | 49 | type Config struct { 50 | DB *DBConfig 51 | AppConfig *AppConfig 52 | Redis *RedisConfig 53 | Mail *MailConfig 54 | } 55 | 56 | func GetConfig() *Config { 57 | loadEnv() 58 | 59 | dbDriver := os.Getenv("DB_DRIVER") 60 | dbUri := os.Getenv("DB_URI") 61 | dbTestUri := os.Getenv("DB_TEST_URI") 62 | port := os.Getenv("PORT") 63 | 64 | redisHost := os.Getenv("REDIS_HOST") 65 | redisPassword := os.Getenv("REDIS_PASSWORD") 66 | redisDb, _ := strconv.Atoi(os.Getenv("REDIS_DB")) 67 | 68 | smtpHost := os.Getenv("CONFIG_SMTP_HOST") 69 | smtpPort, _ := strconv.Atoi(os.Getenv("CONFIG_SMTP_PORT")) 70 | senderName := os.Getenv("CONFIG_SENDER_NAME") 71 | authEmail := os.Getenv("CONFIG_AUTH_EMAIL") 72 | authPassword := os.Getenv("CONFIG_AUTH_PASSWORD") 73 | 74 | return &Config{ 75 | DB: &DBConfig{ 76 | Connection: dbDriver, 77 | URI: dbUri, 78 | Test_URI: dbTestUri, 79 | }, 80 | AppConfig: &AppConfig{ 81 | Port: port, 82 | }, 83 | Redis: &RedisConfig{ 84 | Host: redisHost, 85 | Password: redisPassword, 86 | Db: redisDb, 87 | }, 88 | Mail: &MailConfig{ 89 | SmtpHost: smtpHost, 90 | SmtpPort: smtpPort, 91 | SenderName: senderName, 92 | AuthEmail: authEmail, 93 | AuthPassword: authPassword, 94 | }, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /helpers/json.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func ReadFromRequestBody(request *http.Request, result interface{}) { 9 | decoder := json.NewDecoder(request.Body) 10 | err := decoder.Decode(result) 11 | PanicIfError(err) 12 | } 13 | 14 | func WriteToResponse(writer http.ResponseWriter, response interface{}, statusCode int) { 15 | writer.Header().Add("Content-Type", "application/json") 16 | writer.WriteHeader(statusCode) 17 | encoder := json.NewEncoder(writer) 18 | err := encoder.Encode(response) 19 | PanicIfError(err) 20 | } 21 | -------------------------------------------------------------------------------- /helpers/model.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "task-one/category/model" 5 | "task-one/category/response" 6 | ) 7 | 8 | func ToCategoryResponse(category model.Category) response.CategoryResponse { 9 | return response.CategoryResponse{ 10 | Id: category.Id, 11 | Name: category.Name, 12 | } 13 | } 14 | 15 | func ToCategoryResponses(categories []model.Category) []response.CategoryResponse { 16 | var categoryResponses []response.CategoryResponse 17 | for _, category := range categories { 18 | categoryResponses = append(categoryResponses, ToCategoryResponse(category)) 19 | } 20 | return categoryResponses 21 | } 22 | -------------------------------------------------------------------------------- /helpers/panic_handler.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | func PanicIfError(err error) { 4 | if err != nil { 5 | panic(err) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /helpers/response.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | type ApiResponse struct { 4 | StatusCode int `json:"statusCode"` 5 | Data interface{} `json:"data"` 6 | } 7 | -------------------------------------------------------------------------------- /helpers/tx.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import "database/sql" 4 | 5 | func CommitOrRollback(tx *sql.Tx) { 6 | err := recover() 7 | if err != nil { 8 | errorRollback := tx.Rollback() 9 | PanicIfError(errorRollback) 10 | panic(err) 11 | } else { 12 | errorCommit := tx.Commit() 13 | PanicIfError(errorCommit) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/lib/pq" 5 | "log" 6 | "net/http" 7 | "task-one/configs/route" 8 | "task-one/helpers" 9 | ) 10 | 11 | func main() { 12 | router := route.NewRouter() 13 | env := helpers.GetConfig() 14 | 15 | PORT := env.AppConfig.Port 16 | 17 | server := http.Server{ 18 | Addr: PORT, 19 | Handler: router, 20 | } 21 | 22 | log.Println("Server Running On : http://" + PORT) 23 | 24 | err := server.ListenAndServe() 25 | helpers.PanicIfError(err) 26 | } 27 | -------------------------------------------------------------------------------- /product/dto/create_product_dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ProductCreateDto struct { 4 | Name string `json:"name" validate:"required"` 5 | CategoryId int `json:"category_id" validate:"required"` 6 | } 7 | -------------------------------------------------------------------------------- /product/dto/update_product_dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ProductUpdateDto struct { 4 | Id int 5 | Name string `json:"name"` 6 | CategoryId int `json:"category_id"` 7 | } 8 | -------------------------------------------------------------------------------- /product/model/product_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "task-one/product/response" 4 | 5 | type Product struct { 6 | Id int `json:"id"` 7 | Name string `json:"name"` 8 | CategoryName string `json:"category_name"` 9 | CategoryId int `json:"category_id"` 10 | } 11 | 12 | func ToProductResponse(product Product) response.ProductResponse { 13 | return response.ProductResponse{ 14 | Id: product.Id, 15 | Name: product.Name, 16 | CategoryName: product.CategoryName, 17 | } 18 | } 19 | 20 | func ToProductResponses(products []Product) []response.ProductResponse { 21 | var productResponses []response.ProductResponse 22 | for _, product := range products { 23 | productResponses = append(productResponses, ToProductResponse(product)) 24 | } 25 | return productResponses 26 | } 27 | -------------------------------------------------------------------------------- /product/product_controller.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "github.com/julienschmidt/httprouter" 5 | "net/http" 6 | "strconv" 7 | "task-one/helpers" 8 | "task-one/product/dto" 9 | ) 10 | 11 | type ProductController interface { 12 | Create(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 13 | Update(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 14 | Delete(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 15 | FindById(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 16 | FindAll(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 17 | } 18 | 19 | type ProductControllerImpl struct { 20 | Service ProductService 21 | } 22 | 23 | func NewProductController(service ProductService) ProductController { 24 | return &ProductControllerImpl{Service: service} 25 | } 26 | 27 | func (controller *ProductControllerImpl) Create(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 28 | productRequest := &dto.ProductCreateDto{} 29 | helpers.ReadFromRequestBody(request, productRequest) 30 | 31 | data := controller.Service.Create(request.Context(), productRequest) 32 | result := helpers.ApiResponse{ 33 | StatusCode: 201, 34 | Data: data, 35 | } 36 | 37 | helpers.WriteToResponse(writer, result, 201) 38 | } 39 | 40 | func (controller *ProductControllerImpl) Update(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 41 | productUpdateRequest := &dto.ProductUpdateDto{} 42 | helpers.ReadFromRequestBody(request, productUpdateRequest) 43 | 44 | productId := params.ByName("id") 45 | res, err := strconv.Atoi(productId) 46 | helpers.PanicIfError(err) 47 | 48 | productUpdateRequest.Id = res 49 | 50 | data := controller.Service.Update(request.Context(), productUpdateRequest) 51 | result := helpers.ApiResponse{ 52 | StatusCode: 201, 53 | Data: data, 54 | } 55 | 56 | helpers.WriteToResponse(writer, result, 201) 57 | 58 | } 59 | 60 | func (controller *ProductControllerImpl) Delete(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 61 | productId := params.ByName("id") 62 | res, err := strconv.Atoi(productId) 63 | helpers.PanicIfError(err) 64 | 65 | controller.Service.Delete(request.Context(), res) 66 | result := helpers.ApiResponse{ 67 | StatusCode: 200, 68 | Data: nil, 69 | } 70 | helpers.WriteToResponse(writer, result, 200) 71 | 72 | } 73 | 74 | func (controller *ProductControllerImpl) FindById(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 75 | productId := params.ByName("id") 76 | res, err := strconv.Atoi(productId) 77 | helpers.PanicIfError(err) 78 | 79 | data := controller.Service.FindById(request.Context(), res) 80 | result := helpers.ApiResponse{ 81 | StatusCode: 200, 82 | Data: data, 83 | } 84 | helpers.WriteToResponse(writer, result, 200) 85 | 86 | } 87 | 88 | func (controller *ProductControllerImpl) FindAll(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 89 | data := controller.Service.FindAll(request.Context()) 90 | result := helpers.ApiResponse{ 91 | StatusCode: 200, 92 | Data: data, 93 | } 94 | helpers.WriteToResponse(writer, result, 200) 95 | 96 | } 97 | -------------------------------------------------------------------------------- /product/product_controller_test.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "github.com/go-playground/assert/v2" 8 | "github.com/julienschmidt/httprouter" 9 | _ "github.com/lib/pq" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "net/http/httptest" 14 | "strconv" 15 | "strings" 16 | "task-one/category" 17 | "task-one/category/model" 18 | "task-one/configs/database" 19 | "task-one/configs/redis" 20 | "task-one/exception" 21 | product_model "task-one/product/model" 22 | "testing" 23 | ) 24 | 25 | func setupRouter(db *sql.DB) http.Handler { 26 | router := httprouter.New() 27 | router.PanicHandler = exception.ErrorHandler 28 | RegisterRoute(router, db) 29 | 30 | return router 31 | 32 | } 33 | 34 | func truncateCategory(db *sql.DB) { 35 | db.Exec("TRUNCATE category") 36 | db.Exec("TRUNCATE product") 37 | 38 | } 39 | 40 | func TestMain(m *testing.M) { 41 | m.Run() 42 | } 43 | 44 | func TestGetListProduct(t *testing.T) { 45 | db := database.ConnectToDbTest() 46 | router := setupRouter(db) 47 | 48 | req := httptest.NewRequest("GET", "http://localhost:3001/products", nil) 49 | req.Header.Add("Content-Type", "application/json") 50 | 51 | recorder := httptest.NewRecorder() 52 | 53 | router.ServeHTTP(recorder, req) 54 | res := recorder.Result() 55 | assert.Equal(t, 200, res.StatusCode) 56 | } 57 | 58 | func TestCreateProduct(t *testing.T) { 59 | db := database.ConnectToDbTest() 60 | router := setupRouter(db) 61 | truncateCategory(db) 62 | tx, _ := db.Begin() 63 | 64 | categoryRepository := category.NewCategoryRepository() 65 | category := categoryRepository.Save(context.Background(), tx, model.Category{ 66 | Name: "Furniture", 67 | }) 68 | tx.Commit() 69 | 70 | t.Run("Test Create Product Success", func(t *testing.T) { 71 | reqBody := strings.NewReader(`{"name" : "Table","category_id":` + strconv.Itoa(category.Id) + "}") 72 | req := httptest.NewRequest("POST", "http://localhost:3001/products", reqBody) 73 | req.Header.Add("Content-Type", "application/json") 74 | 75 | recorder := httptest.NewRecorder() 76 | router.ServeHTTP(recorder, req) 77 | 78 | res := recorder.Result() 79 | 80 | body, _ := ioutil.ReadAll(res.Body) 81 | var responseBody map[string]interface{} 82 | json.Unmarshal(body, &responseBody) 83 | 84 | assert.Equal(t, 201, res.StatusCode) 85 | assert.Equal(t, "Table", responseBody["data"].(map[string]interface{})["name"]) 86 | assert.Equal(t, "Furniture", responseBody["data"].(map[string]interface{})["category_name"]) 87 | }) 88 | 89 | t.Run("Test Create Product Failed", func(t *testing.T) { 90 | reqBody := strings.NewReader(`{"name" : "Table","category_id":404}`) 91 | req := httptest.NewRequest("POST", "http://localhost:3001/products", reqBody) 92 | req.Header.Add("Content-Type", "application/json") 93 | 94 | recorder := httptest.NewRecorder() 95 | router.ServeHTTP(recorder, req) 96 | 97 | res := recorder.Result() 98 | 99 | body, _ := ioutil.ReadAll(res.Body) 100 | var responseBody map[string]interface{} 101 | json.Unmarshal(body, &responseBody) 102 | 103 | assert.Equal(t, 404, res.StatusCode) 104 | }) 105 | } 106 | 107 | func TestUpdateProduct(t *testing.T) { 108 | db := database.ConnectToDbTest() 109 | router := setupRouter(db) 110 | truncateCategory(db) 111 | tx, _ := db.Begin() 112 | 113 | ctx := context.Background() 114 | 115 | categoryRepository := category.NewCategoryRepository() 116 | category := categoryRepository.Save(ctx, tx, model.Category{ 117 | Name: "Furniture", 118 | }) 119 | categoryUpdate := categoryRepository.Save(ctx, tx, model.Category{ 120 | Name: "Alat Rumah", 121 | }) 122 | 123 | rdb := redis.InitRedis() 124 | productRepository := NewProductRepository(rdb) 125 | product := productRepository.Save(ctx, tx, product_model.Product{ 126 | Name: "Table", 127 | CategoryId: category.Id, 128 | }) 129 | tx.Commit() 130 | 131 | t.Run("Test Update Product Success", func(t *testing.T) { 132 | reqBody := strings.NewReader(`{"name" : "Meja","category_id":` + strconv.Itoa(categoryUpdate.Id) + "}") 133 | req := httptest.NewRequest("PATCH", "http://localhost:3001/products/"+strconv.Itoa(product.Id), reqBody) 134 | req.Header.Add("Content-Type", "application/json") 135 | recorder := httptest.NewRecorder() 136 | router.ServeHTTP(recorder, req) 137 | 138 | res := recorder.Result() 139 | 140 | body, _ := ioutil.ReadAll(res.Body) 141 | var responseBody map[string]interface{} 142 | json.Unmarshal(body, &responseBody) 143 | 144 | assert.Equal(t, 201, res.StatusCode) 145 | assert.Equal(t, "Meja", responseBody["data"].(map[string]interface{})["name"]) 146 | assert.Equal(t, "Alat Rumah", responseBody["data"].(map[string]interface{})["category_name"]) 147 | }) 148 | 149 | t.Run("Test Update Product Failed", func(t *testing.T) { 150 | reqBody := strings.NewReader(`{"name" : "Table","category_id":404}`) 151 | req := httptest.NewRequest("PATCH", "http://localhost:3001/products/"+strconv.Itoa(product.Id), reqBody) 152 | req.Header.Add("Content-Type", "application/json") 153 | recorder := httptest.NewRecorder() 154 | router.ServeHTTP(recorder, req) 155 | 156 | res := recorder.Result() 157 | 158 | body, _ := ioutil.ReadAll(res.Body) 159 | var responseBody map[string]interface{} 160 | json.Unmarshal(body, &responseBody) 161 | 162 | assert.Equal(t, 404, res.StatusCode) 163 | }) 164 | } 165 | 166 | func TestGetProductById(t *testing.T) { 167 | db := database.ConnectToDbTest() 168 | router := setupRouter(db) 169 | truncateCategory(db) 170 | tx, _ := db.Begin() 171 | 172 | ctx := context.Background() 173 | 174 | categoryRepository := category.NewCategoryRepository() 175 | category := categoryRepository.Save(ctx, tx, model.Category{ 176 | Name: "Furniture", 177 | }) 178 | log.Println(category) 179 | rdb := redis.InitRedis() 180 | productRepository := NewProductRepository(rdb) 181 | log.Println("wow") 182 | product := productRepository.Save(ctx, tx, product_model.Product{ 183 | Name: "Table", 184 | CategoryId: category.Id, 185 | }) 186 | tx.Commit() 187 | 188 | t.Run("Test Get Product By Id Success", func(t *testing.T) { 189 | req := httptest.NewRequest("GET", "http://localhost:3001/products/"+strconv.Itoa(product.Id), nil) 190 | req.Header.Add("Content-Type", "application/json") 191 | recorder := httptest.NewRecorder() 192 | router.ServeHTTP(recorder, req) 193 | 194 | res := recorder.Result() 195 | 196 | body, _ := ioutil.ReadAll(res.Body) 197 | var responseBody map[string]interface{} 198 | json.Unmarshal(body, &responseBody) 199 | 200 | assert.Equal(t, 200, res.StatusCode) 201 | assert.Equal(t, "Table", responseBody["data"].(map[string]interface{})["name"]) 202 | assert.Equal(t, "Furniture", responseBody["data"].(map[string]interface{})["category_name"]) 203 | }) 204 | 205 | t.Run("Test Get Product By Id Failed", func(t *testing.T) { 206 | req := httptest.NewRequest("GET", "http://localhost:3001/products/404", nil) 207 | req.Header.Add("Content-Type", "application/json") 208 | recorder := httptest.NewRecorder() 209 | router.ServeHTTP(recorder, req) 210 | 211 | res := recorder.Result() 212 | 213 | body, _ := ioutil.ReadAll(res.Body) 214 | var responseBody map[string]interface{} 215 | json.Unmarshal(body, &responseBody) 216 | 217 | assert.Equal(t, 404, res.StatusCode) 218 | }) 219 | } 220 | 221 | func TestDeleteProduct(t *testing.T) { 222 | db := database.ConnectToDbTest() 223 | router := setupRouter(db) 224 | truncateCategory(db) 225 | tx, _ := db.Begin() 226 | 227 | ctx := context.Background() 228 | 229 | categoryRepository := category.NewCategoryRepository() 230 | category := categoryRepository.Save(ctx, tx, model.Category{ 231 | Name: "Furniture", 232 | }) 233 | 234 | rdb := redis.InitRedis() 235 | productRepository := NewProductRepository(rdb) 236 | 237 | product := productRepository.Save(ctx, tx, product_model.Product{ 238 | Name: "Table", 239 | CategoryId: category.Id, 240 | }) 241 | tx.Commit() 242 | 243 | t.Run("Test Delete Product Success", func(t *testing.T) { 244 | req := httptest.NewRequest("DELETE", "http://localhost:3001/products/"+strconv.Itoa(product.Id), nil) 245 | req.Header.Add("Content-Type", "application/json") 246 | recorder := httptest.NewRecorder() 247 | router.ServeHTTP(recorder, req) 248 | 249 | res := recorder.Result() 250 | 251 | body, _ := ioutil.ReadAll(res.Body) 252 | var responseBody map[string]interface{} 253 | json.Unmarshal(body, &responseBody) 254 | 255 | assert.Equal(t, 200, res.StatusCode) 256 | }) 257 | 258 | t.Run("Test Delete Product Failed", func(t *testing.T) { 259 | req := httptest.NewRequest("DELETE", "http://localhost:3001/products/404", nil) 260 | req.Header.Add("Content-Type", "application/json") 261 | recorder := httptest.NewRecorder() 262 | router.ServeHTTP(recorder, req) 263 | 264 | res := recorder.Result() 265 | 266 | body, _ := ioutil.ReadAll(res.Body) 267 | var responseBody map[string]interface{} 268 | json.Unmarshal(body, &responseBody) 269 | 270 | assert.Equal(t, 404, res.StatusCode) 271 | }) 272 | } 273 | -------------------------------------------------------------------------------- /product/product_repository.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "errors" 8 | "task-one/configs/redis" 9 | "task-one/helpers" 10 | "task-one/product/model" 11 | ) 12 | 13 | type ProductRepository interface { 14 | Save(ctx context.Context, tx *sql.Tx, product model.Product) model.Product 15 | Update(ctx context.Context, tx *sql.Tx, product model.Product) model.Product 16 | Delete(ctx context.Context, tx *sql.Tx, productId int) 17 | FindAll(ctx context.Context, tx *sql.Tx) []model.Product 18 | FindById(ctx context.Context, tx *sql.Tx, productId int) (model.Product, error) 19 | UpdateCache(ctx context.Context, tx *sql.Tx) 20 | } 21 | 22 | type ProductRepositoryImpl struct { 23 | rdb *redis.RedisClient 24 | } 25 | 26 | func (p *ProductRepositoryImpl) UpdateCache(ctx context.Context, tx *sql.Tx) { 27 | //defer wg.Done() 28 | 29 | query := "SELECT product.id,product.name,category.name FROM product INNER JOIN category ON product.category_id = category.id" 30 | rows, err := tx.QueryContext(ctx, query) 31 | helpers.PanicIfError(err) 32 | defer rows.Close() 33 | 34 | var products []model.Product 35 | 36 | for rows.Next() { 37 | product := model.Product{} 38 | err := rows.Scan(&product.Id, &product.Name, &product.CategoryName) 39 | helpers.PanicIfError(err) 40 | 41 | products = append(products, product) 42 | } 43 | 44 | key := "list:products" 45 | err = p.rdb.Set(ctx, key, products) 46 | helpers.PanicIfError(err) 47 | } 48 | 49 | func NewProductRepository(rdb *redis.RedisClient) ProductRepository { 50 | return &ProductRepositoryImpl{ 51 | rdb: rdb, 52 | } 53 | } 54 | 55 | func (p *ProductRepositoryImpl) Save(ctx context.Context, tx *sql.Tx, product model.Product) model.Product { 56 | query := ` 57 | WITH product AS ( 58 | INSERT INTO product(name, category_id) 59 | VALUES($1, $2) 60 | RETURNING id, name ,category_id 61 | ) 62 | SELECT product.id,product.name, category.name 63 | FROM product 64 | INNER JOIN category ON product.category_id = category.id 65 | ` 66 | 67 | row := tx.QueryRowContext(ctx, query, product.Name, product.CategoryId) 68 | err := row.Scan(&product.Id, &product.Name, &product.CategoryName) 69 | helpers.PanicIfError(err) 70 | 71 | p.UpdateCache(ctx, tx) 72 | return product 73 | } 74 | 75 | func (p *ProductRepositoryImpl) Update(ctx context.Context, tx *sql.Tx, product model.Product) model.Product { 76 | var query string 77 | var err error 78 | 79 | if product.CategoryId != 0 && product.Name != "" { 80 | query = "UPDATE product SET name = $1, category_id = $2 WHERE id = $3 RETURNING id, name" 81 | err = tx.QueryRowContext(ctx, query, product.Name, product.CategoryId, product.Id).Scan(&product.Id, &product.Name) 82 | } else if product.CategoryId != 0 && product.Name == "" { 83 | query = "UPDATE product SET category_id = $1 WHERE id = $2 RETURNING id" 84 | err = tx.QueryRowContext(ctx, query, product.CategoryId, product.Id).Scan(&product.Id) 85 | } else if product.CategoryId == 0 && product.Name != "" { 86 | query = "UPDATE product SET name = $1 WHERE id = $2 RETURNING id, name" 87 | err = tx.QueryRowContext(ctx, query, product.Name, product.Id).Scan(&product.Id, &product.Name) 88 | } else { 89 | return product 90 | } 91 | 92 | helpers.PanicIfError(err) 93 | 94 | selectQuery := ` 95 | SELECT p.id, p.name, c.name 96 | FROM product p 97 | INNER JOIN category c ON p.category_id = c.id 98 | WHERE p.id = $1 99 | ` 100 | row := tx.QueryRowContext(ctx, selectQuery, product.Id) 101 | err = row.Scan(&product.Id, &product.Name, &product.CategoryName) 102 | helpers.PanicIfError(err) 103 | 104 | p.UpdateCache(ctx, tx) 105 | return product 106 | } 107 | 108 | func (p *ProductRepositoryImpl) Delete(ctx context.Context, tx *sql.Tx, productId int) { 109 | query := "DELETE FROM product where id = $1" 110 | _, err := tx.ExecContext(ctx, query, productId) 111 | helpers.PanicIfError(err) 112 | 113 | p.UpdateCache(ctx, tx) 114 | } 115 | 116 | func (p *ProductRepositoryImpl) FindAll(ctx context.Context, tx *sql.Tx) []model.Product { 117 | var products []model.Product 118 | key := "list:products" 119 | productsCache, err := p.rdb.Get(ctx, key) 120 | if err != nil { 121 | query := "SELECT product.id,product.name,category.name FROM product INNER JOIN category ON product.category_id = category.id" 122 | rows, err := tx.QueryContext(ctx, query) 123 | helpers.PanicIfError(err) 124 | defer rows.Close() 125 | 126 | for rows.Next() { 127 | product := model.Product{} 128 | err := rows.Scan(&product.Id, &product.Name, &product.CategoryName) 129 | helpers.PanicIfError(err) 130 | 131 | products = append(products, product) 132 | } 133 | 134 | err = p.rdb.Set(ctx, key, products) 135 | helpers.PanicIfError(err) 136 | return products 137 | } 138 | 139 | err = json.Unmarshal([]byte(productsCache), &products) 140 | helpers.PanicIfError(err) 141 | return products 142 | } 143 | 144 | func (p *ProductRepositoryImpl) FindById(ctx context.Context, tx *sql.Tx, productId int) (model.Product, error) { 145 | query := "SELECT product.id,product.name,category.name FROM product INNER JOIN category ON product.category_id = category.id WHERE product.id = $1" 146 | rows, err := tx.QueryContext(ctx, query, productId) 147 | helpers.PanicIfError(err) 148 | defer rows.Close() 149 | 150 | product := model.Product{} 151 | if rows.Next() { 152 | err := rows.Scan(&product.Id, &product.Name, &product.CategoryName) 153 | helpers.PanicIfError(err) 154 | return product, nil 155 | } else { 156 | return product, errors.New("product Not Found") 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /product/product_routes.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/julienschmidt/httprouter" 6 | "sync" 7 | "task-one/category" 8 | "task-one/configs/mail" 9 | "task-one/configs/redis" 10 | ) 11 | 12 | func RegisterRoute(router *httprouter.Router, db *sql.DB) { 13 | rdb := redis.InitRedis() 14 | wg := new(sync.WaitGroup) 15 | smtp := &mail.SMTPMailer{} 16 | 17 | productRepository := NewProductRepository(rdb) 18 | categoryRepository := category.NewCategoryRepository() 19 | productService := NewProductService(productRepository, db, categoryRepository, wg, smtp) 20 | productController := NewProductController(productService) 21 | 22 | router.GET("/products", productController.FindAll) 23 | router.GET("/products/:id", productController.FindById) 24 | router.PATCH("/products/:id", productController.Update) 25 | router.POST("/products", productController.Create) 26 | router.DELETE("/products/:id", productController.Delete) 27 | } 28 | -------------------------------------------------------------------------------- /product/product_service.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "sync" 7 | "task-one/category" 8 | "task-one/configs/mail" 9 | "task-one/exception" 10 | "task-one/helpers" 11 | "task-one/product/dto" 12 | "task-one/product/model" 13 | "task-one/product/response" 14 | ) 15 | 16 | type ProductService interface { 17 | Create(ctx context.Context, request *dto.ProductCreateDto) response.ProductResponse 18 | Update(ctx context.Context, request *dto.ProductUpdateDto) response.ProductResponse 19 | Delete(ctx context.Context, productId int) 20 | FindById(ctx context.Context, productId int) response.ProductResponse 21 | FindAll(ctx context.Context) []response.ProductResponse 22 | } 23 | 24 | type ProductServiceImpl struct { 25 | Repository ProductRepository 26 | DB *sql.DB 27 | CategoryRepository category.CategoryRepository 28 | Wg *sync.WaitGroup 29 | Smtp mail.Mailer 30 | } 31 | 32 | func NewProductService(repository ProductRepository, DB *sql.DB, categoryRepository category.CategoryRepository, wg *sync.WaitGroup, smtp mail.Mailer) ProductService { 33 | return &ProductServiceImpl{Repository: repository, DB: DB, CategoryRepository: categoryRepository, Wg: wg, Smtp: smtp} 34 | } 35 | 36 | func (service *ProductServiceImpl) Create(ctx context.Context, request *dto.ProductCreateDto) response.ProductResponse { 37 | tx, err := service.DB.Begin() 38 | helpers.PanicIfError(err) 39 | defer helpers.CommitOrRollback(tx) 40 | 41 | _, err = service.CategoryRepository.FindById(ctx, tx, request.CategoryId) 42 | if err != nil { 43 | panic(exception.NewNotFoundError(err.Error())) 44 | } 45 | 46 | go func() { 47 | to := []string{"fazrul.anugrah17@gmail.com", "fazrulsahi@gmail.com"} 48 | cc := []string{"tralalala@gmail.com"} 49 | subject := "Terbaru Asli!" 50 | message := "Hello, Kamu! Kami baru saja meluncurkan Produk baru loh! " 51 | service.Smtp.SendMail(to, cc, subject, message) 52 | }() 53 | 54 | productChannel := make(chan model.Product) 55 | defer close(productChannel) 56 | product := model.Product{ 57 | Name: request.Name, 58 | CategoryId: request.CategoryId, 59 | } 60 | 61 | service.Wg.Add(1) 62 | go func() { 63 | defer service.Wg.Done() 64 | product = service.Repository.Save(ctx, tx, product) 65 | productChannel <- product 66 | }() 67 | 68 | product = <-productChannel 69 | 70 | defer service.Wg.Wait() 71 | return model.ToProductResponse(product) 72 | } 73 | 74 | func (service *ProductServiceImpl) Update(ctx context.Context, request *dto.ProductUpdateDto) response.ProductResponse { 75 | tx, err := service.DB.Begin() 76 | helpers.PanicIfError(err) 77 | defer helpers.CommitOrRollback(tx) 78 | 79 | var product model.Product 80 | 81 | product, err = service.Repository.FindById(ctx, tx, request.Id) 82 | if err != nil { 83 | panic(exception.NewNotFoundError(err.Error())) 84 | } 85 | 86 | if request.CategoryId != 0 { 87 | _, err := service.CategoryRepository.FindById(ctx, tx, request.CategoryId) 88 | if err != nil { 89 | panic(exception.NewNotFoundError(err.Error())) 90 | } 91 | product = model.Product{ 92 | Id: request.Id, 93 | Name: request.Name, 94 | CategoryId: request.CategoryId, 95 | } 96 | } else { 97 | product = model.Product{ 98 | Id: request.Id, 99 | Name: request.Name, 100 | } 101 | } 102 | 103 | productChannel := make(chan model.Product) 104 | defer close(productChannel) 105 | service.Wg.Add(1) 106 | go func() { 107 | defer service.Wg.Done() 108 | product = service.Repository.Update(ctx, tx, product) 109 | productChannel <- product 110 | }() 111 | 112 | product = <-productChannel 113 | defer service.Wg.Wait() 114 | return model.ToProductResponse(product) 115 | 116 | } 117 | 118 | func (service *ProductServiceImpl) Delete(ctx context.Context, productId int) { 119 | tx, err := service.DB.Begin() 120 | helpers.PanicIfError(err) 121 | defer helpers.CommitOrRollback(tx) 122 | 123 | product, err := service.Repository.FindById(ctx, tx, productId) 124 | if err != nil { 125 | panic(exception.NewNotFoundError(err.Error())) 126 | } 127 | 128 | service.Wg.Add(1) 129 | go func() { 130 | defer service.Wg.Done() 131 | service.Repository.Delete(ctx, tx, product.Id) 132 | }() 133 | 134 | service.Wg.Wait() 135 | } 136 | 137 | func (service *ProductServiceImpl) FindById(ctx context.Context, productId int) response.ProductResponse { 138 | tx, err := service.DB.Begin() 139 | helpers.PanicIfError(err) 140 | defer helpers.CommitOrRollback(tx) 141 | 142 | productChannel := make(chan struct { 143 | Product model.Product 144 | Error error 145 | }) 146 | service.Wg.Add(1) 147 | defer close(productChannel) 148 | 149 | go func() { 150 | defer service.Wg.Done() 151 | product, err := service.Repository.FindById(ctx, tx, productId) 152 | productChannel <- struct { 153 | Product model.Product 154 | Error error 155 | }{product, err} 156 | }() 157 | 158 | productResult := <-productChannel 159 | if productResult.Error != nil { 160 | panic(exception.NewNotFoundError(productResult.Error.Error())) 161 | } 162 | 163 | defer service.Wg.Wait() 164 | return model.ToProductResponse(productResult.Product) 165 | } 166 | 167 | func (service *ProductServiceImpl) FindAll(ctx context.Context) []response.ProductResponse { 168 | tx, err := service.DB.Begin() 169 | helpers.PanicIfError(err) 170 | defer helpers.CommitOrRollback(tx) 171 | 172 | productsChannel := make(chan []model.Product) 173 | defer close(productsChannel) 174 | service.Wg.Add(1) 175 | 176 | go func() { 177 | defer service.Wg.Done() 178 | products := service.Repository.FindAll(ctx, tx) 179 | productsChannel <- products 180 | }() 181 | 182 | products := <-productsChannel 183 | defer service.Wg.Wait() 184 | return model.ToProductResponses(products) 185 | } 186 | -------------------------------------------------------------------------------- /product/response/product_response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | type ProductResponse struct { 4 | Id int `json:"id"` 5 | Name string `json:"name"` 6 | CategoryName string `json:"category_name"` 7 | } 8 | --------------------------------------------------------------------------------