From cf44a24da5e52a1258bf4d5b8e195df47bd97341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo?= <44348844+darioscrivano@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:09:30 -0300 Subject: [PATCH] go-api: Postgres & sqlc (#80) * go-api: Postgres & sqlc * linting * linting * refactor: first refactor of the project including ai suggestions * refactor: add layers to separate logic * fix: format issues in .gitignore and dockerfile * fix: add password hashing * fix: error response header * fix: NewText return * fix: add .sqlfluff file * fix: error handling in db conn --------- Co-authored-by: Chelo Doz --- .../.gitignore | 22 +++ .../.sqlfluff | 2 + .../Dockerfile | 26 +++ .../README.md | 56 +++++++ .../compose.yml | 23 +++ .../db/database.go | 37 +++++ .../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 | 15 ++ .../golang-api-with-postgres-and-sqlc/go.sum | 30 ++++ .../handler/handler.go | 148 ++++++++++++++++++ .../golang-api-with-postgres-and-sqlc/main.go | 56 +++++++ .../model/model.go | 11 ++ .../repository/repository.go | 80 ++++++++++ .../service/service.go | 57 +++++++ .../sqlc.yaml | 20 +++ 20 files changed, 803 insertions(+) create mode 100644 examples/golang-api-with-postgres-and-sqlc/.gitignore create mode 100644 examples/golang-api-with-postgres-and-sqlc/.sqlfluff 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/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 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/handler/handler.go create mode 100644 examples/golang-api-with-postgres-and-sqlc/main.go 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 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..6f6f5e6 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/.gitignore @@ -0,0 +1,22 @@ +# 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 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 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..e49fbfb --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/Dockerfile @@ -0,0 +1,26 @@ +# Build stage +FROM golang:1.22 AS builder + +WORKDIR /go/src/app + +# Pre-copy/cache go.mod and go.sum for pre-downloading dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the source code +COPY . . + +# Build the Go application +RUN go build -v -o /go/bin/app . + +# Final stage +FROM gcr.io/distroless/base-debian12:latest + +# Copy the compiled binary from the build stage +COPY --from=builder /go/bin/app / + +# Expose the application port +EXPOSE 8080 + +# 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 new file mode 100644 index 0000000..804b8bc --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/README.md @@ -0,0 +1,56 @@ +# Go API with 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` + +## 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. **Generate SQL Queries and Models with `sqlc`**: + + ```bash + sqlc generate + ``` + +4. **Build and Run the Docker Containers**: + + ```bash + docker-compose build + 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: + +```bash +docker-compose down +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 new file mode 100644 index 0000000..3667cea --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/compose.yml @@ -0,0 +1,23 @@ +version: "3" +services: + db: + container_name: db + image: postgres:16 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: poc + volumes: + - db_data:/var/lib/postgresql/data + ports: + - "5432:5432" + go_api: + container_name: go_api + build: . + depends_on: + - 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 new file mode 100644 index 0000000..a172152 --- /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/db/sqlc" + "log" + + "github.com/jackc/pgx/v5" +) + +// DB holds the database connection configuration +type DB struct { + DSN string + Context context.Context + Conn *pgx.Conn + Queries *sqlc.Queries +} + +// New initializes a new DBConfig instance +func New(dsn string) (*DB, error) { + ctx := context.Background() + conn, err := pgx.Connect(ctx, dsn) + if err != nil { + return nil, fmt.Errorf("unable to connect to database: %v", err) + } + log.Println("Connected to database") + + queries := sqlc.New(conn) + + return &DB{ + DSN: dsn, + Context: ctx, + Conn: conn, + Queries: queries, + }, nil +} 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 new file mode 100644 index 0000000..4630e55 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/go.mod @@ -0,0 +1,15 @@ +module go-postgres-sqlc + +go 1.22.2 + +require ( + github.com/gorilla/mux v1.8.1 + github.com/jackc/pgx/v5 v5.5.5 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // 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..b2d223e --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/go.sum @@ -0,0 +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/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/handler/handler.go b/examples/golang-api-with-postgres-and-sqlc/handler/handler.go new file mode 100644 index 0000000..297d2f9 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/handler/handler.go @@ -0,0 +1,148 @@ +package handler + +import ( + "encoding/json" + "fmt" + "go-postgres-sqlc/model" + "net/http" + "strconv" + "time" + + "github.com/gorilla/mux" + + "go-postgres-sqlc/service" +) + +// UserHandler holds the dependencies for the HTTP handlers. +type UserHandler struct { + userSvc service.IUserService +} + +// NewUser initializes a new Handlers instance. +func NewUser(userSvc service.IUserService) *UserHandler { + return &UserHandler{ + userSvc: userSvc, + } +} + +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 *UserHandler) ListUsers(rw http.ResponseWriter, r *http.Request) { + users, err := h.userSvc.ListUsers(r.Context()) + if err != nil { + writeError(rw, err, http.StatusInternalServerError) + return + } + usersResponse := make([]User, len(users)) + for i, user := range users { + usersResponse[i] = User{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + CreatedAt: user.CreatedAt, + } + } + writeResponse(rw, http.StatusOK, ListUsersResponse{Users: usersResponse}) +} + +type GetUserResponse User + +// GetUser handles retrieving a user by ID. +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.userSvc.GetUser(r.Context(), id) + if err != nil { + writeError(rw, err, http.StatusInternalServerError) + return + } + userResponse := GetUserResponse{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + 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"` + Email string `json:"email,omitempty"` + CreatedAt time.Time `json:"createdAt"` +} + +// CreateUser handles creating a new 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 + } + 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 + } + createUserResponse := CreateUserResponse{ + ID: newUser.ID, + Username: newUser.Username, + Email: newUser.Email, + 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/problem+json") + 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) + } +} 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..cbaedb1 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "go-postgres-sqlc/db" + "go-postgres-sqlc/handler" + "log" + "net/http" + + "go-postgres-sqlc/repository" + "go-postgres-sqlc/service" + + "github.com/gorilla/mux" +) + +func main() { + 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) + + router := mux.NewRouter() + base := router.PathPrefix("/api").Subrouter() + + 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 { + log.Fatal(err) + } +} + +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, + 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 { + return fmt.Errorf("failed to create users table: %v", err) + } + log.Println("Users table created or already exists.") + return 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..18ff91e --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/repository/repository.go @@ -0,0 +1,80 @@ +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 { + return pgtype.Text{String: s, Valid: s != ""} +} 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..6fd11d3 --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/service/service.go @@ -0,0 +1,57 @@ +package service + +import ( + "context" + "crypto/md5" + "encoding/hex" + "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) { + user.Password = GetMd5(user.Password) + 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 +} + +func GetMd5(input string) string { + hash := md5.New() + defer hash.Reset() + hash.Write([]byte(input)) + return hex.EncodeToString(hash.Sum(nil)) +} 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..09814ab --- /dev/null +++ b/examples/golang-api-with-postgres-and-sqlc/sqlc.yaml @@ -0,0 +1,20 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "./db/query/" + schema: "./db/migrations/" + database: + uri: postgresql://postgres:postgres@localhost:5432/poc + gen: + go: + 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