├── Chapter 01 └── basic-concepts-of-go-examples-main │ ├── README.md │ ├── deferone │ └── main.go │ ├── defertwo │ └── main.go │ ├── forloopappend │ └── main.go │ ├── forloopcontinuebreak │ └── main.go │ ├── forloopomit │ └── main.go │ ├── forrangeloop │ └── main.go │ ├── functionarg │ └── main.go │ ├── functionclosures │ └── main.go │ ├── functionone │ └── main.go │ ├── functionpointerarg │ └── main.go │ ├── functionreturn │ └── main.go │ ├── functionslice │ └── main.go │ ├── functiontwo │ └── main.go │ ├── goplaygroud │ └── main.go │ ├── ifstatement │ └── main.go │ ├── maps │ └── main.go │ ├── pointers │ └── main.go │ ├── switchstatement │ └── main.go │ └── variables │ └── main.go ├── Chapter 02 └── advanced-concepts-of-go-main │ ├── README.md │ ├── channels │ └── main.go │ ├── channelsselect │ └── main.go │ ├── genericsfunction │ └── main.go │ ├── genericstype │ └── main.go │ ├── interfaces │ └── main.go │ ├── interfacesfunction │ └── main.go │ ├── interfacesnil │ └── main.go │ ├── interfacesswitch │ └── main.go │ ├── methods │ └── main.go │ ├── methodsbasictype │ └── main.go │ ├── mutex │ └── main.go │ ├── panics │ └── main.go │ ├── panicstrigger │ └── main.go │ └── waitgroup │ └── main.go ├── Chapter 03 └── go-json-main │ ├── README.md │ └── main.go ├── Chapter 04 (a) └── hello-world-and-packages-main │ ├── README.md │ ├── countries │ └── countries.go │ ├── go.mod │ └── main.go ├── Chapter 04 (b) └── go-standard-library-main │ ├── README.md │ └── main.go ├── Chapter 04 (c) └── go-third-party-libraries-main │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── Chapter 04 (d) └── go-simple-http-server-main │ ├── README.md │ └── main.go ├── Chapter 06 └── runners-postgresql-main │ ├── Dockerfile │ ├── README.md │ ├── config │ └── config.go │ ├── controllers │ ├── resultsController.go │ ├── runnersController.go │ ├── runnersController_test.go │ └── usersController.go │ ├── dbscripts │ ├── public_schema.sql │ └── update_schema.sql │ ├── docker-compose.yaml │ ├── go.mod │ ├── go.sum │ ├── kubernetes │ ├── runners-app-deployment.yaml │ └── runners-app-service.yaml │ ├── main.go │ ├── metrics │ └── metrics.go │ ├── models │ ├── responseError.go │ ├── result.go │ ├── runner.go │ └── user.go │ ├── repositories │ ├── resultsRepository.go │ ├── runnersRepository.go │ ├── transactionHandler.go │ └── usersRepository.go │ ├── runners-k8s.toml │ ├── runners.toml │ ├── server │ ├── dbServer.go │ ├── httpServer.go │ └── prometheus.go │ └── services │ ├── resultsService.go │ ├── runnerService_test.go │ ├── runnersService.go │ └── usersService.go ├── Chapter 07 (a) └── runners-postgresql-main │ ├── Dockerfile │ ├── README.md │ ├── config │ └── config.go │ ├── controllers │ ├── resultsController.go │ ├── runnersController.go │ ├── runnersController_test.go │ └── usersController.go │ ├── dbscripts │ ├── public_schema.sql │ └── update_schema.sql │ ├── docker-compose.yaml │ ├── go.mod │ ├── go.sum │ ├── kubernetes │ ├── runners-app-deployment.yaml │ └── runners-app-service.yaml │ ├── main.go │ ├── metrics │ └── metrics.go │ ├── models │ ├── responseError.go │ ├── result.go │ ├── runner.go │ └── user.go │ ├── repositories │ ├── resultsRepository.go │ ├── runnersRepository.go │ ├── transactionHandler.go │ └── usersRepository.go │ ├── runners-k8s.toml │ ├── runners.toml │ ├── server │ ├── dbServer.go │ ├── httpServer.go │ └── prometheus.go │ └── services │ ├── resultsService.go │ ├── runnerService_test.go │ ├── runnersService.go │ └── usersService.go ├── Chapter 07 (b) └── runners-mysql-main │ ├── README.md │ ├── config │ └── config.go │ ├── controllers │ ├── resultsController.go │ └── runnersController.go │ ├── dbscripts │ └── public_schema.sql │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── models │ ├── responseError.go │ ├── result.go │ └── runner.go │ ├── repositories │ ├── resultsRepository.go │ ├── runnersRepository.go │ └── transactionHandler.go │ ├── runners.toml │ ├── server │ ├── dbServer.go │ └── httpServer.go │ └── services │ ├── resultsService.go │ └── runnersService.go ├── Chapter 08 (a) └── runners-mongodb-main │ ├── README.md │ ├── config │ └── config.go │ ├── controllers │ ├── resultsController.go │ └── runnersController.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── models │ ├── responseError.go │ ├── result.go │ └── runner.go │ ├── repositories │ ├── resultsRepository.go │ └── runnersRepository.go │ ├── runners.toml │ ├── server │ ├── dbServer.go │ └── httpServer.go │ └── services │ ├── resultsService.go │ └── runnersService.go ├── Chapter 08 (b) └── runners-dynamodb-main │ ├── README.md │ ├── config │ └── config.go │ ├── controllers │ ├── resultsController.go │ └── runners.Controller.go │ ├── dbscripts │ ├── create-gsi-runners-attributes-schema.json │ ├── create-gsi-runners.json │ ├── create-results-table.json │ └── create-runners-table.json │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── models │ ├── responseError.go │ ├── result.go │ └── runner.go │ ├── repositories │ ├── resultsRepository.go │ └── runnersRepository.go │ ├── runners.toml │ ├── server │ ├── dbServer.go │ └── httpServer.go │ └── services │ ├── resultsService.go │ └── runnersService.go ├── Chapter 09 └── runners-postgresql-main │ ├── Dockerfile │ ├── README.md │ ├── config │ └── config.go │ ├── controllers │ ├── resultsController.go │ ├── runnersController.go │ ├── runnersController_test.go │ └── usersController.go │ ├── dbscripts │ ├── public_schema.sql │ └── update_schema.sql │ ├── docker-compose.yaml │ ├── go.mod │ ├── go.sum │ ├── kubernetes │ ├── runners-app-deployment.yaml │ └── runners-app-service.yaml │ ├── main.go │ ├── metrics │ └── metrics.go │ ├── models │ ├── responseError.go │ ├── result.go │ ├── runner.go │ └── user.go │ ├── repositories │ ├── resultsRepository.go │ ├── runnersRepository.go │ ├── transactionHandler.go │ └── usersRepository.go │ ├── runners-k8s.toml │ ├── runners.toml │ ├── server │ ├── dbServer.go │ ├── httpServer.go │ └── prometheus.go │ └── services │ ├── resultsService.go │ ├── runnerService_test.go │ ├── runnersService.go │ └── usersService.go ├── Chapter 10 └── runners-postgresql-main │ ├── Dockerfile │ ├── README.md │ ├── config │ └── config.go │ ├── controllers │ ├── resultsController.go │ ├── runnersController.go │ ├── runnersController_test.go │ └── usersController.go │ ├── dbscripts │ ├── public_schema.sql │ └── update_schema.sql │ ├── docker-compose.yaml │ ├── go.mod │ ├── go.sum │ ├── kubernetes │ ├── runners-app-deployment.yaml │ └── runners-app-service.yaml │ ├── main.go │ ├── metrics │ └── metrics.go │ ├── models │ ├── responseError.go │ ├── result.go │ ├── runner.go │ └── user.go │ ├── repositories │ ├── resultsRepository.go │ ├── runnersRepository.go │ ├── transactionHandler.go │ └── usersRepository.go │ ├── runners-k8s.toml │ ├── runners.toml │ ├── server │ ├── dbServer.go │ ├── httpServer.go │ └── prometheus.go │ └── services │ ├── resultsService.go │ ├── runnerService_test.go │ ├── runnersService.go │ └── usersService.go ├── Chapter 11 └── runners-postgresql-main │ ├── Dockerfile │ ├── README.md │ ├── config │ └── config.go │ ├── controllers │ ├── resultsController.go │ ├── runnersController.go │ ├── runnersController_test.go │ └── usersController.go │ ├── dbscripts │ ├── public_schema.sql │ └── update_schema.sql │ ├── docker-compose.yaml │ ├── go.mod │ ├── go.sum │ ├── kubernetes │ ├── runners-app-deployment.yaml │ └── runners-app-service.yaml │ ├── main.go │ ├── metrics │ └── metrics.go │ ├── models │ ├── responseError.go │ ├── result.go │ ├── runner.go │ └── user.go │ ├── repositories │ ├── resultsRepository.go │ ├── runnersRepository.go │ ├── transactionHandler.go │ └── usersRepository.go │ ├── runners-k8s.toml │ ├── runners.toml │ ├── server │ ├── dbServer.go │ ├── httpServer.go │ └── prometheus.go │ └── services │ ├── resultsService.go │ ├── runnerService_test.go │ ├── runnersService.go │ └── usersService.go ├── Chapter 12 └── runners-postgresql-main │ ├── Dockerfile │ ├── README.md │ ├── config │ └── config.go │ ├── controllers │ ├── resultsController.go │ ├── runnersController.go │ ├── runnersController_test.go │ └── usersController.go │ ├── dbscripts │ ├── public_schema.sql │ └── update_schema.sql │ ├── docker-compose.yaml │ ├── go.mod │ ├── go.sum │ ├── kubernetes │ ├── runners-app-deployment.yaml │ └── runners-app-service.yaml │ ├── main.go │ ├── metrics │ └── metrics.go │ ├── models │ ├── responseError.go │ ├── result.go │ ├── runner.go │ └── user.go │ ├── repositories │ ├── resultsRepository.go │ ├── runnersRepository.go │ ├── transactionHandler.go │ └── usersRepository.go │ ├── runners-k8s.toml │ ├── runners.toml │ ├── server │ ├── dbServer.go │ ├── httpServer.go │ └── prometheus.go │ └── services │ ├── resultsService.go │ ├── runnerService_test.go │ ├── runnersService.go │ └── usersService.go ├── LICENSE ├── README - Copy.md └── README.md /Chapter 01/basic-concepts-of-go-examples-main/README.md: -------------------------------------------------------------------------------- 1 | # Basic Concepts of Go - Examples 2 | 3 | 4 | 5 | ## How to run examples 6 | 7 | The examples from this projects can be runned with following command: 8 | 9 | ``` 10 | go run directory/main.go 11 | ``` 12 | 13 | For directory one of the following options can be used: 14 | - deferone 15 | - defertwo 16 | - forloopappend 17 | - forloopcontinuebreak 18 | - forloopomit 19 | - forrangeloop 20 | - functionarg 21 | - functionclosures 22 | - functionone 23 | - functionpointerarg 24 | - functionreturn 25 | - functionslice 26 | - functiontwo 27 | - goplayground 28 | - ifstatement 29 | - maps 30 | - pointers 31 | - switchstatement 32 | - variables 33 | 34 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/deferone/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Print(1) 7 | defer fmt.Print(2) 8 | fmt.Print(3) 9 | defer fmt.Print(4) 10 | } 11 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/defertwo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func testDefer(val int) { 6 | fmt.Print(1) 7 | defer fmt.Print(2) 8 | fmt.Print(3) 9 | 10 | if val == 5 { 11 | return 12 | } 13 | 14 | defer fmt.Print(4) 15 | } 16 | 17 | func main() { 18 | testDefer(3) 19 | testDefer(5) 20 | } 21 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/forloopappend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | a := []int{1, 2, 3} 7 | var b []*int 8 | 9 | for _, v := range a { 10 | b = append(b, &v) 11 | } 12 | 13 | for _, v := range b { 14 | fmt.Println(*v) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/forloopcontinuebreak/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | a := []int{1, 2, 3, 4, 5} 7 | for i := 0; i < 5; i++ { 8 | if i == 3 { 9 | continue 10 | } 11 | fmt.Println(a[i]) 12 | } 13 | 14 | for i := 0; i < 5; i++ { 15 | if i == 3 { 16 | break 17 | } 18 | fmt.Println(a[i]) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/forloopomit/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | result := 1 7 | sum := 1 8 | for result < 500 { 9 | result *= sum * 2 10 | } 11 | fmt.Println(result) 12 | } 13 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/forrangeloop/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | var arr [10]int 7 | for i := 0; i < 10; i++ { 8 | arr[i] = i * 2 9 | } 10 | 11 | sum := 0 12 | for i, v := range arr { 13 | if i%2 == 0 { 14 | sum += v 15 | } 16 | } 17 | fmt.Println(sum) 18 | } 19 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/functionarg/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func calc(fn func(int, int) int) int { 6 | return fn(7, 18) 7 | } 8 | 9 | func main() { 10 | add := func(a, b int) int { 11 | return a + b 12 | } 13 | 14 | fmt.Println(calc(add)) 15 | } 16 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/functionclosures/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func calc() func() int { 6 | a := 0 7 | return func() int { 8 | a += 1 9 | return a 10 | } 11 | } 12 | 13 | func main() { 14 | res := calc() 15 | fmt.Println(res()) 16 | } 17 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/functionone/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func pi() float64 { 6 | return 3.14159 7 | } 8 | 9 | func inc(a int) int { 10 | return a + 1 11 | } 12 | 13 | func add(a int, b int) int { 14 | return a + b 15 | } 16 | 17 | func main() { 18 | a := 1 19 | b := 5 20 | 21 | fmt.Println(pi()) 22 | fmt.Println(inc(a)) 23 | fmt.Println(add(a, b)) 24 | } 25 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/functionpointerarg/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func double(a int) { 6 | a = a * 2 7 | fmt.Println(a) 8 | } 9 | 10 | func doublePointer(a *int) { 11 | *a = *a * 2 12 | fmt.Println(*a) 13 | } 14 | 15 | func main() { 16 | a := 5 17 | double(a) 18 | fmt.Println(a) 19 | 20 | doublePointer(&a) 21 | fmt.Println(a) 22 | } 23 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/functionreturn/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func multiply(param int) func(int) int { 6 | if param%2 == 0 { 7 | return func(a int) int { 8 | return a * 2 9 | } 10 | } else { 11 | return func(a int) int { 12 | return a * 3 13 | } 14 | } 15 | } 16 | 17 | func main() { 18 | double := multiply(2) 19 | triple := multiply(3) 20 | fmt.Println(double(5), triple(5)) 21 | } 22 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/functionslice/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func modify(s []int) { 6 | s[0] = 4 7 | s = append(s, 5) 8 | fmt.Println(s) 9 | } 10 | 11 | func main() { 12 | s := []int{1, 2, 3} 13 | modify(s) 14 | fmt.Println(s) 15 | } 16 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/functiontwo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func add(a, b int) int { 6 | return a + b 7 | } 8 | 9 | func swap(a, b int) (int, int) { 10 | return b, a 11 | } 12 | 13 | func main() { 14 | a := 1 15 | b := 5 16 | fmt.Println(add(a, b)) 17 | fmt.Println(swap(a, b)) 18 | } 19 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/goplaygroud/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | a := 2 7 | b := 3 8 | c := a * b 9 | fmt.Println("Result is: ", c) 10 | } 11 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/ifstatement/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | a := 5 7 | if a < 100 { 8 | a += 1 9 | } 10 | fmt.Println(a) 11 | 12 | if b := a * a; b < 100 { 13 | a += 1 14 | } 15 | fmt.Println(a) 16 | 17 | code := "fr" 18 | var country string 19 | if code == "fr" { 20 | country = "France" 21 | } else if code == "uk" { 22 | country = "United Kingdom" 23 | } else { 24 | country = "India" 25 | } 26 | fmt.Println(country) 27 | } 28 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/maps/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | var countryMap = make(map[string]string) 7 | countryMap["fr"] = "France" 8 | country := countryMap["fr"] 9 | fmt.Println("Country in map is:", country) 10 | delete(countryMap, "fr") 11 | if _, ok := countryMap["fr"]; ok { 12 | fmt.Println("Country is still in map") 13 | } else { 14 | fmt.Println("Country is not in map") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/pointers/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | var i int = 18 7 | var pi *int 8 | 9 | pi = &i 10 | *pi = 27 11 | fmt.Println(i) 12 | } 13 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/switchstatement/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | code := "fr" 7 | var country string 8 | switch code { 9 | case "fr": 10 | country = "France" 11 | case "uk": 12 | country = "United Kingdom" 13 | default: 14 | country = "India" 15 | } 16 | fmt.Println(country) 17 | 18 | number := 5 19 | switch { 20 | case number%2 == 0: 21 | fmt.Println("Even Number") 22 | case number%2 == 1: 23 | fmt.Println("Odd Number") 24 | default: 25 | fmt.Println("Invalid Number") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Chapter 01/basic-concepts-of-go-examples-main/variables/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | var a int 7 | var b bool 8 | var c = 1 9 | var d = true 10 | fmt.Println(a) 11 | fmt.Println(b) 12 | fmt.Println(c) 13 | fmt.Println(d) 14 | } 15 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/README.md: -------------------------------------------------------------------------------- 1 | # Advanced Concepts of Go 2 | 3 | 4 | 5 | ## How to run examples 6 | 7 | The examples from this projects can be runned with following command: 8 | 9 | ``` 10 | go run directory/main.go 11 | ``` 12 | 13 | For directory one of the following options can be used: 14 | - channels 15 | - channelsselect 16 | - genericsfunction 17 | - genericstype 18 | - interfaces 19 | - interfacesfunction 20 | - interfacesnil 21 | - interfacesswitch 22 | - methods 23 | - methodsbasictype 24 | - mutex 25 | - panics 26 | - panicstrigger 27 | - waitgroup 28 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/channels/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func sendMessage(message string, ch chan string) { 6 | ch <- message 7 | } 8 | 9 | func main() { 10 | ch := make(chan string, 2) 11 | go sendMessage("Hello", ch) 12 | go sendMessage("World", ch) 13 | fmt.Println(<-ch) 14 | fmt.Println(<-ch) 15 | } 16 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/channelsselect/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func sendMessage(message string, ch chan string) { 6 | ch <- message 7 | } 8 | 9 | func main() { 10 | ch1 := make(chan string) 11 | ch2 := make(chan string) 12 | go sendMessage("Channel 1", ch1) 13 | 14 | for { 15 | select { 16 | case <-ch1: 17 | fmt.Println("Channel One") 18 | return 19 | case <-ch2: 20 | fmt.Println("Channel Two") 21 | default: 22 | fmt.Println("Waiting") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/genericsfunction/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func Contains[T comparable](arr []T, x T) bool { 6 | for _, v := range arr { 7 | if v == x { 8 | return true 9 | } 10 | } 11 | return false 12 | } 13 | 14 | func main() { 15 | intArr := []int{18, 27, 21, 1} 16 | stringArr := []string{"apple", "banana", "ananas", "orange"} 17 | 18 | fmt.Println(Contains(intArr, 27)) 19 | fmt.Println(Contains(stringArr, "lemon")) 20 | } 21 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/genericstype/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type Node[T any] struct { 6 | leftChild *Node[T] 7 | rightChild *Node[T] 8 | value T 9 | } 10 | 11 | func main() { 12 | nodeInt := Node[int]{value: 5} 13 | nodeFloat := Node[float64]{value: 5.2} 14 | 15 | fmt.Println(nodeInt) 16 | fmt.Println(nodeFloat) 17 | } 18 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/interfaces/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Geometry interface { 8 | Perimeter() int 9 | Area() int 10 | } 11 | 12 | type Square struct { 13 | a int 14 | } 15 | 16 | func (s Square) Perimeter() int { 17 | return s.a * 4 18 | } 19 | func (s Square) Area() int { 20 | return s.a * s.a 21 | } 22 | 23 | func main() { 24 | s := Square{5} 25 | fmt.Println(s.Perimeter()) 26 | fmt.Println(s.Area()) 27 | } 28 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/interfacesfunction/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Geometry interface { 8 | Perimeter() int 9 | Area() int 10 | } 11 | 12 | type Square struct { 13 | a int 14 | } 15 | 16 | func (s Square) Perimeter() int { 17 | return s.a * 4 18 | } 19 | func (s Square) Area() int { 20 | return s.a * s.a 21 | } 22 | 23 | func Details(g Geometry) { 24 | fmt.Println("Perimeter:", g.Perimeter()) 25 | fmt.Println("Area:", g.Area()) 26 | } 27 | 28 | func main() { 29 | s := Square{5} 30 | Details(s) 31 | } 32 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/interfacesnil/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type Geometry interface { 6 | Perimeter() int 7 | Area() int 8 | } 9 | 10 | type Square struct { 11 | a int 12 | } 13 | 14 | func (s Square) Perimeter() int { 15 | return s.a * 4 16 | } 17 | func (s Square) Area() int { 18 | return s.a * s.a 19 | } 20 | 21 | func IsNil(a interface{}) bool { 22 | return a == nil 23 | } 24 | 25 | func main() { 26 | var s *Square 27 | fmt.Println(IsNil(s)) 28 | } 29 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/interfacesswitch/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type Geometry interface { 6 | Perimeter() int 7 | Area() int 8 | } 9 | 10 | type Square struct { 11 | a int 12 | } 13 | 14 | func (s Square) Perimeter() int { 15 | return s.a * 4 16 | } 17 | func (s Square) Area() int { 18 | return s.a * s.a 19 | } 20 | 21 | type Rectangle struct { 22 | a, b int 23 | } 24 | 25 | func (r Rectangle) Perimeter() int { 26 | return r.a*2 + r.b*2 27 | } 28 | func (r Rectangle) Area() int { 29 | return r.a * r.b 30 | } 31 | 32 | func main() { 33 | var g Geometry = Square{5} 34 | 35 | switch g.(type) { 36 | case Square: 37 | fmt.Printf("Square") 38 | case Rectangle: 39 | fmt.Printf("Rectangle") 40 | default: 41 | fmt.Printf("Unknown type") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/methods/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | type Circle struct { 9 | r float64 10 | } 11 | 12 | func (c Circle) Perimeter() float64 { 13 | return 2 * c.r * math.Pi 14 | } 15 | 16 | func (c Circle) Area() float64 { 17 | return c.r * c.r * math.Pi 18 | } 19 | 20 | func main() { 21 | c := Circle{5.0} 22 | fmt.Println(c.Perimeter()) 23 | fmt.Println(c.Area()) 24 | } 25 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/methodsbasictype/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type CustomInt int 8 | 9 | func (ci CustomInt) Double() int { 10 | return int(ci * 2) 11 | } 12 | 13 | func main() { 14 | c := CustomInt(5) 15 | fmt.Println(c.Double()) 16 | } 17 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/mutex/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type MutualExclusion struct { 10 | mutex sync.Mutex 11 | value int 12 | } 13 | 14 | func (me *MutualExclusion) Double() { 15 | me.mutex.Lock() 16 | me.value *= 2 17 | me.mutex.Unlock() 18 | } 19 | 20 | func main() { 21 | me := MutualExclusion{value: 5} 22 | go me.Double() 23 | go me.Double() 24 | 25 | time.Sleep(time.Second) 26 | fmt.Println(me.value) 27 | } 28 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/panics/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | s := []int{1, 2, 3} 7 | fmt.Println(s[3]) 8 | } 9 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/panicstrigger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func checkName(name string) { 4 | if name == "" { 5 | panic("Invalid name!!!") 6 | } 7 | } 8 | 9 | func main() { 10 | checkName("test") 11 | checkName("") 12 | } 13 | -------------------------------------------------------------------------------- /Chapter 02/advanced-concepts-of-go-main/waitgroup/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | func main() { 9 | a := []int{5, 8, 4, 9, 3} 10 | wg := &sync.WaitGroup{} 11 | wg.Add(5) 12 | 13 | for i := 0; i < 5; i++ { 14 | go func(i int) { 15 | a[i] *= 3 16 | fmt.Println("Goroutine ", i) 17 | wg.Done() 18 | }(i) 19 | } 20 | 21 | wg.Wait() 22 | fmt.Println(a) 23 | } 24 | -------------------------------------------------------------------------------- /Chapter 03/go-json-main/README.md: -------------------------------------------------------------------------------- 1 | # Go JSON 2 | 3 | 4 | 5 | ## Running code 6 | 7 | Code can be run with the following command. 8 | 9 | ``` 10 | go run main.go 11 | ``` -------------------------------------------------------------------------------- /Chapter 03/go-json-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type spouse struct { 9 | Name string 10 | Age int 11 | } 12 | 13 | type person struct { 14 | Name string 15 | Age int 16 | Married bool 17 | Spouse spouse 18 | Children []string 19 | } 20 | 21 | func main() { 22 | jsonString := `{"name": "Albert Einstein", "age": 76, "married": true, "spouse": {"name": "Mileva", "age": 72}, "children": ["Lieserl", "Hans Albert", "Eduard"]}` 23 | var p person 24 | err := json.Unmarshal([]byte(jsonString), &p) 25 | if err != nil { 26 | fmt.Println("Failed to decode JSON") 27 | } 28 | fmt.Println(p) 29 | 30 | jsonByte, err := json.Marshal(p) 31 | if err != nil { 32 | fmt.Println("Failed to encode JSON") 33 | } 34 | fmt.Println(string(jsonByte)) 35 | } 36 | -------------------------------------------------------------------------------- /Chapter 04 (a)/hello-world-and-packages-main/README.md: -------------------------------------------------------------------------------- 1 | # Hello World and Packages 2 | 3 | 4 | 5 | ## Running code started 6 | 7 | Code can be run with the following command. 8 | 9 | ``` 10 | go run main.go 11 | ``` 12 | -------------------------------------------------------------------------------- /Chapter 04 (a)/hello-world-and-packages-main/countries/countries.go: -------------------------------------------------------------------------------- 1 | package countries 2 | 3 | func GetCountry(code string) (country string) { 4 | switch code { 5 | case "FR": 6 | country = "France" 7 | case "IT": 8 | country = "Italy" 9 | case "IN": 10 | country = "India" 11 | case "US": 12 | country = "United States" 13 | default: 14 | country = "Unknown" 15 | } 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /Chapter 04 (a)/hello-world-and-packages-main/go.mod: -------------------------------------------------------------------------------- 1 | module hello 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /Chapter 04 (a)/hello-world-and-packages-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "hello/countries" 6 | ) 7 | 8 | func main() { 9 | fmt.Println("Hello World!!!") 10 | 11 | fmt.Println("Hello", countries.GetCountry("FR")) 12 | } 13 | -------------------------------------------------------------------------------- /Chapter 04 (b)/go-standard-library-main/README.md: -------------------------------------------------------------------------------- 1 | # Go Standard Library 2 | 3 | 4 | 5 | ## Running code 6 | 7 | Code can be run with the following command. 8 | 9 | ``` 10 | go run main.go 11 | -------------------------------------------------------------------------------- /Chapter 04 (b)/go-standard-library-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | func div(a, b float32) (c float32) { 8 | log.Println("Input parameters:", a, b) 9 | 10 | if b == 0 { 11 | log.Println("Division by zero") 12 | return 0 13 | } 14 | 15 | c = a / b 16 | 17 | log.Println("Division result:", c) 18 | return 19 | } 20 | 21 | func main() { 22 | div(6.0, 3.0) 23 | div(6.0, 0.0) 24 | } 25 | -------------------------------------------------------------------------------- /Chapter 04 (c)/go-third-party-libraries-main/README.md: -------------------------------------------------------------------------------- 1 | # Go Third-party libraries 2 | 3 | 4 | 5 | ## Prerequisites 6 | Download golang-set library with following command 7 | 8 | ``` 9 | go get github.com/deckarep/golang-set 10 | 11 | ``` 12 | 13 | ## Running code 14 | 15 | Code can be run with the following command. 16 | 17 | ``` 18 | go run main.go 19 | 20 | ``` 21 | -------------------------------------------------------------------------------- /Chapter 04 (c)/go-third-party-libraries-main/go.mod: -------------------------------------------------------------------------------- 1 | module set 2 | 3 | go 1.20 4 | 5 | require github.com/deckarep/golang-set v1.8.0 // indirect 6 | -------------------------------------------------------------------------------- /Chapter 04 (c)/go-third-party-libraries-main/go.sum: -------------------------------------------------------------------------------- 1 | github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= 2 | github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= 3 | -------------------------------------------------------------------------------- /Chapter 04 (c)/go-third-party-libraries-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | set "github.com/deckarep/golang-set" 7 | ) 8 | 9 | func main() { 10 | yellowFruit := set.NewSet() 11 | yellowFruit.Add("banana") 12 | yellowFruit.Add("lemon") 13 | yellowFruit.Add("pineapple") 14 | fmt.Println(yellowFruit) 15 | 16 | redFruit := set.NewSetFromSlice([]interface{}{"apple", "cherry", "strawberry"}) 17 | fmt.Println(redFruit) 18 | 19 | fruit := yellowFruit.Union(redFruit) 20 | fmt.Println(fruit) 21 | } 22 | -------------------------------------------------------------------------------- /Chapter 04 (d)/go-simple-http-server-main/README.md: -------------------------------------------------------------------------------- 1 | # Go Simple HTTP server 2 | 3 | 4 | 5 | ## Running code 6 | 7 | Code can be run with the following command. 8 | 9 | ``` 10 | go run main.go 11 | ``` 12 | -------------------------------------------------------------------------------- /Chapter 04 (d)/go-simple-http-server-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func helloWorld(rw http.ResponseWriter, req *http.Request) { 9 | fmt.Fprintf(rw, "Hello World\n") 10 | } 11 | 12 | func main() { 13 | http.HandleFunc("/hello", helloWorld) 14 | 15 | http.ListenAndServe(":8080", nil) 16 | } 17 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from golang alpine base image 2 | FROM golang:1.19-alpine 3 | 4 | # Set the Current Working Directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the source code to the Working Directory inside the container 8 | COPY . . 9 | 10 | # Download all dependencies. 11 | # Dependencies will be cached if the go.mod and go.sum files are not changed 12 | RUN go mod download 13 | 14 | # Build the Go app 15 | RUN go build -o runners-app main.go 16 | 17 | # Export necessary ports 18 | EXPOSE 8080 19 | 20 | # Command to run when starting the container 21 | CMD ["/app/runners-app"] 22 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func InitConfig(fileName string) *viper.Viper { 10 | config := viper.New() 11 | 12 | config.SetConfigName(fileName) 13 | 14 | config.AddConfigPath(".") 15 | config.AddConfigPath("$HOME") 16 | 17 | err := config.ReadInConfig() 18 | if err != nil { 19 | log.Fatal("Error while parsing configuration file", err) 20 | } 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/controllers/resultsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "runners-postgresql/models" 9 | "runners-postgresql/services" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type ResultsController struct { 15 | resultsService *services.ResultsService 16 | usersService *services.UsersService 17 | } 18 | 19 | func NewResultsController(resultsService *services.ResultsService, 20 | userService *services.UsersService) *ResultsController { 21 | 22 | return &ResultsController{ 23 | resultsService: resultsService, 24 | usersService: userService, 25 | } 26 | } 27 | 28 | func (rc ResultsController) CreateResult(ctx *gin.Context) { 29 | accessToken := ctx.Request.Header.Get("Token") 30 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 31 | if responseErr != nil { 32 | ctx.JSON(responseErr.Status, responseErr) 33 | return 34 | } 35 | 36 | if !auth { 37 | ctx.Status(http.StatusUnauthorized) 38 | return 39 | } 40 | 41 | body, err := io.ReadAll(ctx.Request.Body) 42 | if err != nil { 43 | log.Println("Error while reading create result request body", err) 44 | ctx.AbortWithError(http.StatusInternalServerError, err) 45 | return 46 | } 47 | 48 | var result models.Result 49 | err = json.Unmarshal(body, &result) 50 | if err != nil { 51 | log.Println("Error while unmarshaling create result request body", err) 52 | ctx.AbortWithError(http.StatusInternalServerError, err) 53 | return 54 | } 55 | 56 | response, responseErr := rc.resultsService.CreateResult(&result) 57 | if responseErr != nil { 58 | ctx.JSON(responseErr.Status, responseErr) 59 | return 60 | } 61 | 62 | ctx.JSON(http.StatusOK, response) 63 | } 64 | 65 | func (rc ResultsController) DeleteResult(ctx *gin.Context) { 66 | accessToken := ctx.Request.Header.Get("Token") 67 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 68 | if responseErr != nil { 69 | ctx.JSON(responseErr.Status, responseErr) 70 | return 71 | } 72 | 73 | if !auth { 74 | ctx.Status(http.StatusUnauthorized) 75 | return 76 | } 77 | 78 | resultId := ctx.Param("id") 79 | 80 | responseErr = rc.resultsService.DeleteResult(resultId) 81 | if responseErr != nil { 82 | ctx.JSON(responseErr.Status, responseErr) 83 | return 84 | } 85 | 86 | ctx.Status(http.StatusNoContent) 87 | } 88 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/controllers/runnersController_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "runners-postgresql/models" 9 | "runners-postgresql/repositories" 10 | "runners-postgresql/services" 11 | "testing" 12 | 13 | "github.com/DATA-DOG/go-sqlmock" 14 | "github.com/gin-gonic/gin" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestGetRunnersResponse(t *testing.T) { 19 | dbHandler, mock, _ := sqlmock.New() 20 | defer dbHandler.Close() 21 | 22 | columnsUsers := []string{"user_role"} 23 | mock.ExpectQuery("SELECT user_role").WillReturnRows( 24 | sqlmock.NewRows(columnsUsers).AddRow("runner"), 25 | ) 26 | 27 | columns := []string{"id", "first_name", "last_name", "age", "is_active", "country", "personal_best", "season_best"} 28 | mock.ExpectQuery("SELECT *").WillReturnRows( 29 | sqlmock.NewRows(columns). 30 | AddRow("1", "John", "Smith", 30, true, "United States", "02:00:41", "02:13:13"). 31 | AddRow("2", "Marijana", "Komatinovic", 30, true, "Serbia", "01:18:28", "01:18:28")) 32 | 33 | router := initTestRouter(dbHandler) 34 | request, _ := http.NewRequest("GET", "/runner", nil) 35 | request.Header.Set("token", "token") 36 | recorder := httptest.NewRecorder() 37 | 38 | router.ServeHTTP(recorder, request) 39 | 40 | assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) 41 | 42 | var runers []*models.Runner 43 | json.Unmarshal(recorder.Body.Bytes(), &runers) 44 | 45 | assert.NotEmpty(t, runers) 46 | assert.Equal(t, 2, len(runers)) 47 | } 48 | 49 | func initTestRouter(dbHandler *sql.DB) *gin.Engine { 50 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 51 | usersRepository := repositories.NewUsersRepository(dbHandler) 52 | runnersService := services.NewRunnersService(runnersRepository, nil) 53 | usersServices := services.NewUsersService(usersRepository) 54 | runnersController := NewRunnersController(runnersService, usersServices) 55 | 56 | router := gin.Default() 57 | 58 | router.GET("/runner", runnersController.GetRunnersBatch) 59 | 60 | return router 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/controllers/usersController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "runners-postgresql/services" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type UsersController struct { 12 | usersService *services.UsersService 13 | } 14 | 15 | func NewUsersController(usersService *services.UsersService) *UsersController { 16 | return &UsersController{ 17 | usersService: usersService, 18 | } 19 | } 20 | 21 | func (uc UsersController) Login(ctx *gin.Context) { 22 | username, password, ok := ctx.Request.BasicAuth() 23 | if !ok { 24 | log.Println("Error while reading credentials") 25 | ctx.AbortWithStatus(http.StatusBadRequest) 26 | return 27 | } 28 | 29 | accessToken, responseErr := uc.usersService.Login(username, password) 30 | if responseErr != nil { 31 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 32 | return 33 | } 34 | 35 | ctx.JSON(http.StatusOK, accessToken) 36 | } 37 | 38 | func (uc UsersController) Logout(ctx *gin.Context) { 39 | accessToken := ctx.Request.Header.Get("Token") 40 | 41 | responseErr := uc.usersService.Logout(accessToken) 42 | if responseErr != nil { 43 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 44 | return 45 | } 46 | 47 | ctx.Status(http.StatusNoContent) 48 | } 49 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/dbscripts/public_schema.sql: -------------------------------------------------------------------------------- 1 | -- Initial public schema relates to Library 0.x 2 | 3 | SET statement_timeout = 0; 4 | SET lock_timeout = 0; 5 | SET idle_in_transaction_session_timeout = 0; 6 | SET client_encoding = 'UTF8'; 7 | SET standard_conforming_strings = on; 8 | SET client_min_messages = warning; 9 | SET row_security = off; 10 | 11 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 12 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA pg_catalog; 13 | 14 | SET search_path = public, pg_catalog; 15 | SET default_tablespace = ''; 16 | 17 | -- runners 18 | CREATE TABLE runners ( 19 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 20 | first_name text NOT NULL, 21 | last_name text NOT NULL, 22 | age integer, 23 | is_active boolean DEFAULT TRUE, 24 | country text NOT NULL, 25 | personal_best interval, 26 | season_best interval, 27 | CONSTRAINT runners_pk PRIMARY KEY (id) 28 | ); 29 | 30 | CREATE INDEX runners_country 31 | ON runners (country); 32 | 33 | CREATE INDEX runners_season_best 34 | ON runners (season_best); 35 | 36 | -- results 37 | CREATE TABLE results ( 38 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 39 | runner_id uuid NOT NULL, 40 | race_result interval NOT NULL, 41 | location text NOT NULL, 42 | position integer, 43 | year integer NOT NULL, 44 | CONSTRAINT results_pk PRIMARY KEY (id), 45 | CONSTRAINT fk_results_runner_id FOREIGN KEY (runner_id) 46 | REFERENCES runners (id) MATCH SIMPLE 47 | ON UPDATE NO ACTION 48 | ON DELETE NO ACTION 49 | ); 50 | 51 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/dbscripts/update_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | 3 | CREATE TABLE users ( 4 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 5 | username text NOT NULL UNIQUE, 6 | user_password text NOT NULL, 7 | user_role text NOT NULL, 8 | access_token text, 9 | CONSTRAINT users_pk PRIMARY KEY (id) 10 | ); 11 | 12 | CREATE INDEX user_access_token 13 | ON users (access_token); 14 | 15 | INSERT INTO users(username, user_password, user_role) 16 | VALUES 17 | ('admin', crypt('admin', gen_salt('bf')), 'admin'), 18 | ('runner', crypt('runner', gen_salt('bf')), 'runner'); 19 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | runners-app: 4 | image: runners-app:latest 5 | network_mode: "host" -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/go.mod: -------------------------------------------------------------------------------- 1 | module runners-postgresql 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect 7 | github.com/beorn7/perks v1.0.1 // indirect 8 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 9 | github.com/davecgh/go-spew v1.1.1 // indirect 10 | github.com/fsnotify/fsnotify v1.5.4 // indirect 11 | github.com/gin-contrib/sse v0.1.0 // indirect 12 | github.com/gin-gonic/gin v1.8.1 // indirect 13 | github.com/go-playground/locales v0.14.0 // indirect 14 | github.com/go-playground/universal-translator v0.18.0 // indirect 15 | github.com/go-playground/validator/v10 v10.11.1 // indirect 16 | github.com/goccy/go-json v0.9.11 // indirect 17 | github.com/golang/protobuf v1.5.2 // indirect 18 | github.com/hashicorp/hcl v1.0.0 // indirect 19 | github.com/json-iterator/go v1.1.12 // indirect 20 | github.com/leodido/go-urn v1.2.1 // indirect 21 | github.com/lib/pq v1.10.7 // indirect 22 | github.com/magiconair/properties v1.8.6 // indirect 23 | github.com/mattn/go-isatty v0.0.16 // indirect 24 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 25 | github.com/mitchellh/mapstructure v1.5.0 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/pelletier/go-toml v1.9.5 // indirect 29 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/prometheus/client_golang v1.14.0 // indirect 32 | github.com/prometheus/client_model v0.3.0 // indirect 33 | github.com/prometheus/common v0.37.0 // indirect 34 | github.com/prometheus/procfs v0.8.0 // indirect 35 | github.com/spf13/afero v1.8.2 // indirect 36 | github.com/spf13/cast v1.5.0 // indirect 37 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 38 | github.com/spf13/pflag v1.0.5 // indirect 39 | github.com/spf13/viper v1.13.0 // indirect 40 | github.com/stretchr/objx v0.5.0 // indirect 41 | github.com/stretchr/testify v1.8.1 // indirect 42 | github.com/subosito/gotenv v1.4.1 // indirect 43 | github.com/ugorji/go/codec v1.2.7 // indirect 44 | golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect 45 | golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect 46 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect 47 | golang.org/x/text v0.3.7 // indirect 48 | google.golang.org/protobuf v1.28.1 // indirect 49 | gopkg.in/ini.v1 v1.67.0 // indirect 50 | gopkg.in/yaml.v2 v2.4.0 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/kubernetes/runners-app-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: runners-app 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: runners-app 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app: runners-app 14 | spec: 15 | containers: 16 | - image: runners-app:latest 17 | name: runners-app 18 | imagePullPolicy: Never 19 | ports: 20 | - containerPort: 8080 21 | env: 22 | - name: ENV 23 | value: "k8s" -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/kubernetes/runners-app-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: runners-app-service 5 | spec: 6 | selector: 7 | app: runners-app 8 | type: LoadBalancer 9 | ports: 10 | - name: http 11 | protocol: TCP 12 | port: 8080 13 | targetPort: 8080 14 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "runners-postgresql/config" 7 | "runners-postgresql/server" 8 | 9 | _ "github.com/lib/pq" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting Runners App") 14 | 15 | log.Println("Initializing configuration") 16 | config := config.InitConfig(getConfigFileName()) 17 | 18 | log.Println("Initializing database") 19 | dbHandler := server.InitDatabase(config) 20 | 21 | log.Println("Initializing Prometheus") 22 | go server.InitPrometheus() 23 | 24 | log.Println("Initializig HTTP sever") 25 | httpServer := server.InitHttpServer(config, dbHandler) 26 | 27 | httpServer.Start() 28 | } 29 | 30 | func getConfigFileName() string { 31 | env := os.Getenv("ENV") 32 | 33 | if env != "" { 34 | return "runners-" + env 35 | } 36 | 37 | return "runners" 38 | } 39 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | HttpRequestsCounter = promauto.NewCounter( 10 | prometheus.CounterOpts{ 11 | Name: "runners_app_http_requests", 12 | Help: "Total number of HTTP requests", 13 | }, 14 | ) 15 | 16 | GetRunnerHttpResponsesCounter = promauto.NewCounterVec( 17 | prometheus.CounterOpts{ 18 | Name: "runners_app_get_runner_http_responses", 19 | Help: "Total number of HTTP responses for get runner API", 20 | }, 21 | []string{"status"}, 22 | ) 23 | 24 | GetAllRunnersTimer = promauto.NewHistogram( 25 | prometheus.HistogramOpts{ 26 | Name: "runners_app_get_all_runners_duration", 27 | Help: "Duration of get all runners operation", 28 | }, 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/models/responseError.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ResponseError struct { 4 | Message string `json:"message"` 5 | Status int `json:"-"` 6 | } 7 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/models/result.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Result struct { 4 | ID string `json:"id"` 5 | RunnerID string `json:"runner_id"` 6 | RaceResult string `json:"race_result"` 7 | Location string `json:"location"` 8 | Position int `json:"position,omitempty"` 9 | Year int `json:"year"` 10 | } 11 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/models/runner.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Runner struct { 4 | ID string `json:"id"` 5 | FirstName string `json:"first_name"` 6 | LastName string `json:"last_name"` 7 | Age int `json:"age,omitempty"` 8 | IsActive bool `json:"is_active"` 9 | Country string `json:"country"` 10 | PersonalBest string `json:"personal_best,omitempty"` 11 | SeasonBest string `json:"season_best,omitempty"` 12 | Results []*Result `json:"results,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | ID string `json:"id"` 5 | Username string `json:"username"` 6 | Password string `json:"user_password"` 7 | Role string `json:"user_role"` 8 | AccessToken string `json:"access_toke"` 9 | } 10 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/repositories/transactionHandler.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | func BeginTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 9 | ctx := context.Background() 10 | transaction, err := resultsRepository.dbHandler.BeginTx(ctx, &sql.TxOptions{}) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | runnersRepository.transaction = transaction 16 | resultsRepository.transaction = transaction 17 | 18 | return nil 19 | } 20 | 21 | func RollbackTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 22 | transaction := runnersRepository.transaction 23 | 24 | runnersRepository.transaction = nil 25 | resultsRepository.transaction = nil 26 | 27 | return transaction.Rollback() 28 | } 29 | 30 | func CommitTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 31 | transaction := runnersRepository.transaction 32 | 33 | runnersRepository.transaction = nil 34 | resultsRepository.transaction = nil 35 | 36 | return transaction.Commit() 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/runners-k8s.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=192.168.0.25 port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/runners.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=localhost port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### 21 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/server/dbServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func InitDatabase(config *viper.Viper) *sql.DB { 11 | connectionString := config.GetString("database.connection_string") 12 | maxIdleConnections := config.GetInt("database.max_idle_connections") 13 | maxOpenConnections := config.GetInt("database.max_open_connections") 14 | connectionMaxLifetime := config.GetDuration("database.connection_max_lifetime") 15 | driverName := config.GetString("database.driver_name") 16 | 17 | if connectionString == "" { 18 | log.Fatalf("Database connectin string is missing") 19 | } 20 | 21 | dbHandler, err := sql.Open(driverName, connectionString) 22 | if err != nil { 23 | log.Fatalf("Error while initializing database: %v", err) 24 | } 25 | 26 | dbHandler.SetMaxIdleConns(maxIdleConnections) 27 | dbHandler.SetMaxOpenConns(maxOpenConnections) 28 | dbHandler.SetConnMaxLifetime(connectionMaxLifetime) 29 | 30 | err = dbHandler.Ping() 31 | if err != nil { 32 | dbHandler.Close() 33 | log.Fatalf("Error while validating database: %v", err) 34 | } 35 | 36 | return dbHandler 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/server/httpServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "runners-postgresql/controllers" 7 | "runners-postgresql/repositories" 8 | "runners-postgresql/services" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | type HttpServer struct { 15 | config *viper.Viper 16 | router *gin.Engine 17 | runnersController *controllers.RunnersController 18 | resultsController *controllers.ResultsController 19 | usersController *controllers.UsersController 20 | } 21 | 22 | func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { 23 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 24 | resultRepository := repositories.NewResultsRepository(dbHandler) 25 | usersRepository := repositories.NewUsersRepository(dbHandler) 26 | runnersService := services.NewRunnersService(runnersRepository, resultRepository) 27 | resultsService := services.NewResultsService(resultRepository, runnersRepository) 28 | usersService := services.NewUsersService(usersRepository) 29 | runnersController := controllers.NewRunnersController(runnersService, usersService) 30 | resultsController := controllers.NewResultsController(resultsService, usersService) 31 | usersController := controllers.NewUsersController(usersService) 32 | 33 | router := gin.Default() 34 | 35 | router.POST("/runner", runnersController.CreateRunner) 36 | router.PUT("/runner", runnersController.UpdateRunner) 37 | router.DELETE("/runner/:id", runnersController.DeleteRunner) 38 | router.GET("/runner/:id", runnersController.GetRunner) 39 | router.GET("/runner", runnersController.GetRunnersBatch) 40 | 41 | router.POST("/result", resultsController.CreateResult) 42 | router.DELETE("/result/:id", resultsController.DeleteResult) 43 | 44 | router.POST("/login", usersController.Login) 45 | router.POST("/logout", usersController.Logout) 46 | 47 | return HttpServer{ 48 | config: config, 49 | router: router, 50 | runnersController: runnersController, 51 | resultsController: resultsController, 52 | usersController: usersController, 53 | } 54 | } 55 | 56 | func (hs HttpServer) Start() { 57 | err := hs.router.Run(hs.config.GetString("http.server_address")) 58 | if err != nil { 59 | log.Fatalf("Error while starting HTTP server: %v", err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/server/prometheus.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | ) 8 | 9 | func InitPrometheus() { 10 | http.Handle("/metrics", promhttp.Handler()) 11 | http.ListenAndServe(":9000", nil) 12 | } 13 | -------------------------------------------------------------------------------- /Chapter 06/runners-postgresql-main/services/runnerService_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "net/http" 5 | "runners-postgresql/models" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | /*func TestValidateRunnerInvalidFirstName(t *testing.T) { 12 | runner := &models.Runner{ 13 | LastName: "Smith", 14 | Age: 30, 15 | Country: "United States", 16 | } 17 | 18 | responseErr := validateRunner(runner) 19 | assert.NotEmpty(t, responseErr) 20 | assert.Equal(t, "Invalid first name", responseErr.Message) 21 | assert.Equal(t, http.StatusBadRequest, responseErr.Status) 22 | }*/ 23 | 24 | func TestValidateRunner(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | runner *models.Runner 28 | want *models.ResponseError 29 | }{ 30 | { 31 | name: "Invalid_First_Name", 32 | runner: &models.Runner{ 33 | LastName: "Smith", 34 | Age: 30, 35 | Country: "United States", 36 | }, 37 | want: &models.ResponseError{ 38 | Message: "Invalid first name", 39 | Status: http.StatusBadRequest, 40 | }, 41 | }, 42 | { 43 | name: "Invalid_Last_Name", 44 | runner: &models.Runner{ 45 | FirstName: "John", 46 | Age: 30, 47 | Country: "United States", 48 | }, 49 | want: &models.ResponseError{ 50 | Message: "Invalid last name", 51 | Status: http.StatusBadRequest, 52 | }, 53 | }, 54 | { 55 | name: "Invalid_Age", 56 | runner: &models.Runner{ 57 | FirstName: "John", 58 | LastName: "Smith", 59 | Age: 300, 60 | Country: "United States", 61 | }, 62 | want: &models.ResponseError{ 63 | Message: "Invalid age", 64 | Status: http.StatusBadRequest, 65 | }, 66 | }, 67 | { 68 | name: "Invalid_Country", 69 | runner: &models.Runner{ 70 | FirstName: "John", 71 | LastName: "Smith", 72 | Age: 30, 73 | }, 74 | want: &models.ResponseError{ 75 | Message: "Invalid country", 76 | Status: http.StatusBadRequest, 77 | }, 78 | }, 79 | { 80 | name: "Valid_Runner", 81 | runner: &models.Runner{ 82 | FirstName: "John", 83 | LastName: "Smith", 84 | Age: 30, 85 | Country: "United States", 86 | }, 87 | want: nil, 88 | }, 89 | } 90 | 91 | for _, test := range tests { 92 | t.Run(test.name, func(t *testing.T) { 93 | responseErr := validateRunner(test.runner) 94 | assert.Equal(t, test.want, responseErr) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from golang alpine base image 2 | FROM golang:1.19-alpine 3 | 4 | # Set the Current Working Directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the source code to the Working Directory inside the container 8 | COPY . . 9 | 10 | # Download all dependencies. 11 | # Dependencies will be cached if the go.mod and go.sum files are not changed 12 | RUN go mod download 13 | 14 | # Build the Go app 15 | RUN go build -o runners-app main.go 16 | 17 | # Export necessary ports 18 | EXPOSE 8080 19 | 20 | # Command to run when starting the container 21 | CMD ["/app/runners-app"] 22 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func InitConfig(fileName string) *viper.Viper { 10 | config := viper.New() 11 | 12 | config.SetConfigName(fileName) 13 | 14 | config.AddConfigPath(".") 15 | config.AddConfigPath("$HOME") 16 | 17 | err := config.ReadInConfig() 18 | if err != nil { 19 | log.Fatal("Error while parsing configuration file", err) 20 | } 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/controllers/resultsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "runners-postgresql/models" 9 | "runners-postgresql/services" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type ResultsController struct { 15 | resultsService *services.ResultsService 16 | usersService *services.UsersService 17 | } 18 | 19 | func NewResultsController(resultsService *services.ResultsService, 20 | userService *services.UsersService) *ResultsController { 21 | 22 | return &ResultsController{ 23 | resultsService: resultsService, 24 | usersService: userService, 25 | } 26 | } 27 | 28 | func (rc ResultsController) CreateResult(ctx *gin.Context) { 29 | accessToken := ctx.Request.Header.Get("Token") 30 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 31 | if responseErr != nil { 32 | ctx.JSON(responseErr.Status, responseErr) 33 | return 34 | } 35 | 36 | if !auth { 37 | ctx.Status(http.StatusUnauthorized) 38 | return 39 | } 40 | 41 | body, err := io.ReadAll(ctx.Request.Body) 42 | if err != nil { 43 | log.Println("Error while reading create result request body", err) 44 | ctx.AbortWithError(http.StatusInternalServerError, err) 45 | return 46 | } 47 | 48 | var result models.Result 49 | err = json.Unmarshal(body, &result) 50 | if err != nil { 51 | log.Println("Error while unmarshaling create result request body", err) 52 | ctx.AbortWithError(http.StatusInternalServerError, err) 53 | return 54 | } 55 | 56 | response, responseErr := rc.resultsService.CreateResult(&result) 57 | if responseErr != nil { 58 | ctx.JSON(responseErr.Status, responseErr) 59 | return 60 | } 61 | 62 | ctx.JSON(http.StatusOK, response) 63 | } 64 | 65 | func (rc ResultsController) DeleteResult(ctx *gin.Context) { 66 | accessToken := ctx.Request.Header.Get("Token") 67 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 68 | if responseErr != nil { 69 | ctx.JSON(responseErr.Status, responseErr) 70 | return 71 | } 72 | 73 | if !auth { 74 | ctx.Status(http.StatusUnauthorized) 75 | return 76 | } 77 | 78 | resultId := ctx.Param("id") 79 | 80 | responseErr = rc.resultsService.DeleteResult(resultId) 81 | if responseErr != nil { 82 | ctx.JSON(responseErr.Status, responseErr) 83 | return 84 | } 85 | 86 | ctx.Status(http.StatusNoContent) 87 | } 88 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/controllers/runnersController_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "runners-postgresql/models" 9 | "runners-postgresql/repositories" 10 | "runners-postgresql/services" 11 | "testing" 12 | 13 | "github.com/DATA-DOG/go-sqlmock" 14 | "github.com/gin-gonic/gin" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestGetRunnersResponse(t *testing.T) { 19 | dbHandler, mock, _ := sqlmock.New() 20 | defer dbHandler.Close() 21 | 22 | columnsUsers := []string{"user_role"} 23 | mock.ExpectQuery("SELECT user_role").WillReturnRows( 24 | sqlmock.NewRows(columnsUsers).AddRow("runner"), 25 | ) 26 | 27 | columns := []string{"id", "first_name", "last_name", "age", "is_active", "country", "personal_best", "season_best"} 28 | mock.ExpectQuery("SELECT *").WillReturnRows( 29 | sqlmock.NewRows(columns). 30 | AddRow("1", "John", "Smith", 30, true, "United States", "02:00:41", "02:13:13"). 31 | AddRow("2", "Marijana", "Komatinovic", 30, true, "Serbia", "01:18:28", "01:18:28")) 32 | 33 | router := initTestRouter(dbHandler) 34 | request, _ := http.NewRequest("GET", "/runner", nil) 35 | request.Header.Set("token", "token") 36 | recorder := httptest.NewRecorder() 37 | 38 | router.ServeHTTP(recorder, request) 39 | 40 | assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) 41 | 42 | var runers []*models.Runner 43 | json.Unmarshal(recorder.Body.Bytes(), &runers) 44 | 45 | assert.NotEmpty(t, runers) 46 | assert.Equal(t, 2, len(runers)) 47 | } 48 | 49 | func initTestRouter(dbHandler *sql.DB) *gin.Engine { 50 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 51 | usersRepository := repositories.NewUsersRepository(dbHandler) 52 | runnersService := services.NewRunnersService(runnersRepository, nil) 53 | usersServices := services.NewUsersService(usersRepository) 54 | runnersController := NewRunnersController(runnersService, usersServices) 55 | 56 | router := gin.Default() 57 | 58 | router.GET("/runner", runnersController.GetRunnersBatch) 59 | 60 | return router 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/controllers/usersController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "runners-postgresql/services" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type UsersController struct { 12 | usersService *services.UsersService 13 | } 14 | 15 | func NewUsersController(usersService *services.UsersService) *UsersController { 16 | return &UsersController{ 17 | usersService: usersService, 18 | } 19 | } 20 | 21 | func (uc UsersController) Login(ctx *gin.Context) { 22 | username, password, ok := ctx.Request.BasicAuth() 23 | if !ok { 24 | log.Println("Error while reading credentials") 25 | ctx.AbortWithStatus(http.StatusBadRequest) 26 | return 27 | } 28 | 29 | accessToken, responseErr := uc.usersService.Login(username, password) 30 | if responseErr != nil { 31 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 32 | return 33 | } 34 | 35 | ctx.JSON(http.StatusOK, accessToken) 36 | } 37 | 38 | func (uc UsersController) Logout(ctx *gin.Context) { 39 | accessToken := ctx.Request.Header.Get("Token") 40 | 41 | responseErr := uc.usersService.Logout(accessToken) 42 | if responseErr != nil { 43 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 44 | return 45 | } 46 | 47 | ctx.Status(http.StatusNoContent) 48 | } 49 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/dbscripts/public_schema.sql: -------------------------------------------------------------------------------- 1 | -- Initial public schema relates to Library 0.x 2 | 3 | SET statement_timeout = 0; 4 | SET lock_timeout = 0; 5 | SET idle_in_transaction_session_timeout = 0; 6 | SET client_encoding = 'UTF8'; 7 | SET standard_conforming_strings = on; 8 | SET client_min_messages = warning; 9 | SET row_security = off; 10 | 11 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 12 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA pg_catalog; 13 | 14 | SET search_path = public, pg_catalog; 15 | SET default_tablespace = ''; 16 | 17 | -- runners 18 | CREATE TABLE runners ( 19 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 20 | first_name text NOT NULL, 21 | last_name text NOT NULL, 22 | age integer, 23 | is_active boolean DEFAULT TRUE, 24 | country text NOT NULL, 25 | personal_best interval, 26 | season_best interval, 27 | CONSTRAINT runners_pk PRIMARY KEY (id) 28 | ); 29 | 30 | CREATE INDEX runners_country 31 | ON runners (country); 32 | 33 | CREATE INDEX runners_season_best 34 | ON runners (season_best); 35 | 36 | -- results 37 | CREATE TABLE results ( 38 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 39 | runner_id uuid NOT NULL, 40 | race_result interval NOT NULL, 41 | location text NOT NULL, 42 | position integer, 43 | year integer NOT NULL, 44 | CONSTRAINT results_pk PRIMARY KEY (id), 45 | CONSTRAINT fk_results_runner_id FOREIGN KEY (runner_id) 46 | REFERENCES runners (id) MATCH SIMPLE 47 | ON UPDATE NO ACTION 48 | ON DELETE NO ACTION 49 | ); 50 | 51 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/dbscripts/update_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | 3 | CREATE TABLE users ( 4 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 5 | username text NOT NULL UNIQUE, 6 | user_password text NOT NULL, 7 | user_role text NOT NULL, 8 | access_token text, 9 | CONSTRAINT users_pk PRIMARY KEY (id) 10 | ); 11 | 12 | CREATE INDEX user_access_token 13 | ON users (access_token); 14 | 15 | INSERT INTO users(username, user_password, user_role) 16 | VALUES 17 | ('admin', crypt('admin', gen_salt('bf')), 'admin'), 18 | ('runner', crypt('runner', gen_salt('bf')), 'runner'); 19 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | runners-app: 4 | image: runners-app:latest 5 | network_mode: "host" -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/kubernetes/runners-app-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: runners-app 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: runners-app 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app: runners-app 14 | spec: 15 | containers: 16 | - image: runners-app:latest 17 | name: runners-app 18 | imagePullPolicy: Never 19 | ports: 20 | - containerPort: 8080 21 | env: 22 | - name: ENV 23 | value: "k8s" -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/kubernetes/runners-app-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: runners-app-service 5 | spec: 6 | selector: 7 | app: runners-app 8 | type: LoadBalancer 9 | ports: 10 | - name: http 11 | protocol: TCP 12 | port: 8080 13 | targetPort: 8080 14 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "runners-postgresql/config" 7 | "runners-postgresql/server" 8 | 9 | _ "github.com/lib/pq" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting Runners App") 14 | 15 | log.Println("Initializing configuration") 16 | config := config.InitConfig(getConfigFileName()) 17 | 18 | log.Println("Initializing database") 19 | dbHandler := server.InitDatabase(config) 20 | 21 | log.Println("Initializing Prometheus") 22 | go server.InitPrometheus() 23 | 24 | log.Println("Initializig HTTP sever") 25 | httpServer := server.InitHttpServer(config, dbHandler) 26 | 27 | httpServer.Start() 28 | } 29 | 30 | func getConfigFileName() string { 31 | env := os.Getenv("ENV") 32 | 33 | if env != "" { 34 | return "runners-" + env 35 | } 36 | 37 | return "runners" 38 | } 39 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | HttpRequestsCounter = promauto.NewCounter( 10 | prometheus.CounterOpts{ 11 | Name: "runners_app_http_requests", 12 | Help: "Total number of HTTP requests", 13 | }, 14 | ) 15 | 16 | GetRunnerHttpResponsesCounter = promauto.NewCounterVec( 17 | prometheus.CounterOpts{ 18 | Name: "runners_app_get_runner_http_responses", 19 | Help: "Total number of HTTP responses for get runner API", 20 | }, 21 | []string{"status"}, 22 | ) 23 | 24 | GetAllRunnersTimer = promauto.NewHistogram( 25 | prometheus.HistogramOpts{ 26 | Name: "runners_app_get_all_runners_duration", 27 | Help: "Duration of get all runners operation", 28 | }, 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/models/responseError.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ResponseError struct { 4 | Message string `json:"message"` 5 | Status int `json:"-"` 6 | } 7 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/models/result.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Result struct { 4 | ID string `json:"id"` 5 | RunnerID string `json:"runner_id"` 6 | RaceResult string `json:"race_result"` 7 | Location string `json:"location"` 8 | Position int `json:"position,omitempty"` 9 | Year int `json:"year"` 10 | } 11 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/models/runner.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Runner struct { 4 | ID string `json:"id"` 5 | FirstName string `json:"first_name"` 6 | LastName string `json:"last_name"` 7 | Age int `json:"age,omitempty"` 8 | IsActive bool `json:"is_active"` 9 | Country string `json:"country"` 10 | PersonalBest string `json:"personal_best,omitempty"` 11 | SeasonBest string `json:"season_best,omitempty"` 12 | Results []*Result `json:"results,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | ID string `json:"id"` 5 | Username string `json:"username"` 6 | Password string `json:"user_password"` 7 | Role string `json:"user_role"` 8 | AccessToken string `json:"access_toke"` 9 | } 10 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/repositories/transactionHandler.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | func BeginTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 9 | ctx := context.Background() 10 | transaction, err := resultsRepository.dbHandler.BeginTx(ctx, &sql.TxOptions{}) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | runnersRepository.transaction = transaction 16 | resultsRepository.transaction = transaction 17 | 18 | return nil 19 | } 20 | 21 | func RollbackTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 22 | transaction := runnersRepository.transaction 23 | 24 | runnersRepository.transaction = nil 25 | resultsRepository.transaction = nil 26 | 27 | return transaction.Rollback() 28 | } 29 | 30 | func CommitTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 31 | transaction := runnersRepository.transaction 32 | 33 | runnersRepository.transaction = nil 34 | resultsRepository.transaction = nil 35 | 36 | return transaction.Commit() 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/runners-k8s.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=192.168.0.25 port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/runners.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=localhost port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### 21 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/server/dbServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func InitDatabase(config *viper.Viper) *sql.DB { 11 | connectionString := config.GetString("database.connection_string") 12 | maxIdleConnections := config.GetInt("database.max_idle_connections") 13 | maxOpenConnections := config.GetInt("database.max_open_connections") 14 | connectionMaxLifetime := config.GetDuration("database.connection_max_lifetime") 15 | driverName := config.GetString("database.driver_name") 16 | 17 | if connectionString == "" { 18 | log.Fatalf("Database connectin string is missing") 19 | } 20 | 21 | dbHandler, err := sql.Open(driverName, connectionString) 22 | if err != nil { 23 | log.Fatalf("Error while initializing database: %v", err) 24 | } 25 | 26 | dbHandler.SetMaxIdleConns(maxIdleConnections) 27 | dbHandler.SetMaxOpenConns(maxOpenConnections) 28 | dbHandler.SetConnMaxLifetime(connectionMaxLifetime) 29 | 30 | err = dbHandler.Ping() 31 | if err != nil { 32 | dbHandler.Close() 33 | log.Fatalf("Error while validating database: %v", err) 34 | } 35 | 36 | return dbHandler 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/server/httpServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "runners-postgresql/controllers" 7 | "runners-postgresql/repositories" 8 | "runners-postgresql/services" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | type HttpServer struct { 15 | config *viper.Viper 16 | router *gin.Engine 17 | runnersController *controllers.RunnersController 18 | resultsController *controllers.ResultsController 19 | usersController *controllers.UsersController 20 | } 21 | 22 | func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { 23 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 24 | resultRepository := repositories.NewResultsRepository(dbHandler) 25 | usersRepository := repositories.NewUsersRepository(dbHandler) 26 | runnersService := services.NewRunnersService(runnersRepository, resultRepository) 27 | resultsService := services.NewResultsService(resultRepository, runnersRepository) 28 | usersService := services.NewUsersService(usersRepository) 29 | runnersController := controllers.NewRunnersController(runnersService, usersService) 30 | resultsController := controllers.NewResultsController(resultsService, usersService) 31 | usersController := controllers.NewUsersController(usersService) 32 | 33 | router := gin.Default() 34 | 35 | router.POST("/runner", runnersController.CreateRunner) 36 | router.PUT("/runner", runnersController.UpdateRunner) 37 | router.DELETE("/runner/:id", runnersController.DeleteRunner) 38 | router.GET("/runner/:id", runnersController.GetRunner) 39 | router.GET("/runner", runnersController.GetRunnersBatch) 40 | 41 | router.POST("/result", resultsController.CreateResult) 42 | router.DELETE("/result/:id", resultsController.DeleteResult) 43 | 44 | router.POST("/login", usersController.Login) 45 | router.POST("/logout", usersController.Logout) 46 | 47 | return HttpServer{ 48 | config: config, 49 | router: router, 50 | runnersController: runnersController, 51 | resultsController: resultsController, 52 | usersController: usersController, 53 | } 54 | } 55 | 56 | func (hs HttpServer) Start() { 57 | err := hs.router.Run(hs.config.GetString("http.server_address")) 58 | if err != nil { 59 | log.Fatalf("Error while starting HTTP server: %v", err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/server/prometheus.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | ) 8 | 9 | func InitPrometheus() { 10 | http.Handle("/metrics", promhttp.Handler()) 11 | http.ListenAndServe(":9000", nil) 12 | } 13 | -------------------------------------------------------------------------------- /Chapter 07 (a)/runners-postgresql-main/services/runnerService_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "net/http" 5 | "runners-postgresql/models" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | /*func TestValidateRunnerInvalidFirstName(t *testing.T) { 12 | runner := &models.Runner{ 13 | LastName: "Smith", 14 | Age: 30, 15 | Country: "United States", 16 | } 17 | 18 | responseErr := validateRunner(runner) 19 | assert.NotEmpty(t, responseErr) 20 | assert.Equal(t, "Invalid first name", responseErr.Message) 21 | assert.Equal(t, http.StatusBadRequest, responseErr.Status) 22 | }*/ 23 | 24 | func TestValidateRunner(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | runner *models.Runner 28 | want *models.ResponseError 29 | }{ 30 | { 31 | name: "Invalid_First_Name", 32 | runner: &models.Runner{ 33 | LastName: "Smith", 34 | Age: 30, 35 | Country: "United States", 36 | }, 37 | want: &models.ResponseError{ 38 | Message: "Invalid first name", 39 | Status: http.StatusBadRequest, 40 | }, 41 | }, 42 | { 43 | name: "Invalid_Last_Name", 44 | runner: &models.Runner{ 45 | FirstName: "John", 46 | Age: 30, 47 | Country: "United States", 48 | }, 49 | want: &models.ResponseError{ 50 | Message: "Invalid last name", 51 | Status: http.StatusBadRequest, 52 | }, 53 | }, 54 | { 55 | name: "Invalid_Age", 56 | runner: &models.Runner{ 57 | FirstName: "John", 58 | LastName: "Smith", 59 | Age: 300, 60 | Country: "United States", 61 | }, 62 | want: &models.ResponseError{ 63 | Message: "Invalid age", 64 | Status: http.StatusBadRequest, 65 | }, 66 | }, 67 | { 68 | name: "Invalid_Country", 69 | runner: &models.Runner{ 70 | FirstName: "John", 71 | LastName: "Smith", 72 | Age: 30, 73 | }, 74 | want: &models.ResponseError{ 75 | Message: "Invalid country", 76 | Status: http.StatusBadRequest, 77 | }, 78 | }, 79 | { 80 | name: "Valid_Runner", 81 | runner: &models.Runner{ 82 | FirstName: "John", 83 | LastName: "Smith", 84 | Age: 30, 85 | Country: "United States", 86 | }, 87 | want: nil, 88 | }, 89 | } 90 | 91 | for _, test := range tests { 92 | t.Run(test.name, func(t *testing.T) { 93 | responseErr := validateRunner(test.runner) 94 | assert.Equal(t, test.want, responseErr) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func InitConfig(fileName string) *viper.Viper { 10 | config := viper.New() 11 | 12 | config.SetConfigName(fileName) 13 | 14 | config.AddConfigPath(".") 15 | config.AddConfigPath("$HOME") 16 | 17 | err := config.ReadInConfig() 18 | if err != nil { 19 | log.Fatal("Error while parsing configuration file", err) 20 | } 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/controllers/resultsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "runners-mysql/models" 9 | "runners-mysql/services" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type ResultsController struct { 15 | resultsService *services.ResultsService 16 | } 17 | 18 | func NewResultsController(resultsService *services.ResultsService) *ResultsController { 19 | return &ResultsController{ 20 | resultsService: resultsService, 21 | } 22 | } 23 | 24 | func (rh ResultsController) CreateResult(ctx *gin.Context) { 25 | body, err := io.ReadAll(ctx.Request.Body) 26 | if err != nil { 27 | log.Println("Error while reading create result request body", err) 28 | ctx.AbortWithError(http.StatusInternalServerError, err) 29 | return 30 | } 31 | 32 | var result models.Result 33 | err = json.Unmarshal(body, &result) 34 | if err != nil { 35 | log.Println("Error while unmarshaling create result request body", err) 36 | ctx.AbortWithError(http.StatusInternalServerError, err) 37 | return 38 | } 39 | 40 | response, responseErr := rh.resultsService.CreateResult(&result) 41 | if responseErr != nil { 42 | ctx.JSON(responseErr.Status, responseErr) 43 | return 44 | } 45 | 46 | ctx.JSON(http.StatusOK, response) 47 | } 48 | 49 | func (rh ResultsController) DeleteResult(ctx *gin.Context) { 50 | resultId := ctx.Param("id") 51 | 52 | responseErr := rh.resultsService.DeleteResult(resultId) 53 | if responseErr != nil { 54 | ctx.JSON(responseErr.Status, responseErr) 55 | return 56 | } 57 | 58 | ctx.Status(http.StatusNoContent) 59 | } 60 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/dbscripts/public_schema.sql: -------------------------------------------------------------------------------- 1 | -- runners 2 | CREATE TABLE runners ( 3 | id int NOT NULL AUTO_INCREMENT, 4 | first_name varchar(100) NOT NULL, 5 | last_name varchar(100) NOT NULL, 6 | age integer, 7 | is_active boolean DEFAULT TRUE, 8 | country varchar(60) NOT NULL, 9 | personal_best time, 10 | season_best time, 11 | CONSTRAINT runners_pk PRIMARY KEY (id) 12 | ) 13 | ENGINE = InnoDB; 14 | 15 | CREATE INDEX runners_country 16 | ON runners (country); 17 | 18 | CREATE INDEX runners_season_best 19 | ON runners (season_best); 20 | 21 | -- results 22 | CREATE TABLE results ( 23 | id int NOT NULL AUTO_INCREMENT, 24 | runner_id int NOT NULL, 25 | race_result time NOT NULL, 26 | location varchar(100) NOT NULL, 27 | position integer, 28 | result_year integer NOT NULL, 29 | CONSTRAINT results_pk PRIMARY KEY (id), 30 | CONSTRAINT fk_results_runner_id FOREIGN KEY (runner_id) 31 | REFERENCES runners (id) 32 | ON UPDATE NO ACTION 33 | ON DELETE NO ACTION 34 | ) 35 | ENGINE = InnoDB; 36 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/go.mod: -------------------------------------------------------------------------------- 1 | module runners-mysql 2 | 3 | go 1.20 4 | 5 | require github.com/spf13/viper v1.13.0 6 | 7 | require ( 8 | github.com/gin-contrib/sse v0.1.0 // indirect 9 | github.com/go-playground/locales v0.14.0 // indirect 10 | github.com/go-playground/universal-translator v0.18.0 // indirect 11 | github.com/go-playground/validator/v10 v10.10.0 // indirect 12 | github.com/goccy/go-json v0.9.7 // indirect 13 | github.com/json-iterator/go v1.1.12 // indirect 14 | github.com/leodido/go-urn v1.2.1 // indirect 15 | github.com/mattn/go-isatty v0.0.14 // indirect 16 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 17 | github.com/modern-go/reflect2 v1.0.2 // indirect 18 | github.com/ugorji/go/codec v1.2.7 // indirect 19 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect 20 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect 21 | google.golang.org/protobuf v1.28.0 // indirect 22 | ) 23 | 24 | require ( 25 | github.com/fsnotify/fsnotify v1.5.4 // indirect 26 | github.com/gin-gonic/gin v1.8.1 27 | github.com/go-sql-driver/mysql v1.6.0 // indirect 28 | github.com/hashicorp/hcl v1.0.0 // indirect 29 | github.com/magiconair/properties v1.8.6 // indirect 30 | github.com/mitchellh/mapstructure v1.5.0 // indirect 31 | github.com/pelletier/go-toml v1.9.5 // indirect 32 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 33 | github.com/spf13/afero v1.8.2 // indirect 34 | github.com/spf13/cast v1.5.0 // indirect 35 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 36 | github.com/spf13/pflag v1.0.5 // indirect 37 | github.com/subosito/gotenv v1.4.1 // indirect 38 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 39 | golang.org/x/text v0.3.7 // indirect 40 | gopkg.in/ini.v1 v1.67.0 // indirect 41 | gopkg.in/yaml.v2 v2.4.0 // indirect 42 | gopkg.in/yaml.v3 v3.0.1 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "runners-mysql/config" 6 | "runners-mysql/server" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | func main() { 12 | log.Println("Starting Runers App") 13 | 14 | log.Println("Initializig configuration") 15 | config := config.InitConfig("runners") 16 | 17 | log.Println("Initializig database") 18 | dbHandler := server.InitDatabase(config) 19 | 20 | log.Println("Initializig HTTP sever") 21 | httpServer := server.InitHttpServer(config, dbHandler) 22 | 23 | httpServer.Start() 24 | } 25 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/models/responseError.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ResponseError struct { 4 | Message string `json:"message"` 5 | Status int `json:"-"` 6 | } 7 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/models/result.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Result struct { 4 | ID string `json:"id"` 5 | RunnerID string `json:"runner_id"` 6 | RaceResult string `json:"race_result"` 7 | Location string `json:"location"` 8 | Position int `json:"position,omitempty"` 9 | Year int `json:"year"` 10 | } 11 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/models/runner.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Runner struct { 4 | ID string `json:"id"` 5 | FirstName string `json:"first_name"` 6 | LastName string `json:"last_name"` 7 | Age int `json:"age,omitempty"` 8 | IsActive bool `json:"is_active"` 9 | Country string `json:"country"` 10 | PersonalBest string `json:"personal_best,omitempty"` 11 | SeasonBest string `json:"season_best,omitempty"` 12 | Results []*Result `json:"results,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/repositories/transactionHandler.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | func BeginTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 9 | ctx := context.Background() 10 | transaction, err := resultsRepository.dbHandler.BeginTx(ctx, &sql.TxOptions{}) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | runnersRepository.transaction = transaction 16 | resultsRepository.transaction = transaction 17 | 18 | return nil 19 | } 20 | 21 | func StopTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 22 | transaction := runnersRepository.transaction 23 | 24 | runnersRepository.transaction = nil 25 | resultsRepository.transaction = nil 26 | 27 | return transaction.Rollback() 28 | } 29 | 30 | func EndTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 31 | transaction := runnersRepository.transaction 32 | 33 | runnersRepository.transaction = nil 34 | resultsRepository.transaction = nil 35 | 36 | return transaction.Commit() 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/runners.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go mysql driver format: 5 | # databaseUser:databaseUserPassword@tcp(host:port)/databaseName 6 | 7 | [database] 8 | 9 | connection_string = "root:root123@tcp(localhost:3306)/runners_db" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "mysql" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### 21 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/server/dbServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func InitDatabase(config *viper.Viper) *sql.DB { 11 | connectionString := config.GetString("database.connection_string") 12 | maxIdleConnections := config.GetInt("database.max_idle_connections") 13 | maxOpenConnections := config.GetInt("database.max_open_connections") 14 | connectionMaxLifetime := config.GetDuration("database.connection_max_lifetime") 15 | driverName := config.GetString("database.driver_name") 16 | 17 | if connectionString == "" { 18 | log.Fatalf("Database connectin string is missing") 19 | } 20 | 21 | dbHandler, err := sql.Open(driverName, connectionString) 22 | if err != nil { 23 | log.Fatalf("Error while initializing database: %v", err) 24 | } 25 | 26 | dbHandler.SetMaxIdleConns(maxIdleConnections) 27 | dbHandler.SetMaxOpenConns(maxOpenConnections) 28 | dbHandler.SetConnMaxLifetime(connectionMaxLifetime) 29 | 30 | err = dbHandler.Ping() 31 | if err != nil { 32 | dbHandler.Close() 33 | log.Fatalf("Error while validating database: %v", err) 34 | } 35 | 36 | return dbHandler 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 07 (b)/runners-mysql-main/server/httpServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "runners-mysql/controllers" 7 | "runners-mysql/repositories" 8 | "runners-mysql/services" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | type HttpServer struct { 15 | config *viper.Viper 16 | router *gin.Engine 17 | runnersController *controllers.RunnersController 18 | resultsController *controllers.ResultsController 19 | } 20 | 21 | func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { 22 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 23 | resultRepository := repositories.NewResultsRepository(dbHandler) 24 | runnersService := services.NewRunnersService(runnersRepository, resultRepository) 25 | resultsService := services.NewResultsService(resultRepository, runnersRepository) 26 | runnersController := controllers.NewRunnersController(runnersService) 27 | resultsController := controllers.NewResultsController(resultsService) 28 | 29 | router := gin.Default() 30 | 31 | router.POST("/runner", runnersController.CreateRunner) 32 | router.PUT("/runner", runnersController.UpdateRunner) 33 | router.DELETE("/runner/:id", runnersController.DeleteRunner) 34 | router.GET("/runner/:id", runnersController.GetRunner) 35 | router.GET("/runner", runnersController.GetRunnersBatch) 36 | 37 | router.POST("/result", resultsController.CreateResult) 38 | router.DELETE("/result/:id", resultsController.DeleteResult) 39 | 40 | return HttpServer{ 41 | config: config, 42 | router: router, 43 | runnersController: runnersController, 44 | resultsController: resultsController, 45 | } 46 | } 47 | 48 | func (hs HttpServer) Start() { 49 | err := hs.router.Run(hs.config.GetString("http.server_address")) 50 | if err != nil { 51 | log.Fatalf("Error while starting HTTP server: %v", err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Chapter 08 (a)/runners-mongodb-main/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func InitConfig(fileName string) *viper.Viper { 10 | config := viper.New() 11 | 12 | config.SetConfigName(fileName) 13 | 14 | config.AddConfigPath(".") 15 | config.AddConfigPath("$HOME") 16 | 17 | err := config.ReadInConfig() 18 | if err != nil { 19 | log.Fatal("Error while parsing configuration file", err) 20 | } 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /Chapter 08 (a)/runners-mongodb-main/controllers/resultsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "runners-mongodb/models" 9 | "runners-mongodb/services" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type ResultsController struct { 15 | resultsService *services.ResultsService 16 | } 17 | 18 | func NewResultsController(resultsService *services.ResultsService) *ResultsController { 19 | return &ResultsController{ 20 | resultsService: resultsService, 21 | } 22 | } 23 | 24 | func (rh ResultsController) CreateResult(ctx *gin.Context) { 25 | body, err := io.ReadAll(ctx.Request.Body) 26 | if err != nil { 27 | log.Println("Error while reading create result request body", err) 28 | ctx.AbortWithError(http.StatusInternalServerError, err) 29 | return 30 | } 31 | 32 | var result models.Result 33 | err = json.Unmarshal(body, &result) 34 | if err != nil { 35 | log.Println("Error while unmarshaling create result request body", err) 36 | ctx.AbortWithError(http.StatusInternalServerError, err) 37 | return 38 | } 39 | 40 | response, responseErr := rh.resultsService.CreateResult(&result) 41 | if responseErr != nil { 42 | ctx.JSON(responseErr.Status, responseErr) 43 | return 44 | } 45 | 46 | ctx.JSON(http.StatusOK, response) 47 | } 48 | 49 | func (rh ResultsController) DeleteResult(ctx *gin.Context) { 50 | resultId := ctx.Param("id") 51 | 52 | responseErr := rh.resultsService.DeleteResult(resultId) 53 | if responseErr != nil { 54 | ctx.JSON(responseErr.Status, responseErr) 55 | return 56 | } 57 | 58 | ctx.Status(http.StatusNoContent) 59 | } 60 | -------------------------------------------------------------------------------- /Chapter 08 (a)/runners-mongodb-main/go.mod: -------------------------------------------------------------------------------- 1 | module runners-mongodb 2 | 3 | go 1.20 4 | 5 | require github.com/spf13/viper v1.13.0 6 | 7 | require ( 8 | github.com/gin-contrib/sse v0.1.0 // indirect 9 | github.com/go-playground/locales v0.14.0 // indirect 10 | github.com/go-playground/universal-translator v0.18.0 // indirect 11 | github.com/go-playground/validator/v10 v10.10.0 // indirect 12 | github.com/goccy/go-json v0.9.7 // indirect 13 | github.com/golang/snappy v0.0.1 // indirect 14 | github.com/json-iterator/go v1.1.12 // indirect 15 | github.com/klauspost/compress v1.13.6 // indirect 16 | github.com/leodido/go-urn v1.2.1 // indirect 17 | github.com/mattn/go-isatty v0.0.14 // indirect 18 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 19 | github.com/modern-go/reflect2 v1.0.2 // indirect 20 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect 21 | github.com/pkg/errors v0.9.1 // indirect 22 | github.com/ugorji/go/codec v1.2.7 // indirect 23 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 24 | github.com/xdg-go/scram v1.1.1 // indirect 25 | github.com/xdg-go/stringprep v1.0.3 // indirect 26 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect 27 | go.mongodb.org/mongo-driver v1.10.3 // indirect 28 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect 29 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect 30 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 31 | google.golang.org/protobuf v1.28.0 // indirect 32 | ) 33 | 34 | require ( 35 | github.com/fsnotify/fsnotify v1.5.4 // indirect 36 | github.com/gin-gonic/gin v1.8.1 37 | github.com/hashicorp/hcl v1.0.0 // indirect 38 | github.com/magiconair/properties v1.8.6 // indirect 39 | github.com/mitchellh/mapstructure v1.5.0 // indirect 40 | github.com/pelletier/go-toml v1.9.5 // indirect 41 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 42 | github.com/spf13/afero v1.8.2 // indirect 43 | github.com/spf13/cast v1.5.0 // indirect 44 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 45 | github.com/spf13/pflag v1.0.5 // indirect 46 | github.com/subosito/gotenv v1.4.1 // indirect 47 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 48 | golang.org/x/text v0.3.7 // indirect 49 | gopkg.in/ini.v1 v1.67.0 // indirect 50 | gopkg.in/yaml.v2 v2.4.0 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /Chapter 08 (a)/runners-mongodb-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "runners-mongodb/config" 6 | "runners-mongodb/server" 7 | ) 8 | 9 | func main() { 10 | log.Println("Starting Runers App") 11 | 12 | log.Println("Initializig configuration") 13 | config := config.InitConfig("runners") 14 | 15 | log.Println("Initializig database") 16 | client := server.InitDatabase(config) 17 | 18 | log.Println("Initializig HTTP sever") 19 | httpServer := server.InitHttpServer(config, client) 20 | 21 | httpServer.Start() 22 | } 23 | -------------------------------------------------------------------------------- /Chapter 08 (a)/runners-mongodb-main/models/responseError.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ResponseError struct { 4 | Message string `json:"message"` 5 | Status int `json:"-"` 6 | } 7 | -------------------------------------------------------------------------------- /Chapter 08 (a)/runners-mongodb-main/models/result.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "go.mongodb.org/mongo-driver/bson/primitive" 4 | 5 | type Result struct { 6 | ID string `json:"id" bson:"id,omitempty"` 7 | ObjectID primitive.ObjectID `json:"-" bson:"_id"` 8 | RunnerID string `json:"runner_id"` 9 | RaceResult string `json:"race_result"` 10 | Location string `json:"location"` 11 | Position int `json:"position,omitempty"` 12 | Year int `json:"year"` 13 | } 14 | -------------------------------------------------------------------------------- /Chapter 08 (a)/runners-mongodb-main/models/runner.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "go.mongodb.org/mongo-driver/bson/primitive" 4 | 5 | type Runner struct { 6 | ID string `json:"id" bson:"-"` 7 | ObjectID primitive.ObjectID `json:"-" bson:"_id"` 8 | FirstName string `json:"first_name"` 9 | LastName string `json:"last_name"` 10 | Age int `json:"age,omitempty" bson:"age,omitempty"` 11 | IsActive bool `json:"is_active"` 12 | Country string `json:"country"` 13 | PersonalBest string `json:"personal_best,omitempty" bson:"personalbest,omitempty"` 14 | SeasonBest string `json:"season_best,omitempty" bson:"seasonbest,omitempty"` 15 | Results []*Result `json:"results,omitempty" bson:"results,omitempty"` 16 | } 17 | -------------------------------------------------------------------------------- /Chapter 08 (a)/runners-mongodb-main/runners.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go mongodb driver format: 5 | # mongodb://host:port 6 | 7 | [database] 8 | 9 | connection_string = "mongodb://localhost:27017" 10 | ############################################################################### 11 | # HTTP server configuration 12 | 13 | [http] 14 | 15 | server_address = ":8080" 16 | ############################################################################### 17 | -------------------------------------------------------------------------------- /Chapter 08 (a)/runners-mongodb-main/server/dbServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/spf13/viper" 8 | "go.mongodb.org/mongo-driver/mongo" 9 | "go.mongodb.org/mongo-driver/mongo/options" 10 | ) 11 | 12 | func InitDatabase(config *viper.Viper) *mongo.Client { 13 | connectionString := config.GetString("database.connection_string") 14 | 15 | if connectionString == "" { 16 | log.Fatalf("Database connectin string is missing") 17 | } 18 | 19 | clientOptions := options.Client().ApplyURI(connectionString) 20 | 21 | client, err := mongo.Connect(context.TODO(), clientOptions) 22 | if err != nil { 23 | log.Fatalf("Error while initializing database: %v", err) 24 | } 25 | 26 | err = client.Ping(context.TODO(), nil) 27 | if err != nil { 28 | client.Disconnect(context.TODO()) 29 | log.Fatalf("Error while validating database: %v", err) 30 | } 31 | 32 | return client 33 | } 34 | -------------------------------------------------------------------------------- /Chapter 08 (a)/runners-mongodb-main/server/httpServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "log" 5 | "runners-mongodb/controllers" 6 | "runners-mongodb/repositories" 7 | "runners-mongodb/services" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/spf13/viper" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | ) 13 | 14 | type HttpServer struct { 15 | config *viper.Viper 16 | router *gin.Engine 17 | runnersController *controllers.RunnersController 18 | resultsController *controllers.ResultsController 19 | } 20 | 21 | func InitHttpServer(config *viper.Viper, client *mongo.Client) HttpServer { 22 | runnersRepository := repositories.NewRunnersRepository(client) 23 | resultRepository := repositories.NewResultsRepository(client) 24 | runnersService := services.NewRunnersService(runnersRepository, resultRepository) 25 | resultsService := services.NewResultsService(resultRepository, runnersRepository) 26 | runnersController := controllers.NewRunnersController(runnersService) 27 | resultsController := controllers.NewResultsController(resultsService) 28 | 29 | router := gin.Default() 30 | 31 | router.POST("/runner", runnersController.CreateRunner) 32 | router.PUT("/runner", runnersController.UpdateRunner) 33 | router.DELETE("/runner/:id", runnersController.DeleteRunner) 34 | router.GET("/runner/:id", runnersController.GetRunner) 35 | router.GET("/runner", runnersController.GetRunnersBatch) 36 | 37 | router.POST("/result", resultsController.CreateResult) 38 | router.DELETE("/result/:id", resultsController.DeleteResult) 39 | 40 | return HttpServer{ 41 | config: config, 42 | router: router, 43 | runnersController: runnersController, 44 | resultsController: resultsController, 45 | } 46 | } 47 | 48 | func (hs HttpServer) Start() { 49 | err := hs.router.Run(hs.config.GetString("http.server_address")) 50 | if err != nil { 51 | log.Fatalf("Error while starting HTTP server: %v", err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func InitConfig(fileName string) *viper.Viper { 10 | config := viper.New() 11 | 12 | config.SetConfigName(fileName) 13 | 14 | config.AddConfigPath(".") 15 | config.AddConfigPath("$HOME") 16 | 17 | err := config.ReadInConfig() 18 | if err != nil { 19 | log.Fatal("Error while parsing configuration file", err) 20 | } 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/controllers/resultsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "runners-dynamodb/models" 9 | "runners-dynamodb/services" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type ResultsController struct { 15 | resultsService *services.ResultsService 16 | } 17 | 18 | func NewResultsController(resultsService *services.ResultsService) *ResultsController { 19 | return &ResultsController{ 20 | resultsService: resultsService, 21 | } 22 | } 23 | 24 | func (rh ResultsController) CreateResult(ctx *gin.Context) { 25 | body, err := io.ReadAll(ctx.Request.Body) 26 | if err != nil { 27 | log.Println("Error while reading create result request body", err) 28 | ctx.AbortWithError(http.StatusInternalServerError, err) 29 | return 30 | } 31 | 32 | var result models.Result 33 | err = json.Unmarshal(body, &result) 34 | if err != nil { 35 | log.Println("Error while unmarshaling create result request body", err) 36 | ctx.AbortWithError(http.StatusInternalServerError, err) 37 | return 38 | } 39 | 40 | response, responseErr := rh.resultsService.CreateResult(&result) 41 | if responseErr != nil { 42 | ctx.JSON(responseErr.Status, responseErr) 43 | return 44 | } 45 | 46 | ctx.JSON(http.StatusOK, response) 47 | } 48 | 49 | func (rh ResultsController) DeleteResult(ctx *gin.Context) { 50 | resultId := ctx.Param("id") 51 | 52 | responseErr := rh.resultsService.DeleteResult(resultId) 53 | if responseErr != nil { 54 | ctx.JSON(responseErr.Status, responseErr) 55 | return 56 | } 57 | 58 | ctx.Status(http.StatusNoContent) 59 | } 60 | -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/dbscripts/create-gsi-runners-attributes-schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "AttributeName": "country", "AttributeType": "S" }, 3 | { "AttributeName": "personal_best", "AttributeType": "S" } 4 | ] -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/dbscripts/create-gsi-runners.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "Create": { 3 | "IndexName": "runners_global_index", 4 | "KeySchema": [ 5 | { "AttributeName": "country", "KeyType": "HASH" }, 6 | { "AttributeName": "personal_best", "KeyType": "RANGE" } 7 | ], 8 | "Projection": { 9 | "ProjectionType": "ALL" 10 | }, 11 | "ProvisionedThroughput": { 12 | "ReadCapacityUnits": 5, 13 | "WriteCapacityUnits": 5 14 | } 15 | } 16 | }] -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/dbscripts/create-results-table.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "Results", 3 | "KeySchema": [ 4 | { "AttributeName": "runner_id", "KeyType": "HASH" }, 5 | { "AttributeName": "race_result", "KeyType": "RANGE" } 6 | ], 7 | "AttributeDefinitions": [ 8 | { "AttributeName": "runner_id", "AttributeType": "S" }, 9 | { "AttributeName": "race_result", "AttributeType": "S" } 10 | ], 11 | "ProvisionedThroughput": { 12 | "ReadCapacityUnits": 5, 13 | "WriteCapacityUnits": 5 14 | } 15 | } -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/dbscripts/create-runners-table.json: -------------------------------------------------------------------------------- 1 | { 2 | "TableName": "Runners", 3 | "KeySchema": [ 4 | { "AttributeName": "id", "KeyType": "HASH" } 5 | ], 6 | "AttributeDefinitions": [ 7 | { "AttributeName": "id", "AttributeType": "S" } 8 | ], 9 | "ProvisionedThroughput": { 10 | "ReadCapacityUnits": 5, 11 | "WriteCapacityUnits": 5 12 | } 13 | } -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/go.mod: -------------------------------------------------------------------------------- 1 | module runners-dynamodb 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.44.132 // indirect 7 | github.com/fsnotify/fsnotify v1.6.0 // indirect 8 | github.com/gin-contrib/sse v0.1.0 // indirect 9 | github.com/gin-gonic/gin v1.8.1 // indirect 10 | github.com/go-playground/locales v0.14.0 // indirect 11 | github.com/go-playground/universal-translator v0.18.0 // indirect 12 | github.com/go-playground/validator/v10 v10.10.0 // indirect 13 | github.com/goccy/go-json v0.9.7 // indirect 14 | github.com/google/uuid v1.3.0 // indirect 15 | github.com/hashicorp/hcl v1.0.0 // indirect 16 | github.com/jmespath/go-jmespath v0.4.0 // indirect 17 | github.com/json-iterator/go v1.1.12 // indirect 18 | github.com/leodido/go-urn v1.2.1 // indirect 19 | github.com/magiconair/properties v1.8.6 // indirect 20 | github.com/mattn/go-isatty v0.0.14 // indirect 21 | github.com/mitchellh/mapstructure v1.5.0 // indirect 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 23 | github.com/modern-go/reflect2 v1.0.2 // indirect 24 | github.com/pelletier/go-toml v1.9.5 // indirect 25 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 26 | github.com/spf13/afero v1.9.2 // indirect 27 | github.com/spf13/cast v1.5.0 // indirect 28 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 29 | github.com/spf13/pflag v1.0.5 // indirect 30 | github.com/spf13/viper v1.14.0 // indirect 31 | github.com/subosito/gotenv v1.4.1 // indirect 32 | github.com/ugorji/go/codec v1.2.7 // indirect 33 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect 34 | golang.org/x/net v0.1.0 // indirect 35 | golang.org/x/sys v0.1.0 // indirect 36 | golang.org/x/text v0.4.0 // indirect 37 | google.golang.org/protobuf v1.28.1 // indirect 38 | gopkg.in/ini.v1 v1.67.0 // indirect 39 | gopkg.in/yaml.v2 v2.4.0 // indirect 40 | gopkg.in/yaml.v3 v3.0.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "runners-dynamodb/config" 6 | "runners-dynamodb/server" 7 | ) 8 | 9 | func main() { 10 | log.Println("Starting Runers App") 11 | 12 | log.Println("Initializig configuration") 13 | config := config.InitConfig("runners") 14 | 15 | log.Println("Initializig database") 16 | dynamoDB := server.InitDatabase(config) 17 | 18 | log.Println("Initializig HTTP sever") 19 | httpServer := server.InitHttpServer(config, dynamoDB) 20 | 21 | httpServer.Start() 22 | } 23 | -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/models/responseError.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ResponseError struct { 4 | Message string `json:"message"` 5 | Status int `json:"-"` 6 | } 7 | -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/models/result.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Result struct { 4 | ID string `json:"id"` 5 | RunnerID string `json:"runner_id"` 6 | RaceResult string `json:"race_result"` 7 | Location string `json:"location"` 8 | Position int `json:"position,omitempty"` 9 | Year int `json:"race_year"` 10 | } 11 | -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/models/runner.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Runner struct { 4 | ID string `json:"id"` 5 | FirstName string `json:"first_name"` 6 | LastName string `json:"last_name"` 7 | Age int `json:"age,omitempty"` 8 | IsActive bool `json:"is_active"` 9 | Country string `json:"country"` 10 | PersonalBest string `json:"personal_best,omitempty"` 11 | SeasonBest string `json:"season_best,omitempty"` 12 | Results []*Result `json:"results,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/runners.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go dynamodb driver format: 5 | # http://host:port 6 | 7 | [database] 8 | 9 | connection_string = "http://localhost:8000" 10 | aws_region = "region" 11 | aws_access_key_id = "dusan" 12 | aws_secret_access_key = "dusan" 13 | ############################################################################### 14 | # HTTP server configuration 15 | 16 | [http] 17 | 18 | server_address = ":8080" 19 | ############################################################################### -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/server/dbServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/credentials" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/dynamodb" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | func InitDatabase(config *viper.Viper) *dynamodb.DynamoDB { 14 | connectionString := config.GetString("database.connection_string") 15 | awsRegion := config.GetString("database.aws_region") 16 | awsAccessKeyID := config.GetString("database.aws_access_key_id") 17 | awsSecretAccessKey := config.GetString("database.aws_secret_access_key") 18 | 19 | if connectionString == "" { 20 | log.Fatalf("Database connectin string is missing") 21 | } 22 | 23 | session := session.Must(session.NewSession( 24 | &aws.Config{ 25 | Region: aws.String(awsRegion), 26 | Credentials: credentials.NewStaticCredentials(awsAccessKeyID, awsSecretAccessKey, ""), 27 | Endpoint: aws.String(connectionString), 28 | }, 29 | )) 30 | 31 | return dynamodb.New(session) 32 | } 33 | -------------------------------------------------------------------------------- /Chapter 08 (b)/runners-dynamodb-main/server/httpServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "log" 5 | "runners-dynamodb/controllers" 6 | "runners-dynamodb/repositories" 7 | "runners-dynamodb/services" 8 | 9 | "github.com/aws/aws-sdk-go/service/dynamodb" 10 | "github.com/gin-gonic/gin" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | type HttpServer struct { 15 | config *viper.Viper 16 | router *gin.Engine 17 | runnersController *controllers.RunnersController 18 | resultsController *controllers.ResultsController 19 | } 20 | 21 | func InitHttpServer(config *viper.Viper, dynamoDB *dynamodb.DynamoDB) HttpServer { 22 | runnersRepository := repositories.NewRunnersRepository(dynamoDB) 23 | resultRepository := repositories.NewResultsRepository(dynamoDB) 24 | runnersService := services.NewRunnersService(runnersRepository, resultRepository) 25 | resultsService := services.NewResultsService(resultRepository, runnersRepository) 26 | runnersController := controllers.NewRunnersController(runnersService) 27 | resultsController := controllers.NewResultsController(resultsService) 28 | 29 | router := gin.Default() 30 | 31 | router.POST("/runner", runnersController.CreateRunner) 32 | router.PUT("/runner", runnersController.UpdateRunner) 33 | router.DELETE("/runner/:id", runnersController.DeleteRunner) 34 | router.GET("/runner/:id", runnersController.GetRunner) 35 | router.GET("/runner", runnersController.GetRunnersBatch) 36 | 37 | router.POST("/result", resultsController.CreateResult) 38 | router.DELETE("/result/:id", resultsController.DeleteResult) 39 | 40 | return HttpServer{ 41 | config: config, 42 | router: router, 43 | runnersController: runnersController, 44 | resultsController: resultsController, 45 | } 46 | } 47 | 48 | func (hs HttpServer) Start() { 49 | err := hs.router.Run(hs.config.GetString("http.server_address")) 50 | if err != nil { 51 | log.Fatalf("Error while starting HTTP server: %v", err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from golang alpine base image 2 | FROM golang:1.19-alpine 3 | 4 | # Set the Current Working Directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the source code to the Working Directory inside the container 8 | COPY . . 9 | 10 | # Download all dependencies. 11 | # Dependencies will be cached if the go.mod and go.sum files are not changed 12 | RUN go mod download 13 | 14 | # Build the Go app 15 | RUN go build -o runners-app main.go 16 | 17 | # Export necessary ports 18 | EXPOSE 8080 19 | 20 | # Command to run when starting the container 21 | CMD ["/app/runners-app"] 22 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func InitConfig(fileName string) *viper.Viper { 10 | config := viper.New() 11 | 12 | config.SetConfigName(fileName) 13 | 14 | config.AddConfigPath(".") 15 | config.AddConfigPath("$HOME") 16 | 17 | err := config.ReadInConfig() 18 | if err != nil { 19 | log.Fatal("Error while parsing configuration file", err) 20 | } 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/controllers/resultsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "runners-postgresql/models" 9 | "runners-postgresql/services" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type ResultsController struct { 15 | resultsService *services.ResultsService 16 | usersService *services.UsersService 17 | } 18 | 19 | func NewResultsController(resultsService *services.ResultsService, 20 | userService *services.UsersService) *ResultsController { 21 | 22 | return &ResultsController{ 23 | resultsService: resultsService, 24 | usersService: userService, 25 | } 26 | } 27 | 28 | func (rc ResultsController) CreateResult(ctx *gin.Context) { 29 | accessToken := ctx.Request.Header.Get("Token") 30 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 31 | if responseErr != nil { 32 | ctx.JSON(responseErr.Status, responseErr) 33 | return 34 | } 35 | 36 | if !auth { 37 | ctx.Status(http.StatusUnauthorized) 38 | return 39 | } 40 | 41 | body, err := io.ReadAll(ctx.Request.Body) 42 | if err != nil { 43 | log.Println("Error while reading create result request body", err) 44 | ctx.AbortWithError(http.StatusInternalServerError, err) 45 | return 46 | } 47 | 48 | var result models.Result 49 | err = json.Unmarshal(body, &result) 50 | if err != nil { 51 | log.Println("Error while unmarshaling create result request body", err) 52 | ctx.AbortWithError(http.StatusInternalServerError, err) 53 | return 54 | } 55 | 56 | response, responseErr := rc.resultsService.CreateResult(&result) 57 | if responseErr != nil { 58 | ctx.JSON(responseErr.Status, responseErr) 59 | return 60 | } 61 | 62 | ctx.JSON(http.StatusOK, response) 63 | } 64 | 65 | func (rc ResultsController) DeleteResult(ctx *gin.Context) { 66 | accessToken := ctx.Request.Header.Get("Token") 67 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 68 | if responseErr != nil { 69 | ctx.JSON(responseErr.Status, responseErr) 70 | return 71 | } 72 | 73 | if !auth { 74 | ctx.Status(http.StatusUnauthorized) 75 | return 76 | } 77 | 78 | resultId := ctx.Param("id") 79 | 80 | responseErr = rc.resultsService.DeleteResult(resultId) 81 | if responseErr != nil { 82 | ctx.JSON(responseErr.Status, responseErr) 83 | return 84 | } 85 | 86 | ctx.Status(http.StatusNoContent) 87 | } 88 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/controllers/runnersController_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "runners-postgresql/models" 9 | "runners-postgresql/repositories" 10 | "runners-postgresql/services" 11 | "testing" 12 | 13 | "github.com/DATA-DOG/go-sqlmock" 14 | "github.com/gin-gonic/gin" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestGetRunnersResponse(t *testing.T) { 19 | dbHandler, mock, _ := sqlmock.New() 20 | defer dbHandler.Close() 21 | 22 | columnsUsers := []string{"user_role"} 23 | mock.ExpectQuery("SELECT user_role").WillReturnRows( 24 | sqlmock.NewRows(columnsUsers).AddRow("runner"), 25 | ) 26 | 27 | columns := []string{"id", "first_name", "last_name", "age", "is_active", "country", "personal_best", "season_best"} 28 | mock.ExpectQuery("SELECT *").WillReturnRows( 29 | sqlmock.NewRows(columns). 30 | AddRow("1", "John", "Smith", 30, true, "United States", "02:00:41", "02:13:13"). 31 | AddRow("2", "Marijana", "Komatinovic", 30, true, "Serbia", "01:18:28", "01:18:28")) 32 | 33 | router := initTestRouter(dbHandler) 34 | request, _ := http.NewRequest("GET", "/runner", nil) 35 | request.Header.Set("token", "token") 36 | recorder := httptest.NewRecorder() 37 | 38 | router.ServeHTTP(recorder, request) 39 | 40 | assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) 41 | 42 | var runers []*models.Runner 43 | json.Unmarshal(recorder.Body.Bytes(), &runers) 44 | 45 | assert.NotEmpty(t, runers) 46 | assert.Equal(t, 2, len(runers)) 47 | } 48 | 49 | func initTestRouter(dbHandler *sql.DB) *gin.Engine { 50 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 51 | usersRepository := repositories.NewUsersRepository(dbHandler) 52 | runnersService := services.NewRunnersService(runnersRepository, nil) 53 | usersServices := services.NewUsersService(usersRepository) 54 | runnersController := NewRunnersController(runnersService, usersServices) 55 | 56 | router := gin.Default() 57 | 58 | router.GET("/runner", runnersController.GetRunnersBatch) 59 | 60 | return router 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/controllers/usersController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "runners-postgresql/services" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type UsersController struct { 12 | usersService *services.UsersService 13 | } 14 | 15 | func NewUsersController(usersService *services.UsersService) *UsersController { 16 | return &UsersController{ 17 | usersService: usersService, 18 | } 19 | } 20 | 21 | func (uc UsersController) Login(ctx *gin.Context) { 22 | username, password, ok := ctx.Request.BasicAuth() 23 | if !ok { 24 | log.Println("Error while reading credentials") 25 | ctx.AbortWithStatus(http.StatusBadRequest) 26 | return 27 | } 28 | 29 | accessToken, responseErr := uc.usersService.Login(username, password) 30 | if responseErr != nil { 31 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 32 | return 33 | } 34 | 35 | ctx.JSON(http.StatusOK, accessToken) 36 | } 37 | 38 | func (uc UsersController) Logout(ctx *gin.Context) { 39 | accessToken := ctx.Request.Header.Get("Token") 40 | 41 | responseErr := uc.usersService.Logout(accessToken) 42 | if responseErr != nil { 43 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 44 | return 45 | } 46 | 47 | ctx.Status(http.StatusNoContent) 48 | } 49 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/dbscripts/public_schema.sql: -------------------------------------------------------------------------------- 1 | -- Initial public schema relates to Library 0.x 2 | 3 | SET statement_timeout = 0; 4 | SET lock_timeout = 0; 5 | SET idle_in_transaction_session_timeout = 0; 6 | SET client_encoding = 'UTF8'; 7 | SET standard_conforming_strings = on; 8 | SET client_min_messages = warning; 9 | SET row_security = off; 10 | 11 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 12 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA pg_catalog; 13 | 14 | SET search_path = public, pg_catalog; 15 | SET default_tablespace = ''; 16 | 17 | -- runners 18 | CREATE TABLE runners ( 19 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 20 | first_name text NOT NULL, 21 | last_name text NOT NULL, 22 | age integer, 23 | is_active boolean DEFAULT TRUE, 24 | country text NOT NULL, 25 | personal_best interval, 26 | season_best interval, 27 | CONSTRAINT runners_pk PRIMARY KEY (id) 28 | ); 29 | 30 | CREATE INDEX runners_country 31 | ON runners (country); 32 | 33 | CREATE INDEX runners_season_best 34 | ON runners (season_best); 35 | 36 | -- results 37 | CREATE TABLE results ( 38 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 39 | runner_id uuid NOT NULL, 40 | race_result interval NOT NULL, 41 | location text NOT NULL, 42 | position integer, 43 | year integer NOT NULL, 44 | CONSTRAINT results_pk PRIMARY KEY (id), 45 | CONSTRAINT fk_results_runner_id FOREIGN KEY (runner_id) 46 | REFERENCES runners (id) MATCH SIMPLE 47 | ON UPDATE NO ACTION 48 | ON DELETE NO ACTION 49 | ); 50 | 51 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/dbscripts/update_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | 3 | CREATE TABLE users ( 4 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 5 | username text NOT NULL UNIQUE, 6 | user_password text NOT NULL, 7 | user_role text NOT NULL, 8 | access_token text, 9 | CONSTRAINT users_pk PRIMARY KEY (id) 10 | ); 11 | 12 | CREATE INDEX user_access_token 13 | ON users (access_token); 14 | 15 | INSERT INTO users(username, user_password, user_role) 16 | VALUES 17 | ('admin', crypt('admin', gen_salt('bf')), 'admin'), 18 | ('runner', crypt('runner', gen_salt('bf')), 'runner'); 19 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | runners-app: 4 | image: runners-app:latest 5 | network_mode: "host" -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/go.mod: -------------------------------------------------------------------------------- 1 | module runners-postgresql 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect 7 | github.com/beorn7/perks v1.0.1 // indirect 8 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 9 | github.com/davecgh/go-spew v1.1.1 // indirect 10 | github.com/fsnotify/fsnotify v1.5.4 // indirect 11 | github.com/gin-contrib/sse v0.1.0 // indirect 12 | github.com/gin-gonic/gin v1.8.1 // indirect 13 | github.com/go-playground/locales v0.14.0 // indirect 14 | github.com/go-playground/universal-translator v0.18.0 // indirect 15 | github.com/go-playground/validator/v10 v10.11.1 // indirect 16 | github.com/goccy/go-json v0.9.11 // indirect 17 | github.com/golang/protobuf v1.5.2 // indirect 18 | github.com/hashicorp/hcl v1.0.0 // indirect 19 | github.com/json-iterator/go v1.1.12 // indirect 20 | github.com/leodido/go-urn v1.2.1 // indirect 21 | github.com/lib/pq v1.10.7 // indirect 22 | github.com/magiconair/properties v1.8.6 // indirect 23 | github.com/mattn/go-isatty v0.0.16 // indirect 24 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 25 | github.com/mitchellh/mapstructure v1.5.0 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/pelletier/go-toml v1.9.5 // indirect 29 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/prometheus/client_golang v1.14.0 // indirect 32 | github.com/prometheus/client_model v0.3.0 // indirect 33 | github.com/prometheus/common v0.37.0 // indirect 34 | github.com/prometheus/procfs v0.8.0 // indirect 35 | github.com/spf13/afero v1.8.2 // indirect 36 | github.com/spf13/cast v1.5.0 // indirect 37 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 38 | github.com/spf13/pflag v1.0.5 // indirect 39 | github.com/spf13/viper v1.13.0 // indirect 40 | github.com/stretchr/objx v0.5.0 // indirect 41 | github.com/stretchr/testify v1.8.1 // indirect 42 | github.com/subosito/gotenv v1.4.1 // indirect 43 | github.com/ugorji/go/codec v1.2.7 // indirect 44 | golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect 45 | golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect 46 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect 47 | golang.org/x/text v0.3.7 // indirect 48 | google.golang.org/protobuf v1.28.1 // indirect 49 | gopkg.in/ini.v1 v1.67.0 // indirect 50 | gopkg.in/yaml.v2 v2.4.0 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/kubernetes/runners-app-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: runners-app 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: runners-app 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app: runners-app 14 | spec: 15 | containers: 16 | - image: runners-app:latest 17 | name: runners-app 18 | imagePullPolicy: Never 19 | ports: 20 | - containerPort: 8080 21 | env: 22 | - name: ENV 23 | value: "k8s" -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/kubernetes/runners-app-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: runners-app-service 5 | spec: 6 | selector: 7 | app: runners-app 8 | type: LoadBalancer 9 | ports: 10 | - name: http 11 | protocol: TCP 12 | port: 8080 13 | targetPort: 8080 14 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "runners-postgresql/config" 7 | "runners-postgresql/server" 8 | 9 | _ "github.com/lib/pq" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting Runners App") 14 | 15 | log.Println("Initializing configuration") 16 | config := config.InitConfig(getConfigFileName()) 17 | 18 | log.Println("Initializing database") 19 | dbHandler := server.InitDatabase(config) 20 | 21 | log.Println("Initializing Prometheus") 22 | go server.InitPrometheus() 23 | 24 | log.Println("Initializig HTTP sever") 25 | httpServer := server.InitHttpServer(config, dbHandler) 26 | 27 | httpServer.Start() 28 | } 29 | 30 | func getConfigFileName() string { 31 | env := os.Getenv("ENV") 32 | 33 | if env != "" { 34 | return "runners-" + env 35 | } 36 | 37 | return "runners" 38 | } 39 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | HttpRequestsCounter = promauto.NewCounter( 10 | prometheus.CounterOpts{ 11 | Name: "runners_app_http_requests", 12 | Help: "Total number of HTTP requests", 13 | }, 14 | ) 15 | 16 | GetRunnerHttpResponsesCounter = promauto.NewCounterVec( 17 | prometheus.CounterOpts{ 18 | Name: "runners_app_get_runner_http_responses", 19 | Help: "Total number of HTTP responses for get runner API", 20 | }, 21 | []string{"status"}, 22 | ) 23 | 24 | GetAllRunnersTimer = promauto.NewHistogram( 25 | prometheus.HistogramOpts{ 26 | Name: "runners_app_get_all_runners_duration", 27 | Help: "Duration of get all runners operation", 28 | }, 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/models/responseError.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ResponseError struct { 4 | Message string `json:"message"` 5 | Status int `json:"-"` 6 | } 7 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/models/result.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Result struct { 4 | ID string `json:"id"` 5 | RunnerID string `json:"runner_id"` 6 | RaceResult string `json:"race_result"` 7 | Location string `json:"location"` 8 | Position int `json:"position,omitempty"` 9 | Year int `json:"year"` 10 | } 11 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/models/runner.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Runner struct { 4 | ID string `json:"id"` 5 | FirstName string `json:"first_name"` 6 | LastName string `json:"last_name"` 7 | Age int `json:"age,omitempty"` 8 | IsActive bool `json:"is_active"` 9 | Country string `json:"country"` 10 | PersonalBest string `json:"personal_best,omitempty"` 11 | SeasonBest string `json:"season_best,omitempty"` 12 | Results []*Result `json:"results,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | ID string `json:"id"` 5 | Username string `json:"username"` 6 | Password string `json:"user_password"` 7 | Role string `json:"user_role"` 8 | AccessToken string `json:"access_toke"` 9 | } 10 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/repositories/transactionHandler.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | func BeginTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 9 | ctx := context.Background() 10 | transaction, err := resultsRepository.dbHandler.BeginTx(ctx, &sql.TxOptions{}) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | runnersRepository.transaction = transaction 16 | resultsRepository.transaction = transaction 17 | 18 | return nil 19 | } 20 | 21 | func RollbackTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 22 | transaction := runnersRepository.transaction 23 | 24 | runnersRepository.transaction = nil 25 | resultsRepository.transaction = nil 26 | 27 | return transaction.Rollback() 28 | } 29 | 30 | func CommitTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 31 | transaction := runnersRepository.transaction 32 | 33 | runnersRepository.transaction = nil 34 | resultsRepository.transaction = nil 35 | 36 | return transaction.Commit() 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/runners-k8s.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=192.168.0.25 port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/runners.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=localhost port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### 21 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/server/dbServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func InitDatabase(config *viper.Viper) *sql.DB { 11 | connectionString := config.GetString("database.connection_string") 12 | maxIdleConnections := config.GetInt("database.max_idle_connections") 13 | maxOpenConnections := config.GetInt("database.max_open_connections") 14 | connectionMaxLifetime := config.GetDuration("database.connection_max_lifetime") 15 | driverName := config.GetString("database.driver_name") 16 | 17 | if connectionString == "" { 18 | log.Fatalf("Database connectin string is missing") 19 | } 20 | 21 | dbHandler, err := sql.Open(driverName, connectionString) 22 | if err != nil { 23 | log.Fatalf("Error while initializing database: %v", err) 24 | } 25 | 26 | dbHandler.SetMaxIdleConns(maxIdleConnections) 27 | dbHandler.SetMaxOpenConns(maxOpenConnections) 28 | dbHandler.SetConnMaxLifetime(connectionMaxLifetime) 29 | 30 | err = dbHandler.Ping() 31 | if err != nil { 32 | dbHandler.Close() 33 | log.Fatalf("Error while validating database: %v", err) 34 | } 35 | 36 | return dbHandler 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/server/httpServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "runners-postgresql/controllers" 7 | "runners-postgresql/repositories" 8 | "runners-postgresql/services" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | type HttpServer struct { 15 | config *viper.Viper 16 | router *gin.Engine 17 | runnersController *controllers.RunnersController 18 | resultsController *controllers.ResultsController 19 | usersController *controllers.UsersController 20 | } 21 | 22 | func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { 23 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 24 | resultRepository := repositories.NewResultsRepository(dbHandler) 25 | usersRepository := repositories.NewUsersRepository(dbHandler) 26 | runnersService := services.NewRunnersService(runnersRepository, resultRepository) 27 | resultsService := services.NewResultsService(resultRepository, runnersRepository) 28 | usersService := services.NewUsersService(usersRepository) 29 | runnersController := controllers.NewRunnersController(runnersService, usersService) 30 | resultsController := controllers.NewResultsController(resultsService, usersService) 31 | usersController := controllers.NewUsersController(usersService) 32 | 33 | router := gin.Default() 34 | 35 | router.POST("/runner", runnersController.CreateRunner) 36 | router.PUT("/runner", runnersController.UpdateRunner) 37 | router.DELETE("/runner/:id", runnersController.DeleteRunner) 38 | router.GET("/runner/:id", runnersController.GetRunner) 39 | router.GET("/runner", runnersController.GetRunnersBatch) 40 | 41 | router.POST("/result", resultsController.CreateResult) 42 | router.DELETE("/result/:id", resultsController.DeleteResult) 43 | 44 | router.POST("/login", usersController.Login) 45 | router.POST("/logout", usersController.Logout) 46 | 47 | return HttpServer{ 48 | config: config, 49 | router: router, 50 | runnersController: runnersController, 51 | resultsController: resultsController, 52 | usersController: usersController, 53 | } 54 | } 55 | 56 | func (hs HttpServer) Start() { 57 | err := hs.router.Run(hs.config.GetString("http.server_address")) 58 | if err != nil { 59 | log.Fatalf("Error while starting HTTP server: %v", err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/server/prometheus.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | ) 8 | 9 | func InitPrometheus() { 10 | http.Handle("/metrics", promhttp.Handler()) 11 | http.ListenAndServe(":9000", nil) 12 | } 13 | -------------------------------------------------------------------------------- /Chapter 09/runners-postgresql-main/services/runnerService_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "net/http" 5 | "runners-postgresql/models" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | /*func TestValidateRunnerInvalidFirstName(t *testing.T) { 12 | runner := &models.Runner{ 13 | LastName: "Smith", 14 | Age: 30, 15 | Country: "United States", 16 | } 17 | 18 | responseErr := validateRunner(runner) 19 | assert.NotEmpty(t, responseErr) 20 | assert.Equal(t, "Invalid first name", responseErr.Message) 21 | assert.Equal(t, http.StatusBadRequest, responseErr.Status) 22 | }*/ 23 | 24 | func TestValidateRunner(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | runner *models.Runner 28 | want *models.ResponseError 29 | }{ 30 | { 31 | name: "Invalid_First_Name", 32 | runner: &models.Runner{ 33 | LastName: "Smith", 34 | Age: 30, 35 | Country: "United States", 36 | }, 37 | want: &models.ResponseError{ 38 | Message: "Invalid first name", 39 | Status: http.StatusBadRequest, 40 | }, 41 | }, 42 | { 43 | name: "Invalid_Last_Name", 44 | runner: &models.Runner{ 45 | FirstName: "John", 46 | Age: 30, 47 | Country: "United States", 48 | }, 49 | want: &models.ResponseError{ 50 | Message: "Invalid last name", 51 | Status: http.StatusBadRequest, 52 | }, 53 | }, 54 | { 55 | name: "Invalid_Age", 56 | runner: &models.Runner{ 57 | FirstName: "John", 58 | LastName: "Smith", 59 | Age: 300, 60 | Country: "United States", 61 | }, 62 | want: &models.ResponseError{ 63 | Message: "Invalid age", 64 | Status: http.StatusBadRequest, 65 | }, 66 | }, 67 | { 68 | name: "Invalid_Country", 69 | runner: &models.Runner{ 70 | FirstName: "John", 71 | LastName: "Smith", 72 | Age: 30, 73 | }, 74 | want: &models.ResponseError{ 75 | Message: "Invalid country", 76 | Status: http.StatusBadRequest, 77 | }, 78 | }, 79 | { 80 | name: "Valid_Runner", 81 | runner: &models.Runner{ 82 | FirstName: "John", 83 | LastName: "Smith", 84 | Age: 30, 85 | Country: "United States", 86 | }, 87 | want: nil, 88 | }, 89 | } 90 | 91 | for _, test := range tests { 92 | t.Run(test.name, func(t *testing.T) { 93 | responseErr := validateRunner(test.runner) 94 | assert.Equal(t, test.want, responseErr) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from golang alpine base image 2 | FROM golang:1.19-alpine 3 | 4 | # Set the Current Working Directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the source code to the Working Directory inside the container 8 | COPY . . 9 | 10 | # Download all dependencies. 11 | # Dependencies will be cached if the go.mod and go.sum files are not changed 12 | RUN go mod download 13 | 14 | # Build the Go app 15 | RUN go build -o runners-app main.go 16 | 17 | # Export necessary ports 18 | EXPOSE 8080 19 | 20 | # Command to run when starting the container 21 | CMD ["/app/runners-app"] 22 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func InitConfig(fileName string) *viper.Viper { 10 | config := viper.New() 11 | 12 | config.SetConfigName(fileName) 13 | 14 | config.AddConfigPath(".") 15 | config.AddConfigPath("$HOME") 16 | 17 | err := config.ReadInConfig() 18 | if err != nil { 19 | log.Fatal("Error while parsing configuration file", err) 20 | } 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/controllers/resultsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "runners-postgresql/models" 9 | "runners-postgresql/services" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type ResultsController struct { 15 | resultsService *services.ResultsService 16 | usersService *services.UsersService 17 | } 18 | 19 | func NewResultsController(resultsService *services.ResultsService, 20 | userService *services.UsersService) *ResultsController { 21 | 22 | return &ResultsController{ 23 | resultsService: resultsService, 24 | usersService: userService, 25 | } 26 | } 27 | 28 | func (rc ResultsController) CreateResult(ctx *gin.Context) { 29 | accessToken := ctx.Request.Header.Get("Token") 30 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 31 | if responseErr != nil { 32 | ctx.JSON(responseErr.Status, responseErr) 33 | return 34 | } 35 | 36 | if !auth { 37 | ctx.Status(http.StatusUnauthorized) 38 | return 39 | } 40 | 41 | body, err := io.ReadAll(ctx.Request.Body) 42 | if err != nil { 43 | log.Println("Error while reading create result request body", err) 44 | ctx.AbortWithError(http.StatusInternalServerError, err) 45 | return 46 | } 47 | 48 | var result models.Result 49 | err = json.Unmarshal(body, &result) 50 | if err != nil { 51 | log.Println("Error while unmarshaling create result request body", err) 52 | ctx.AbortWithError(http.StatusInternalServerError, err) 53 | return 54 | } 55 | 56 | response, responseErr := rc.resultsService.CreateResult(&result) 57 | if responseErr != nil { 58 | ctx.JSON(responseErr.Status, responseErr) 59 | return 60 | } 61 | 62 | ctx.JSON(http.StatusOK, response) 63 | } 64 | 65 | func (rc ResultsController) DeleteResult(ctx *gin.Context) { 66 | accessToken := ctx.Request.Header.Get("Token") 67 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 68 | if responseErr != nil { 69 | ctx.JSON(responseErr.Status, responseErr) 70 | return 71 | } 72 | 73 | if !auth { 74 | ctx.Status(http.StatusUnauthorized) 75 | return 76 | } 77 | 78 | resultId := ctx.Param("id") 79 | 80 | responseErr = rc.resultsService.DeleteResult(resultId) 81 | if responseErr != nil { 82 | ctx.JSON(responseErr.Status, responseErr) 83 | return 84 | } 85 | 86 | ctx.Status(http.StatusNoContent) 87 | } 88 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/controllers/runnersController_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "runners-postgresql/models" 9 | "runners-postgresql/repositories" 10 | "runners-postgresql/services" 11 | "testing" 12 | 13 | "github.com/DATA-DOG/go-sqlmock" 14 | "github.com/gin-gonic/gin" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestGetRunnersResponse(t *testing.T) { 19 | dbHandler, mock, _ := sqlmock.New() 20 | defer dbHandler.Close() 21 | 22 | columnsUsers := []string{"user_role"} 23 | mock.ExpectQuery("SELECT user_role").WillReturnRows( 24 | sqlmock.NewRows(columnsUsers).AddRow("runner"), 25 | ) 26 | 27 | columns := []string{"id", "first_name", "last_name", "age", "is_active", "country", "personal_best", "season_best"} 28 | mock.ExpectQuery("SELECT *").WillReturnRows( 29 | sqlmock.NewRows(columns). 30 | AddRow("1", "John", "Smith", 30, true, "United States", "02:00:41", "02:13:13"). 31 | AddRow("2", "Marijana", "Komatinovic", 30, true, "Serbia", "01:18:28", "01:18:28")) 32 | 33 | router := initTestRouter(dbHandler) 34 | request, _ := http.NewRequest("GET", "/runner", nil) 35 | request.Header.Set("token", "token") 36 | recorder := httptest.NewRecorder() 37 | 38 | router.ServeHTTP(recorder, request) 39 | 40 | assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) 41 | 42 | var runers []*models.Runner 43 | json.Unmarshal(recorder.Body.Bytes(), &runers) 44 | 45 | assert.NotEmpty(t, runers) 46 | assert.Equal(t, 2, len(runers)) 47 | } 48 | 49 | func initTestRouter(dbHandler *sql.DB) *gin.Engine { 50 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 51 | usersRepository := repositories.NewUsersRepository(dbHandler) 52 | runnersService := services.NewRunnersService(runnersRepository, nil) 53 | usersServices := services.NewUsersService(usersRepository) 54 | runnersController := NewRunnersController(runnersService, usersServices) 55 | 56 | router := gin.Default() 57 | 58 | router.GET("/runner", runnersController.GetRunnersBatch) 59 | 60 | return router 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/controllers/usersController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "runners-postgresql/services" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type UsersController struct { 12 | usersService *services.UsersService 13 | } 14 | 15 | func NewUsersController(usersService *services.UsersService) *UsersController { 16 | return &UsersController{ 17 | usersService: usersService, 18 | } 19 | } 20 | 21 | func (uc UsersController) Login(ctx *gin.Context) { 22 | username, password, ok := ctx.Request.BasicAuth() 23 | if !ok { 24 | log.Println("Error while reading credentials") 25 | ctx.AbortWithStatus(http.StatusBadRequest) 26 | return 27 | } 28 | 29 | accessToken, responseErr := uc.usersService.Login(username, password) 30 | if responseErr != nil { 31 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 32 | return 33 | } 34 | 35 | ctx.JSON(http.StatusOK, accessToken) 36 | } 37 | 38 | func (uc UsersController) Logout(ctx *gin.Context) { 39 | accessToken := ctx.Request.Header.Get("Token") 40 | 41 | responseErr := uc.usersService.Logout(accessToken) 42 | if responseErr != nil { 43 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 44 | return 45 | } 46 | 47 | ctx.Status(http.StatusNoContent) 48 | } 49 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/dbscripts/public_schema.sql: -------------------------------------------------------------------------------- 1 | -- Initial public schema relates to Library 0.x 2 | 3 | SET statement_timeout = 0; 4 | SET lock_timeout = 0; 5 | SET idle_in_transaction_session_timeout = 0; 6 | SET client_encoding = 'UTF8'; 7 | SET standard_conforming_strings = on; 8 | SET client_min_messages = warning; 9 | SET row_security = off; 10 | 11 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 12 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA pg_catalog; 13 | 14 | SET search_path = public, pg_catalog; 15 | SET default_tablespace = ''; 16 | 17 | -- runners 18 | CREATE TABLE runners ( 19 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 20 | first_name text NOT NULL, 21 | last_name text NOT NULL, 22 | age integer, 23 | is_active boolean DEFAULT TRUE, 24 | country text NOT NULL, 25 | personal_best interval, 26 | season_best interval, 27 | CONSTRAINT runners_pk PRIMARY KEY (id) 28 | ); 29 | 30 | CREATE INDEX runners_country 31 | ON runners (country); 32 | 33 | CREATE INDEX runners_season_best 34 | ON runners (season_best); 35 | 36 | -- results 37 | CREATE TABLE results ( 38 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 39 | runner_id uuid NOT NULL, 40 | race_result interval NOT NULL, 41 | location text NOT NULL, 42 | position integer, 43 | year integer NOT NULL, 44 | CONSTRAINT results_pk PRIMARY KEY (id), 45 | CONSTRAINT fk_results_runner_id FOREIGN KEY (runner_id) 46 | REFERENCES runners (id) MATCH SIMPLE 47 | ON UPDATE NO ACTION 48 | ON DELETE NO ACTION 49 | ); 50 | 51 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/dbscripts/update_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | 3 | CREATE TABLE users ( 4 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 5 | username text NOT NULL UNIQUE, 6 | user_password text NOT NULL, 7 | user_role text NOT NULL, 8 | access_token text, 9 | CONSTRAINT users_pk PRIMARY KEY (id) 10 | ); 11 | 12 | CREATE INDEX user_access_token 13 | ON users (access_token); 14 | 15 | INSERT INTO users(username, user_password, user_role) 16 | VALUES 17 | ('admin', crypt('admin', gen_salt('bf')), 'admin'), 18 | ('runner', crypt('runner', gen_salt('bf')), 'runner'); 19 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | runners-app: 4 | image: runners-app:latest 5 | network_mode: "host" -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/go.mod: -------------------------------------------------------------------------------- 1 | module runners-postgresql 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect 7 | github.com/beorn7/perks v1.0.1 // indirect 8 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 9 | github.com/davecgh/go-spew v1.1.1 // indirect 10 | github.com/fsnotify/fsnotify v1.5.4 // indirect 11 | github.com/gin-contrib/sse v0.1.0 // indirect 12 | github.com/gin-gonic/gin v1.8.1 // indirect 13 | github.com/go-playground/locales v0.14.0 // indirect 14 | github.com/go-playground/universal-translator v0.18.0 // indirect 15 | github.com/go-playground/validator/v10 v10.11.1 // indirect 16 | github.com/goccy/go-json v0.9.11 // indirect 17 | github.com/golang/protobuf v1.5.2 // indirect 18 | github.com/hashicorp/hcl v1.0.0 // indirect 19 | github.com/json-iterator/go v1.1.12 // indirect 20 | github.com/leodido/go-urn v1.2.1 // indirect 21 | github.com/lib/pq v1.10.7 // indirect 22 | github.com/magiconair/properties v1.8.6 // indirect 23 | github.com/mattn/go-isatty v0.0.16 // indirect 24 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 25 | github.com/mitchellh/mapstructure v1.5.0 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/pelletier/go-toml v1.9.5 // indirect 29 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/prometheus/client_golang v1.14.0 // indirect 32 | github.com/prometheus/client_model v0.3.0 // indirect 33 | github.com/prometheus/common v0.37.0 // indirect 34 | github.com/prometheus/procfs v0.8.0 // indirect 35 | github.com/spf13/afero v1.8.2 // indirect 36 | github.com/spf13/cast v1.5.0 // indirect 37 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 38 | github.com/spf13/pflag v1.0.5 // indirect 39 | github.com/spf13/viper v1.13.0 // indirect 40 | github.com/stretchr/objx v0.5.0 // indirect 41 | github.com/stretchr/testify v1.8.1 // indirect 42 | github.com/subosito/gotenv v1.4.1 // indirect 43 | github.com/ugorji/go/codec v1.2.7 // indirect 44 | golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect 45 | golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect 46 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect 47 | golang.org/x/text v0.3.7 // indirect 48 | google.golang.org/protobuf v1.28.1 // indirect 49 | gopkg.in/ini.v1 v1.67.0 // indirect 50 | gopkg.in/yaml.v2 v2.4.0 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/kubernetes/runners-app-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: runners-app 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: runners-app 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app: runners-app 14 | spec: 15 | containers: 16 | - image: runners-app:latest 17 | name: runners-app 18 | imagePullPolicy: Never 19 | ports: 20 | - containerPort: 8080 21 | env: 22 | - name: ENV 23 | value: "k8s" -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/kubernetes/runners-app-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: runners-app-service 5 | spec: 6 | selector: 7 | app: runners-app 8 | type: LoadBalancer 9 | ports: 10 | - name: http 11 | protocol: TCP 12 | port: 8080 13 | targetPort: 8080 14 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "runners-postgresql/config" 7 | "runners-postgresql/server" 8 | 9 | _ "github.com/lib/pq" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting Runners App") 14 | 15 | log.Println("Initializing configuration") 16 | config := config.InitConfig(getConfigFileName()) 17 | 18 | log.Println("Initializing database") 19 | dbHandler := server.InitDatabase(config) 20 | 21 | log.Println("Initializing Prometheus") 22 | go server.InitPrometheus() 23 | 24 | log.Println("Initializig HTTP sever") 25 | httpServer := server.InitHttpServer(config, dbHandler) 26 | 27 | httpServer.Start() 28 | } 29 | 30 | func getConfigFileName() string { 31 | env := os.Getenv("ENV") 32 | 33 | if env != "" { 34 | return "runners-" + env 35 | } 36 | 37 | return "runners" 38 | } 39 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | HttpRequestsCounter = promauto.NewCounter( 10 | prometheus.CounterOpts{ 11 | Name: "runners_app_http_requests", 12 | Help: "Total number of HTTP requests", 13 | }, 14 | ) 15 | 16 | GetRunnerHttpResponsesCounter = promauto.NewCounterVec( 17 | prometheus.CounterOpts{ 18 | Name: "runners_app_get_runner_http_responses", 19 | Help: "Total number of HTTP responses for get runner API", 20 | }, 21 | []string{"status"}, 22 | ) 23 | 24 | GetAllRunnersTimer = promauto.NewHistogram( 25 | prometheus.HistogramOpts{ 26 | Name: "runners_app_get_all_runners_duration", 27 | Help: "Duration of get all runners operation", 28 | }, 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/models/responseError.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ResponseError struct { 4 | Message string `json:"message"` 5 | Status int `json:"-"` 6 | } 7 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/models/result.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Result struct { 4 | ID string `json:"id"` 5 | RunnerID string `json:"runner_id"` 6 | RaceResult string `json:"race_result"` 7 | Location string `json:"location"` 8 | Position int `json:"position,omitempty"` 9 | Year int `json:"year"` 10 | } 11 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/models/runner.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Runner struct { 4 | ID string `json:"id"` 5 | FirstName string `json:"first_name"` 6 | LastName string `json:"last_name"` 7 | Age int `json:"age,omitempty"` 8 | IsActive bool `json:"is_active"` 9 | Country string `json:"country"` 10 | PersonalBest string `json:"personal_best,omitempty"` 11 | SeasonBest string `json:"season_best,omitempty"` 12 | Results []*Result `json:"results,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | ID string `json:"id"` 5 | Username string `json:"username"` 6 | Password string `json:"user_password"` 7 | Role string `json:"user_role"` 8 | AccessToken string `json:"access_toke"` 9 | } 10 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/repositories/transactionHandler.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | func BeginTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 9 | ctx := context.Background() 10 | transaction, err := resultsRepository.dbHandler.BeginTx(ctx, &sql.TxOptions{}) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | runnersRepository.transaction = transaction 16 | resultsRepository.transaction = transaction 17 | 18 | return nil 19 | } 20 | 21 | func RollbackTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 22 | transaction := runnersRepository.transaction 23 | 24 | runnersRepository.transaction = nil 25 | resultsRepository.transaction = nil 26 | 27 | return transaction.Rollback() 28 | } 29 | 30 | func CommitTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 31 | transaction := runnersRepository.transaction 32 | 33 | runnersRepository.transaction = nil 34 | resultsRepository.transaction = nil 35 | 36 | return transaction.Commit() 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/runners-k8s.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=192.168.0.25 port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/runners.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=localhost port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### 21 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/server/dbServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func InitDatabase(config *viper.Viper) *sql.DB { 11 | connectionString := config.GetString("database.connection_string") 12 | maxIdleConnections := config.GetInt("database.max_idle_connections") 13 | maxOpenConnections := config.GetInt("database.max_open_connections") 14 | connectionMaxLifetime := config.GetDuration("database.connection_max_lifetime") 15 | driverName := config.GetString("database.driver_name") 16 | 17 | if connectionString == "" { 18 | log.Fatalf("Database connectin string is missing") 19 | } 20 | 21 | dbHandler, err := sql.Open(driverName, connectionString) 22 | if err != nil { 23 | log.Fatalf("Error while initializing database: %v", err) 24 | } 25 | 26 | dbHandler.SetMaxIdleConns(maxIdleConnections) 27 | dbHandler.SetMaxOpenConns(maxOpenConnections) 28 | dbHandler.SetConnMaxLifetime(connectionMaxLifetime) 29 | 30 | err = dbHandler.Ping() 31 | if err != nil { 32 | dbHandler.Close() 33 | log.Fatalf("Error while validating database: %v", err) 34 | } 35 | 36 | return dbHandler 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/server/httpServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "runners-postgresql/controllers" 7 | "runners-postgresql/repositories" 8 | "runners-postgresql/services" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | type HttpServer struct { 15 | config *viper.Viper 16 | router *gin.Engine 17 | runnersController *controllers.RunnersController 18 | resultsController *controllers.ResultsController 19 | usersController *controllers.UsersController 20 | } 21 | 22 | func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { 23 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 24 | resultRepository := repositories.NewResultsRepository(dbHandler) 25 | usersRepository := repositories.NewUsersRepository(dbHandler) 26 | runnersService := services.NewRunnersService(runnersRepository, resultRepository) 27 | resultsService := services.NewResultsService(resultRepository, runnersRepository) 28 | usersService := services.NewUsersService(usersRepository) 29 | runnersController := controllers.NewRunnersController(runnersService, usersService) 30 | resultsController := controllers.NewResultsController(resultsService, usersService) 31 | usersController := controllers.NewUsersController(usersService) 32 | 33 | router := gin.Default() 34 | 35 | router.POST("/runner", runnersController.CreateRunner) 36 | router.PUT("/runner", runnersController.UpdateRunner) 37 | router.DELETE("/runner/:id", runnersController.DeleteRunner) 38 | router.GET("/runner/:id", runnersController.GetRunner) 39 | router.GET("/runner", runnersController.GetRunnersBatch) 40 | 41 | router.POST("/result", resultsController.CreateResult) 42 | router.DELETE("/result/:id", resultsController.DeleteResult) 43 | 44 | router.POST("/login", usersController.Login) 45 | router.POST("/logout", usersController.Logout) 46 | 47 | return HttpServer{ 48 | config: config, 49 | router: router, 50 | runnersController: runnersController, 51 | resultsController: resultsController, 52 | usersController: usersController, 53 | } 54 | } 55 | 56 | func (hs HttpServer) Start() { 57 | err := hs.router.Run(hs.config.GetString("http.server_address")) 58 | if err != nil { 59 | log.Fatalf("Error while starting HTTP server: %v", err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/server/prometheus.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | ) 8 | 9 | func InitPrometheus() { 10 | http.Handle("/metrics", promhttp.Handler()) 11 | http.ListenAndServe(":9000", nil) 12 | } 13 | -------------------------------------------------------------------------------- /Chapter 10/runners-postgresql-main/services/runnerService_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "net/http" 5 | "runners-postgresql/models" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | /*func TestValidateRunnerInvalidFirstName(t *testing.T) { 12 | runner := &models.Runner{ 13 | LastName: "Smith", 14 | Age: 30, 15 | Country: "United States", 16 | } 17 | 18 | responseErr := validateRunner(runner) 19 | assert.NotEmpty(t, responseErr) 20 | assert.Equal(t, "Invalid first name", responseErr.Message) 21 | assert.Equal(t, http.StatusBadRequest, responseErr.Status) 22 | }*/ 23 | 24 | func TestValidateRunner(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | runner *models.Runner 28 | want *models.ResponseError 29 | }{ 30 | { 31 | name: "Invalid_First_Name", 32 | runner: &models.Runner{ 33 | LastName: "Smith", 34 | Age: 30, 35 | Country: "United States", 36 | }, 37 | want: &models.ResponseError{ 38 | Message: "Invalid first name", 39 | Status: http.StatusBadRequest, 40 | }, 41 | }, 42 | { 43 | name: "Invalid_Last_Name", 44 | runner: &models.Runner{ 45 | FirstName: "John", 46 | Age: 30, 47 | Country: "United States", 48 | }, 49 | want: &models.ResponseError{ 50 | Message: "Invalid last name", 51 | Status: http.StatusBadRequest, 52 | }, 53 | }, 54 | { 55 | name: "Invalid_Age", 56 | runner: &models.Runner{ 57 | FirstName: "John", 58 | LastName: "Smith", 59 | Age: 300, 60 | Country: "United States", 61 | }, 62 | want: &models.ResponseError{ 63 | Message: "Invalid age", 64 | Status: http.StatusBadRequest, 65 | }, 66 | }, 67 | { 68 | name: "Invalid_Country", 69 | runner: &models.Runner{ 70 | FirstName: "John", 71 | LastName: "Smith", 72 | Age: 30, 73 | }, 74 | want: &models.ResponseError{ 75 | Message: "Invalid country", 76 | Status: http.StatusBadRequest, 77 | }, 78 | }, 79 | { 80 | name: "Valid_Runner", 81 | runner: &models.Runner{ 82 | FirstName: "John", 83 | LastName: "Smith", 84 | Age: 30, 85 | Country: "United States", 86 | }, 87 | want: nil, 88 | }, 89 | } 90 | 91 | for _, test := range tests { 92 | t.Run(test.name, func(t *testing.T) { 93 | responseErr := validateRunner(test.runner) 94 | assert.Equal(t, test.want, responseErr) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from golang alpine base image 2 | FROM golang:1.19-alpine 3 | 4 | # Set the Current Working Directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the source code to the Working Directory inside the container 8 | COPY . . 9 | 10 | # Download all dependencies. 11 | # Dependencies will be cached if the go.mod and go.sum files are not changed 12 | RUN go mod download 13 | 14 | # Build the Go app 15 | RUN go build -o runners-app main.go 16 | 17 | # Export necessary ports 18 | EXPOSE 8080 19 | 20 | # Command to run when starting the container 21 | CMD ["/app/runners-app"] 22 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func InitConfig(fileName string) *viper.Viper { 10 | config := viper.New() 11 | 12 | config.SetConfigName(fileName) 13 | 14 | config.AddConfigPath(".") 15 | config.AddConfigPath("$HOME") 16 | 17 | err := config.ReadInConfig() 18 | if err != nil { 19 | log.Fatal("Error while parsing configuration file", err) 20 | } 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/controllers/resultsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "runners-postgresql/models" 9 | "runners-postgresql/services" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type ResultsController struct { 15 | resultsService *services.ResultsService 16 | usersService *services.UsersService 17 | } 18 | 19 | func NewResultsController(resultsService *services.ResultsService, 20 | userService *services.UsersService) *ResultsController { 21 | 22 | return &ResultsController{ 23 | resultsService: resultsService, 24 | usersService: userService, 25 | } 26 | } 27 | 28 | func (rc ResultsController) CreateResult(ctx *gin.Context) { 29 | accessToken := ctx.Request.Header.Get("Token") 30 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 31 | if responseErr != nil { 32 | ctx.JSON(responseErr.Status, responseErr) 33 | return 34 | } 35 | 36 | if !auth { 37 | ctx.Status(http.StatusUnauthorized) 38 | return 39 | } 40 | 41 | body, err := io.ReadAll(ctx.Request.Body) 42 | if err != nil { 43 | log.Println("Error while reading create result request body", err) 44 | ctx.AbortWithError(http.StatusInternalServerError, err) 45 | return 46 | } 47 | 48 | var result models.Result 49 | err = json.Unmarshal(body, &result) 50 | if err != nil { 51 | log.Println("Error while unmarshaling create result request body", err) 52 | ctx.AbortWithError(http.StatusInternalServerError, err) 53 | return 54 | } 55 | 56 | response, responseErr := rc.resultsService.CreateResult(&result) 57 | if responseErr != nil { 58 | ctx.JSON(responseErr.Status, responseErr) 59 | return 60 | } 61 | 62 | ctx.JSON(http.StatusOK, response) 63 | } 64 | 65 | func (rc ResultsController) DeleteResult(ctx *gin.Context) { 66 | accessToken := ctx.Request.Header.Get("Token") 67 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 68 | if responseErr != nil { 69 | ctx.JSON(responseErr.Status, responseErr) 70 | return 71 | } 72 | 73 | if !auth { 74 | ctx.Status(http.StatusUnauthorized) 75 | return 76 | } 77 | 78 | resultId := ctx.Param("id") 79 | 80 | responseErr = rc.resultsService.DeleteResult(resultId) 81 | if responseErr != nil { 82 | ctx.JSON(responseErr.Status, responseErr) 83 | return 84 | } 85 | 86 | ctx.Status(http.StatusNoContent) 87 | } 88 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/controllers/runnersController_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "runners-postgresql/models" 9 | "runners-postgresql/repositories" 10 | "runners-postgresql/services" 11 | "testing" 12 | 13 | "github.com/DATA-DOG/go-sqlmock" 14 | "github.com/gin-gonic/gin" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestGetRunnersResponse(t *testing.T) { 19 | dbHandler, mock, _ := sqlmock.New() 20 | defer dbHandler.Close() 21 | 22 | columnsUsers := []string{"user_role"} 23 | mock.ExpectQuery("SELECT user_role").WillReturnRows( 24 | sqlmock.NewRows(columnsUsers).AddRow("runner"), 25 | ) 26 | 27 | columns := []string{"id", "first_name", "last_name", "age", "is_active", "country", "personal_best", "season_best"} 28 | mock.ExpectQuery("SELECT *").WillReturnRows( 29 | sqlmock.NewRows(columns). 30 | AddRow("1", "John", "Smith", 30, true, "United States", "02:00:41", "02:13:13"). 31 | AddRow("2", "Marijana", "Komatinovic", 30, true, "Serbia", "01:18:28", "01:18:28")) 32 | 33 | router := initTestRouter(dbHandler) 34 | request, _ := http.NewRequest("GET", "/runner", nil) 35 | request.Header.Set("token", "token") 36 | recorder := httptest.NewRecorder() 37 | 38 | router.ServeHTTP(recorder, request) 39 | 40 | assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) 41 | 42 | var runers []*models.Runner 43 | json.Unmarshal(recorder.Body.Bytes(), &runers) 44 | 45 | assert.NotEmpty(t, runers) 46 | assert.Equal(t, 2, len(runers)) 47 | } 48 | 49 | func initTestRouter(dbHandler *sql.DB) *gin.Engine { 50 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 51 | usersRepository := repositories.NewUsersRepository(dbHandler) 52 | runnersService := services.NewRunnersService(runnersRepository, nil) 53 | usersServices := services.NewUsersService(usersRepository) 54 | runnersController := NewRunnersController(runnersService, usersServices) 55 | 56 | router := gin.Default() 57 | 58 | router.GET("/runner", runnersController.GetRunnersBatch) 59 | 60 | return router 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/controllers/usersController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "runners-postgresql/services" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type UsersController struct { 12 | usersService *services.UsersService 13 | } 14 | 15 | func NewUsersController(usersService *services.UsersService) *UsersController { 16 | return &UsersController{ 17 | usersService: usersService, 18 | } 19 | } 20 | 21 | func (uc UsersController) Login(ctx *gin.Context) { 22 | username, password, ok := ctx.Request.BasicAuth() 23 | if !ok { 24 | log.Println("Error while reading credentials") 25 | ctx.AbortWithStatus(http.StatusBadRequest) 26 | return 27 | } 28 | 29 | accessToken, responseErr := uc.usersService.Login(username, password) 30 | if responseErr != nil { 31 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 32 | return 33 | } 34 | 35 | ctx.JSON(http.StatusOK, accessToken) 36 | } 37 | 38 | func (uc UsersController) Logout(ctx *gin.Context) { 39 | accessToken := ctx.Request.Header.Get("Token") 40 | 41 | responseErr := uc.usersService.Logout(accessToken) 42 | if responseErr != nil { 43 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 44 | return 45 | } 46 | 47 | ctx.Status(http.StatusNoContent) 48 | } 49 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/dbscripts/public_schema.sql: -------------------------------------------------------------------------------- 1 | -- Initial public schema relates to Library 0.x 2 | 3 | SET statement_timeout = 0; 4 | SET lock_timeout = 0; 5 | SET idle_in_transaction_session_timeout = 0; 6 | SET client_encoding = 'UTF8'; 7 | SET standard_conforming_strings = on; 8 | SET client_min_messages = warning; 9 | SET row_security = off; 10 | 11 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 12 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA pg_catalog; 13 | 14 | SET search_path = public, pg_catalog; 15 | SET default_tablespace = ''; 16 | 17 | -- runners 18 | CREATE TABLE runners ( 19 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 20 | first_name text NOT NULL, 21 | last_name text NOT NULL, 22 | age integer, 23 | is_active boolean DEFAULT TRUE, 24 | country text NOT NULL, 25 | personal_best interval, 26 | season_best interval, 27 | CONSTRAINT runners_pk PRIMARY KEY (id) 28 | ); 29 | 30 | CREATE INDEX runners_country 31 | ON runners (country); 32 | 33 | CREATE INDEX runners_season_best 34 | ON runners (season_best); 35 | 36 | -- results 37 | CREATE TABLE results ( 38 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 39 | runner_id uuid NOT NULL, 40 | race_result interval NOT NULL, 41 | location text NOT NULL, 42 | position integer, 43 | year integer NOT NULL, 44 | CONSTRAINT results_pk PRIMARY KEY (id), 45 | CONSTRAINT fk_results_runner_id FOREIGN KEY (runner_id) 46 | REFERENCES runners (id) MATCH SIMPLE 47 | ON UPDATE NO ACTION 48 | ON DELETE NO ACTION 49 | ); 50 | 51 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/dbscripts/update_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | 3 | CREATE TABLE users ( 4 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 5 | username text NOT NULL UNIQUE, 6 | user_password text NOT NULL, 7 | user_role text NOT NULL, 8 | access_token text, 9 | CONSTRAINT users_pk PRIMARY KEY (id) 10 | ); 11 | 12 | CREATE INDEX user_access_token 13 | ON users (access_token); 14 | 15 | INSERT INTO users(username, user_password, user_role) 16 | VALUES 17 | ('admin', crypt('admin', gen_salt('bf')), 'admin'), 18 | ('runner', crypt('runner', gen_salt('bf')), 'runner'); 19 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | runners-app: 4 | image: runners-app:latest 5 | network_mode: "host" -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/go.mod: -------------------------------------------------------------------------------- 1 | module runners-postgresql 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect 7 | github.com/beorn7/perks v1.0.1 // indirect 8 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 9 | github.com/davecgh/go-spew v1.1.1 // indirect 10 | github.com/fsnotify/fsnotify v1.5.4 // indirect 11 | github.com/gin-contrib/sse v0.1.0 // indirect 12 | github.com/gin-gonic/gin v1.8.1 // indirect 13 | github.com/go-playground/locales v0.14.0 // indirect 14 | github.com/go-playground/universal-translator v0.18.0 // indirect 15 | github.com/go-playground/validator/v10 v10.11.1 // indirect 16 | github.com/goccy/go-json v0.9.11 // indirect 17 | github.com/golang/protobuf v1.5.2 // indirect 18 | github.com/hashicorp/hcl v1.0.0 // indirect 19 | github.com/json-iterator/go v1.1.12 // indirect 20 | github.com/leodido/go-urn v1.2.1 // indirect 21 | github.com/lib/pq v1.10.7 // indirect 22 | github.com/magiconair/properties v1.8.6 // indirect 23 | github.com/mattn/go-isatty v0.0.16 // indirect 24 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 25 | github.com/mitchellh/mapstructure v1.5.0 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/pelletier/go-toml v1.9.5 // indirect 29 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/prometheus/client_golang v1.14.0 // indirect 32 | github.com/prometheus/client_model v0.3.0 // indirect 33 | github.com/prometheus/common v0.37.0 // indirect 34 | github.com/prometheus/procfs v0.8.0 // indirect 35 | github.com/spf13/afero v1.8.2 // indirect 36 | github.com/spf13/cast v1.5.0 // indirect 37 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 38 | github.com/spf13/pflag v1.0.5 // indirect 39 | github.com/spf13/viper v1.13.0 // indirect 40 | github.com/stretchr/objx v0.5.0 // indirect 41 | github.com/stretchr/testify v1.8.1 // indirect 42 | github.com/subosito/gotenv v1.4.1 // indirect 43 | github.com/ugorji/go/codec v1.2.7 // indirect 44 | golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect 45 | golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect 46 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect 47 | golang.org/x/text v0.3.7 // indirect 48 | google.golang.org/protobuf v1.28.1 // indirect 49 | gopkg.in/ini.v1 v1.67.0 // indirect 50 | gopkg.in/yaml.v2 v2.4.0 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/kubernetes/runners-app-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: runners-app 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: runners-app 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app: runners-app 14 | spec: 15 | containers: 16 | - image: runners-app:latest 17 | name: runners-app 18 | imagePullPolicy: Never 19 | ports: 20 | - containerPort: 8080 21 | env: 22 | - name: ENV 23 | value: "k8s" -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/kubernetes/runners-app-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: runners-app-service 5 | spec: 6 | selector: 7 | app: runners-app 8 | type: LoadBalancer 9 | ports: 10 | - name: http 11 | protocol: TCP 12 | port: 8080 13 | targetPort: 8080 14 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "runners-postgresql/config" 7 | "runners-postgresql/server" 8 | 9 | _ "github.com/lib/pq" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting Runners App") 14 | 15 | log.Println("Initializing configuration") 16 | config := config.InitConfig(getConfigFileName()) 17 | 18 | log.Println("Initializing database") 19 | dbHandler := server.InitDatabase(config) 20 | 21 | log.Println("Initializing Prometheus") 22 | go server.InitPrometheus() 23 | 24 | log.Println("Initializig HTTP sever") 25 | httpServer := server.InitHttpServer(config, dbHandler) 26 | 27 | httpServer.Start() 28 | } 29 | 30 | func getConfigFileName() string { 31 | env := os.Getenv("ENV") 32 | 33 | if env != "" { 34 | return "runners-" + env 35 | } 36 | 37 | return "runners" 38 | } 39 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | HttpRequestsCounter = promauto.NewCounter( 10 | prometheus.CounterOpts{ 11 | Name: "runners_app_http_requests", 12 | Help: "Total number of HTTP requests", 13 | }, 14 | ) 15 | 16 | GetRunnerHttpResponsesCounter = promauto.NewCounterVec( 17 | prometheus.CounterOpts{ 18 | Name: "runners_app_get_runner_http_responses", 19 | Help: "Total number of HTTP responses for get runner API", 20 | }, 21 | []string{"status"}, 22 | ) 23 | 24 | GetAllRunnersTimer = promauto.NewHistogram( 25 | prometheus.HistogramOpts{ 26 | Name: "runners_app_get_all_runners_duration", 27 | Help: "Duration of get all runners operation", 28 | }, 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/models/responseError.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ResponseError struct { 4 | Message string `json:"message"` 5 | Status int `json:"-"` 6 | } 7 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/models/result.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Result struct { 4 | ID string `json:"id"` 5 | RunnerID string `json:"runner_id"` 6 | RaceResult string `json:"race_result"` 7 | Location string `json:"location"` 8 | Position int `json:"position,omitempty"` 9 | Year int `json:"year"` 10 | } 11 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/models/runner.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Runner struct { 4 | ID string `json:"id"` 5 | FirstName string `json:"first_name"` 6 | LastName string `json:"last_name"` 7 | Age int `json:"age,omitempty"` 8 | IsActive bool `json:"is_active"` 9 | Country string `json:"country"` 10 | PersonalBest string `json:"personal_best,omitempty"` 11 | SeasonBest string `json:"season_best,omitempty"` 12 | Results []*Result `json:"results,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | ID string `json:"id"` 5 | Username string `json:"username"` 6 | Password string `json:"user_password"` 7 | Role string `json:"user_role"` 8 | AccessToken string `json:"access_toke"` 9 | } 10 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/repositories/transactionHandler.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | func BeginTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 9 | ctx := context.Background() 10 | transaction, err := resultsRepository.dbHandler.BeginTx(ctx, &sql.TxOptions{}) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | runnersRepository.transaction = transaction 16 | resultsRepository.transaction = transaction 17 | 18 | return nil 19 | } 20 | 21 | func RollbackTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 22 | transaction := runnersRepository.transaction 23 | 24 | runnersRepository.transaction = nil 25 | resultsRepository.transaction = nil 26 | 27 | return transaction.Rollback() 28 | } 29 | 30 | func CommitTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 31 | transaction := runnersRepository.transaction 32 | 33 | runnersRepository.transaction = nil 34 | resultsRepository.transaction = nil 35 | 36 | return transaction.Commit() 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/runners-k8s.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=192.168.0.25 port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/runners.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=localhost port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### 21 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/server/dbServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func InitDatabase(config *viper.Viper) *sql.DB { 11 | connectionString := config.GetString("database.connection_string") 12 | maxIdleConnections := config.GetInt("database.max_idle_connections") 13 | maxOpenConnections := config.GetInt("database.max_open_connections") 14 | connectionMaxLifetime := config.GetDuration("database.connection_max_lifetime") 15 | driverName := config.GetString("database.driver_name") 16 | 17 | if connectionString == "" { 18 | log.Fatalf("Database connectin string is missing") 19 | } 20 | 21 | dbHandler, err := sql.Open(driverName, connectionString) 22 | if err != nil { 23 | log.Fatalf("Error while initializing database: %v", err) 24 | } 25 | 26 | dbHandler.SetMaxIdleConns(maxIdleConnections) 27 | dbHandler.SetMaxOpenConns(maxOpenConnections) 28 | dbHandler.SetConnMaxLifetime(connectionMaxLifetime) 29 | 30 | err = dbHandler.Ping() 31 | if err != nil { 32 | dbHandler.Close() 33 | log.Fatalf("Error while validating database: %v", err) 34 | } 35 | 36 | return dbHandler 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/server/httpServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "runners-postgresql/controllers" 7 | "runners-postgresql/repositories" 8 | "runners-postgresql/services" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | type HttpServer struct { 15 | config *viper.Viper 16 | router *gin.Engine 17 | runnersController *controllers.RunnersController 18 | resultsController *controllers.ResultsController 19 | usersController *controllers.UsersController 20 | } 21 | 22 | func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { 23 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 24 | resultRepository := repositories.NewResultsRepository(dbHandler) 25 | usersRepository := repositories.NewUsersRepository(dbHandler) 26 | runnersService := services.NewRunnersService(runnersRepository, resultRepository) 27 | resultsService := services.NewResultsService(resultRepository, runnersRepository) 28 | usersService := services.NewUsersService(usersRepository) 29 | runnersController := controllers.NewRunnersController(runnersService, usersService) 30 | resultsController := controllers.NewResultsController(resultsService, usersService) 31 | usersController := controllers.NewUsersController(usersService) 32 | 33 | router := gin.Default() 34 | 35 | router.POST("/runner", runnersController.CreateRunner) 36 | router.PUT("/runner", runnersController.UpdateRunner) 37 | router.DELETE("/runner/:id", runnersController.DeleteRunner) 38 | router.GET("/runner/:id", runnersController.GetRunner) 39 | router.GET("/runner", runnersController.GetRunnersBatch) 40 | 41 | router.POST("/result", resultsController.CreateResult) 42 | router.DELETE("/result/:id", resultsController.DeleteResult) 43 | 44 | router.POST("/login", usersController.Login) 45 | router.POST("/logout", usersController.Logout) 46 | 47 | return HttpServer{ 48 | config: config, 49 | router: router, 50 | runnersController: runnersController, 51 | resultsController: resultsController, 52 | usersController: usersController, 53 | } 54 | } 55 | 56 | func (hs HttpServer) Start() { 57 | err := hs.router.Run(hs.config.GetString("http.server_address")) 58 | if err != nil { 59 | log.Fatalf("Error while starting HTTP server: %v", err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/server/prometheus.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | ) 8 | 9 | func InitPrometheus() { 10 | http.Handle("/metrics", promhttp.Handler()) 11 | http.ListenAndServe(":9000", nil) 12 | } 13 | -------------------------------------------------------------------------------- /Chapter 11/runners-postgresql-main/services/runnerService_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "net/http" 5 | "runners-postgresql/models" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | /*func TestValidateRunnerInvalidFirstName(t *testing.T) { 12 | runner := &models.Runner{ 13 | LastName: "Smith", 14 | Age: 30, 15 | Country: "United States", 16 | } 17 | 18 | responseErr := validateRunner(runner) 19 | assert.NotEmpty(t, responseErr) 20 | assert.Equal(t, "Invalid first name", responseErr.Message) 21 | assert.Equal(t, http.StatusBadRequest, responseErr.Status) 22 | }*/ 23 | 24 | func TestValidateRunner(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | runner *models.Runner 28 | want *models.ResponseError 29 | }{ 30 | { 31 | name: "Invalid_First_Name", 32 | runner: &models.Runner{ 33 | LastName: "Smith", 34 | Age: 30, 35 | Country: "United States", 36 | }, 37 | want: &models.ResponseError{ 38 | Message: "Invalid first name", 39 | Status: http.StatusBadRequest, 40 | }, 41 | }, 42 | { 43 | name: "Invalid_Last_Name", 44 | runner: &models.Runner{ 45 | FirstName: "John", 46 | Age: 30, 47 | Country: "United States", 48 | }, 49 | want: &models.ResponseError{ 50 | Message: "Invalid last name", 51 | Status: http.StatusBadRequest, 52 | }, 53 | }, 54 | { 55 | name: "Invalid_Age", 56 | runner: &models.Runner{ 57 | FirstName: "John", 58 | LastName: "Smith", 59 | Age: 300, 60 | Country: "United States", 61 | }, 62 | want: &models.ResponseError{ 63 | Message: "Invalid age", 64 | Status: http.StatusBadRequest, 65 | }, 66 | }, 67 | { 68 | name: "Invalid_Country", 69 | runner: &models.Runner{ 70 | FirstName: "John", 71 | LastName: "Smith", 72 | Age: 30, 73 | }, 74 | want: &models.ResponseError{ 75 | Message: "Invalid country", 76 | Status: http.StatusBadRequest, 77 | }, 78 | }, 79 | { 80 | name: "Valid_Runner", 81 | runner: &models.Runner{ 82 | FirstName: "John", 83 | LastName: "Smith", 84 | Age: 30, 85 | Country: "United States", 86 | }, 87 | want: nil, 88 | }, 89 | } 90 | 91 | for _, test := range tests { 92 | t.Run(test.name, func(t *testing.T) { 93 | responseErr := validateRunner(test.runner) 94 | assert.Equal(t, test.want, responseErr) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from golang alpine base image 2 | FROM golang:1.19-alpine 3 | 4 | # Set the Current Working Directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the source code to the Working Directory inside the container 8 | COPY . . 9 | 10 | # Download all dependencies. 11 | # Dependencies will be cached if the go.mod and go.sum files are not changed 12 | RUN go mod download 13 | 14 | # Build the Go app 15 | RUN go build -o runners-app main.go 16 | 17 | # Export necessary ports 18 | EXPOSE 8080 19 | 20 | # Command to run when starting the container 21 | CMD ["/app/runners-app"] 22 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func InitConfig(fileName string) *viper.Viper { 10 | config := viper.New() 11 | 12 | config.SetConfigName(fileName) 13 | 14 | config.AddConfigPath(".") 15 | config.AddConfigPath("$HOME") 16 | 17 | err := config.ReadInConfig() 18 | if err != nil { 19 | log.Fatal("Error while parsing configuration file", err) 20 | } 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/controllers/resultsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "runners-postgresql/models" 9 | "runners-postgresql/services" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type ResultsController struct { 15 | resultsService *services.ResultsService 16 | usersService *services.UsersService 17 | } 18 | 19 | func NewResultsController(resultsService *services.ResultsService, 20 | userService *services.UsersService) *ResultsController { 21 | 22 | return &ResultsController{ 23 | resultsService: resultsService, 24 | usersService: userService, 25 | } 26 | } 27 | 28 | func (rc ResultsController) CreateResult(ctx *gin.Context) { 29 | accessToken := ctx.Request.Header.Get("Token") 30 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 31 | if responseErr != nil { 32 | ctx.JSON(responseErr.Status, responseErr) 33 | return 34 | } 35 | 36 | if !auth { 37 | ctx.Status(http.StatusUnauthorized) 38 | return 39 | } 40 | 41 | body, err := io.ReadAll(ctx.Request.Body) 42 | if err != nil { 43 | log.Println("Error while reading create result request body", err) 44 | ctx.AbortWithError(http.StatusInternalServerError, err) 45 | return 46 | } 47 | 48 | var result models.Result 49 | err = json.Unmarshal(body, &result) 50 | if err != nil { 51 | log.Println("Error while unmarshaling create result request body", err) 52 | ctx.AbortWithError(http.StatusInternalServerError, err) 53 | return 54 | } 55 | 56 | response, responseErr := rc.resultsService.CreateResult(&result) 57 | if responseErr != nil { 58 | ctx.JSON(responseErr.Status, responseErr) 59 | return 60 | } 61 | 62 | ctx.JSON(http.StatusOK, response) 63 | } 64 | 65 | func (rc ResultsController) DeleteResult(ctx *gin.Context) { 66 | accessToken := ctx.Request.Header.Get("Token") 67 | auth, responseErr := rc.usersService.AuthorizeUser(accessToken, []string{ROLE_ADMIN}) 68 | if responseErr != nil { 69 | ctx.JSON(responseErr.Status, responseErr) 70 | return 71 | } 72 | 73 | if !auth { 74 | ctx.Status(http.StatusUnauthorized) 75 | return 76 | } 77 | 78 | resultId := ctx.Param("id") 79 | 80 | responseErr = rc.resultsService.DeleteResult(resultId) 81 | if responseErr != nil { 82 | ctx.JSON(responseErr.Status, responseErr) 83 | return 84 | } 85 | 86 | ctx.Status(http.StatusNoContent) 87 | } 88 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/controllers/runnersController_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "runners-postgresql/models" 9 | "runners-postgresql/repositories" 10 | "runners-postgresql/services" 11 | "testing" 12 | 13 | "github.com/DATA-DOG/go-sqlmock" 14 | "github.com/gin-gonic/gin" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestGetRunnersResponse(t *testing.T) { 19 | dbHandler, mock, _ := sqlmock.New() 20 | defer dbHandler.Close() 21 | 22 | columnsUsers := []string{"user_role"} 23 | mock.ExpectQuery("SELECT user_role").WillReturnRows( 24 | sqlmock.NewRows(columnsUsers).AddRow("runner"), 25 | ) 26 | 27 | columns := []string{"id", "first_name", "last_name", "age", "is_active", "country", "personal_best", "season_best"} 28 | mock.ExpectQuery("SELECT *").WillReturnRows( 29 | sqlmock.NewRows(columns). 30 | AddRow("1", "John", "Smith", 30, true, "United States", "02:00:41", "02:13:13"). 31 | AddRow("2", "Marijana", "Komatinovic", 30, true, "Serbia", "01:18:28", "01:18:28")) 32 | 33 | router := initTestRouter(dbHandler) 34 | request, _ := http.NewRequest("GET", "/runner", nil) 35 | request.Header.Set("token", "token") 36 | recorder := httptest.NewRecorder() 37 | 38 | router.ServeHTTP(recorder, request) 39 | 40 | assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) 41 | 42 | var runers []*models.Runner 43 | json.Unmarshal(recorder.Body.Bytes(), &runers) 44 | 45 | assert.NotEmpty(t, runers) 46 | assert.Equal(t, 2, len(runers)) 47 | } 48 | 49 | func initTestRouter(dbHandler *sql.DB) *gin.Engine { 50 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 51 | usersRepository := repositories.NewUsersRepository(dbHandler) 52 | runnersService := services.NewRunnersService(runnersRepository, nil) 53 | usersServices := services.NewUsersService(usersRepository) 54 | runnersController := NewRunnersController(runnersService, usersServices) 55 | 56 | router := gin.Default() 57 | 58 | router.GET("/runner", runnersController.GetRunnersBatch) 59 | 60 | return router 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/controllers/usersController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "runners-postgresql/services" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type UsersController struct { 12 | usersService *services.UsersService 13 | } 14 | 15 | func NewUsersController(usersService *services.UsersService) *UsersController { 16 | return &UsersController{ 17 | usersService: usersService, 18 | } 19 | } 20 | 21 | func (uc UsersController) Login(ctx *gin.Context) { 22 | username, password, ok := ctx.Request.BasicAuth() 23 | if !ok { 24 | log.Println("Error while reading credentials") 25 | ctx.AbortWithStatus(http.StatusBadRequest) 26 | return 27 | } 28 | 29 | accessToken, responseErr := uc.usersService.Login(username, password) 30 | if responseErr != nil { 31 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 32 | return 33 | } 34 | 35 | ctx.JSON(http.StatusOK, accessToken) 36 | } 37 | 38 | func (uc UsersController) Logout(ctx *gin.Context) { 39 | accessToken := ctx.Request.Header.Get("Token") 40 | 41 | responseErr := uc.usersService.Logout(accessToken) 42 | if responseErr != nil { 43 | ctx.AbortWithStatusJSON(responseErr.Status, responseErr) 44 | return 45 | } 46 | 47 | ctx.Status(http.StatusNoContent) 48 | } 49 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/dbscripts/public_schema.sql: -------------------------------------------------------------------------------- 1 | -- Initial public schema relates to Library 0.x 2 | 3 | SET statement_timeout = 0; 4 | SET lock_timeout = 0; 5 | SET idle_in_transaction_session_timeout = 0; 6 | SET client_encoding = 'UTF8'; 7 | SET standard_conforming_strings = on; 8 | SET client_min_messages = warning; 9 | SET row_security = off; 10 | 11 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 12 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA pg_catalog; 13 | 14 | SET search_path = public, pg_catalog; 15 | SET default_tablespace = ''; 16 | 17 | -- runners 18 | CREATE TABLE runners ( 19 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 20 | first_name text NOT NULL, 21 | last_name text NOT NULL, 22 | age integer, 23 | is_active boolean DEFAULT TRUE, 24 | country text NOT NULL, 25 | personal_best interval, 26 | season_best interval, 27 | CONSTRAINT runners_pk PRIMARY KEY (id) 28 | ); 29 | 30 | CREATE INDEX runners_country 31 | ON runners (country); 32 | 33 | CREATE INDEX runners_season_best 34 | ON runners (season_best); 35 | 36 | -- results 37 | CREATE TABLE results ( 38 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 39 | runner_id uuid NOT NULL, 40 | race_result interval NOT NULL, 41 | location text NOT NULL, 42 | position integer, 43 | year integer NOT NULL, 44 | CONSTRAINT results_pk PRIMARY KEY (id), 45 | CONSTRAINT fk_results_runner_id FOREIGN KEY (runner_id) 46 | REFERENCES runners (id) MATCH SIMPLE 47 | ON UPDATE NO ACTION 48 | ON DELETE NO ACTION 49 | ); 50 | 51 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/dbscripts/update_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | 3 | CREATE TABLE users ( 4 | id uuid NOT NULL DEFAULT uuid_generate_v1mc(), 5 | username text NOT NULL UNIQUE, 6 | user_password text NOT NULL, 7 | user_role text NOT NULL, 8 | access_token text, 9 | CONSTRAINT users_pk PRIMARY KEY (id) 10 | ); 11 | 12 | CREATE INDEX user_access_token 13 | ON users (access_token); 14 | 15 | INSERT INTO users(username, user_password, user_role) 16 | VALUES 17 | ('admin', crypt('admin', gen_salt('bf')), 'admin'), 18 | ('runner', crypt('runner', gen_salt('bf')), 'runner'); 19 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | runners-app: 4 | image: runners-app:latest 5 | network_mode: "host" -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/kubernetes/runners-app-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: runners-app 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: runners-app 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app: runners-app 14 | spec: 15 | containers: 16 | - image: runners-app:latest 17 | name: runners-app 18 | imagePullPolicy: Never 19 | ports: 20 | - containerPort: 8080 21 | env: 22 | - name: ENV 23 | value: "k8s" -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/kubernetes/runners-app-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: runners-app-service 5 | spec: 6 | selector: 7 | app: runners-app 8 | type: LoadBalancer 9 | ports: 10 | - name: http 11 | protocol: TCP 12 | port: 8080 13 | targetPort: 8080 14 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "runners-postgresql/config" 7 | "runners-postgresql/server" 8 | 9 | _ "github.com/lib/pq" 10 | ) 11 | 12 | func main() { 13 | log.Println("Starting Runners App") 14 | 15 | log.Println("Initializing configuration") 16 | config := config.InitConfig(getConfigFileName()) 17 | 18 | log.Println("Initializing database") 19 | dbHandler := server.InitDatabase(config) 20 | 21 | log.Println("Initializing Prometheus") 22 | go server.InitPrometheus() 23 | 24 | log.Println("Initializig HTTP sever") 25 | httpServer := server.InitHttpServer(config, dbHandler) 26 | 27 | httpServer.Start() 28 | } 29 | 30 | func getConfigFileName() string { 31 | env := os.Getenv("ENV") 32 | 33 | if env != "" { 34 | return "runners-" + env 35 | } 36 | 37 | return "runners" 38 | } 39 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | HttpRequestsCounter = promauto.NewCounter( 10 | prometheus.CounterOpts{ 11 | Name: "runners_app_http_requests", 12 | Help: "Total number of HTTP requests", 13 | }, 14 | ) 15 | 16 | GetRunnerHttpResponsesCounter = promauto.NewCounterVec( 17 | prometheus.CounterOpts{ 18 | Name: "runners_app_get_runner_http_responses", 19 | Help: "Total number of HTTP responses for get runner API", 20 | }, 21 | []string{"status"}, 22 | ) 23 | 24 | GetAllRunnersTimer = promauto.NewHistogram( 25 | prometheus.HistogramOpts{ 26 | Name: "runners_app_get_all_runners_duration", 27 | Help: "Duration of get all runners operation", 28 | }, 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/models/responseError.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ResponseError struct { 4 | Message string `json:"message"` 5 | Status int `json:"-"` 6 | } 7 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/models/result.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Result struct { 4 | ID string `json:"id"` 5 | RunnerID string `json:"runner_id"` 6 | RaceResult string `json:"race_result"` 7 | Location string `json:"location"` 8 | Position int `json:"position,omitempty"` 9 | Year int `json:"year"` 10 | } 11 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/models/runner.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Runner struct { 4 | ID string `json:"id"` 5 | FirstName string `json:"first_name"` 6 | LastName string `json:"last_name"` 7 | Age int `json:"age,omitempty"` 8 | IsActive bool `json:"is_active"` 9 | Country string `json:"country"` 10 | PersonalBest string `json:"personal_best,omitempty"` 11 | SeasonBest string `json:"season_best,omitempty"` 12 | Results []*Result `json:"results,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | ID string `json:"id"` 5 | Username string `json:"username"` 6 | Password string `json:"user_password"` 7 | Role string `json:"user_role"` 8 | AccessToken string `json:"access_toke"` 9 | } 10 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/repositories/transactionHandler.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | func BeginTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 9 | ctx := context.Background() 10 | transaction, err := resultsRepository.dbHandler.BeginTx(ctx, &sql.TxOptions{}) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | runnersRepository.transaction = transaction 16 | resultsRepository.transaction = transaction 17 | 18 | return nil 19 | } 20 | 21 | func RollbackTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 22 | transaction := runnersRepository.transaction 23 | 24 | runnersRepository.transaction = nil 25 | resultsRepository.transaction = nil 26 | 27 | return transaction.Rollback() 28 | } 29 | 30 | func CommitTransaction(runnersRepository *RunnersRepository, resultsRepository *ResultsRepository) error { 31 | transaction := runnersRepository.transaction 32 | 33 | runnersRepository.transaction = nil 34 | resultsRepository.transaction = nil 35 | 36 | return transaction.Commit() 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/runners-k8s.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=192.168.0.25 port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/runners.toml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Database configuration 3 | 4 | # Connection string is in Go pq driver format: 5 | # host= port= user= password= dbname= 6 | 7 | [database] 8 | 9 | connection_string = "host=localhost port=5432 user=postgres password=postgres dbname=runners_db sslmode=disable" 10 | max_idle_connections = 5 11 | max_open_connections = 20 12 | connection_max_lifetime = "60s" 13 | driver_name = "postgres" 14 | ############################################################################### 15 | # HTTP server configuration 16 | 17 | [http] 18 | 19 | server_address = ":8080" 20 | ############################################################################### 21 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/server/dbServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func InitDatabase(config *viper.Viper) *sql.DB { 11 | connectionString := config.GetString("database.connection_string") 12 | maxIdleConnections := config.GetInt("database.max_idle_connections") 13 | maxOpenConnections := config.GetInt("database.max_open_connections") 14 | connectionMaxLifetime := config.GetDuration("database.connection_max_lifetime") 15 | driverName := config.GetString("database.driver_name") 16 | 17 | if connectionString == "" { 18 | log.Fatalf("Database connectin string is missing") 19 | } 20 | 21 | dbHandler, err := sql.Open(driverName, connectionString) 22 | if err != nil { 23 | log.Fatalf("Error while initializing database: %v", err) 24 | } 25 | 26 | dbHandler.SetMaxIdleConns(maxIdleConnections) 27 | dbHandler.SetMaxOpenConns(maxOpenConnections) 28 | dbHandler.SetConnMaxLifetime(connectionMaxLifetime) 29 | 30 | err = dbHandler.Ping() 31 | if err != nil { 32 | dbHandler.Close() 33 | log.Fatalf("Error while validating database: %v", err) 34 | } 35 | 36 | return dbHandler 37 | } 38 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/server/httpServer.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "runners-postgresql/controllers" 7 | "runners-postgresql/repositories" 8 | "runners-postgresql/services" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | type HttpServer struct { 15 | config *viper.Viper 16 | router *gin.Engine 17 | runnersController *controllers.RunnersController 18 | resultsController *controllers.ResultsController 19 | usersController *controllers.UsersController 20 | } 21 | 22 | func InitHttpServer(config *viper.Viper, dbHandler *sql.DB) HttpServer { 23 | runnersRepository := repositories.NewRunnersRepository(dbHandler) 24 | resultRepository := repositories.NewResultsRepository(dbHandler) 25 | usersRepository := repositories.NewUsersRepository(dbHandler) 26 | runnersService := services.NewRunnersService(runnersRepository, resultRepository) 27 | resultsService := services.NewResultsService(resultRepository, runnersRepository) 28 | usersService := services.NewUsersService(usersRepository) 29 | runnersController := controllers.NewRunnersController(runnersService, usersService) 30 | resultsController := controllers.NewResultsController(resultsService, usersService) 31 | usersController := controllers.NewUsersController(usersService) 32 | 33 | router := gin.Default() 34 | 35 | router.POST("/runner", runnersController.CreateRunner) 36 | router.PUT("/runner", runnersController.UpdateRunner) 37 | router.DELETE("/runner/:id", runnersController.DeleteRunner) 38 | router.GET("/runner/:id", runnersController.GetRunner) 39 | router.GET("/runner", runnersController.GetRunnersBatch) 40 | 41 | router.POST("/result", resultsController.CreateResult) 42 | router.DELETE("/result/:id", resultsController.DeleteResult) 43 | 44 | router.POST("/login", usersController.Login) 45 | router.POST("/logout", usersController.Logout) 46 | 47 | return HttpServer{ 48 | config: config, 49 | router: router, 50 | runnersController: runnersController, 51 | resultsController: resultsController, 52 | usersController: usersController, 53 | } 54 | } 55 | 56 | func (hs HttpServer) Start() { 57 | err := hs.router.Run(hs.config.GetString("http.server_address")) 58 | if err != nil { 59 | log.Fatalf("Error while starting HTTP server: %v", err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/server/prometheus.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | ) 8 | 9 | func InitPrometheus() { 10 | http.Handle("/metrics", promhttp.Handler()) 11 | http.ListenAndServe(":9000", nil) 12 | } 13 | -------------------------------------------------------------------------------- /Chapter 12/runners-postgresql-main/services/runnerService_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "net/http" 5 | "runners-postgresql/models" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | /*func TestValidateRunnerInvalidFirstName(t *testing.T) { 12 | runner := &models.Runner{ 13 | LastName: "Smith", 14 | Age: 30, 15 | Country: "United States", 16 | } 17 | 18 | responseErr := validateRunner(runner) 19 | assert.NotEmpty(t, responseErr) 20 | assert.Equal(t, "Invalid first name", responseErr.Message) 21 | assert.Equal(t, http.StatusBadRequest, responseErr.Status) 22 | }*/ 23 | 24 | func TestValidateRunner(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | runner *models.Runner 28 | want *models.ResponseError 29 | }{ 30 | { 31 | name: "Invalid_First_Name", 32 | runner: &models.Runner{ 33 | LastName: "Smith", 34 | Age: 30, 35 | Country: "United States", 36 | }, 37 | want: &models.ResponseError{ 38 | Message: "Invalid first name", 39 | Status: http.StatusBadRequest, 40 | }, 41 | }, 42 | { 43 | name: "Invalid_Last_Name", 44 | runner: &models.Runner{ 45 | FirstName: "John", 46 | Age: 30, 47 | Country: "United States", 48 | }, 49 | want: &models.ResponseError{ 50 | Message: "Invalid last name", 51 | Status: http.StatusBadRequest, 52 | }, 53 | }, 54 | { 55 | name: "Invalid_Age", 56 | runner: &models.Runner{ 57 | FirstName: "John", 58 | LastName: "Smith", 59 | Age: 300, 60 | Country: "United States", 61 | }, 62 | want: &models.ResponseError{ 63 | Message: "Invalid age", 64 | Status: http.StatusBadRequest, 65 | }, 66 | }, 67 | { 68 | name: "Invalid_Country", 69 | runner: &models.Runner{ 70 | FirstName: "John", 71 | LastName: "Smith", 72 | Age: 30, 73 | }, 74 | want: &models.ResponseError{ 75 | Message: "Invalid country", 76 | Status: http.StatusBadRequest, 77 | }, 78 | }, 79 | { 80 | name: "Valid_Runner", 81 | runner: &models.Runner{ 82 | FirstName: "John", 83 | LastName: "Smith", 84 | Age: 30, 85 | Country: "United States", 86 | }, 87 | want: nil, 88 | }, 89 | } 90 | 91 | for _, test := range tests { 92 | t.Run(test.name, func(t *testing.T) { 93 | responseErr := validateRunner(test.runner) 94 | assert.Equal(t, test.want, responseErr) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 AVA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README - Copy.md: -------------------------------------------------------------------------------- 1 | 2 | ## Modern Web Development with Go 3 | 4 | This is the repository for [Modern Web Development with Go](https://orangeava.com/products/web-development-with-golang-fast-and-efficient-development-of-web-server-applications-with-state-of-the-art-technologies), published by Orange AVA™ 5 | 6 | 7 | ## About the Book 8 | In this book, we are going to learn how to design, develop and deploy Web Server Applications using the Go programming language. In recent years, Go has become the industrial standard for these kinds of applications; so by learning this, a lot of good opportunities can be opened in the market. All subjects will be covered through various practical examples. 9 | 10 | This book will cover the state-of-the-art technology for the development of Web Applications and follow all industrial standards. At the beginning we will do the preparation for development. Here, we will learn the basics of the Go programming language, the basics of Web Servers, how to set up a project with Go, and how to design software solutions. 11 | 12 | Later, we will concentrate more on development. We will learn how to develop the application designed in the previous chapters, how to use different types of databases, how to test our application, and how to make it secure. At the end of the book, we will show how to deploy the application and monitor it after deployment. 13 | 14 | After reading this book, the readers can independently develop Web Server Applications or include themselves in already-started projects. 15 | 16 | ## What you will learn 17 | 18 | ● Solve common problems with the Go programming language. 19 | 20 | ● Be familiar with the terms related to server applications. 21 | 22 | ● Understand the phases in the software development process. 23 | 24 | ● Be able to independently design software solutions and use some best practices. 25 | 26 | ● Learn how to deploy applications. 27 | 28 | ● Understand and know how to apply monitoring and alerting concepts. 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Modern Web Development with Go 3 | 4 | This is the repository for [Modern Web Development with Go](https://orangeava.com/products/web-development-with-golang-fast-and-efficient-development-of-web-server-applications-with-state-of-the-art-technologies), published by Orange AVA™ 5 | 6 | 7 | ## About the Book 8 | In this book, we are going to learn how to design, develop and deploy Web Server Applications using the Go programming language. In recent years, Go has become the industrial standard for these kinds of applications; so by learning this, a lot of good opportunities can be opened in the market. All subjects will be covered through various practical examples. 9 | 10 | This book will cover the state-of-the-art technology for the development of Web Applications and follow all industrial standards. At the beginning we will do the preparation for development. Here, we will learn the basics of the Go programming language, the basics of Web Servers, how to set up a project with Go, and how to design software solutions. 11 | 12 | Later, we will concentrate more on development. We will learn how to develop the application designed in the previous chapters, how to use different types of databases, how to test our application, and how to make it secure. At the end of the book, we will show how to deploy the application and monitor it after deployment. 13 | 14 | After reading this book, the readers can independently develop Web Server Applications or include themselves in already-started projects. 15 | 16 | ## What you will learn 17 | 18 | ● Solve common problems with the Go programming language. 19 | 20 | ● Be familiar with the terms related to server applications. 21 | 22 | ● Understand the phases in the software development process. 23 | 24 | ● Be able to independently design software solutions and use some best practices. 25 | 26 | ● Learn how to deploy applications. 27 | 28 | ● Understand and know how to apply monitoring and alerting concepts. 29 | 30 | --------------------------------------------------------------------------------