├── .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 |
5 |
6 |
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 |
--------------------------------------------------------------------------------