From 197ecee86c413158f3d06f85ef3a0a04dffa19eb Mon Sep 17 00:00:00 2001 From: darioscrivano Date: Thu, 22 Feb 2024 15:03:13 -0300 Subject: [PATCH 01/11] go-api: Postgres & sqlc --- .../.gitignore | 2 + .../Dockerfile | 23 ++++++++ .../README.md | 53 +++++++++++++++++++ .../compose.yml | 18 +++++++ .../db/database.go | 37 +++++++++++++ .../golang-api-with-postgres-and-sqlc/go.mod | 15 ++++++ .../golang-api-with-postgres-and-sqlc/go.sum | 23 ++++++++ .../handlers/handlers.go | 42 +++++++++++++++ .../golang-api-with-postgres-and-sqlc/main.go | 21 ++++++++ .../query.sql | 14 +++++ .../schema.sql | 7 +++ .../sqlc.yaml | 10 ++++ 12 files changed, 265 insertions(+) create mode 100644 examples/golang-api-with-postgres-and-sqlc/.gitignore create mode 100644 examples/golang-api-with-postgres-and-sqlc/Dockerfile create mode 100644 examples/golang-api-with-postgres-and-sqlc/README.md create mode 100644 examples/golang-api-with-postgres-and-sqlc/compose.yml create mode 100644 examples/golang-api-with-postgres-and-sqlc/db/database.go create mode 100644 examples/golang-api-with-postgres-and-sqlc/go.mod create mode 100644 examples/golang-api-with-postgres-and-sqlc/go.sum create mode 100644 examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go create mode 100644 examples/golang-api-with-postgres-and-sqlc/main.go create mode 100644 examples/golang-api-with-postgres-and-sqlc/query.sql create mode 100644 examples/golang-api-with-postgres-and-sqlc/schema.sql create mode 100644 examples/golang-api-with-postgres-and-sqlc/sqlc.yaml diff --git a/examples/golang-api-with-postgres-and-sqlc/.gitignore b/examples/golang-api-with-postgres-and-sqlc/.gitignore new file mode 100644 index 0000000..1b16a3a --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/.gitignore @@ -0,0 +1,2 @@ +users/ +data/ \ No newline at end of file diff --git a/examples/golang-api-with-postgres-and-sqlc/Dockerfile b/examples/golang-api-with-postgres-and-sqlc/Dockerfile new file mode 100644 index 0000000..e62f31f --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/Dockerfile @@ -0,0 +1,23 @@ +# This Dockerfile is used to build a Docker image for a Go application with PostgreSQL. +# It sets the working directory to /go/src/app, copies the application code into the container, +# installs the dependencies using go get, and builds the application using go install. +# The resulting executable is set as the command to run when the container starts. + +FROM golang:1.22 + +WORKDIR /go/src/app + +# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change +COPY go.mod go.sum ./ + +# RUN go get -d -v ./... +RUN go mod download && go mod verify + +COPY . . + +# RUN go install -v ./... +RUN go build -v -o /usr/local/bin/app . + +EXPOSE 8080 + +CMD ["app"] diff --git a/examples/golang-api-with-postgres-and-sqlc/README.md b/examples/golang-api-with-postgres-and-sqlc/README.md new file mode 100644 index 0000000..3fcae24 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/README.md @@ -0,0 +1,53 @@ +# Go API with Docker Compose + +This project is a Go API that uses PostgreSQL as its database. The application and the database are containerized using Docker Compose. + +## Prerequisites + +- Docker +- Docker Compose +- sqlc + +## Setup + +1. Clone the repository: + +```bash +git clone https://github.com/nanlabs/backend-reference +``` + +2. Navigate to the project directory + +```bash +cd backend-reference/examples/golang-api-with-postgres-and-sqlc +``` + +3. We are using [sqlc](https://docs.sqlc.dev/en/stable/index.html) to generate the queries and models. +To generate the queries run + +```bash +sqlc generate +``` + +3. Build and run the Docker containers: + +```bash +docker-compose build +``` + +4. Run the containers + +```bash +docker-compose up +``` + + + +The Go API will be accessible at localhost:8080. + +## Stopping the Application +To stop the application and remove the containers, networks, and volumes defined in docker-compose.yml, run the following command: + +```bash +docker-compose down +``` diff --git a/examples/golang-api-with-postgres-and-sqlc/compose.yml b/examples/golang-api-with-postgres-and-sqlc/compose.yml new file mode 100644 index 0000000..a0d4a38 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/compose.yml @@ -0,0 +1,18 @@ +version: "3" +services: + db: + container_name: db + image: postgres:16 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: poc + volumes: + - ./data/db:/var/lib/postgresql/data + go_api: + container_name: go_api + build: . + depends_on: + - db + ports: + - "8080:8080" \ No newline at end of file diff --git a/examples/golang-api-with-postgres-and-sqlc/db/database.go b/examples/golang-api-with-postgres-and-sqlc/db/database.go new file mode 100644 index 0000000..e85b152 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/db/database.go @@ -0,0 +1,37 @@ +package db + +import ( + "context" + "fmt" + "go-postgres-sqlc/users" + + "github.com/jackc/pgx/v5" +) + +var dsn = "host=db user=postgres password=postgres dbname=poc port=5432 sslmode=disable" + +var Context = context.Background() +var db = func() (db *pgx.Conn) { + conn, err := pgx.Connect(Context, dsn) + if err != nil { + panic(err) + } + fmt.Println("Connected to database") + return conn +}() + +var Queries = users.New(db) + +func CreateUserTable() { + query := `CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(30) NOT NULL, + password VARCHAR(100) NOT NULL, + email VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )` + _, err := db.Exec(Context, query) + if err != nil { + panic(err) + } +} diff --git a/examples/golang-api-with-postgres-and-sqlc/go.mod b/examples/golang-api-with-postgres-and-sqlc/go.mod new file mode 100644 index 0000000..f768bf3 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/go.mod @@ -0,0 +1,15 @@ +module go-postgres-sqlc + +go 1.22.0 + +require github.com/jackc/pgx v3.6.2+incompatible + +require ( + github.com/gorilla/mux v1.8.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/examples/golang-api-with-postgres-and-sqlc/go.sum b/examples/golang-api-with-postgres-and-sqlc/go.sum new file mode 100644 index 0000000..098951d --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/go.sum @@ -0,0 +1,23 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= +github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= +github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go b/examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go new file mode 100644 index 0000000..226dd26 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go @@ -0,0 +1,42 @@ +package handlers + +import ( + "encoding/json" + "go-postgres-sqlc/db" + "go-postgres-sqlc/users" + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +func ListUsers(res http.ResponseWriter, req *http.Request) { + users, err := db.Queries.ListUsers(db.Context) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(res).Encode(users) +} + +func GetUser(res http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + id, _ := strconv.Atoi(vars["id"]) + user, err := db.Queries.GetUser(db.Context, int32(id)) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(res).Encode(user) +} + +func CreateUser(res http.ResponseWriter, req *http.Request) { + user := users.User{} + err := json.NewDecoder(req.Body).Decode(&user) + db.Queries.CreateUser(db.Context, users.CreateUserParams{Username: user.Username, Password: user.Password, Email: user.Email}) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(res).Encode(user) +} diff --git a/examples/golang-api-with-postgres-and-sqlc/main.go b/examples/golang-api-with-postgres-and-sqlc/main.go new file mode 100644 index 0000000..5f4f52f --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "go-postgres-sqlc/handlers" + "log" + "net/http" + + "github.com/gorilla/mux" +) + +func main() { + db.CreateUserTable() + + mux := mux.NewRouter() + mux.HandleFunc("/api/user", handlers.ListUsers).Methods("GET") + mux.HandleFunc("/api/user/{id:[0-9]+}", handlers.GetUser).Methods("GET") + mux.HandleFunc("/api/user", handlers.CreateUser).Methods("POST") + + log.Fatal(http.ListenAndServe(":8080", mux)) + +} diff --git a/examples/golang-api-with-postgres-and-sqlc/query.sql b/examples/golang-api-with-postgres-and-sqlc/query.sql new file mode 100644 index 0000000..1dc0a79 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/query.sql @@ -0,0 +1,14 @@ +-- name: GetUser :one +SELECT * FROM users +WHERE id = $1 LIMIT 1; + +-- name: ListUsers :many +SELECT * FROM users +ORDER BY username; + +-- name: CreateUser :one +INSERT INTO users ( + username, password, email +) VALUES ( + $1, $2, $3 +) RETURNING *; diff --git a/examples/golang-api-with-postgres-and-sqlc/schema.sql b/examples/golang-api-with-postgres-and-sqlc/schema.sql new file mode 100644 index 0000000..30748b0 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(30) NOT NULL, + password VARCHAR(100) NOT NULL, + email VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml b/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml new file mode 100644 index 0000000..8388f57 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "query.sql" + schema: "schema.sql" + gen: + go: + package: "users" + out: "users" + sql_package: "pgx/v5" # you can use "database/sql" or "pgx/v5" \ No newline at end of file From 56b4e5909c0b6a8cc75baeff12248cb9d5bd654b Mon Sep 17 00:00:00 2001 From: darioscrivano Date: Thu, 22 Feb 2024 15:35:52 -0300 Subject: [PATCH 02/11] linting --- examples/golang-api-with-postgres-and-sqlc/README.md | 9 ++++----- examples/golang-api-with-postgres-and-sqlc/compose.yml | 2 +- examples/golang-api-with-postgres-and-sqlc/schema.sql | 10 +++++----- examples/golang-api-with-postgres-and-sqlc/sqlc.yaml | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/examples/golang-api-with-postgres-and-sqlc/README.md b/examples/golang-api-with-postgres-and-sqlc/README.md index 3fcae24..85cf6a1 100644 --- a/examples/golang-api-with-postgres-and-sqlc/README.md +++ b/examples/golang-api-with-postgres-and-sqlc/README.md @@ -23,29 +23,28 @@ cd backend-reference/examples/golang-api-with-postgres-and-sqlc ``` 3. We are using [sqlc](https://docs.sqlc.dev/en/stable/index.html) to generate the queries and models. -To generate the queries run + To generate the queries run ```bash sqlc generate ``` -3. Build and run the Docker containers: +4. Build and run the Docker containers: ```bash docker-compose build ``` -4. Run the containers +5. Run the containers ```bash docker-compose up ``` - - The Go API will be accessible at localhost:8080. ## Stopping the Application + To stop the application and remove the containers, networks, and volumes defined in docker-compose.yml, run the following command: ```bash diff --git a/examples/golang-api-with-postgres-and-sqlc/compose.yml b/examples/golang-api-with-postgres-and-sqlc/compose.yml index a0d4a38..f55704b 100644 --- a/examples/golang-api-with-postgres-and-sqlc/compose.yml +++ b/examples/golang-api-with-postgres-and-sqlc/compose.yml @@ -15,4 +15,4 @@ services: depends_on: - db ports: - - "8080:8080" \ No newline at end of file + - "8080:8080" diff --git a/examples/golang-api-with-postgres-and-sqlc/schema.sql b/examples/golang-api-with-postgres-and-sqlc/schema.sql index 30748b0..16e44c5 100644 --- a/examples/golang-api-with-postgres-and-sqlc/schema.sql +++ b/examples/golang-api-with-postgres-and-sqlc/schema.sql @@ -1,7 +1,7 @@ CREATE TABLE users ( - id SERIAL PRIMARY KEY, - username VARCHAR(30) NOT NULL, - password VARCHAR(100) NOT NULL, - email VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + id SERIAL PRIMARY KEY, + username VARCHAR(30) NOT NULL, + password VARCHAR(100) NOT NULL, + email VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml b/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml index 8388f57..0897b55 100644 --- a/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml +++ b/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml @@ -7,4 +7,4 @@ sql: go: package: "users" out: "users" - sql_package: "pgx/v5" # you can use "database/sql" or "pgx/v5" \ No newline at end of file + sql_package: "pgx/v5" # you can use "database/sql" or "pgx/v5" From 463cc9942f56ec5bd8676003e7523d30666c7edf Mon Sep 17 00:00:00 2001 From: darioscrivano Date: Thu, 22 Feb 2024 15:44:28 -0300 Subject: [PATCH 03/11] linting --- examples/golang-api-with-postgres-and-sqlc/.gitignore | 2 +- examples/golang-api-with-postgres-and-sqlc/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/golang-api-with-postgres-and-sqlc/.gitignore b/examples/golang-api-with-postgres-and-sqlc/.gitignore index 1b16a3a..7953070 100644 --- a/examples/golang-api-with-postgres-and-sqlc/.gitignore +++ b/examples/golang-api-with-postgres-and-sqlc/.gitignore @@ -1,2 +1,2 @@ users/ -data/ \ No newline at end of file +data/ diff --git a/examples/golang-api-with-postgres-and-sqlc/README.md b/examples/golang-api-with-postgres-and-sqlc/README.md index 85cf6a1..88afa0a 100644 --- a/examples/golang-api-with-postgres-and-sqlc/README.md +++ b/examples/golang-api-with-postgres-and-sqlc/README.md @@ -23,7 +23,7 @@ cd backend-reference/examples/golang-api-with-postgres-and-sqlc ``` 3. We are using [sqlc](https://docs.sqlc.dev/en/stable/index.html) to generate the queries and models. - To generate the queries run + To generate the queries run ```bash sqlc generate @@ -49,4 +49,4 @@ To stop the application and remove the containers, networks, and volumes defined ```bash docker-compose down -``` +``` \ No newline at end of file From 46d32c8cb62eea07c3e73723356ba56ff3668928 Mon Sep 17 00:00:00 2001 From: Chelo Doz Date: Mon, 20 May 2024 10:23:39 -0300 Subject: [PATCH 04/11] refactor: first refactor of the project including ai suggestions --- .../.gitignore | 24 ++- .../Dockerfile | 29 ++-- .../README.md | 60 +++---- .../compose.yml | 7 +- .../db/database.go | 43 +++-- .../db/migrations/schema.sql | 7 + .../db/query/user.sql | 29 ++++ .../db/sqlc/db.go | 32 ++++ .../db/sqlc/models.go | 19 +++ .../db/sqlc/querier.go | 17 ++ .../db/sqlc/user.sql.go | 116 ++++++++++++++ .../golang-api-with-postgres-and-sqlc/go.mod | 10 +- .../golang-api-with-postgres-and-sqlc/go.sum | 19 ++- .../handlers/handlers.go | 151 ++++++++++++++++-- .../golang-api-with-postgres-and-sqlc/main.go | 35 +++- .../query.sql | 14 -- .../schema.sql | 7 - .../sqlc.yaml | 18 ++- 18 files changed, 512 insertions(+), 125 deletions(-) create mode 100644 examples/golang-api-with-postgres-and-sqlc/db/migrations/schema.sql create mode 100644 examples/golang-api-with-postgres-and-sqlc/db/query/user.sql create mode 100644 examples/golang-api-with-postgres-and-sqlc/db/sqlc/db.go create mode 100644 examples/golang-api-with-postgres-and-sqlc/db/sqlc/models.go create mode 100644 examples/golang-api-with-postgres-and-sqlc/db/sqlc/querier.go create mode 100644 examples/golang-api-with-postgres-and-sqlc/db/sqlc/user.sql.go delete mode 100644 examples/golang-api-with-postgres-and-sqlc/query.sql delete mode 100644 examples/golang-api-with-postgres-and-sqlc/schema.sql diff --git a/examples/golang-api-with-postgres-and-sqlc/.gitignore b/examples/golang-api-with-postgres-and-sqlc/.gitignore index 7953070..a49c5a0 100644 --- a/examples/golang-api-with-postgres-and-sqlc/.gitignore +++ b/examples/golang-api-with-postgres-and-sqlc/.gitignore @@ -1,2 +1,22 @@ -users/ -data/ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum \ No newline at end of file diff --git a/examples/golang-api-with-postgres-and-sqlc/Dockerfile b/examples/golang-api-with-postgres-and-sqlc/Dockerfile index e62f31f..e7c0ab7 100644 --- a/examples/golang-api-with-postgres-and-sqlc/Dockerfile +++ b/examples/golang-api-with-postgres-and-sqlc/Dockerfile @@ -1,23 +1,26 @@ -# This Dockerfile is used to build a Docker image for a Go application with PostgreSQL. -# It sets the working directory to /go/src/app, copies the application code into the container, -# installs the dependencies using go get, and builds the application using go install. -# The resulting executable is set as the command to run when the container starts. - -FROM golang:1.22 +# Build stage +FROM golang:1.22 AS builder WORKDIR /go/src/app -# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change +# Pre-copy/cache go.mod and go.sum for pre-downloading dependencies COPY go.mod go.sum ./ +RUN go mod download -# RUN go get -d -v ./... -RUN go mod download && go mod verify - +# Copy the source code COPY . . -# RUN go install -v ./... -RUN go build -v -o /usr/local/bin/app . +# Build the Go application +RUN go build -v -o /go/bin/app . + +# Final stage +FROM gcr.io/distroless/base-debian12 + +# Copy the compiled binary from the build stage +COPY --from=builder /go/bin/app / +# Expose the application port EXPOSE 8080 -CMD ["app"] +# Set the command to run the application +CMD ["/app"] diff --git a/examples/golang-api-with-postgres-and-sqlc/README.md b/examples/golang-api-with-postgres-and-sqlc/README.md index 88afa0a..804b8bc 100644 --- a/examples/golang-api-with-postgres-and-sqlc/README.md +++ b/examples/golang-api-with-postgres-and-sqlc/README.md @@ -1,52 +1,56 @@ # Go API with Docker Compose -This project is a Go API that uses PostgreSQL as its database. The application and the database are containerized using Docker Compose. +This project containerizes a Go API and a PostgreSQL database using Docker Compose. + +## Motivation for Using `sqlc` + +Incorporating `sqlc` into a Go project provides numerous advantages: + +- **Strong Type Safety**: Compile-time checks ensure that type errors are caught early in the development process, reducing runtime errors and increasing code reliability. +- **Enhanced Developer Productivity**: By automating the generation of SQL queries and their corresponding Go code, `sqlc` allows developers to concentrate on building application features instead of writing boilerplate database interaction code. +- **Improved Readability and Maintainability**: Using native SQL queries directly in the codebase makes the queries more transparent and easier to understand, debug, and optimize. This approach aligns well with the principles of clean code and maintainability. +- **Optimized Performance**: `sqlc` enables developers to write and fine-tune raw SQL queries, providing greater control over database interactions compared to ORM-based solutions. This can lead to more efficient query execution and better overall performance. ## Prerequisites - Docker - Docker Compose -- sqlc +- `sqlc` ## Setup -1. Clone the repository: - -```bash -git clone https://github.com/nanlabs/backend-reference -``` +1. **Clone the Repository**: -2. Navigate to the project directory - -```bash -cd backend-reference/examples/golang-api-with-postgres-and-sqlc -``` + ```bash + git clone https://github.com/nanlabs/backend-reference + ``` -3. We are using [sqlc](https://docs.sqlc.dev/en/stable/index.html) to generate the queries and models. - To generate the queries run +2. **Navigate to the Project Directory**: -```bash -sqlc generate -``` + ```bash + cd backend-reference/examples/golang-api-with-postgres-and-sqlc + ``` -4. Build and run the Docker containers: +3. **Generate SQL Queries and Models with `sqlc`**: -```bash -docker-compose build -``` + ```bash + sqlc generate + ``` -5. Run the containers +4. **Build and Run the Docker Containers**: -```bash -docker-compose up -``` + ```bash + docker-compose build + docker-compose up + ``` -The Go API will be accessible at localhost:8080. +The Go API will be accessible at `localhost:8080`. ## Stopping the Application -To stop the application and remove the containers, networks, and volumes defined in docker-compose.yml, run the following command: +To stop the application and remove the containers, networks, and volumes defined in `docker-compose.yml`, run: ```bash docker-compose down -``` \ No newline at end of file +docker volume rm db_data +``` diff --git a/examples/golang-api-with-postgres-and-sqlc/compose.yml b/examples/golang-api-with-postgres-and-sqlc/compose.yml index f55704b..3667cea 100644 --- a/examples/golang-api-with-postgres-and-sqlc/compose.yml +++ b/examples/golang-api-with-postgres-and-sqlc/compose.yml @@ -8,7 +8,9 @@ services: POSTGRES_USER: postgres POSTGRES_DB: poc volumes: - - ./data/db:/var/lib/postgresql/data + - db_data:/var/lib/postgresql/data + ports: + - "5432:5432" go_api: container_name: go_api build: . @@ -16,3 +18,6 @@ services: - db ports: - "8080:8080" +volumes: + db_data: + name: db_data \ No newline at end of file diff --git a/examples/golang-api-with-postgres-and-sqlc/db/database.go b/examples/golang-api-with-postgres-and-sqlc/db/database.go index e85b152..dceea2c 100644 --- a/examples/golang-api-with-postgres-and-sqlc/db/database.go +++ b/examples/golang-api-with-postgres-and-sqlc/db/database.go @@ -2,36 +2,35 @@ package db import ( "context" - "fmt" - "go-postgres-sqlc/users" + "go-postgres-sqlc/db/sqlc" + "log" "github.com/jackc/pgx/v5" ) -var dsn = "host=db user=postgres password=postgres dbname=poc port=5432 sslmode=disable" +// DB holds the database connection configuration +type DB struct { + DSN string + Context context.Context + Conn *pgx.Conn + Queries *sqlc.Queries +} -var Context = context.Background() -var db = func() (db *pgx.Conn) { - conn, err := pgx.Connect(Context, dsn) +// New initializes a new DBConfig instance +func New(dsn string) *DB { + ctx := context.Background() + conn, err := pgx.Connect(ctx, dsn) if err != nil { - panic(err) + log.Fatalf("Unable to connect to database: %v", err) } - fmt.Println("Connected to database") - return conn -}() + log.Println("Connected to database") -var Queries = users.New(db) + queries := sqlc.New(conn) -func CreateUserTable() { - query := `CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - username VARCHAR(30) NOT NULL, - password VARCHAR(100) NOT NULL, - email VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )` - _, err := db.Exec(Context, query) - if err != nil { - panic(err) + return &DB{ + DSN: dsn, + Context: ctx, + Conn: conn, + Queries: queries, } } diff --git a/examples/golang-api-with-postgres-and-sqlc/db/migrations/schema.sql b/examples/golang-api-with-postgres-and-sqlc/db/migrations/schema.sql new file mode 100644 index 0000000..f3f9c3a --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/db/migrations/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE users ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + username VARCHAR(30) NOT NULL, + password VARCHAR(100) NOT NULL, + email VARCHAR(50), + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/examples/golang-api-with-postgres-and-sqlc/db/query/user.sql b/examples/golang-api-with-postgres-and-sqlc/db/query/user.sql new file mode 100644 index 0000000..ab046a6 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/db/query/user.sql @@ -0,0 +1,29 @@ +-- name: GetUser :one +SELECT + id, + username, + email, + created_at +FROM users +WHERE id = @user_id +LIMIT 1; + +-- name: ListUsers :many +SELECT + id, + username, + email, + created_at +FROM users +ORDER BY username; + +-- name: CreateUser :one +INSERT INTO users ( + username, + password, + email +) VALUES ( + @username, + @password, + @email +) RETURNING *; diff --git a/examples/golang-api-with-postgres-and-sqlc/db/sqlc/db.go b/examples/golang-api-with-postgres-and-sqlc/db/sqlc/db.go new file mode 100644 index 0000000..278c210 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/db/sqlc/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package sqlc + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/examples/golang-api-with-postgres-and-sqlc/db/sqlc/models.go b/examples/golang-api-with-postgres-and-sqlc/db/sqlc/models.go new file mode 100644 index 0000000..7a52bce --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/db/sqlc/models.go @@ -0,0 +1,19 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package sqlc + +import ( + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +type User struct { + ID int64 + Username string + Password string + Email pgtype.Text + CreatedAt time.Time +} diff --git a/examples/golang-api-with-postgres-and-sqlc/db/sqlc/querier.go b/examples/golang-api-with-postgres-and-sqlc/db/sqlc/querier.go new file mode 100644 index 0000000..17361d7 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/db/sqlc/querier.go @@ -0,0 +1,17 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package sqlc + +import ( + "context" +) + +type Querier interface { + CreateUser(ctx context.Context, arg CreateUserParams) (User, error) + GetUser(ctx context.Context, userID int64) (GetUserRow, error) + ListUsers(ctx context.Context) ([]ListUsersRow, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/examples/golang-api-with-postgres-and-sqlc/db/sqlc/user.sql.go b/examples/golang-api-with-postgres-and-sqlc/db/sqlc/user.sql.go new file mode 100644 index 0000000..c7c8857 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/db/sqlc/user.sql.go @@ -0,0 +1,116 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: user.sql + +package sqlc + +import ( + "context" + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users ( + username, + password, + email +) VALUES ( + $1, + $2, + $3 +) RETURNING id, username, password, email, created_at +` + +type CreateUserParams struct { + Username string + Password string + Email pgtype.Text +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRow(ctx, createUser, arg.Username, arg.Password, arg.Email) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Password, + &i.Email, + &i.CreatedAt, + ) + return i, err +} + +const getUser = `-- name: GetUser :one +SELECT + id, + username, + email, + created_at +FROM users +WHERE id = $1 +LIMIT 1 +` + +type GetUserRow struct { + ID int64 + Username string + Email pgtype.Text + CreatedAt time.Time +} + +func (q *Queries) GetUser(ctx context.Context, userID int64) (GetUserRow, error) { + row := q.db.QueryRow(ctx, getUser, userID) + var i GetUserRow + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.CreatedAt, + ) + return i, err +} + +const listUsers = `-- name: ListUsers :many +SELECT + id, + username, + email, + created_at +FROM users +ORDER BY username +` + +type ListUsersRow struct { + ID int64 + Username string + Email pgtype.Text + CreatedAt time.Time +} + +func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) { + rows, err := q.db.Query(ctx, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListUsersRow{} + for rows.Next() { + var i ListUsersRow + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/examples/golang-api-with-postgres-and-sqlc/go.mod b/examples/golang-api-with-postgres-and-sqlc/go.mod index f768bf3..4630e55 100644 --- a/examples/golang-api-with-postgres-and-sqlc/go.mod +++ b/examples/golang-api-with-postgres-and-sqlc/go.mod @@ -1,15 +1,15 @@ module go-postgres-sqlc -go 1.22.0 +go 1.22.2 -require github.com/jackc/pgx v3.6.2+incompatible +require ( + github.com/gorilla/mux v1.8.1 + github.com/jackc/pgx/v5 v5.5.5 +) require ( - github.com/gorilla/mux v1.8.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.5.3 // indirect - github.com/pkg/errors v0.9.1 // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/examples/golang-api-with-postgres-and-sqlc/go.sum b/examples/golang-api-with-postgres-and-sqlc/go.sum index 098951d..b2d223e 100644 --- a/examples/golang-api-with-postgres-and-sqlc/go.sum +++ b/examples/golang-api-with-postgres-and-sqlc/go.sum @@ -1,23 +1,30 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= -github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= -github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= -github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go b/examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go index 226dd26..e655b28 100644 --- a/examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go +++ b/examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go @@ -2,41 +2,158 @@ package handlers import ( "encoding/json" + "fmt" "go-postgres-sqlc/db" - "go-postgres-sqlc/users" + "go-postgres-sqlc/db/sqlc" "net/http" "strconv" + "time" "github.com/gorilla/mux" + "github.com/jackc/pgx/v5/pgtype" ) -func ListUsers(res http.ResponseWriter, req *http.Request) { - users, err := db.Queries.ListUsers(db.Context) +// Handlers holds the dependencies for the HTTP handlers. +type Handlers struct { + DB *db.DB +} + +// New initializes a new Handlers instance. +func New(db *db.DB) *Handlers { + return &Handlers{ + DB: db, + } +} + +type ListUsersResponse struct { + Users []User `json:"users"` +} +type User struct { + ID int64 `json:"id"` + Username string `json:"username"` + Email string `json:"email,omitempty"` + CreatedAt time.Time `json:"createdAt"` +} + +// ListUsers handles listing all users. +func (h *Handlers) ListUsers(rw http.ResponseWriter, r *http.Request) { + users, err := h.DB.Queries.ListUsers(r.Context()) if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) + writeError(rw, err, http.StatusInternalServerError) return } - json.NewEncoder(res).Encode(users) + usersResponse := make([]User, len(users)) + for i, user := range users { + usersResponse[i] = User{ + ID: user.ID, + Username: user.Username, + Email: user.Email.String, + CreatedAt: user.CreatedAt, + } + } + writeResponse(rw, http.StatusOK, ListUsersResponse{Users: usersResponse}) } -func GetUser(res http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) - id, _ := strconv.Atoi(vars["id"]) - user, err := db.Queries.GetUser(db.Context, int32(id)) +type GetUserResponse User + +// GetUser handles retrieving a user by ID. +func (h *Handlers) GetUser(rw http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.ParseInt(vars["id"], 10, 64) + if err != nil { + writeError(rw, fmt.Errorf("invalid user ID: %v", err), http.StatusBadRequest) + return + } + user, err := h.DB.Queries.GetUser(r.Context(), id) if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) + writeError(rw, err, http.StatusInternalServerError) return } - json.NewEncoder(res).Encode(user) + userResponse := GetUserResponse{ + ID: user.ID, + Username: user.Username, + Email: user.Email.String, + CreatedAt: user.CreatedAt, + } + writeResponse(rw, http.StatusOK, userResponse) +} + +type CreateUserRequest struct { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email,omitempty"` +} +type CreateUserResponse struct { + ID int64 `json:"id"` + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email,omitempty"` + CreatedAt time.Time `json:"createdAt"` } -func CreateUser(res http.ResponseWriter, req *http.Request) { - user := users.User{} - err := json.NewDecoder(req.Body).Decode(&user) - db.Queries.CreateUser(db.Context, users.CreateUserParams{Username: user.Username, Password: user.Password, Email: user.Email}) +// CreateUser handles creating a new user. +func (h *Handlers) CreateUser(rw http.ResponseWriter, r *http.Request) { + var user CreateUserRequest + err := json.NewDecoder(r.Body).Decode(&user) if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) + writeError(rw, err, http.StatusUnprocessableEntity) return } - json.NewEncoder(res).Encode(user) + newUser, err := h.DB.Queries.CreateUser(r.Context(), sqlc.CreateUserParams{ + Username: user.Username, + Password: user.Password, + Email: NewText(user.Email), + }) + if err != nil { + writeError(rw, err, http.StatusInternalServerError) + return + } + createUserResponse := CreateUserResponse{ + ID: newUser.ID, + Username: newUser.Username, + Password: newUser.Password, + Email: newUser.Email.String, + CreatedAt: newUser.CreatedAt, + } + writeResponse(rw, http.StatusCreated, createUserResponse) +} + +// writeResponse writes the response as JSON to the ResponseWriter. +func writeResponse(rw http.ResponseWriter, statusCode int, data interface{}) { + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(statusCode) + if data != nil { + err := json.NewEncoder(rw).Encode(data) + if err != nil { + http.Error(rw, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) + } + } +} + +type ErrorResponse struct { + Message string `json:"message"` + StatusCode int `json:"statusCode"` +} + +// writeError writes an error response to the ResponseWriter. +func writeError(rw http.ResponseWriter, err error, statusCode int) { + rw.Header().Set("Content-Type", "application/json+problem") + rw.WriteHeader(statusCode) + err = json.NewEncoder(rw).Encode(ErrorResponse{ + Message: err.Error(), + StatusCode: statusCode, + }) + if err != nil { + http.Error(rw, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) + } +} + +func NewText(s string) pgtype.Text { + if s == "" { + return pgtype.Text{} + } + return pgtype.Text{ + String: s, + Valid: true, + } } diff --git a/examples/golang-api-with-postgres-and-sqlc/main.go b/examples/golang-api-with-postgres-and-sqlc/main.go index 5f4f52f..aad23ee 100644 --- a/examples/golang-api-with-postgres-and-sqlc/main.go +++ b/examples/golang-api-with-postgres-and-sqlc/main.go @@ -1,6 +1,7 @@ package main import ( + "go-postgres-sqlc/db" "go-postgres-sqlc/handlers" "log" "net/http" @@ -9,13 +10,35 @@ import ( ) func main() { - db.CreateUserTable() + db := db.New("postgres://postgres:postgres@db:5432/poc?sslmode=disable") + runMigrations(db) - mux := mux.NewRouter() - mux.HandleFunc("/api/user", handlers.ListUsers).Methods("GET") - mux.HandleFunc("/api/user/{id:[0-9]+}", handlers.GetUser).Methods("GET") - mux.HandleFunc("/api/user", handlers.CreateUser).Methods("POST") + handlers := handlers.New(db) - log.Fatal(http.ListenAndServe(":8080", mux)) + router := mux.NewRouter() + base := router.PathPrefix("/api").Subrouter() + base.HandleFunc("/user", handlers.ListUsers).Methods(http.MethodGet) + base.HandleFunc("/user/{id:[0-9]+}", handlers.GetUser).Methods(http.MethodGet) + base.HandleFunc("/user", handlers.CreateUser).Methods(http.MethodPost) + + err := http.ListenAndServe(":8080", base) + if err != nil { + log.Fatal(err) + } +} + +func runMigrations(db *db.DB) { + query := `CREATE TABLE IF NOT EXISTS users ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + username VARCHAR(30) NOT NULL, + password VARCHAR(100) NOT NULL, + email VARCHAR(50), + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + )` + _, err := db.Conn.Exec(db.Context, query) + if err != nil { + log.Fatalf("Failed to create users table: %v", err) + } + log.Println("Users table created or already exists.") } diff --git a/examples/golang-api-with-postgres-and-sqlc/query.sql b/examples/golang-api-with-postgres-and-sqlc/query.sql deleted file mode 100644 index 1dc0a79..0000000 --- a/examples/golang-api-with-postgres-and-sqlc/query.sql +++ /dev/null @@ -1,14 +0,0 @@ --- name: GetUser :one -SELECT * FROM users -WHERE id = $1 LIMIT 1; - --- name: ListUsers :many -SELECT * FROM users -ORDER BY username; - --- name: CreateUser :one -INSERT INTO users ( - username, password, email -) VALUES ( - $1, $2, $3 -) RETURNING *; diff --git a/examples/golang-api-with-postgres-and-sqlc/schema.sql b/examples/golang-api-with-postgres-and-sqlc/schema.sql deleted file mode 100644 index 16e44c5..0000000 --- a/examples/golang-api-with-postgres-and-sqlc/schema.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - username VARCHAR(30) NOT NULL, - password VARCHAR(100) NOT NULL, - email VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); diff --git a/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml b/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml index 0897b55..09814ab 100644 --- a/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml +++ b/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml @@ -1,10 +1,20 @@ version: "2" sql: - engine: "postgresql" - queries: "query.sql" - schema: "schema.sql" + queries: "./db/query/" + schema: "./db/migrations/" + database: + uri: postgresql://postgres:postgres@localhost:5432/poc gen: go: - package: "users" - out: "users" + package: "sqlc" + out: "db/sqlc" sql_package: "pgx/v5" # you can use "database/sql" or "pgx/v5" + emit_json_tags: false + emit_interface: true + emit_empty_slices: true + overrides: + - db_type: "timestamptz" + go_type: + import: "time" + type: "Time" \ No newline at end of file From 9f1a61b8195449eb54a926842706100f7720b0bc Mon Sep 17 00:00:00 2001 From: Chelo Doz Date: Mon, 20 May 2024 12:40:20 -0300 Subject: [PATCH 05/11] refactor: add layers to separate logic --- .../handlers.go => handler/handler.go} | 65 ++++++-------- .../golang-api-with-postgres-and-sqlc/main.go | 15 ++-- .../model/model.go | 11 +++ .../repository/repository.go | 86 +++++++++++++++++++ .../service/service.go | 47 ++++++++++ 5 files changed, 181 insertions(+), 43 deletions(-) rename examples/golang-api-with-postgres-and-sqlc/{handlers/handlers.go => handler/handler.go} (71%) create mode 100644 examples/golang-api-with-postgres-and-sqlc/model/model.go create mode 100644 examples/golang-api-with-postgres-and-sqlc/repository/repository.go create mode 100644 examples/golang-api-with-postgres-and-sqlc/service/service.go diff --git a/examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go b/examples/golang-api-with-postgres-and-sqlc/handler/handler.go similarity index 71% rename from examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go rename to examples/golang-api-with-postgres-and-sqlc/handler/handler.go index e655b28..88d749c 100644 --- a/examples/golang-api-with-postgres-and-sqlc/handlers/handlers.go +++ b/examples/golang-api-with-postgres-and-sqlc/handler/handler.go @@ -1,27 +1,27 @@ -package handlers +package handler import ( "encoding/json" "fmt" - "go-postgres-sqlc/db" - "go-postgres-sqlc/db/sqlc" + "go-postgres-sqlc/model" "net/http" "strconv" "time" "github.com/gorilla/mux" - "github.com/jackc/pgx/v5/pgtype" + + "go-postgres-sqlc/service" ) -// Handlers holds the dependencies for the HTTP handlers. -type Handlers struct { - DB *db.DB +// UserHandler holds the dependencies for the HTTP handlers. +type UserHandler struct { + userSvc service.IUserService } -// New initializes a new Handlers instance. -func New(db *db.DB) *Handlers { - return &Handlers{ - DB: db, +// NewUser initializes a new Handlers instance. +func NewUser(userSvc service.IUserService) *UserHandler { + return &UserHandler{ + userSvc: userSvc, } } @@ -36,8 +36,8 @@ type User struct { } // ListUsers handles listing all users. -func (h *Handlers) ListUsers(rw http.ResponseWriter, r *http.Request) { - users, err := h.DB.Queries.ListUsers(r.Context()) +func (h *UserHandler) ListUsers(rw http.ResponseWriter, r *http.Request) { + users, err := h.userSvc.ListUsers(r.Context()) if err != nil { writeError(rw, err, http.StatusInternalServerError) return @@ -47,7 +47,7 @@ func (h *Handlers) ListUsers(rw http.ResponseWriter, r *http.Request) { usersResponse[i] = User{ ID: user.ID, Username: user.Username, - Email: user.Email.String, + Email: user.Email, CreatedAt: user.CreatedAt, } } @@ -57,14 +57,14 @@ func (h *Handlers) ListUsers(rw http.ResponseWriter, r *http.Request) { type GetUserResponse User // GetUser handles retrieving a user by ID. -func (h *Handlers) GetUser(rw http.ResponseWriter, r *http.Request) { +func (h *UserHandler) GetUser(rw http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.ParseInt(vars["id"], 10, 64) if err != nil { writeError(rw, fmt.Errorf("invalid user ID: %v", err), http.StatusBadRequest) return } - user, err := h.DB.Queries.GetUser(r.Context(), id) + user, err := h.userSvc.GetUser(r.Context(), id) if err != nil { writeError(rw, err, http.StatusInternalServerError) return @@ -72,7 +72,7 @@ func (h *Handlers) GetUser(rw http.ResponseWriter, r *http.Request) { userResponse := GetUserResponse{ ID: user.ID, Username: user.Username, - Email: user.Email.String, + Email: user.Email, CreatedAt: user.CreatedAt, } writeResponse(rw, http.StatusOK, userResponse) @@ -86,24 +86,24 @@ type CreateUserRequest struct { type CreateUserResponse struct { ID int64 `json:"id"` Username string `json:"username"` - Password string `json:"password"` Email string `json:"email,omitempty"` CreatedAt time.Time `json:"createdAt"` } // CreateUser handles creating a new user. -func (h *Handlers) CreateUser(rw http.ResponseWriter, r *http.Request) { - var user CreateUserRequest - err := json.NewDecoder(r.Body).Decode(&user) +func (h *UserHandler) CreateUser(rw http.ResponseWriter, r *http.Request) { + var createUserReq CreateUserRequest + err := json.NewDecoder(r.Body).Decode(&createUserReq) if err != nil { writeError(rw, err, http.StatusUnprocessableEntity) return } - newUser, err := h.DB.Queries.CreateUser(r.Context(), sqlc.CreateUserParams{ - Username: user.Username, - Password: user.Password, - Email: NewText(user.Email), - }) + user := model.User{ + Username: createUserReq.Username, + Password: createUserReq.Password, + Email: createUserReq.Email, + } + newUser, err := h.userSvc.CreateUser(r.Context(), user) if err != nil { writeError(rw, err, http.StatusInternalServerError) return @@ -111,8 +111,7 @@ func (h *Handlers) CreateUser(rw http.ResponseWriter, r *http.Request) { createUserResponse := CreateUserResponse{ ID: newUser.ID, Username: newUser.Username, - Password: newUser.Password, - Email: newUser.Email.String, + Email: newUser.Email, CreatedAt: newUser.CreatedAt, } writeResponse(rw, http.StatusCreated, createUserResponse) @@ -147,13 +146,3 @@ func writeError(rw http.ResponseWriter, err error, statusCode int) { http.Error(rw, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) } } - -func NewText(s string) pgtype.Text { - if s == "" { - return pgtype.Text{} - } - return pgtype.Text{ - String: s, - Valid: true, - } -} diff --git a/examples/golang-api-with-postgres-and-sqlc/main.go b/examples/golang-api-with-postgres-and-sqlc/main.go index aad23ee..2436b54 100644 --- a/examples/golang-api-with-postgres-and-sqlc/main.go +++ b/examples/golang-api-with-postgres-and-sqlc/main.go @@ -2,10 +2,13 @@ package main import ( "go-postgres-sqlc/db" - "go-postgres-sqlc/handlers" + "go-postgres-sqlc/handler" "log" "net/http" + "go-postgres-sqlc/repository" + "go-postgres-sqlc/service" + "github.com/gorilla/mux" ) @@ -13,14 +16,16 @@ func main() { db := db.New("postgres://postgres:postgres@db:5432/poc?sslmode=disable") runMigrations(db) - handlers := handlers.New(db) + userRepo := repository.NewUser(db) + userSvc := service.NewUser(userRepo) + userHandler := handler.NewUser(userSvc) router := mux.NewRouter() base := router.PathPrefix("/api").Subrouter() - base.HandleFunc("/user", handlers.ListUsers).Methods(http.MethodGet) - base.HandleFunc("/user/{id:[0-9]+}", handlers.GetUser).Methods(http.MethodGet) - base.HandleFunc("/user", handlers.CreateUser).Methods(http.MethodPost) + base.HandleFunc("/user", userHandler.ListUsers).Methods(http.MethodGet) + base.HandleFunc("/user/{id:[0-9]+}", userHandler.GetUser).Methods(http.MethodGet) + base.HandleFunc("/user", userHandler.CreateUser).Methods(http.MethodPost) err := http.ListenAndServe(":8080", base) if err != nil { diff --git a/examples/golang-api-with-postgres-and-sqlc/model/model.go b/examples/golang-api-with-postgres-and-sqlc/model/model.go new file mode 100644 index 0000000..51a1051 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/model/model.go @@ -0,0 +1,11 @@ +package model + +import "time" + +type User struct { + ID int64 + Username string + Password string + Email string + CreatedAt time.Time +} diff --git a/examples/golang-api-with-postgres-and-sqlc/repository/repository.go b/examples/golang-api-with-postgres-and-sqlc/repository/repository.go new file mode 100644 index 0000000..32c64c9 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/repository/repository.go @@ -0,0 +1,86 @@ +package repository + +import ( + "context" + "go-postgres-sqlc/db" + "go-postgres-sqlc/db/sqlc" + "go-postgres-sqlc/model" + + "github.com/jackc/pgx/v5/pgtype" +) + +type IUserRepository interface { + CreateUser(ctx context.Context, user model.User) (model.User, error) + GetUser(ctx context.Context, userID int64) (model.User, error) + ListUsers(ctx context.Context) ([]model.User, error) +} + +type UserRepository struct { + db *db.DB +} + +func NewUser(db *db.DB) IUserRepository { + return &UserRepository{ + db: db, + } +} + +func (r *UserRepository) CreateUser(ctx context.Context, user model.User) (model.User, error) { + userRepo, err := r.db.Queries.CreateUser(ctx, sqlc.CreateUserParams{ + Username: user.Username, + Password: user.Password, + Email: NewText(user.Email), + }) + if err != nil { + return model.User{}, err + } + newUser := model.User{ + ID: userRepo.ID, + Username: userRepo.Username, + Email: userRepo.Email.String, + CreatedAt: userRepo.CreatedAt, + } + return newUser, nil +} + +func (r *UserRepository) GetUser(ctx context.Context, userID int64) (model.User, error) { + userRepo, err := r.db.Queries.GetUser(ctx, userID) + if err != nil { + return model.User{}, err + } + user := model.User{ + ID: userRepo.ID, + Username: userRepo.Username, + Email: userRepo.Email.String, + CreatedAt: userRepo.CreatedAt, + } + return user, nil +} + +func (r *UserRepository) ListUsers(ctx context.Context) ([]model.User, error) { + var users []model.User + usersRepo, err := r.db.Queries.ListUsers(ctx) + if err != nil { + return users, err + } + for _, userRepo := range usersRepo { + user := model.User{ + ID: userRepo.ID, + Username: userRepo.Username, + Email: userRepo.Email.String, + CreatedAt: userRepo.CreatedAt, + } + users = append(users, user) + } + return users, nil +} + +func NewText(s string) pgtype.Text { + if s == "" { + return pgtype.Text{} + } + return pgtype.Text{ + String: s, + Valid: true, + } +} diff --git a/examples/golang-api-with-postgres-and-sqlc/service/service.go b/examples/golang-api-with-postgres-and-sqlc/service/service.go new file mode 100644 index 0000000..66454e0 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/service/service.go @@ -0,0 +1,47 @@ +package service + +import ( + "context" + "go-postgres-sqlc/model" + "go-postgres-sqlc/repository" +) + +type IUserService interface { + CreateUser(ctx context.Context, user model.User) (model.User, error) + GetUser(ctx context.Context, userID int64) (model.User, error) + ListUsers(ctx context.Context) ([]model.User, error) +} + +type UserService struct { + repo repository.IUserRepository +} + +func NewUser(repo repository.IUserRepository) IUserService { + return &UserService{ + repo: repo, + } +} + +func (s *UserService) CreateUser(ctx context.Context, user model.User) (model.User, error) { + newUser, err := s.repo.CreateUser(ctx, user) + if err != nil { + return model.User{}, err + } + return newUser, nil +} + +func (s *UserService) GetUser(ctx context.Context, userID int64) (model.User, error) { + user, err := s.repo.GetUser(ctx, userID) + if err != nil { + return model.User{}, err + } + return user, nil +} + +func (s *UserService) ListUsers(ctx context.Context) ([]model.User, error) { + users, err := s.repo.ListUsers(ctx) + if err != nil { + return nil, err + } + return users, nil +} From 7fe166dc742ba772c10627e64f1f84701b5e1851 Mon Sep 17 00:00:00 2001 From: Chelo Doz Date: Mon, 20 May 2024 12:47:45 -0300 Subject: [PATCH 06/11] fix: format issues in .gitignore and dockerfile --- examples/golang-api-with-postgres-and-sqlc/.gitignore | 2 +- examples/golang-api-with-postgres-and-sqlc/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/golang-api-with-postgres-and-sqlc/.gitignore b/examples/golang-api-with-postgres-and-sqlc/.gitignore index a49c5a0..6f6f5e6 100644 --- a/examples/golang-api-with-postgres-and-sqlc/.gitignore +++ b/examples/golang-api-with-postgres-and-sqlc/.gitignore @@ -19,4 +19,4 @@ # Go workspace file go.work -go.work.sum \ No newline at end of file +go.work.sum diff --git a/examples/golang-api-with-postgres-and-sqlc/Dockerfile b/examples/golang-api-with-postgres-and-sqlc/Dockerfile index e7c0ab7..e49fbfb 100644 --- a/examples/golang-api-with-postgres-and-sqlc/Dockerfile +++ b/examples/golang-api-with-postgres-and-sqlc/Dockerfile @@ -14,7 +14,7 @@ COPY . . RUN go build -v -o /go/bin/app . # Final stage -FROM gcr.io/distroless/base-debian12 +FROM gcr.io/distroless/base-debian12:latest # Copy the compiled binary from the build stage COPY --from=builder /go/bin/app / From 39d38a0ac0a03264337494f37b9be6d64a8d8637 Mon Sep 17 00:00:00 2001 From: Chelo Doz Date: Mon, 20 May 2024 15:08:27 -0300 Subject: [PATCH 07/11] fix: add password hashing --- .../service/service.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/golang-api-with-postgres-and-sqlc/service/service.go b/examples/golang-api-with-postgres-and-sqlc/service/service.go index 66454e0..6fd11d3 100644 --- a/examples/golang-api-with-postgres-and-sqlc/service/service.go +++ b/examples/golang-api-with-postgres-and-sqlc/service/service.go @@ -2,6 +2,8 @@ package service import ( "context" + "crypto/md5" + "encoding/hex" "go-postgres-sqlc/model" "go-postgres-sqlc/repository" ) @@ -23,6 +25,7 @@ func NewUser(repo repository.IUserRepository) IUserService { } func (s *UserService) CreateUser(ctx context.Context, user model.User) (model.User, error) { + user.Password = GetMd5(user.Password) newUser, err := s.repo.CreateUser(ctx, user) if err != nil { return model.User{}, err @@ -45,3 +48,10 @@ func (s *UserService) ListUsers(ctx context.Context) ([]model.User, error) { } return users, nil } + +func GetMd5(input string) string { + hash := md5.New() + defer hash.Reset() + hash.Write([]byte(input)) + return hex.EncodeToString(hash.Sum(nil)) +} From b31ae51267338182d63919c076e4573be14f123f Mon Sep 17 00:00:00 2001 From: Chelo Doz Date: Mon, 20 May 2024 15:08:53 -0300 Subject: [PATCH 08/11] fix: error response header --- examples/golang-api-with-postgres-and-sqlc/handler/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/golang-api-with-postgres-and-sqlc/handler/handler.go b/examples/golang-api-with-postgres-and-sqlc/handler/handler.go index 88d749c..297d2f9 100644 --- a/examples/golang-api-with-postgres-and-sqlc/handler/handler.go +++ b/examples/golang-api-with-postgres-and-sqlc/handler/handler.go @@ -136,7 +136,7 @@ type ErrorResponse struct { // writeError writes an error response to the ResponseWriter. func writeError(rw http.ResponseWriter, err error, statusCode int) { - rw.Header().Set("Content-Type", "application/json+problem") + rw.Header().Set("Content-Type", "application/problem+json") rw.WriteHeader(statusCode) err = json.NewEncoder(rw).Encode(ErrorResponse{ Message: err.Error(), From 80835fa2ae2b19b942d805b4f7333aba8575c552 Mon Sep 17 00:00:00 2001 From: Chelo Doz Date: Mon, 20 May 2024 15:09:50 -0300 Subject: [PATCH 09/11] fix: NewText return --- .../repository/repository.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/examples/golang-api-with-postgres-and-sqlc/repository/repository.go b/examples/golang-api-with-postgres-and-sqlc/repository/repository.go index 32c64c9..18ff91e 100644 --- a/examples/golang-api-with-postgres-and-sqlc/repository/repository.go +++ b/examples/golang-api-with-postgres-and-sqlc/repository/repository.go @@ -76,11 +76,5 @@ func (r *UserRepository) ListUsers(ctx context.Context) ([]model.User, error) { } func NewText(s string) pgtype.Text { - if s == "" { - return pgtype.Text{} - } - return pgtype.Text{ - String: s, - Valid: true, - } + return pgtype.Text{String: s, Valid: s != ""} } From a30be161207fe35aa5fb9ac2700b00a8c47336b0 Mon Sep 17 00:00:00 2001 From: Chelo Doz Date: Tue, 21 May 2024 12:00:05 -0300 Subject: [PATCH 10/11] fix: add .sqlfluff file --- examples/golang-api-with-postgres-and-sqlc/.sqlfluff | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 examples/golang-api-with-postgres-and-sqlc/.sqlfluff diff --git a/examples/golang-api-with-postgres-and-sqlc/.sqlfluff b/examples/golang-api-with-postgres-and-sqlc/.sqlfluff new file mode 100644 index 0000000..7fb4ec3 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/.sqlfluff @@ -0,0 +1,2 @@ +[sqlfluff] +dialect = postgres From 44ad8f0a1365dc376221072092bb61836530525f Mon Sep 17 00:00:00 2001 From: Chelo Doz Date: Tue, 21 May 2024 12:12:25 -0300 Subject: [PATCH 11/11] fix: error handling in db conn --- .../db/database.go | 7 ++++--- .../golang-api-with-postgres-and-sqlc/main.go | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/examples/golang-api-with-postgres-and-sqlc/db/database.go b/examples/golang-api-with-postgres-and-sqlc/db/database.go index dceea2c..a172152 100644 --- a/examples/golang-api-with-postgres-and-sqlc/db/database.go +++ b/examples/golang-api-with-postgres-and-sqlc/db/database.go @@ -2,6 +2,7 @@ package db import ( "context" + "fmt" "go-postgres-sqlc/db/sqlc" "log" @@ -17,11 +18,11 @@ type DB struct { } // New initializes a new DBConfig instance -func New(dsn string) *DB { +func New(dsn string) (*DB, error) { ctx := context.Background() conn, err := pgx.Connect(ctx, dsn) if err != nil { - log.Fatalf("Unable to connect to database: %v", err) + return nil, fmt.Errorf("unable to connect to database: %v", err) } log.Println("Connected to database") @@ -32,5 +33,5 @@ func New(dsn string) *DB { Context: ctx, Conn: conn, Queries: queries, - } + }, nil } diff --git a/examples/golang-api-with-postgres-and-sqlc/main.go b/examples/golang-api-with-postgres-and-sqlc/main.go index 2436b54..cbaedb1 100644 --- a/examples/golang-api-with-postgres-and-sqlc/main.go +++ b/examples/golang-api-with-postgres-and-sqlc/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "go-postgres-sqlc/db" "go-postgres-sqlc/handler" "log" @@ -13,9 +14,14 @@ import ( ) func main() { - db := db.New("postgres://postgres:postgres@db:5432/poc?sslmode=disable") - runMigrations(db) - + db, err := db.New("postgres://postgres:postgres@db:5432/poc?sslmode=disable") + if err != nil { + log.Fatal(err) + } + err = runMigrations(db) + if err != nil { + log.Fatal(err) + } userRepo := repository.NewUser(db) userSvc := service.NewUser(userRepo) userHandler := handler.NewUser(userSvc) @@ -27,13 +33,13 @@ func main() { base.HandleFunc("/user/{id:[0-9]+}", userHandler.GetUser).Methods(http.MethodGet) base.HandleFunc("/user", userHandler.CreateUser).Methods(http.MethodPost) - err := http.ListenAndServe(":8080", base) + err = http.ListenAndServe(":8080", base) if err != nil { log.Fatal(err) } } -func runMigrations(db *db.DB) { +func runMigrations(db *db.DB) error { query := `CREATE TABLE IF NOT EXISTS users ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, username VARCHAR(30) NOT NULL, @@ -43,7 +49,8 @@ func runMigrations(db *db.DB) { )` _, err := db.Conn.Exec(db.Context, query) if err != nil { - log.Fatalf("Failed to create users table: %v", err) + return fmt.Errorf("failed to create users table: %v", err) } log.Println("Users table created or already exists.") + return nil }