├── LICENSE ├── README.md ├── _examples ├── authors │ ├── README.md │ ├── pgx │ │ ├── Dockerfile │ │ ├── configs │ │ │ └── reflex.conf │ │ ├── docker-compose.yml │ │ ├── gen.sh │ │ ├── go.mod │ │ ├── go.sum │ │ ├── internal │ │ │ ├── authors │ │ │ │ ├── db.go │ │ │ │ ├── models.go │ │ │ │ ├── querier.go │ │ │ │ ├── queries.sql.go │ │ │ │ ├── routes.go │ │ │ │ ├── service.go │ │ │ │ └── service_factory.go │ │ │ └── server │ │ │ │ └── encoding.go │ │ ├── main.go │ │ ├── migration.go │ │ ├── openapi.yml │ │ ├── registry.go │ │ ├── sql │ │ │ ├── migrations │ │ │ │ ├── 001_authors.down.sql │ │ │ │ └── 001_authors.up.sql │ │ │ └── queries.sql │ │ └── sqlc.yaml │ ├── sqlite-frontend │ │ ├── README.md │ │ ├── gen.sh │ │ ├── go.mod │ │ ├── go.sum │ │ ├── internal │ │ │ ├── authors │ │ │ │ ├── db.go │ │ │ │ ├── models.go │ │ │ │ ├── queries.sql.go │ │ │ │ ├── routes.go │ │ │ │ ├── service.go │ │ │ │ └── service_factory.go │ │ │ └── server │ │ │ │ └── encoding.go │ │ ├── main.go │ │ ├── migration.go │ │ ├── openapi.yml │ │ ├── registry.go │ │ ├── sql │ │ │ ├── migrations │ │ │ │ └── 001_authors.sql │ │ │ └── queries.sql │ │ ├── sqlc.yaml │ │ └── view │ │ │ ├── breadcrumbs.go │ │ │ ├── content.go │ │ │ ├── etag │ │ │ └── etag.go │ │ │ ├── message.go │ │ │ ├── pagination.go │ │ │ ├── static │ │ │ ├── css │ │ │ │ ├── bootstrap-icons.css │ │ │ │ ├── bootstrap.min.css │ │ │ │ ├── fonts │ │ │ │ │ ├── bootstrap-icons.woff │ │ │ │ │ └── bootstrap-icons.woff2 │ │ │ │ └── style.css │ │ │ ├── js │ │ │ │ ├── bootstrap.bundle.min.js │ │ │ │ ├── htmx.min.js │ │ │ │ └── init.js │ │ │ └── swagger │ │ │ │ └── index.html │ │ │ ├── templates │ │ │ ├── app │ │ │ │ └── authors │ │ │ │ │ ├── create_author.html │ │ │ │ │ ├── delete_author.html │ │ │ │ │ ├── get_author.html │ │ │ │ │ ├── list_authors.html │ │ │ │ │ ├── update_author.html │ │ │ │ │ └── update_author_bio.html │ │ │ ├── authors.html │ │ │ ├── authors │ │ │ │ └── {id}.html │ │ │ ├── components │ │ │ │ ├── breadcrumbs.html │ │ │ │ ├── hx-context.html │ │ │ │ ├── message.html │ │ │ │ └── messages-context.html │ │ │ ├── index.html │ │ │ └── layout │ │ │ │ ├── base.html │ │ │ │ ├── content.html │ │ │ │ ├── footer.html │ │ │ │ └── header.html │ │ │ ├── view.go │ │ │ └── watcher │ │ │ └── watcher.go │ └── sqlite │ │ ├── Dockerfile │ │ ├── configs │ │ └── reflex.conf │ │ ├── docker-compose.yml │ │ ├── gen.sh │ │ ├── go.mod │ │ ├── go.sum │ │ ├── internal │ │ ├── authors │ │ │ ├── db.go │ │ │ ├── models.go │ │ │ ├── queries.sql.go │ │ │ ├── routes.go │ │ │ ├── service.go │ │ │ └── service_factory.go │ │ └── server │ │ │ ├── encoding.go │ │ │ ├── litefs │ │ │ ├── forward.go │ │ │ └── litefs.go │ │ │ └── litestream │ │ │ └── litestream.go │ │ ├── main.go │ │ ├── migration.go │ │ ├── openapi.yml │ │ ├── registry.go │ │ ├── sql │ │ ├── migrations │ │ │ └── 001_authors.sql │ │ └── queries.sql │ │ └── sqlc.yaml └── booktest │ ├── Dockerfile │ ├── README.md │ ├── configs │ ├── grafana │ │ ├── dashboards │ │ │ ├── dashboard.yml │ │ │ └── grpc-dashboard.json │ │ └── datasources │ │ │ └── datasource.yml │ ├── prometheus.yml │ └── reflex.conf │ ├── docker-compose.yml │ ├── gen.sh │ ├── go.mod │ ├── go.sum │ ├── internal │ ├── books │ │ ├── db.go │ │ ├── models.go │ │ ├── queries.sql.go │ │ ├── routes.go │ │ ├── service.go │ │ └── service_factory.go │ └── server │ │ ├── encoding.go │ │ └── instrumentation │ │ ├── metric │ │ └── metric.go │ │ └── trace │ │ └── tracing.go │ ├── main.go │ ├── openapi.yml │ ├── registry.go │ ├── sql │ ├── queries.sql │ └── schema.sql │ └── sqlc.yaml ├── engine.go ├── go.mod ├── go.sum ├── main.go ├── metadata ├── bind.go ├── converter.go ├── frontend │ ├── funcs.go │ └── service_ui.go └── openapi.go ├── templates ├── internal │ └── server │ │ ├── encoding.go.tmpl │ │ ├── instrumentation │ │ ├── metric │ │ │ └── metric.go.tmpl │ │ └── trace │ │ │ └── tracing.go.tmpl │ │ ├── litefs │ │ ├── forward.go.tmpl │ │ └── litefs.go.tmpl │ │ └── litestream │ │ └── litestream.go.tmpl ├── main.go.tmpl ├── migration.go.tmpl ├── openapi.yml.tmpl ├── registry.go.tmpl ├── routes.go.tmpl ├── service.go.tmpl ├── service_factory.go.tmpl ├── templates.go └── view │ ├── breadcrumbs.go.tmpl │ ├── content.go.tmpl │ ├── etag │ └── etag.go │ ├── message.go.tmpl │ ├── pagination.go.tmpl │ ├── static │ ├── css │ │ ├── bootstrap-icons.css │ │ ├── bootstrap.min.css │ │ ├── fonts │ │ │ ├── bootstrap-icons.woff │ │ │ └── bootstrap-icons.woff2 │ │ └── style.css │ ├── js │ │ ├── bootstrap.bundle.min.js │ │ ├── htmx.min.js │ │ └── init.js │ └── swagger │ │ └── index.html.tmpl │ ├── templates │ ├── app │ │ ├── request.html.tmpl │ │ └── response.html.tmpl │ ├── components │ │ ├── breadcrumbs.html.tmpl │ │ ├── hx-context.html │ │ ├── message.html │ │ └── messages-context.html │ ├── index.html.tmpl │ └── layout │ │ ├── base.html │ │ ├── content.html │ │ ├── footer.html │ │ └── header.html.tmpl │ ├── view.go.tmpl │ └── watcher │ └── watcher.go.tmpl └── version.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Walter Wanderley 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.md: -------------------------------------------------------------------------------- 1 | ## sqlc-http 2 | 3 | Create a **net/http Server** from the generated code by the awesome [sqlc](https://sqlc.dev/) project. If you’re searching for a SQLC plugin, use [sqlc-gen-go-server](https://github.com/walterwanderley/sqlc-gen-go-server/). 4 | 5 | ### Requirements 6 | 7 | - Go 1.23 or superior 8 | - [sqlc](https://sqlc.dev/) 9 | 10 | ```sh 11 | go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest 12 | ``` 13 | 14 | ### Installation 15 | 16 | ```sh 17 | go install github.com/walterwanderley/sqlc-http@latest 18 | ``` 19 | 20 | ### Fullstack application example (HTMX) 21 | 22 | If you want to generate a complete application (including htmx frontend), check [this example](https://github.com/walterwanderley/sqlc-http/blob/main/_examples/authors/sqlite-frontend/README.md). 23 | 24 | ### Example 25 | 26 | 1. Create a queries.sql file: 27 | 28 | ```sql 29 | --queries.sql 30 | 31 | CREATE TABLE authors ( 32 | id BIGSERIAL PRIMARY KEY, 33 | name text NOT NULL, 34 | bio text, 35 | created_at TIMESTAMP 36 | ); 37 | 38 | -- name: GetAuthor :one 39 | -- http: GET /authors/{id} 40 | SELECT * FROM authors 41 | WHERE id = $1 LIMIT 1; 42 | 43 | -- name: ListAuthors :many 44 | -- http: GET /authors 45 | SELECT * FROM authors 46 | ORDER BY name; 47 | 48 | -- name: CreateAuthor :one 49 | -- http: POST /authors 50 | INSERT INTO authors ( 51 | name, bio, created_at 52 | ) VALUES ( 53 | $1, $2, $3 54 | ) 55 | RETURNING *; 56 | 57 | -- name: DeleteAuthor :exec 58 | -- http: DELETE /authors/{id} 59 | DELETE FROM authors 60 | WHERE id = $1; 61 | 62 | -- name: UpdateAuthorBio :exec 63 | -- http: PATCH /authors/{id}/bio 64 | UPDATE authors 65 | SET bio = $1 66 | WHERE id = $2; 67 | ``` 68 | 69 | 2. Create a sqlc.yaml file 70 | 71 | ```yaml 72 | version: "2" 73 | sql: 74 | - schema: "./queries.sql" 75 | queries: "./queries.sql" 76 | engine: "postgresql" 77 | gen: 78 | go: 79 | out: "internal/author" 80 | ``` 81 | 82 | 3. Execute sqlc 83 | 84 | ```sh 85 | sqlc generate 86 | ``` 87 | 88 | 4. Execute sqlc-http 89 | 90 | ```sh 91 | sqlc-http -m "mymodule" 92 | ``` 93 | 94 | If you want to generate the frontend (htmx): 95 | 96 | ```sh 97 | sqlc-http -m "mymodule" -frontend 98 | ``` 99 | 100 | 101 | 5. Run the generated server 102 | 103 | ```sh 104 | go run . -db [Database Connection URL] -dev 105 | ``` 106 | 107 | 6. Enjoy! 108 | 109 | If you do not generate the frontend in step 4? 110 | 111 | - Swagger UI: [http://localhost:5000/swagger](http://localhost:5000/swagger) 112 | 113 | If you generate the frontend in step 4: 114 | 115 | - [HTMX](https://htmx.org) frontend: [http://localhost:5000](http://localhost:5000) 116 | - Swagger UI: [http://localhost:5000/static/swagger](http://localhost:5000/static/swagger) 117 | 118 | ### Customizing HTTP endpoints 119 | 120 | You can customize the HTTP endpoints by adding comments to the queries. 121 | 122 | ```sql 123 | -- http: Method Path 124 | ``` 125 | 126 | Here’s an example of a queries file that has a custom HTTP endpoint: 127 | ```sql 128 | -- name: ListAuthors :many 129 | -- http: GET /authors 130 | SELECT * FROM authors 131 | ORDER BY name; 132 | 133 | -- name: UpdateAuthorBio :exec 134 | -- http: PATCH /authors/{id}/bio 135 | UPDATE authors 136 | SET bio = $1 137 | WHERE id = $2; 138 | ``` 139 | 140 | 141 | ### Editing the generated code 142 | 143 | - It's safe to edit any generated code that doesn't have the `DO NOT EDIT` indication at the very first line. 144 | 145 | - After modify a SQL file, execute these commands below: 146 | 147 | ```sh 148 | sqlc generate 149 | go generate 150 | ``` 151 | 152 | ### Similar Projects 153 | 154 | - [sqlc-connect](https://github.com/walterwanderley/sqlc-connect) 155 | - [sqlc-grpc](https://github.com/walterwanderley/sqlc-grpc) 156 | - [xo-grpc](https://github.com/walterwanderley/xo-grpc) -------------------------------------------------------------------------------- /_examples/authors/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Author example taken from [sqlc][sqlc] Git repository [examples][sqlc-git]. 4 | 5 | [sqlc]: https://sqlc.dev 6 | [sqlc-git]: https://github.com/sqlc-dev/sqlc/tree/main/examples/authors 7 | 8 | ## Running 9 | 10 | ```sh 11 | ./gen.sh 12 | docker compose up 13 | ``` 14 | 15 | ### Exploring the API 16 | 17 | http://localhost:8080/swagger 18 | 19 | -------------------------------------------------------------------------------- /_examples/authors/pgx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 2 | 3 | RUN go install github.com/cespare/reflex@latest 4 | 5 | WORKDIR /app 6 | COPY go.mod . 7 | COPY go.sum . 8 | 9 | RUN go mod download -x 10 | 11 | COPY configs/reflex.conf / 12 | 13 | ENTRYPOINT ["reflex", "-c", "/reflex.conf"] -------------------------------------------------------------------------------- /_examples/authors/pgx/configs/reflex.conf: -------------------------------------------------------------------------------- 1 | -r '(\.go$|go\.mod)' -s -- sh -c 'go run . -db postgres://postgres:secret@postgres:5432/postgres?sslmode=disable -port 8080 -dev' 2 | -------------------------------------------------------------------------------- /_examples/authors/pgx/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | ports: 6 | - "8080:8080" 7 | volumes: 8 | - .:/app 9 | depends_on: 10 | - postgres 11 | 12 | postgres: 13 | image: postgres 14 | environment: 15 | - POSTGRES_USER=postgres 16 | - POSTGRES_PASSWORD=secret 17 | -------------------------------------------------------------------------------- /_examples/authors/pgx/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -u 3 | set -e 4 | set -x 5 | 6 | go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest 7 | 8 | rm -rf internal proto api go.mod go.sum main.go registry.go openapi.yml 9 | 10 | sqlc generate 11 | sqlc-http -m authors -migration-path sql/migrations -migration-lib migrate 12 | -------------------------------------------------------------------------------- /_examples/authors/pgx/go.mod: -------------------------------------------------------------------------------- 1 | module authors 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/flowchartsman/swaggerui v0.0.0-20221017034628-909ed4f3701b 7 | github.com/go-playground/form/v4 v4.2.1 8 | github.com/golang-migrate/migrate/v4 v4.18.3 9 | github.com/jackc/pgx/v5 v5.7.5 10 | go.uber.org/automaxprocs v1.6.0 11 | ) 12 | 13 | require ( 14 | github.com/hashicorp/errwrap v1.1.0 // indirect 15 | github.com/hashicorp/go-multierror v1.1.1 // indirect 16 | github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect 17 | github.com/jackc/pgpassfile v1.0.0 // indirect 18 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 19 | github.com/jackc/puddle/v2 v2.2.2 // indirect 20 | go.uber.org/atomic v1.7.0 // indirect 21 | golang.org/x/crypto v0.37.0 // indirect 22 | golang.org/x/sync v0.13.0 // indirect 23 | golang.org/x/text v0.24.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /_examples/authors/pgx/internal/authors/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | 5 | package authors 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/jackc/pgx/v5" 11 | "github.com/jackc/pgx/v5/pgconn" 12 | ) 13 | 14 | type DBTX interface { 15 | Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) 16 | Query(context.Context, string, ...interface{}) (pgx.Rows, error) 17 | QueryRow(context.Context, string, ...interface{}) pgx.Row 18 | } 19 | 20 | func New() *Queries { 21 | return &Queries{} 22 | } 23 | 24 | type Queries struct { 25 | } 26 | -------------------------------------------------------------------------------- /_examples/authors/pgx/internal/authors/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | 5 | package authors 6 | 7 | import ( 8 | "github.com/jackc/pgx/v5/pgtype" 9 | ) 10 | 11 | type Authors struct { 12 | ID int64 `json:"id"` 13 | Name string `json:"name"` 14 | Bio pgtype.Text `json:"bio"` 15 | } 16 | -------------------------------------------------------------------------------- /_examples/authors/pgx/internal/authors/querier.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | 5 | package authors 6 | 7 | import ( 8 | "context" 9 | ) 10 | 11 | type Querier interface { 12 | CreateAuthor(ctx context.Context, db DBTX, arg *CreateAuthorParams) (*Authors, error) 13 | DeleteAuthor(ctx context.Context, db DBTX, id int64) error 14 | GetAuthor(ctx context.Context, db DBTX, id int64) (*Authors, error) 15 | ListAuthors(ctx context.Context, db DBTX) ([]*Authors, error) 16 | } 17 | 18 | var _ Querier = (*Queries)(nil) 19 | -------------------------------------------------------------------------------- /_examples/authors/pgx/internal/authors/queries.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | // source: queries.sql 5 | 6 | package authors 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/jackc/pgx/v5/pgtype" 12 | ) 13 | 14 | const CreateAuthor = `-- name: CreateAuthor :one 15 | INSERT INTO authors ( 16 | name, bio 17 | ) VALUES ( 18 | $1, $2 19 | ) 20 | RETURNING id, name, bio 21 | ` 22 | 23 | type CreateAuthorParams struct { 24 | Name string `json:"name"` 25 | Bio pgtype.Text `json:"bio"` 26 | } 27 | 28 | func (q *Queries) CreateAuthor(ctx context.Context, db DBTX, arg *CreateAuthorParams) (*Authors, error) { 29 | row := db.QueryRow(ctx, CreateAuthor, arg.Name, arg.Bio) 30 | var i Authors 31 | err := row.Scan(&i.ID, &i.Name, &i.Bio) 32 | return &i, err 33 | } 34 | 35 | const DeleteAuthor = `-- name: DeleteAuthor :exec 36 | DELETE FROM authors 37 | WHERE id = $1 38 | ` 39 | 40 | func (q *Queries) DeleteAuthor(ctx context.Context, db DBTX, id int64) error { 41 | _, err := db.Exec(ctx, DeleteAuthor, id) 42 | return err 43 | } 44 | 45 | const GetAuthor = `-- name: GetAuthor :one 46 | SELECT id, name, bio FROM authors 47 | WHERE id = $1 LIMIT 1 48 | ` 49 | 50 | func (q *Queries) GetAuthor(ctx context.Context, db DBTX, id int64) (*Authors, error) { 51 | row := db.QueryRow(ctx, GetAuthor, id) 52 | var i Authors 53 | err := row.Scan(&i.ID, &i.Name, &i.Bio) 54 | return &i, err 55 | } 56 | 57 | const ListAuthors = `-- name: ListAuthors :many 58 | SELECT id, name, bio FROM authors 59 | ORDER BY name 60 | ` 61 | 62 | func (q *Queries) ListAuthors(ctx context.Context, db DBTX) ([]*Authors, error) { 63 | rows, err := db.Query(ctx, ListAuthors) 64 | if err != nil { 65 | return nil, err 66 | } 67 | defer rows.Close() 68 | items := []*Authors{} 69 | for rows.Next() { 70 | var i Authors 71 | if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { 72 | return nil, err 73 | } 74 | items = append(items, &i) 75 | } 76 | if err := rows.Err(); err != nil { 77 | return nil, err 78 | } 79 | return items, nil 80 | } 81 | -------------------------------------------------------------------------------- /_examples/authors/pgx/internal/authors/routes.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package authors 4 | 5 | import "net/http" 6 | 7 | func (s *Service) RegisterHandlers(mux *http.ServeMux) { 8 | mux.HandleFunc("POST /author", s.handleCreateAuthor()) 9 | mux.HandleFunc("DELETE /author/{id}", s.handleDeleteAuthor()) 10 | mux.HandleFunc("GET /author/{id}", s.handleGetAuthor()) 11 | mux.HandleFunc("GET /authors", s.handleListAuthors()) 12 | } 13 | -------------------------------------------------------------------------------- /_examples/authors/pgx/internal/authors/service.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package authors 4 | 5 | import ( 6 | "log/slog" 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/jackc/pgx/v5/pgtype" 11 | "github.com/jackc/pgx/v5/pgxpool" 12 | 13 | "authors/internal/server" 14 | ) 15 | 16 | type Service struct { 17 | querier Querier 18 | db *pgxpool.Pool 19 | } 20 | 21 | func (s *Service) handleCreateAuthor() http.HandlerFunc { 22 | type request struct { 23 | Name string `json:"name"` 24 | Bio *string `json:"bio"` 25 | } 26 | type response struct { 27 | ID int64 `json:"id,omitempty"` 28 | Name string `json:"name,omitempty"` 29 | Bio *string `json:"bio,omitempty"` 30 | } 31 | 32 | return func(w http.ResponseWriter, r *http.Request) { 33 | req, err := server.Decode[request](r) 34 | if err != nil { 35 | http.Error(w, err.Error(), http.StatusUnprocessableEntity) 36 | return 37 | } 38 | arg := new(CreateAuthorParams) 39 | arg.Name = req.Name 40 | if req.Bio != nil { 41 | arg.Bio = pgtype.Text{Valid: true, String: *req.Bio} 42 | } 43 | 44 | result, err := s.querier.CreateAuthor(r.Context(), s.db, arg) 45 | if err != nil { 46 | slog.Error("sql call failed", "error", err, "method", "CreateAuthor") 47 | http.Error(w, err.Error(), http.StatusInternalServerError) 48 | return 49 | } 50 | 51 | var res response 52 | res.ID = result.ID 53 | res.Name = result.Name 54 | if result.Bio.Valid { 55 | res.Bio = &result.Bio.String 56 | } 57 | server.Encode(w, r, http.StatusOK, res) 58 | } 59 | } 60 | 61 | func (s *Service) handleDeleteAuthor() http.HandlerFunc { 62 | type request struct { 63 | Id int64 `json:"id"` 64 | } 65 | 66 | return func(w http.ResponseWriter, r *http.Request) { 67 | var req request 68 | if str := r.PathValue("id"); str != "" { 69 | if v, err := strconv.ParseInt(str, 10, 64); err != nil { 70 | http.Error(w, err.Error(), http.StatusBadRequest) 71 | return 72 | } else { 73 | req.Id = v 74 | } 75 | } 76 | id := req.Id 77 | 78 | err := s.querier.DeleteAuthor(r.Context(), s.db, id) 79 | if err != nil { 80 | slog.Error("sql call failed", "error", err, "method", "DeleteAuthor") 81 | http.Error(w, err.Error(), http.StatusInternalServerError) 82 | return 83 | } 84 | 85 | } 86 | } 87 | 88 | func (s *Service) handleGetAuthor() http.HandlerFunc { 89 | type request struct { 90 | Id int64 `json:"id"` 91 | } 92 | type response struct { 93 | ID int64 `json:"id,omitempty"` 94 | Name string `json:"name,omitempty"` 95 | Bio *string `json:"bio,omitempty"` 96 | } 97 | 98 | return func(w http.ResponseWriter, r *http.Request) { 99 | var req request 100 | if str := r.PathValue("id"); str != "" { 101 | if v, err := strconv.ParseInt(str, 10, 64); err != nil { 102 | http.Error(w, err.Error(), http.StatusBadRequest) 103 | return 104 | } else { 105 | req.Id = v 106 | } 107 | } 108 | id := req.Id 109 | 110 | result, err := s.querier.GetAuthor(r.Context(), s.db, id) 111 | if err != nil { 112 | slog.Error("sql call failed", "error", err, "method", "GetAuthor") 113 | http.Error(w, err.Error(), http.StatusInternalServerError) 114 | return 115 | } 116 | 117 | var res response 118 | res.ID = result.ID 119 | res.Name = result.Name 120 | if result.Bio.Valid { 121 | res.Bio = &result.Bio.String 122 | } 123 | server.Encode(w, r, http.StatusOK, res) 124 | } 125 | } 126 | 127 | func (s *Service) handleListAuthors() http.HandlerFunc { 128 | type response struct { 129 | ID int64 `json:"id,omitempty"` 130 | Name string `json:"name,omitempty"` 131 | Bio *string `json:"bio,omitempty"` 132 | } 133 | 134 | return func(w http.ResponseWriter, r *http.Request) { 135 | 136 | result, err := s.querier.ListAuthors(r.Context(), s.db) 137 | if err != nil { 138 | slog.Error("sql call failed", "error", err, "method", "ListAuthors") 139 | http.Error(w, err.Error(), http.StatusInternalServerError) 140 | return 141 | } 142 | 143 | res := make([]response, 0) 144 | for _, r := range result { 145 | var item response 146 | item.ID = r.ID 147 | item.Name = r.Name 148 | if r.Bio.Valid { 149 | item.Bio = &r.Bio.String 150 | } 151 | res = append(res, item) 152 | } 153 | server.Encode(w, r, http.StatusOK, res) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /_examples/authors/pgx/internal/authors/service_factory.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package authors 4 | 5 | import ( 6 | "github.com/jackc/pgx/v5/pgxpool" 7 | ) 8 | 9 | // NewService is a constructor of a interface { func RegisterHandlers(*http.ServeMux) } implementation. 10 | // Use this function to customize the server by adding middlewares to it. 11 | func NewService(querier Querier, db *pgxpool.Pool) *Service { 12 | return &Service{querier: querier, db: db} 13 | } 14 | -------------------------------------------------------------------------------- /_examples/authors/pgx/internal/server/encoding.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package server 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/go-playground/form/v4" 11 | ) 12 | 13 | var formDecoder *form.Decoder 14 | 15 | func init() { 16 | formDecoder = form.NewDecoder() 17 | formDecoder.SetTagName("json") 18 | 19 | } 20 | 21 | func Decode[T any](r *http.Request) (T, error) { 22 | var v T 23 | if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" { 24 | if err := r.ParseForm(); err != nil { 25 | return v, fmt.Errorf("parse form: %w", err) 26 | } 27 | if err := formDecoder.Decode(&v, r.Form); err != nil { 28 | return v, fmt.Errorf("decode form: %w", err) 29 | } 30 | } else { 31 | if err := json.NewDecoder(r.Body).Decode(&v); err != nil { 32 | return v, fmt.Errorf("decode json: %w", err) 33 | } 34 | } 35 | return v, nil 36 | } 37 | 38 | func Encode[T any](w http.ResponseWriter, r *http.Request, status int, v T) error { 39 | 40 | w.Header().Set("Content-Type", "application/json") 41 | w.WriteHeader(status) 42 | if err := json.NewEncoder(w).Encode(v); err != nil { 43 | return fmt.Errorf("encode json: %w", err) 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /_examples/authors/pgx/main.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | _ "embed" 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "log/slog" 13 | "net/http" 14 | "os" 15 | "os/signal" 16 | "runtime" 17 | "syscall" 18 | "time" 19 | 20 | "github.com/flowchartsman/swaggerui" 21 | "go.uber.org/automaxprocs/maxprocs" 22 | 23 | // database driver 24 | "github.com/jackc/pgx/v5/pgxpool" 25 | _ "github.com/jackc/pgx/v5/stdlib" 26 | ) 27 | 28 | //go:generate sqlc-http -m authors -migration-path sql/migrations -migration-lib migrate -append 29 | 30 | const serviceName = "authors" 31 | 32 | var ( 33 | dev bool 34 | dbURL string 35 | port int 36 | 37 | //go:embed openapi.yml 38 | openAPISpec []byte 39 | ) 40 | 41 | func main() { 42 | flag.StringVar(&dbURL, "db", "", "The Database connection URL") 43 | flag.IntVar(&port, "port", 5000, "The server port") 44 | 45 | flag.BoolVar(&dev, "dev", false, "Set logger to development mode") 46 | 47 | flag.Parse() 48 | 49 | initLogger() 50 | 51 | if err := run(); err != nil && !errors.Is(err, http.ErrServerClosed) { 52 | slog.Error("server error", "error", err) 53 | os.Exit(1) 54 | } 55 | } 56 | 57 | func run() error { 58 | _, err := maxprocs.Set() 59 | if err != nil { 60 | slog.Warn("startup", "error", err) 61 | } 62 | slog.Info("startup", "GOMAXPROCS", runtime.GOMAXPROCS(0)) 63 | 64 | db, err := pgxpool.New(context.Background(), dbURL) 65 | if err != nil { 66 | return err 67 | } 68 | defer db.Close() 69 | 70 | dbMigration, err := sql.Open("pgx", dbURL) 71 | if err != nil { 72 | return err 73 | } 74 | err = ensureSchema(dbMigration) 75 | if err != nil { 76 | slog.Error("migration error", "error", err) 77 | } 78 | dbMigration.Close() 79 | 80 | mux := http.NewServeMux() 81 | registerHandlers(mux, db) 82 | 83 | mux.Handle("GET /swagger/", http.StripPrefix("/swagger", swaggerui.Handler(openAPISpec))) 84 | 85 | server := &http.Server{ 86 | Addr: fmt.Sprintf(":%d", port), 87 | Handler: mux, 88 | // Please, configure timeouts! 89 | } 90 | 91 | done := make(chan os.Signal, 1) 92 | signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 93 | go func() { 94 | sig := <-done 95 | slog.Warn("signal detected...", "signal", sig) 96 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 97 | defer cancel() 98 | server.Shutdown(ctx) 99 | }() 100 | slog.Info("Listening...", "port", port) 101 | return server.ListenAndServe() 102 | } 103 | 104 | func initLogger() { 105 | var handler slog.Handler 106 | opts := slog.HandlerOptions{ 107 | AddSource: true, 108 | } 109 | switch { 110 | case dev: 111 | handler = slog.NewTextHandler(os.Stderr, &opts) 112 | default: 113 | handler = slog.NewJSONHandler(os.Stderr, &opts) 114 | } 115 | 116 | logger := slog.New(handler) 117 | slog.SetDefault(logger) 118 | } 119 | -------------------------------------------------------------------------------- /_examples/authors/pgx/migration.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "embed" 8 | 9 | "github.com/golang-migrate/migrate/v4" 10 | driver "github.com/golang-migrate/migrate/v4/database/pgx/v5" 11 | "github.com/golang-migrate/migrate/v4/source/iofs" 12 | ) 13 | 14 | //go:embed sql/migrations 15 | var migrations embed.FS 16 | 17 | func ensureSchema(db *sql.DB) error { 18 | source, err := iofs.New(migrations, "sql/migrations") 19 | if err != nil { 20 | return err 21 | } 22 | target, err := driver.WithInstance(db, new(driver.Config)) 23 | if err != nil { 24 | return err 25 | } 26 | m, err := migrate.NewWithInstance("iofs", source, "postgresql", target) 27 | if err != nil { 28 | return err 29 | } 30 | err = m.Up() 31 | if err != nil && err != migrate.ErrNoChange { 32 | return err 33 | } 34 | return source.Close() 35 | } 36 | -------------------------------------------------------------------------------- /_examples/authors/pgx/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | description: authors Services 4 | title: authors 5 | version: 0.0.1 6 | contact: 7 | name: sqlc-http 8 | url: https://github.com/walterwanderley/sqlc-http 9 | tags: 10 | - authors 11 | 12 | paths: 13 | /author: 14 | post: 15 | tags: 16 | - authors 17 | summary: CreateAuthor 18 | requestBody: 19 | content: 20 | application/json: 21 | schema: 22 | type: object 23 | properties: 24 | name: 25 | type: string 26 | bio: 27 | type: string 28 | application/x-www-form-urlencoded: 29 | schema: 30 | type: object 31 | properties: 32 | name: 33 | type: string 34 | bio: 35 | type: string 36 | 37 | responses: 38 | "200": 39 | description: OK 40 | content: 41 | application/json: 42 | schema: 43 | $ref: "#/components/schemas/authorsAuthors" 44 | 45 | "default": 46 | description: Error message 47 | content: 48 | text/plain: 49 | schema: 50 | type: string 51 | 52 | /author/{id}: 53 | delete: 54 | tags: 55 | - authors 56 | summary: DeleteAuthor 57 | parameters: 58 | - name: id 59 | in: path 60 | schema: 61 | type: integer 62 | format: int64 63 | 64 | responses: 65 | "200": 66 | description: OK 67 | 68 | "default": 69 | description: Error message 70 | content: 71 | text/plain: 72 | schema: 73 | type: string 74 | get: 75 | tags: 76 | - authors 77 | summary: GetAuthor 78 | parameters: 79 | - name: id 80 | in: path 81 | schema: 82 | type: integer 83 | format: int64 84 | 85 | responses: 86 | "200": 87 | description: OK 88 | content: 89 | application/json: 90 | schema: 91 | $ref: "#/components/schemas/authorsAuthors" 92 | 93 | "default": 94 | description: Error message 95 | content: 96 | text/plain: 97 | schema: 98 | type: string 99 | 100 | /authors: 101 | get: 102 | tags: 103 | - authors 104 | summary: ListAuthors 105 | 106 | responses: 107 | "200": 108 | description: OK 109 | content: 110 | application/json: 111 | schema: 112 | type: array 113 | items: 114 | $ref: "#/components/schemas/authorsAuthors" 115 | 116 | "default": 117 | description: Error message 118 | content: 119 | text/plain: 120 | schema: 121 | type: string 122 | 123 | 124 | components: 125 | schemas: 126 | authorsAuthors: 127 | type: object 128 | properties: 129 | id: 130 | type: integer 131 | format: int64 132 | name: 133 | type: string 134 | bio: 135 | type: string 136 | 137 | 138 | -------------------------------------------------------------------------------- /_examples/authors/pgx/registry.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package main 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/jackc/pgx/v5/pgxpool" 9 | 10 | authors_app "authors/internal/authors" 11 | ) 12 | 13 | func registerHandlers(mux *http.ServeMux, db *pgxpool.Pool) { 14 | authorsService := authors_app.NewService(authors_app.New(), db) 15 | authorsService.RegisterHandlers(mux) 16 | } 17 | -------------------------------------------------------------------------------- /_examples/authors/pgx/sql/migrations/001_authors.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS authors; -------------------------------------------------------------------------------- /_examples/authors/pgx/sql/migrations/001_authors.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS authors ( 2 | id BIGSERIAL PRIMARY KEY, 3 | name text NOT NULL, 4 | bio text 5 | ); -------------------------------------------------------------------------------- /_examples/authors/pgx/sql/queries.sql: -------------------------------------------------------------------------------- 1 | -- name: GetAuthor :one 2 | SELECT * FROM authors 3 | WHERE id = $1 LIMIT 1; 4 | 5 | -- name: ListAuthors :many 6 | SELECT * FROM authors 7 | ORDER BY name; 8 | 9 | -- name: CreateAuthor :one 10 | INSERT INTO authors ( 11 | name, bio 12 | ) VALUES ( 13 | $1, $2 14 | ) 15 | RETURNING *; 16 | 17 | -- name: DeleteAuthor :exec 18 | DELETE FROM authors 19 | WHERE id = $1; -------------------------------------------------------------------------------- /_examples/authors/pgx/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - schema: "./sql/migrations" 4 | queries: "./sql/queries.sql" 5 | engine: "postgresql" 6 | gen: 7 | go: 8 | package: "authors" 9 | out: "internal/authors" 10 | sql_package: "pgx/v5" 11 | emit_interface: true 12 | emit_exact_table_names: true 13 | emit_empty_slices: true 14 | emit_exported_queries: true 15 | emit_json_tags: true 16 | emit_result_struct_pointers: true 17 | emit_params_struct_pointers: true 18 | emit_methods_with_db_argument: true 19 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/README.md: -------------------------------------------------------------------------------- 1 | # Fullstack application go + sqlite + htmx 2 | 3 | The most productive and efficient stack of the world! 4 | 5 | ## Steps to generate the code 6 | 7 | 0. Install the required tools. 8 | 9 | ```sh 10 | go install github.com/walterwanderley/sqlc-http@latest 11 | ``` 12 | ```sh 13 | go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest 14 | ``` 15 | 16 | 1. Create a directory to store SQL scripts. 17 | 18 | ```sh 19 | mkdir -p sql/migrations 20 | ``` 21 | 22 | 2. Create migrations scripts using [goose](https://github.com/pressly/goose?tab=readme-ov-file#migrations) rules. 23 | 24 | ```sh 25 | echo "-- +goose Up 26 | CREATE TABLE IF NOT EXISTS authors ( 27 | id integer PRIMARY KEY AUTOINCREMENT, 28 | name text NOT NULL, 29 | bio text, 30 | birth_date date 31 | ); 32 | 33 | -- +goose Down 34 | DROP TABLE IF EXISTS authors; 35 | " > sql/migrations/001_authors.sql 36 | ``` 37 | 38 | 3. Create SQL queries and use [sqlc](https://sqlc.dev/) and [sqlc-http](http://github.com/walterwanderley/sqlc-http) comments syntax. 39 | 40 | ```sh 41 | echo "/* name: GetAuthor :one */ 42 | /* http: GET /authors/{id}*/ 43 | SELECT * FROM authors 44 | WHERE id = ? LIMIT 1; 45 | 46 | /* name: ListAuthors :many */ 47 | /* http: GET /authors */ 48 | SELECT * FROM authors 49 | ORDER BY name 50 | LIMIT ? OFFSET ?; 51 | 52 | /* name: CreateAuthor :execresult */ 53 | /* http: POST /authors */ 54 | INSERT INTO authors ( 55 | name, bio, birth_date 56 | ) VALUES ( 57 | ?, ?, ? 58 | ); 59 | 60 | /* name: UpdateAuthor :execresult */ 61 | /* http: PUT /authors/{id} */ 62 | UPDATE authors 63 | SET name = ?, 64 | bio = ?, 65 | birth_date = ? 66 | WHERE id = ?; 67 | 68 | /* name: UpdateAuthorBio :execresult */ 69 | /* http: PATCH /authors/{id}/bio */ 70 | UPDATE authors 71 | SET bio = ? 72 | WHERE id = ?; 73 | 74 | /* name: DeleteAuthor :exec */ 75 | /* http: DELETE /authors/{id} */ 76 | DELETE FROM authors 77 | WHERE id = ?; 78 | " > sql/queries.sql 79 | ``` 80 | 81 | 4. Create the sqlc.yaml configuration file 82 | 83 | ```sh 84 | echo " 85 | version: "2" 86 | sql: 87 | - schema: "./sql/migrations" 88 | queries: "./sql/queries.sql" 89 | engine: "sqlite" 90 | gen: 91 | go: 92 | out: "internal/authors" 93 | " > sqlc.yaml 94 | ``` 95 | 96 | 5. Execute sqlc 97 | 98 | ```sh 99 | sqlc generate 100 | ``` 101 | 102 | 6. Execute sqlc-http 103 | 104 | ```sh 105 | sqlc-http -m sqlite-htmx -migration-path sql/migrations -frontend 106 | ``` 107 | 108 | ## Running 109 | 110 | ```sh 111 | go run . -db test.db 112 | ``` 113 | 114 | Go to [http://localhost:5000](http://localhost:5000) 115 | 116 | ## Hot reload 117 | 118 | If you want to automatic refresh the browser after change html files, use the **-dev** parameter: 119 | 120 | ```sh 121 | go run . -db test.db -dev 122 | ``` 123 | 124 | "Computers make art, artists make money" (Chico Science) -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -u 3 | set -e 4 | set -x 5 | 6 | go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest 7 | 8 | rm -rf internal view go.mod go.sum *.go openapi.yml 9 | 10 | sqlc generate 11 | sqlc-http -m sqlite-htmx -migration-path sql/migrations -frontend 12 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/go.mod: -------------------------------------------------------------------------------- 1 | module sqlite-htmx 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.9.0 7 | github.com/go-playground/form/v4 v4.2.1 8 | github.com/pressly/goose/v3 v3.24.3 9 | go.uber.org/automaxprocs v1.6.0 10 | modernc.org/sqlite v1.37.0 11 | ) 12 | 13 | require ( 14 | github.com/dustin/go-humanize v1.0.1 // indirect 15 | github.com/google/uuid v1.6.0 // indirect 16 | github.com/mattn/go-isatty v0.0.20 // indirect 17 | github.com/mfridman/interpolate v0.0.2 // indirect 18 | github.com/ncruces/go-strftime v0.1.9 // indirect 19 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 20 | github.com/sethvargo/go-retry v0.3.0 // indirect 21 | go.uber.org/multierr v1.11.0 // indirect 22 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect 23 | golang.org/x/sync v0.14.0 // indirect 24 | golang.org/x/sys v0.33.0 // indirect 25 | modernc.org/libc v1.65.0 // indirect 26 | modernc.org/mathutil v1.7.1 // indirect 27 | modernc.org/memory v1.10.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 4 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 5 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 6 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 7 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 8 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 9 | github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw= 10 | github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U= 11 | github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= 12 | github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 13 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 14 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 15 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 16 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 17 | github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= 18 | github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= 19 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 20 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 24 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 25 | github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM= 26 | github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E= 27 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 28 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 29 | github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= 30 | github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= 31 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 32 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 33 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 34 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 35 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 36 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 37 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= 38 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= 39 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 40 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 41 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 42 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 43 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 45 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 46 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 47 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 48 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 49 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA= 51 | modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 52 | modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc= 53 | modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY= 54 | modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= 55 | modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= 56 | modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= 57 | modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= 58 | modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y= 59 | modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= 60 | modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 61 | modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 62 | modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= 63 | modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 64 | modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= 65 | modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 66 | modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= 67 | modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= 68 | modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= 69 | modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= 70 | modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 71 | modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= 72 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 73 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 74 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/internal/authors/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | 5 | package authors 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/internal/authors/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | 5 | package authors 6 | 7 | import ( 8 | "database/sql" 9 | ) 10 | 11 | type Author struct { 12 | ID int64 13 | Name string 14 | Bio sql.NullString 15 | BirthDate sql.NullTime 16 | } 17 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/internal/authors/queries.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | // source: queries.sql 5 | 6 | package authors 7 | 8 | import ( 9 | "context" 10 | "database/sql" 11 | ) 12 | 13 | const createAuthor = `-- name: CreateAuthor :execresult 14 | INSERT INTO authors ( 15 | name, bio, birth_date 16 | ) VALUES ( 17 | ?, ?, ? 18 | ) 19 | ` 20 | 21 | type CreateAuthorParams struct { 22 | Name string 23 | Bio sql.NullString 24 | BirthDate sql.NullTime 25 | } 26 | 27 | // http: POST /authors 28 | func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (sql.Result, error) { 29 | return q.db.ExecContext(ctx, createAuthor, arg.Name, arg.Bio, arg.BirthDate) 30 | } 31 | 32 | const deleteAuthor = `-- name: DeleteAuthor :exec 33 | DELETE FROM authors 34 | WHERE id = ? 35 | ` 36 | 37 | // http: DELETE /authors/{id} 38 | func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { 39 | _, err := q.db.ExecContext(ctx, deleteAuthor, id) 40 | return err 41 | } 42 | 43 | const getAuthor = `-- name: GetAuthor :one 44 | SELECT id, name, bio, birth_date FROM authors 45 | WHERE id = ? LIMIT 1 46 | ` 47 | 48 | // http: GET /authors/{id} 49 | func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { 50 | row := q.db.QueryRowContext(ctx, getAuthor, id) 51 | var i Author 52 | err := row.Scan( 53 | &i.ID, 54 | &i.Name, 55 | &i.Bio, 56 | &i.BirthDate, 57 | ) 58 | return i, err 59 | } 60 | 61 | const listAuthors = `-- name: ListAuthors :many 62 | SELECT id, name, bio, birth_date FROM authors 63 | ORDER BY name 64 | LIMIT ? OFFSET ? 65 | ` 66 | 67 | type ListAuthorsParams struct { 68 | Limit int64 69 | Offset int64 70 | } 71 | 72 | // http: GET /authors 73 | func (q *Queries) ListAuthors(ctx context.Context, arg ListAuthorsParams) ([]Author, error) { 74 | rows, err := q.db.QueryContext(ctx, listAuthors, arg.Limit, arg.Offset) 75 | if err != nil { 76 | return nil, err 77 | } 78 | defer rows.Close() 79 | var items []Author 80 | for rows.Next() { 81 | var i Author 82 | if err := rows.Scan( 83 | &i.ID, 84 | &i.Name, 85 | &i.Bio, 86 | &i.BirthDate, 87 | ); err != nil { 88 | return nil, err 89 | } 90 | items = append(items, i) 91 | } 92 | if err := rows.Close(); err != nil { 93 | return nil, err 94 | } 95 | if err := rows.Err(); err != nil { 96 | return nil, err 97 | } 98 | return items, nil 99 | } 100 | 101 | const updateAuthor = `-- name: UpdateAuthor :execresult 102 | UPDATE authors 103 | SET name = ?, 104 | bio = ?, 105 | birth_date = ? 106 | WHERE id = ? 107 | ` 108 | 109 | type UpdateAuthorParams struct { 110 | Name string 111 | Bio sql.NullString 112 | BirthDate sql.NullTime 113 | ID int64 114 | } 115 | 116 | // http: PUT /authors/{id} 117 | func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) (sql.Result, error) { 118 | return q.db.ExecContext(ctx, updateAuthor, 119 | arg.Name, 120 | arg.Bio, 121 | arg.BirthDate, 122 | arg.ID, 123 | ) 124 | } 125 | 126 | const updateAuthorBio = `-- name: UpdateAuthorBio :execresult 127 | UPDATE authors 128 | SET bio = ? 129 | WHERE id = ? 130 | ` 131 | 132 | type UpdateAuthorBioParams struct { 133 | Bio sql.NullString 134 | ID int64 135 | } 136 | 137 | // http: PATCH /authors/{id}/bio 138 | func (q *Queries) UpdateAuthorBio(ctx context.Context, arg UpdateAuthorBioParams) (sql.Result, error) { 139 | return q.db.ExecContext(ctx, updateAuthorBio, arg.Bio, arg.ID) 140 | } 141 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/internal/authors/routes.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package authors 4 | 5 | import "net/http" 6 | 7 | func (s *Service) RegisterHandlers(mux *http.ServeMux) { 8 | mux.HandleFunc("POST /authors", s.handleCreateAuthor()) 9 | mux.HandleFunc("DELETE /authors/{id}", s.handleDeleteAuthor()) 10 | mux.HandleFunc("GET /authors/{id}", s.handleGetAuthor()) 11 | mux.HandleFunc("GET /authors", s.handleListAuthors()) 12 | mux.HandleFunc("PUT /authors/{id}", s.handleUpdateAuthor()) 13 | mux.HandleFunc("PATCH /authors/{id}/bio", s.handleUpdateAuthorBio()) 14 | } 15 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/internal/authors/service_factory.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package authors 4 | 5 | // NewService is a constructor of a interface { func RegisterHandlers(*http.ServeMux) } implementation. 6 | // Use this function to customize the server by adding middlewares to it. 7 | func NewService(querier *Queries) *Service { 8 | return &Service{querier: querier} 9 | } 10 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/internal/server/encoding.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package server 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "log/slog" 9 | "net/http" 10 | "strings" 11 | "time" 12 | 13 | "github.com/go-playground/form/v4" 14 | 15 | "sqlite-htmx/view" 16 | ) 17 | 18 | var formDecoder *form.Decoder 19 | 20 | func init() { 21 | formDecoder = form.NewDecoder() 22 | formDecoder.SetTagName("json") 23 | 24 | formDecoder.RegisterCustomTypeFunc(func(vals []string) (interface{}, error) { 25 | if vals[0] == "" { 26 | return time.Time{}, nil 27 | } 28 | return time.ParseInLocation(time.DateOnly, vals[0], time.Local) 29 | }, time.Time{}) 30 | } 31 | 32 | func Decode[T any](r *http.Request) (T, error) { 33 | var v T 34 | if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" { 35 | if err := r.ParseForm(); err != nil { 36 | return v, fmt.Errorf("parse form: %w", err) 37 | } 38 | if err := formDecoder.Decode(&v, r.Form); err != nil { 39 | return v, fmt.Errorf("decode form: %w", err) 40 | } 41 | } else { 42 | if err := json.NewDecoder(r.Body).Decode(&v); err != nil { 43 | return v, fmt.Errorf("decode json: %w", err) 44 | } 45 | } 46 | return v, nil 47 | } 48 | 49 | func Encode[T any](w http.ResponseWriter, r *http.Request, status int, v T) error { 50 | if !strings.Contains(r.Header.Get("Accept"), "application/json") { 51 | if err := view.RenderHTML(w, r, v); err != nil { 52 | slog.Error("render html", "error", err) 53 | } 54 | return nil 55 | } 56 | w.Header().Set("Content-Type", "application/json") 57 | w.WriteHeader(status) 58 | if err := json.NewEncoder(w).Encode(v); err != nil { 59 | return fmt.Errorf("encode json: %w", err) 60 | } 61 | return nil 62 | } 63 | 64 | func Info(w http.ResponseWriter, r *http.Request, code int, text string) error { 65 | return view.InfoMessage(code, text).Render(w, r) 66 | } 67 | 68 | func Success(w http.ResponseWriter, r *http.Request, code int, text string) error { 69 | return view.SuccessMessage(code, text).Render(w, r) 70 | } 71 | 72 | func Error(w http.ResponseWriter, r *http.Request, code int, text string) error { 73 | return view.ErrorMessage(code, text).Render(w, r) 74 | } 75 | 76 | func Warning(w http.ResponseWriter, r *http.Request, code int, text string) error { 77 | return view.WarningMessage(code, text).Render(w, r) 78 | } 79 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/main.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | _ "embed" 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "log/slog" 13 | "net/http" 14 | "os" 15 | "os/signal" 16 | "runtime" 17 | "syscall" 18 | "time" 19 | 20 | "go.uber.org/automaxprocs/maxprocs" 21 | // database driver 22 | _ "modernc.org/sqlite" 23 | 24 | "sqlite-htmx/view" 25 | ) 26 | 27 | //go:generate sqlc-http -m sqlite-htmx -migration-path sql/migrations -frontend -append 28 | 29 | const serviceName = "sqlite-htmx" 30 | 31 | var ( 32 | dev bool 33 | dbURL string 34 | port int 35 | 36 | //go:embed openapi.yml 37 | openAPISpec []byte 38 | ) 39 | 40 | func main() { 41 | flag.StringVar(&dbURL, "db", "", "The Database connection URL") 42 | flag.IntVar(&port, "port", 5000, "The server port") 43 | 44 | flag.BoolVar(&dev, "dev", false, "Set logger to development mode and enable Hot Reload on edit templates files") 45 | 46 | flag.Parse() 47 | 48 | initLogger() 49 | 50 | if err := run(); err != nil && !errors.Is(err, http.ErrServerClosed) { 51 | slog.Error("server error", "error", err) 52 | os.Exit(1) 53 | } 54 | } 55 | 56 | func run() error { 57 | _, err := maxprocs.Set() 58 | if err != nil { 59 | slog.Warn("startup", "error", err) 60 | } 61 | slog.Info("startup", "GOMAXPROCS", runtime.GOMAXPROCS(0)) 62 | 63 | db, err := sql.Open("sqlite", dbURL) 64 | if err != nil { 65 | return err 66 | } 67 | defer db.Close() 68 | 69 | if err := ensureSchema(db); err != nil { 70 | return fmt.Errorf("migration error: %w", err) 71 | } 72 | 73 | mux := http.NewServeMux() 74 | registerHandlers(mux, db) 75 | 76 | mux.HandleFunc("GET /openapi.yaml", func(w http.ResponseWriter, r *http.Request) { 77 | w.Header().Set("Content-Type", "application/yaml") 78 | w.Write(openAPISpec) 79 | }) 80 | 81 | err = view.RegisterHandlers(mux, dev) 82 | if err != nil { 83 | return fmt.Errorf("frontend templates error: %w", err) 84 | } 85 | 86 | server := &http.Server{ 87 | Addr: fmt.Sprintf(":%d", port), 88 | Handler: mux, 89 | // Please, configure timeouts! 90 | } 91 | 92 | done := make(chan os.Signal, 1) 93 | signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 94 | go func() { 95 | sig := <-done 96 | slog.Warn("signal detected...", "signal", sig) 97 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 98 | defer cancel() 99 | server.Shutdown(ctx) 100 | }() 101 | slog.Info("Listening...", "port", port) 102 | return server.ListenAndServe() 103 | } 104 | 105 | func initLogger() { 106 | var handler slog.Handler 107 | opts := slog.HandlerOptions{ 108 | AddSource: true, 109 | } 110 | switch { 111 | case dev: 112 | handler = slog.NewTextHandler(os.Stderr, &opts) 113 | default: 114 | handler = slog.NewJSONHandler(os.Stderr, &opts) 115 | } 116 | 117 | logger := slog.New(handler) 118 | slog.SetDefault(logger) 119 | } 120 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/migration.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "embed" 8 | 9 | "github.com/pressly/goose/v3" 10 | ) 11 | 12 | //go:embed sql/migrations 13 | var migrations embed.FS 14 | 15 | func ensureSchema(db *sql.DB) error { 16 | goose.SetBaseFS(migrations) 17 | 18 | if err := goose.SetDialect("sqlite"); err != nil { 19 | return err 20 | } 21 | 22 | return goose.Up(db, "sql/migrations") 23 | 24 | } 25 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/registry.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "net/http" 8 | 9 | authors_app "sqlite-htmx/internal/authors" 10 | ) 11 | 12 | func registerHandlers(mux *http.ServeMux, db *sql.DB) { 13 | authorsService := authors_app.NewService(authors_app.New(db)) 14 | authorsService.RegisterHandlers(mux) 15 | } 16 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/sql/migrations/001_authors.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | CREATE TABLE IF NOT EXISTS authors ( 3 | id integer PRIMARY KEY AUTOINCREMENT, 4 | name text NOT NULL, 5 | bio text, 6 | birth_date date 7 | ); 8 | 9 | -- +goose Down 10 | DROP TABLE IF EXISTS authors; -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/sql/queries.sql: -------------------------------------------------------------------------------- 1 | /* name: GetAuthor :one */ 2 | /* http: GET /authors/{id}*/ 3 | SELECT * FROM authors 4 | WHERE id = ? LIMIT 1; 5 | 6 | /* name: ListAuthors :many */ 7 | /* http: GET /authors */ 8 | SELECT * FROM authors 9 | ORDER BY name 10 | LIMIT ? OFFSET ?; 11 | 12 | /* name: CreateAuthor :execresult */ 13 | /* http: POST /authors */ 14 | INSERT INTO authors ( 15 | name, bio, birth_date 16 | ) VALUES ( 17 | ?, ?, ? 18 | ); 19 | 20 | /* name: UpdateAuthor :execresult */ 21 | /* http: PUT /authors/{id} */ 22 | UPDATE authors 23 | SET name = ?, 24 | bio = ?, 25 | birth_date = ? 26 | WHERE id = ?; 27 | 28 | /* name: UpdateAuthorBio :execresult */ 29 | /* http: PATCH /authors/{id}/bio */ 30 | UPDATE authors 31 | SET bio = ? 32 | WHERE id = ?; 33 | 34 | /* name: DeleteAuthor :exec */ 35 | /* http: DELETE /authors/{id} */ 36 | DELETE FROM authors 37 | WHERE id = ?; -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - schema: "./sql/migrations" 4 | queries: "./sql/queries.sql" 5 | engine: "sqlite" 6 | gen: 7 | go: 8 | out: "internal/authors" -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/breadcrumbs.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import "net/http" 4 | 5 | type breadCrumb struct { 6 | Name string 7 | Href string 8 | } 9 | 10 | func breadCrumbsFromRequest(r *http.Request) []breadCrumb { 11 | switch r.Pattern { 12 | case "POST /authors": 13 | return breadCrumbsFromStrings("Authors", "/", "Create Author") 14 | case "DELETE /authors/{id}": 15 | return breadCrumbsFromStrings("Authors", "/", "List Authors", "/authors", "Delete Author") 16 | case "GET /authors/{id}": 17 | serviceName := "Get Author" 18 | if r.URL.Query().Has("edit") { 19 | serviceName = "Update Author" 20 | } 21 | return breadCrumbsFromStrings("Authors", "/", "List Authors", "/authors", serviceName) 22 | case "GET /authors": 23 | return breadCrumbsFromStrings("Authors", "/", "List Authors") 24 | case "PUT /authors/{id}": 25 | return breadCrumbsFromStrings("Authors", "/", "List Authors", "/authors", "Update Author") 26 | case "PATCH /authors/{id}/bio": 27 | return breadCrumbsFromStrings("Authors", "/", "List Authors", "/authors", "Get Author", "/authors/"+r.PathValue("id"), "Update Author Bio") 28 | default: 29 | switch r.URL.Path { 30 | case "/app/authors/create_author": 31 | return breadCrumbsFromStrings("Authors", "/", "Create Author") 32 | case "/app/authors/delete_author": 33 | return breadCrumbsFromStrings("Authors", "/", "List Authors", "/authors", "Delete Author") 34 | case "/app/authors/get_author": 35 | return breadCrumbsFromStrings("Authors", "/", "List Authors", "/authors", "Get Author") 36 | case "/app/authors/list_authors": 37 | return breadCrumbsFromStrings("Authors", "/", "List Authors") 38 | case "/app/authors/update_author": 39 | return breadCrumbsFromStrings("Authors", "/", "List Authors", "/authors", "Update Author") 40 | case "/app/authors/update_author_bio": 41 | return breadCrumbsFromStrings("Authors", "/", "List Authors", "/authors", "Update Author Bio") 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func breadCrumbsFromStrings(items ...string) []breadCrumb { 49 | breadcrumbs := make([]breadCrumb, 0) 50 | for i := 0; i < len(items); i = i + 2 { 51 | var bc breadCrumb 52 | bc.Name = items[i] 53 | j := i + 1 54 | if j < len(items) { 55 | bc.Href = items[j] 56 | } 57 | breadcrumbs = append(breadcrumbs, bc) 58 | } 59 | return breadcrumbs 60 | } 61 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/content.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | type Content[T any] struct { 11 | Data T 12 | Request *http.Request 13 | } 14 | 15 | func (c Content[T]) HxRequest() bool { 16 | return HXRequest(c.Request) 17 | } 18 | 19 | func (c Content[T]) BreadCrumbsFromRequest() []breadCrumb { 20 | return breadCrumbsFromRequest(c.Request) 21 | } 22 | 23 | func (c Content[T]) Pagination() *Pagination { 24 | pagination, _ := c.Request.Context().Value(paginationContext).(*Pagination) 25 | if pagination != nil { 26 | pagination.request = c.Request 27 | } 28 | return pagination 29 | } 30 | 31 | func (c Content[T]) BaseHref() string { 32 | schema := "http" 33 | if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto != "" { 34 | schema = forwardedProto 35 | } else if c.Request.TLS != nil { 36 | schema = "https" 37 | } 38 | context := os.Getenv("WEB_CONTEXT") 39 | return fmt.Sprintf("%s://%s%s/", schema, c.Request.Host, strings.TrimSuffix(context, "/")) 40 | } 41 | 42 | func (c Content[T]) HasQuery(key string) bool { 43 | return c.Request.URL.Query().Has(key) 44 | } 45 | 46 | func (c Content[T]) Query(key string) string { 47 | return c.Request.URL.Query().Get(key) 48 | } 49 | 50 | func (c Content[T]) MessageContext() *Message { 51 | if msg, ok := c.Request.Context().Value(messageContext).(Message); ok { 52 | return &msg 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/etag/etag.go: -------------------------------------------------------------------------------- 1 | package etag 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "io" 7 | "io/fs" 8 | "log" 9 | "net/http" 10 | ) 11 | 12 | func HandlerFunc(fileSystem fs.FS, stripPrefix string) http.HandlerFunc { 13 | etags := generateETags(fileSystem, stripPrefix) 14 | handler := http.FileServer(http.FS(fileSystem)) 15 | if stripPrefix != "" { 16 | handler = http.StripPrefix(stripPrefix, handler) 17 | } 18 | return func(w http.ResponseWriter, r *http.Request) { 19 | if sum, ok := etags[r.URL.Path]; ok { 20 | w.Header().Set("ETag", sum) 21 | if r.Header.Get("If-None-Match") == sum { 22 | w.WriteHeader(http.StatusNotModified) 23 | return 24 | } 25 | } 26 | handler.ServeHTTP(w, r) 27 | } 28 | } 29 | 30 | func Handler(fileSystem fs.FS, stripPrefix string) http.Handler { 31 | return http.HandlerFunc(HandlerFunc(fileSystem, stripPrefix)) 32 | } 33 | 34 | func generateETags(fileSystem fs.FS, stripPrefix string) map[string]string { 35 | etags := make(map[string]string) 36 | err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error { 37 | if err != nil { 38 | return err 39 | } 40 | 41 | if d.IsDir() { 42 | return nil 43 | } 44 | 45 | in, err := fileSystem.Open(path) 46 | if err != nil { 47 | return err 48 | } 49 | defer in.Close() 50 | 51 | hasher := sha1.New() 52 | if _, err := io.Copy(hasher, in); err != nil { 53 | return err 54 | } 55 | 56 | sum := hex.EncodeToString(hasher.Sum(nil)) 57 | etags[stripPrefix+"/"+path] = sum 58 | return nil 59 | }) 60 | if err != nil { 61 | log.Println("[error] ETags:", err.Error()) 62 | } 63 | return etags 64 | } 65 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/message.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "encoding/json" 5 | "html/template" 6 | "log/slog" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | retarget = "#messages" 13 | reswap = "beforeend show:body:top" 14 | ) 15 | 16 | var ( 17 | messageTemplate = template.Must(template.New("message.html").ParseFS(templatesFS, "templates/components/message.html")) 18 | messagesContextTemplate = template.Must(template.New("messages-context.html").ParseFS(templatesFS, 19 | "templates/components/messages-context.html", "templates/components/message.html")) 20 | ) 21 | 22 | type MessageType string 23 | 24 | const ( 25 | TypeInfo = MessageType("info") 26 | TypeSuccess = MessageType("success") 27 | TypeError = MessageType("error") 28 | TypeWarning = MessageType("warning") 29 | ) 30 | 31 | func (t MessageType) Icon() string { 32 | switch t { 33 | case TypeInfo: 34 | return "info-fill" 35 | case TypeSuccess: 36 | return "check-circle-fill" 37 | default: 38 | return "exclamation-triangle-fill" 39 | } 40 | } 41 | 42 | func (t MessageType) Class() string { 43 | switch t { 44 | case TypeInfo: 45 | return "primary" 46 | case TypeSuccess: 47 | return "success" 48 | case TypeError: 49 | return "danger" 50 | default: 51 | return "warning" 52 | } 53 | } 54 | 55 | type Message struct { 56 | Code int `json:"code"` 57 | Text string `json:"text"` 58 | Type MessageType `json:"type"` 59 | } 60 | 61 | func NewMessage(code int, text string, typ MessageType) Message { 62 | return Message{Code: code, 63 | Text: text, 64 | Type: typ} 65 | } 66 | 67 | func ErrorMessage(code int, text string) Message { 68 | return NewMessage(code, text, TypeError) 69 | } 70 | 71 | func InfoMessage(code int, text string) Message { 72 | return NewMessage(code, text, TypeInfo) 73 | } 74 | 75 | func SuccessMessage(code int, text string) Message { 76 | return NewMessage(code, text, TypeSuccess) 77 | } 78 | 79 | func WarningMessage(code int, text string) Message { 80 | return NewMessage(code, text, TypeWarning) 81 | } 82 | 83 | func (m Message) Render(w http.ResponseWriter, r *http.Request) error { 84 | if strings.Contains(r.Header.Get("accept"), "application/json") { 85 | w.WriteHeader(m.Code) 86 | return json.NewEncoder(w).Encode(m) 87 | } 88 | 89 | if HXRequest(r) { 90 | if r.Method == http.MethodDelete { 91 | err := messagesContextTemplate.Execute(w, m) 92 | if err != nil { 93 | slog.Error("render messages-context", "err", err) 94 | } 95 | return err 96 | } 97 | w.Header().Set("HX-Retarget", retarget) 98 | w.Header().Set("HX-Reswap", reswap) 99 | } 100 | 101 | err := messageTemplate.Execute(w, m) 102 | if err != nil { 103 | slog.Error("render message", "err", err) 104 | } 105 | return err 106 | } 107 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/pagination.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | const defaultLimit = 10 10 | 11 | type Pagination struct { 12 | request *http.Request 13 | Limit int64 14 | Offset int64 15 | } 16 | 17 | func (p *Pagination) From() int64 { 18 | return p.Offset + 1 19 | } 20 | 21 | func (p *Pagination) To() int64 { 22 | return p.Offset + p.validLimit() 23 | } 24 | 25 | func (p *Pagination) Next() int64 { 26 | return p.Offset + p.validLimit() 27 | } 28 | 29 | func (p *Pagination) Prev() int64 { 30 | limit := p.validLimit() 31 | offset := p.Offset - limit 32 | if offset < 0 { 33 | offset = 0 34 | } 35 | return offset 36 | } 37 | 38 | func (p *Pagination) URL(limit, offset int64) string { 39 | if p == nil { 40 | return "" 41 | } 42 | if offset < 0 { 43 | offset = 0 44 | } 45 | if limit == 0 { 46 | limit = defaultLimit 47 | } 48 | var url strings.Builder 49 | url.WriteString(p.request.URL.Path) 50 | url.WriteString("?") 51 | for k := range p.request.URL.Query() { 52 | if k == "limit" || k == "offset" { 53 | continue 54 | } 55 | url.WriteString(fmt.Sprintf("%s=%s&", k, p.request.URL.Query().Get(k))) 56 | } 57 | if limit > 0 { 58 | url.WriteString(fmt.Sprintf("limit=%d&offset=%d", limit, offset)) 59 | } else { 60 | url.WriteString(fmt.Sprintf("offset=%d", offset)) 61 | } 62 | return url.String() 63 | } 64 | 65 | func (p *Pagination) validLimit() int64 { 66 | limit := p.Limit 67 | if limit == 0 { 68 | limit = 10 69 | } 70 | return limit 71 | } 72 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/static/css/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterwanderley/sqlc-http/f9920c63dbd0a9b7b52a62f093bbd522ef628468/_examples/authors/sqlite-frontend/view/static/css/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/static/css/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterwanderley/sqlc-http/f9920c63dbd0a9b7b52a62f093bbd522ef628468/_examples/authors/sqlite-frontend/view/static/css/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/static/css/style.css: -------------------------------------------------------------------------------- 1 | tr.htmx-swapping td { 2 | opacity: 0; 3 | transition: opacity 1s ease-out; 4 | } 5 | 6 | footer { 7 | position: fixed; 8 | left: 0; 9 | bottom: 0; 10 | width: 100%; 11 | background-color: orange; 12 | color: white; 13 | text-align: center; 14 | } 15 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/static/js/init.js: -------------------------------------------------------------------------------- 1 | loadComponents(document); 2 | 3 | htmx.onLoad(function (content) { 4 | loadComponents(content); 5 | }); 6 | 7 | function loadComponents(content) { 8 | const alertList = content.querySelectorAll('.alert') 9 | const alerts = [...alertList].map(element => new bootstrap.Alert(element)) 10 | } 11 | 12 | function replacePathParams(event) { 13 | let pathWithParameters = event.detail.path.replace(/{([A-Za-z0-9_]+)}/g, function (_match, parameterName) { 14 | let parameterValue = event.detail.parameters[parameterName] 15 | delete event.detail.parameters[parameterName] 16 | return parameterValue 17 | }) 18 | event.detail.path = pathWithParameters 19 | } 20 | 21 | function showMessage(msg) { 22 | var msgIcon = 'exclamation-triangle-fill'; 23 | var msgClass = 'warning'; 24 | switch (msg.type) { 25 | case 'error': 26 | msgClass = 'danger'; 27 | break 28 | case 'info': 29 | msgClass = 'primary'; 30 | msgIcon = 'info-fill' 31 | break 32 | case 'success': 33 | msgClass = 'success'; 34 | msgIcon = 'check-circle-fill' 35 | break 36 | } 37 | 38 | const messageDiv = 39 | ``; 48 | 49 | var messages = htmx.find('#messages'); 50 | console.log('messages', messages); 51 | messages.innerHTML = messageDiv; 52 | } 53 | 54 | htmx.on('htmx:responseError', function (evt) { 55 | try { 56 | const msg = JSON.parse(evt.detail.xhr.response) 57 | showMessage(msg) 58 | } catch (e) { 59 | const msg = { 60 | type: 'error', 61 | text: evt.detail.xhr.response 62 | } 63 | showMessage(msg) 64 | } 65 | }); 66 | 67 | htmx.on('htmx:sendError', function () { 68 | const msg = { 69 | type: 'warning', 70 | text: 'Server unavailable. Try again in a few minutes.' 71 | } 72 | showMessage(msg) 73 | }); -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/static/swagger/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sqlite-htmx - API 8 | 9 | 10 | 11 |
12 | 13 | 21 | 22 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/app/authors/create_author.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | {{template "hx-context.html" . -}} 3 |
Create Author
4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 |
20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 | {{end}} -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/app/authors/delete_author.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | {{template "hx-context.html" . -}} 3 |
Delete Author
4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 |
16 |
17 | 18 | 21 | {{end}} -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/app/authors/get_author.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | {{template "hx-context.html" . -}} 3 |
Get Author
4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 |
16 |
17 | 18 | 21 | {{end}} -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/app/authors/list_authors.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | {{template "hx-context.html" . -}} 3 |
List Authors
4 |
5 | 6 |
7 | {{end}} -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/app/authors/update_author.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | {{template "hx-context.html" . -}} 3 |
Update Author
4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 | 28 |
29 |
30 |
31 | 32 | 35 | {{end}} -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/app/authors/update_author_bio.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | {{template "hx-context.html" . -}} 3 |
Update Author Bio
4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 | 25 | {{end}} -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/authors.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | {{template "hx-context.html" .}} 3 | {{$pagination := .Pagination}}{{if $pagination}} 4 |
5 | {{end}}
6 | Create Author 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{range .Data}} 19 | 20 | 21 | 22 | 23 | 28 | {{end}} 29 | 30 |
IDNameBioBirth Date
{{.ID}}{{.Name}}{{.Bio}}{{if .BirthDate}}{{.BirthDate.Format "02/01/2006"}}{{end}} 24 | 25 | 26 | 27 |
31 |
32 | {{if $pagination}} 33 | 53 |
54 | {{end}}{{end}} -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/authors/{id}.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | {{template "hx-context.html" .}} 3 | {{- if .HasQuery "edit"}} 4 |
Update Author
5 |
6 |
7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 | 30 | {{- else -}} 31 |
32 |
33 |
34 |

ID: {{.Data.ID}}

35 |
36 |
37 |
38 |
39 |

Name: {{.Data.Name}}

40 |
41 |
42 |
43 |
44 |

Bio: {{.Data.Bio}}

45 |
46 |
47 |
48 |
49 |

Birth Date: {{.Data.BirthDate}}

50 |
51 |
52 |
53 | {{- end -}} 54 | {{end}} -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/components/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/components/hx-context.html: -------------------------------------------------------------------------------- 1 | {{if and .HxRequest (not (eq (.Query "nested") "true"))}} 2 | {{ template "messages-context.html" .MessageContext }} 3 | {{ template "breadcrumbs.html" . }} 4 | {{end -}} 5 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/components/message.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/components/messages-context.html: -------------------------------------------------------------------------------- 1 |
2 | {{if .}} 3 | {{ template "message.html" .}} 4 | {{end -}} 5 |
-------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/index.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | {{template "hx-context.html" .}} 4 |

5 |

Sqlite-htmx

6 |
Generated by SQLC-HTTP
7 |

8 |
9 | {{end}} -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/layout/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{.Title}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {{ template "header.html" .}} 18 |
19 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 32 | 33 | 34 |
35 |
36 | Loading... 37 |
38 | {{ template "messages-context.html" .Content.MessageContext }} 39 | {{ template "breadcrumbs.html" .Content}} 40 |
41 | {{ template "content" .Content}} 42 |
43 |
44 |
45 | {{ template "footer.html" .}} 46 | {{if .DevMode}} 47 | 56 | {{end}} 57 | 58 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/layout/content.html: -------------------------------------------------------------------------------- 1 | {{- template "content" . -}} -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/templates/layout/header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_examples/authors/sqlite-frontend/view/watcher/watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "log/slog" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "time" 12 | 13 | "github.com/fsnotify/fsnotify" 14 | ) 15 | 16 | const defaultPingInterval = 15 * time.Second 17 | 18 | type client chan []byte 19 | 20 | type WatchStreamer struct { 21 | watcher *fsnotify.Watcher 22 | clients map[client]struct{} 23 | connecting chan client 24 | disconnecting chan client 25 | event chan []byte 26 | pingInterval time.Duration 27 | } 28 | 29 | func New(dirs ...string) (*WatchStreamer, error) { 30 | w, err := fsnotify.NewWatcher() 31 | if err != nil { 32 | return nil, err 33 | } 34 | for _, dir := range dirs { 35 | if err := addRecurssive(w, dir); err != nil { 36 | return nil, err 37 | } 38 | } 39 | ws := WatchStreamer{ 40 | watcher: w, 41 | clients: make(map[client]struct{}), 42 | connecting: make(chan client), 43 | disconnecting: make(chan client), 44 | event: make(chan []byte, 1), 45 | pingInterval: defaultPingInterval, 46 | } 47 | return &ws, nil 48 | } 49 | 50 | func (ws *WatchStreamer) Add(dir string) error { 51 | return addRecurssive(ws.watcher, dir) 52 | } 53 | 54 | func (ws *WatchStreamer) SetPingInterval(interval time.Duration) { 55 | ws.pingInterval = interval 56 | } 57 | 58 | func (ws *WatchStreamer) Start(ctx context.Context) { 59 | go ws.run(ctx) 60 | go ws.watch(ctx) 61 | } 62 | 63 | func (ws *WatchStreamer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 64 | if r.URL.Query().Has("watchList") { 65 | json.NewEncoder(w).Encode(ws.watcher.WatchList()) 66 | return 67 | } 68 | 69 | fl, ok := w.(http.Flusher) 70 | if !ok { 71 | http.Error(w, "Flushing not supported", http.StatusNotImplemented) 72 | return 73 | } 74 | 75 | w.Header().Set("Cache-Control", "no-cache") 76 | w.Header().Set("Connection", "keep-alive") 77 | w.Header().Set("Content-Type", "text/event-stream") 78 | 79 | cl := make(client, 2) 80 | ws.connecting <- cl 81 | 82 | for { 83 | select { 84 | case <-time.After(ws.pingInterval): 85 | if _, err := w.Write([]byte("event: ping\n\n")); err != nil { 86 | slog.Error("[watcher] send ping", "error", err.Error()) 87 | return 88 | } 89 | 90 | case <-r.Context().Done(): 91 | ws.disconnecting <- cl 92 | return 93 | 94 | case event := <-cl: 95 | if _, err := w.Write(formatData(event)); err != nil { 96 | slog.Error("[watcher] send message", "error", err.Error()) 97 | return 98 | } 99 | fl.Flush() 100 | } 101 | } 102 | } 103 | 104 | func formatData(data []byte) []byte { 105 | var buf bytes.Buffer 106 | buf.WriteString("data: ") 107 | buf.Write(data) 108 | buf.WriteString("\n\n") 109 | return buf.Bytes() 110 | } 111 | 112 | func (ws *WatchStreamer) run(ctx context.Context) { 113 | for { 114 | select { 115 | case <-ctx.Done(): 116 | return 117 | case cl := <-ws.connecting: 118 | slog.Debug("[watcher] connected", "client", cl) 119 | ws.clients[cl] = struct{}{} 120 | 121 | case cl := <-ws.disconnecting: 122 | slog.Debug("[watcher] disconnected", "client", cl) 123 | delete(ws.clients, cl) 124 | 125 | case event := <-ws.event: 126 | for cl := range ws.clients { 127 | cl <- event 128 | } 129 | } 130 | } 131 | } 132 | 133 | func (ws *WatchStreamer) watch(ctx context.Context) { 134 | for { 135 | select { 136 | case <-ctx.Done(): 137 | ws.watcher.Close() 138 | return 139 | case event, ok := <-ws.watcher.Events: 140 | if !ok { 141 | return 142 | } 143 | if !event.Has(fsnotify.Write) { 144 | continue 145 | } 146 | slog.Debug("[watcher] file changed", "name", event.Name) 147 | ws.event <- []byte(event.Name) 148 | 149 | case err, ok := <-ws.watcher.Errors: 150 | if !ok { 151 | return 152 | } 153 | slog.Error("[watcher] fsnotify", "error", err) 154 | } 155 | } 156 | } 157 | 158 | func addRecurssive(w *fsnotify.Watcher, dir string) error { 159 | err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 160 | if info.IsDir() { 161 | return w.Add(path) 162 | } 163 | return nil 164 | }) 165 | return err 166 | } 167 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 2 | 3 | RUN go install github.com/cespare/reflex@latest 4 | 5 | WORKDIR /app 6 | COPY go.mod . 7 | COPY go.sum . 8 | 9 | RUN go mod download -x 10 | 11 | COPY configs/reflex.conf / 12 | 13 | ENTRYPOINT ["reflex", "-c", "/reflex.conf"] -------------------------------------------------------------------------------- /_examples/authors/sqlite/configs/reflex.conf: -------------------------------------------------------------------------------- 1 | -r '(\.go$|go\.mod)' -s -- sh -c 'go run . -dev=true -db=example.db -port=8080 -replication=s3://somebucketname/example.db' 2 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | ports: 6 | - "8080:8080" 7 | environment: 8 | - "AWS_ACCESS_KEY_ID=minio" 9 | - "AWS_SECRET_ACCESS_KEY=minio123" 10 | - "LITESTREAM_ENDPOINT=minio:9000" 11 | - "LITESTREAM_FORCE_PATH_STYLE=true" 12 | - "LITESTREAM_SCHEME=http" 13 | - "GODEBUG=httpmuxgo121=0" 14 | volumes: 15 | - .:/app 16 | depends_on: 17 | - minio 18 | 19 | minio: 20 | image: minio/minio 21 | command: server /data --console-address ":9001" 22 | environment: 23 | - "MINIO_ROOT_USER=minio" 24 | - "MINIO_ROOT_PASSWORD=minio123" 25 | ports: 26 | - "9000:9000" 27 | - "9001:9001" 28 | 29 | createbuckets: 30 | image: minio/mc 31 | depends_on: 32 | - minio 33 | entrypoint: > 34 | /bin/sh -c " 35 | /usr/bin/mc config host add myminio http://minio:9000 minio minio123; 36 | /usr/bin/mc rm -r --force myminio/somebucketname; 37 | /usr/bin/mc mb myminio/somebucketname; 38 | /usr/bin/mc policy set public myminio/somebucketname; 39 | exit 0; 40 | " -------------------------------------------------------------------------------- /_examples/authors/sqlite/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -u 3 | set -e 4 | set -x 5 | 6 | go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest 7 | 8 | rm -rf internal proto api go.mod go.sum *.go openapi.yml 9 | 10 | sqlc generate 11 | sqlc-http -m authors -migration-path sql/migrations -litefs -litestream 12 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/go.mod: -------------------------------------------------------------------------------- 1 | module authors 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/benbjohnson/litestream v0.3.13 7 | github.com/flowchartsman/swaggerui v0.0.0-20221017034628-909ed4f3701b 8 | github.com/go-playground/form/v4 v4.2.1 9 | github.com/hashicorp/raft v1.7.3 10 | github.com/hashicorp/raft-boltdb v0.0.0-20250225060035-8f7048cdfa53 11 | github.com/mattn/go-sqlite3 v1.14.28 12 | github.com/pressly/goose/v3 v3.24.3 13 | github.com/superfly/litefs v0.5.14 14 | github.com/superfly/ltx v0.3.14 15 | github.com/walterwanderley/litefs-raft v0.0.0-20231130032048-a2979ac7e817 16 | go.uber.org/automaxprocs v1.6.0 17 | ) 18 | 19 | require ( 20 | bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5 // indirect 21 | filippo.io/age v1.1.1 // indirect 22 | github.com/armon/go-metrics v0.4.1 // indirect 23 | github.com/aws/aws-sdk-go v1.44.318 // indirect 24 | github.com/beorn7/perks v1.0.1 // indirect 25 | github.com/boltdb/bolt v1.3.1 // indirect 26 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 27 | github.com/fatih/color v1.16.0 // indirect 28 | github.com/hashicorp/go-hclog v1.6.2 // indirect 29 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 30 | github.com/hashicorp/go-metrics v0.5.4 // indirect 31 | github.com/hashicorp/go-msgpack v0.5.5 // indirect 32 | github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect 33 | github.com/hashicorp/golang-lru v1.0.2 // indirect 34 | github.com/jmespath/go-jmespath v0.4.0 // indirect 35 | github.com/mattn/go-colorable v0.1.13 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 38 | github.com/mfridman/interpolate v0.0.2 // indirect 39 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 40 | github.com/prometheus/client_golang v1.17.0 // indirect 41 | github.com/prometheus/client_model v0.5.0 // indirect 42 | github.com/prometheus/common v0.45.0 // indirect 43 | github.com/prometheus/procfs v0.16.1 // indirect 44 | github.com/sethvargo/go-retry v0.3.0 // indirect 45 | go.uber.org/multierr v1.11.0 // indirect 46 | golang.org/x/crypto v0.38.0 // indirect 47 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect 48 | golang.org/x/net v0.40.0 // indirect 49 | golang.org/x/sync v0.14.0 // indirect 50 | golang.org/x/sys v0.33.0 // indirect 51 | golang.org/x/text v0.25.0 // indirect 52 | google.golang.org/protobuf v1.36.6 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/internal/authors/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | 5 | package authors 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/internal/authors/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | 5 | package authors 6 | 7 | import ( 8 | "database/sql" 9 | ) 10 | 11 | type Author struct { 12 | ID int64 13 | Name string 14 | Bio sql.NullString 15 | } 16 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/internal/authors/queries.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | // source: queries.sql 5 | 6 | package authors 7 | 8 | import ( 9 | "context" 10 | "database/sql" 11 | ) 12 | 13 | const createAuthor = `-- name: CreateAuthor :execresult 14 | INSERT INTO authors ( 15 | name, bio 16 | ) VALUES ( 17 | ?, ? 18 | ) 19 | ` 20 | 21 | type CreateAuthorParams struct { 22 | Name string 23 | Bio sql.NullString 24 | } 25 | 26 | // http: POST /authors 27 | func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (sql.Result, error) { 28 | return q.db.ExecContext(ctx, createAuthor, arg.Name, arg.Bio) 29 | } 30 | 31 | const deleteAuthor = `-- name: DeleteAuthor :exec 32 | DELETE FROM authors 33 | WHERE id = ? 34 | ` 35 | 36 | // http: DELETE /authors/{id} 37 | func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { 38 | _, err := q.db.ExecContext(ctx, deleteAuthor, id) 39 | return err 40 | } 41 | 42 | const getAuthor = `-- name: GetAuthor :one 43 | SELECT id, name, bio FROM authors 44 | WHERE id = ? LIMIT 1 45 | ` 46 | 47 | // http: GET /authors/{id} 48 | func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { 49 | row := q.db.QueryRowContext(ctx, getAuthor, id) 50 | var i Author 51 | err := row.Scan(&i.ID, &i.Name, &i.Bio) 52 | return i, err 53 | } 54 | 55 | const listAuthors = `-- name: ListAuthors :many 56 | SELECT id, name, bio FROM authors 57 | ORDER BY name 58 | ` 59 | 60 | // http: GET /authors 61 | func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { 62 | rows, err := q.db.QueryContext(ctx, listAuthors) 63 | if err != nil { 64 | return nil, err 65 | } 66 | defer rows.Close() 67 | var items []Author 68 | for rows.Next() { 69 | var i Author 70 | if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { 71 | return nil, err 72 | } 73 | items = append(items, i) 74 | } 75 | if err := rows.Close(); err != nil { 76 | return nil, err 77 | } 78 | if err := rows.Err(); err != nil { 79 | return nil, err 80 | } 81 | return items, nil 82 | } 83 | 84 | const updateAuthor = `-- name: UpdateAuthor :execresult 85 | UPDATE authors 86 | SET name = ?, 87 | bio = ? 88 | WHERE id = ? 89 | ` 90 | 91 | type UpdateAuthorParams struct { 92 | Name string 93 | Bio sql.NullString 94 | ID int64 95 | } 96 | 97 | // http: PUT /authors/{id} 98 | func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) (sql.Result, error) { 99 | return q.db.ExecContext(ctx, updateAuthor, arg.Name, arg.Bio, arg.ID) 100 | } 101 | 102 | const updateAuthorBio = `-- name: UpdateAuthorBio :execresult 103 | UPDATE authors 104 | SET bio = ? 105 | WHERE id = ? 106 | ` 107 | 108 | type UpdateAuthorBioParams struct { 109 | Bio sql.NullString 110 | ID int64 111 | } 112 | 113 | // http: PATCH /authors/{id}/bio 114 | func (q *Queries) UpdateAuthorBio(ctx context.Context, arg UpdateAuthorBioParams) (sql.Result, error) { 115 | return q.db.ExecContext(ctx, updateAuthorBio, arg.Bio, arg.ID) 116 | } 117 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/internal/authors/routes.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package authors 4 | 5 | import "net/http" 6 | 7 | func (s *Service) RegisterHandlers(mux *http.ServeMux) { 8 | mux.HandleFunc("POST /authors", s.handleCreateAuthor()) 9 | mux.HandleFunc("DELETE /authors/{id}", s.handleDeleteAuthor()) 10 | mux.HandleFunc("GET /authors/{id}", s.handleGetAuthor()) 11 | mux.HandleFunc("GET /authors", s.handleListAuthors()) 12 | mux.HandleFunc("PUT /authors/{id}", s.handleUpdateAuthor()) 13 | mux.HandleFunc("PATCH /authors/{id}/bio", s.handleUpdateAuthorBio()) 14 | } 15 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/internal/authors/service.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package authors 4 | 5 | import ( 6 | "database/sql" 7 | "log/slog" 8 | "net/http" 9 | "strconv" 10 | 11 | "authors/internal/server" 12 | ) 13 | 14 | type Service struct { 15 | querier *Queries 16 | } 17 | 18 | func (s *Service) handleCreateAuthor() http.HandlerFunc { 19 | type request struct { 20 | Name string `json:"name"` 21 | Bio *string `json:"bio"` 22 | } 23 | type response struct { 24 | LastInsertId int64 `json:"last_insert_id"` 25 | RowsAffected int64 `json:"rows_affected"` 26 | } 27 | 28 | return func(w http.ResponseWriter, r *http.Request) { 29 | req, err := server.Decode[request](r) 30 | if err != nil { 31 | http.Error(w, err.Error(), http.StatusUnprocessableEntity) 32 | return 33 | } 34 | var arg CreateAuthorParams 35 | arg.Name = req.Name 36 | if req.Bio != nil { 37 | arg.Bio = sql.NullString{Valid: true, String: *req.Bio} 38 | } 39 | 40 | result, err := s.querier.CreateAuthor(r.Context(), arg) 41 | if err != nil { 42 | slog.Error("sql call failed", "error", err, "method", "CreateAuthor") 43 | http.Error(w, err.Error(), http.StatusInternalServerError) 44 | return 45 | } 46 | 47 | lastInsertId, _ := result.LastInsertId() 48 | rowsAffected, _ := result.RowsAffected() 49 | server.Encode(w, r, http.StatusOK, response{ 50 | LastInsertId: lastInsertId, 51 | RowsAffected: rowsAffected, 52 | }) 53 | } 54 | } 55 | 56 | func (s *Service) handleDeleteAuthor() http.HandlerFunc { 57 | type request struct { 58 | Id int64 `json:"id"` 59 | } 60 | 61 | return func(w http.ResponseWriter, r *http.Request) { 62 | var req request 63 | if str := r.PathValue("id"); str != "" { 64 | if v, err := strconv.ParseInt(str, 10, 64); err != nil { 65 | http.Error(w, err.Error(), http.StatusBadRequest) 66 | return 67 | } else { 68 | req.Id = v 69 | } 70 | } 71 | id := req.Id 72 | 73 | err := s.querier.DeleteAuthor(r.Context(), id) 74 | if err != nil { 75 | slog.Error("sql call failed", "error", err, "method", "DeleteAuthor") 76 | http.Error(w, err.Error(), http.StatusInternalServerError) 77 | return 78 | } 79 | 80 | } 81 | } 82 | 83 | func (s *Service) handleGetAuthor() http.HandlerFunc { 84 | type request struct { 85 | Id int64 `json:"id"` 86 | } 87 | type response struct { 88 | ID int64 `json:"id,omitempty"` 89 | Name string `json:"name,omitempty"` 90 | Bio *string `json:"bio,omitempty"` 91 | } 92 | 93 | return func(w http.ResponseWriter, r *http.Request) { 94 | var req request 95 | if str := r.PathValue("id"); str != "" { 96 | if v, err := strconv.ParseInt(str, 10, 64); err != nil { 97 | http.Error(w, err.Error(), http.StatusBadRequest) 98 | return 99 | } else { 100 | req.Id = v 101 | } 102 | } 103 | id := req.Id 104 | 105 | result, err := s.querier.GetAuthor(r.Context(), id) 106 | if err != nil { 107 | slog.Error("sql call failed", "error", err, "method", "GetAuthor") 108 | http.Error(w, err.Error(), http.StatusInternalServerError) 109 | return 110 | } 111 | 112 | var res response 113 | res.ID = result.ID 114 | res.Name = result.Name 115 | if result.Bio.Valid { 116 | res.Bio = &result.Bio.String 117 | } 118 | server.Encode(w, r, http.StatusOK, res) 119 | } 120 | } 121 | 122 | func (s *Service) handleListAuthors() http.HandlerFunc { 123 | type response struct { 124 | ID int64 `json:"id,omitempty"` 125 | Name string `json:"name,omitempty"` 126 | Bio *string `json:"bio,omitempty"` 127 | } 128 | 129 | return func(w http.ResponseWriter, r *http.Request) { 130 | 131 | result, err := s.querier.ListAuthors(r.Context()) 132 | if err != nil { 133 | slog.Error("sql call failed", "error", err, "method", "ListAuthors") 134 | http.Error(w, err.Error(), http.StatusInternalServerError) 135 | return 136 | } 137 | 138 | res := make([]response, 0) 139 | for _, r := range result { 140 | var item response 141 | item.ID = r.ID 142 | item.Name = r.Name 143 | if r.Bio.Valid { 144 | item.Bio = &r.Bio.String 145 | } 146 | res = append(res, item) 147 | } 148 | server.Encode(w, r, http.StatusOK, res) 149 | } 150 | } 151 | 152 | func (s *Service) handleUpdateAuthor() http.HandlerFunc { 153 | type request struct { 154 | Name string `json:"name"` 155 | Bio *string `json:"bio"` 156 | ID int64 `json:"id"` 157 | } 158 | type response struct { 159 | LastInsertId int64 `json:"last_insert_id"` 160 | RowsAffected int64 `json:"rows_affected"` 161 | } 162 | 163 | return func(w http.ResponseWriter, r *http.Request) { 164 | req, err := server.Decode[request](r) 165 | if err != nil { 166 | http.Error(w, err.Error(), http.StatusUnprocessableEntity) 167 | return 168 | } 169 | if str := r.PathValue("id"); str != "" { 170 | if v, err := strconv.ParseInt(str, 10, 64); err != nil { 171 | http.Error(w, err.Error(), http.StatusBadRequest) 172 | return 173 | } else { 174 | req.ID = v 175 | } 176 | } 177 | var arg UpdateAuthorParams 178 | arg.Name = req.Name 179 | if req.Bio != nil { 180 | arg.Bio = sql.NullString{Valid: true, String: *req.Bio} 181 | } 182 | arg.ID = req.ID 183 | 184 | result, err := s.querier.UpdateAuthor(r.Context(), arg) 185 | if err != nil { 186 | slog.Error("sql call failed", "error", err, "method", "UpdateAuthor") 187 | http.Error(w, err.Error(), http.StatusInternalServerError) 188 | return 189 | } 190 | 191 | lastInsertId, _ := result.LastInsertId() 192 | rowsAffected, _ := result.RowsAffected() 193 | server.Encode(w, r, http.StatusOK, response{ 194 | LastInsertId: lastInsertId, 195 | RowsAffected: rowsAffected, 196 | }) 197 | } 198 | } 199 | 200 | func (s *Service) handleUpdateAuthorBio() http.HandlerFunc { 201 | type request struct { 202 | Bio *string `json:"bio"` 203 | ID int64 `json:"id"` 204 | } 205 | type response struct { 206 | LastInsertId int64 `json:"last_insert_id"` 207 | RowsAffected int64 `json:"rows_affected"` 208 | } 209 | 210 | return func(w http.ResponseWriter, r *http.Request) { 211 | req, err := server.Decode[request](r) 212 | if err != nil { 213 | http.Error(w, err.Error(), http.StatusUnprocessableEntity) 214 | return 215 | } 216 | if str := r.PathValue("id"); str != "" { 217 | if v, err := strconv.ParseInt(str, 10, 64); err != nil { 218 | http.Error(w, err.Error(), http.StatusBadRequest) 219 | return 220 | } else { 221 | req.ID = v 222 | } 223 | } 224 | var arg UpdateAuthorBioParams 225 | if req.Bio != nil { 226 | arg.Bio = sql.NullString{Valid: true, String: *req.Bio} 227 | } 228 | arg.ID = req.ID 229 | 230 | result, err := s.querier.UpdateAuthorBio(r.Context(), arg) 231 | if err != nil { 232 | slog.Error("sql call failed", "error", err, "method", "UpdateAuthorBio") 233 | http.Error(w, err.Error(), http.StatusInternalServerError) 234 | return 235 | } 236 | 237 | lastInsertId, _ := result.LastInsertId() 238 | rowsAffected, _ := result.RowsAffected() 239 | server.Encode(w, r, http.StatusOK, response{ 240 | LastInsertId: lastInsertId, 241 | RowsAffected: rowsAffected, 242 | }) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/internal/authors/service_factory.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package authors 4 | 5 | // NewService is a constructor of a interface { func RegisterHandlers(*http.ServeMux) } implementation. 6 | // Use this function to customize the server by adding middlewares to it. 7 | func NewService(querier *Queries) *Service { 8 | return &Service{querier: querier} 9 | } 10 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/internal/server/encoding.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package server 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/go-playground/form/v4" 11 | ) 12 | 13 | var formDecoder *form.Decoder 14 | 15 | func init() { 16 | formDecoder = form.NewDecoder() 17 | formDecoder.SetTagName("json") 18 | 19 | } 20 | 21 | func Decode[T any](r *http.Request) (T, error) { 22 | var v T 23 | if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" { 24 | if err := r.ParseForm(); err != nil { 25 | return v, fmt.Errorf("parse form: %w", err) 26 | } 27 | if err := formDecoder.Decode(&v, r.Form); err != nil { 28 | return v, fmt.Errorf("decode form: %w", err) 29 | } 30 | } else { 31 | if err := json.NewDecoder(r.Body).Decode(&v); err != nil { 32 | return v, fmt.Errorf("decode json: %w", err) 33 | } 34 | } 35 | return v, nil 36 | } 37 | 38 | func Encode[T any](w http.ResponseWriter, r *http.Request, status int, v T) error { 39 | 40 | w.Header().Set("Content-Type", "application/json") 41 | w.WriteHeader(status) 42 | if err := json.NewEncoder(w).Encode(v); err != nil { 43 | return fmt.Errorf("encode json: %w", err) 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/internal/server/litefs/forward.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package litefs 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "io" 10 | "log/slog" 11 | "net/http" 12 | "time" 13 | 14 | "github.com/superfly/ltx" 15 | ) 16 | 17 | const txCookieName = "_txid" 18 | 19 | type RedirectTarget func() string 20 | 21 | func (lfs *LiteFS) ForwardToLeader(timeout time.Duration, methods ...string) func(http.Handler) http.Handler { 22 | return func(h http.Handler) http.Handler { 23 | return http.HandlerFunc(lfs.ForwardToLeaderFunc(h.ServeHTTP, timeout, methods...)) 24 | } 25 | } 26 | 27 | func (lfs *LiteFS) ForwardToLeaderFunc(h http.HandlerFunc, timeout time.Duration, methods ...string) http.HandlerFunc { 28 | return func(w http.ResponseWriter, r *http.Request) { 29 | var match bool 30 | 31 | for _, method := range methods { 32 | if r.Method == method { 33 | match = true 34 | break 35 | } 36 | } 37 | 38 | if !match { 39 | h(w, r) 40 | return 41 | } 42 | 43 | isLeader := lfs.store.IsPrimary() 44 | if isLeader { 45 | h(&responseWriter{w: w, lfs: lfs}, r) 46 | return 47 | } 48 | 49 | target := lfs.redirectTarget() 50 | if target == "" { 51 | http.Error(w, "leader redirect URL not found", http.StatusInternalServerError) 52 | return 53 | } 54 | 55 | if r.URL.Query().Get("forward") == "false" { 56 | w.Header().Set("location", string(target)) 57 | w.WriteHeader(http.StatusMovedPermanently) 58 | return 59 | } 60 | 61 | resp, err := forwardTo(target, r, timeout) 62 | if err != nil { 63 | http.Error(w, err.Error(), http.StatusInternalServerError) 64 | return 65 | } 66 | defer resp.Body.Close() 67 | for k, v := range resp.Header { 68 | for i, value := range v { 69 | if i == 0 { 70 | w.Header().Set(k, value) 71 | continue 72 | } 73 | w.Header().Add(k, value) 74 | } 75 | } 76 | w.WriteHeader(resp.StatusCode) 77 | io.Copy(w, resp.Body) 78 | } 79 | } 80 | 81 | func (lfs *LiteFS) ConsistentReader(timeout time.Duration, methods ...string) func(http.Handler) http.Handler { 82 | return func(h http.Handler) http.Handler { 83 | return http.HandlerFunc(lfs.ConsistentReaderFunc(h.ServeHTTP, timeout, methods...)) 84 | } 85 | } 86 | 87 | func (lfs *LiteFS) ConsistentReaderFunc(h http.HandlerFunc, timeout time.Duration, methods ...string) http.HandlerFunc { 88 | return func(w http.ResponseWriter, r *http.Request) { 89 | var match bool 90 | 91 | for _, method := range methods { 92 | if r.Method == method { 93 | match = true 94 | break 95 | } 96 | } 97 | 98 | if !match || lfs.store.IsPrimary() { 99 | h(w, r) 100 | return 101 | } 102 | 103 | var txID ltx.TXID 104 | if cookie, _ := r.Cookie(txCookieName); cookie != nil { 105 | var err error 106 | txID, err = ltx.ParseTXID(cookie.Value) 107 | if err != nil { 108 | slog.Warn("invalid cookie", "name", txCookieName, "error", err) 109 | h(w, r) 110 | return 111 | } 112 | } 113 | 114 | ticker := time.NewTicker(time.Millisecond) 115 | defer ticker.Stop() 116 | 117 | ctx, cancel := context.WithTimeout(r.Context(), timeout) 118 | defer cancel() 119 | 120 | var pos ltx.Pos 121 | LOOP: 122 | for { 123 | if pos = lfs.store.DBs()[0].Pos(); pos.TXID >= txID { 124 | break LOOP 125 | } 126 | 127 | select { 128 | case <-ctx.Done(): 129 | if r.URL.Query().Get("forward") == "false" { 130 | http.Error(w, "cosistent reader timeout", http.StatusGatewayTimeout) 131 | return 132 | } 133 | target := lfs.redirectTarget() 134 | if target == "" { 135 | http.Error(w, "leader redirect URL not found", http.StatusInternalServerError) 136 | return 137 | } 138 | resp, err := forwardTo(target, r, timeout) 139 | if err != nil { 140 | http.Error(w, err.Error(), http.StatusInternalServerError) 141 | return 142 | } 143 | defer resp.Body.Close() 144 | for k, v := range resp.Header { 145 | for i, value := range v { 146 | if i == 0 { 147 | w.Header().Set(k, value) 148 | continue 149 | } 150 | w.Header().Add(k, value) 151 | } 152 | } 153 | w.WriteHeader(resp.StatusCode) 154 | io.Copy(w, resp.Body) 155 | return 156 | case <-ticker.C: 157 | } 158 | } 159 | h(w, r) 160 | } 161 | } 162 | 163 | func forwardTo(addr string, req *http.Request, timeout time.Duration) (*http.Response, error) { 164 | newURL := addr + req.URL.Path + "?" + req.URL.RawQuery 165 | 166 | var buf bytes.Buffer 167 | defer req.Body.Close() 168 | _, err := io.Copy(&buf, req.Body) 169 | if err != nil { 170 | return nil, err 171 | } 172 | ctx, cancel := context.WithTimeout(req.Context(), timeout) 173 | defer cancel() 174 | newReq, err := http.NewRequestWithContext(ctx, req.Method, newURL, &buf) 175 | if err != nil { 176 | return nil, err 177 | } 178 | for k, v := range req.Header { 179 | for i, value := range v { 180 | if i == 0 { 181 | newReq.Header.Set(k, value) 182 | continue 183 | } 184 | newReq.Header.Add(k, value) 185 | } 186 | } 187 | return http.DefaultClient.Do(newReq) 188 | } 189 | 190 | type responseWriter struct { 191 | w http.ResponseWriter 192 | lfs *LiteFS 193 | statusCode int 194 | } 195 | 196 | func (rw *responseWriter) Header() http.Header { 197 | return rw.w.Header() 198 | } 199 | 200 | func (rw *responseWriter) Write(b []byte) (int, error) { 201 | if rw.statusCode == 0 || (rw.statusCode >= 200 && rw.statusCode < 300) { 202 | http.SetCookie(rw.w, &http.Cookie{ 203 | Name: txCookieName, 204 | Value: fmt.Sprint(rw.lfs.store.DBs()[0].Pos().TXID.String()), 205 | Expires: time.Now().Add(5 * time.Minute), 206 | HttpOnly: true, 207 | }) 208 | } 209 | return rw.w.Write(b) 210 | } 211 | 212 | func (rw *responseWriter) WriteHeader(statusCode int) { 213 | rw.statusCode = statusCode 214 | rw.w.WriteHeader(statusCode) 215 | } 216 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/internal/server/litestream/litestream.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package litestream 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/url" 9 | "os" 10 | "path" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/benbjohnson/litestream" 15 | lss3 "github.com/benbjohnson/litestream/s3" 16 | ) 17 | 18 | func Replicate(ctx context.Context, dsn, replicaURL string) (*litestream.DB, error) { 19 | if i := strings.Index(dsn, "?"); i > 0 { 20 | dsn = dsn[0:i] 21 | } 22 | dsn = strings.TrimPrefix(dsn, "file:") 23 | 24 | lsdb := litestream.NewDB(dsn) 25 | 26 | u, err := url.Parse(replicaURL) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | scheme := "https" 32 | host := u.Host 33 | path := strings.TrimPrefix(path.Clean(u.Path), "/") 34 | bucket, region, endpoint, forcePathStyle := lss3.ParseHost(host) 35 | 36 | if s := os.Getenv("LITESTREAM_SCHEME"); s != "" { 37 | if s != "https" && s != "http" { 38 | return nil, fmt.Errorf("unsupported LITESTREAM_SCHEME value: %q", s) 39 | } else { 40 | scheme = s 41 | } 42 | } 43 | 44 | if e := os.Getenv("LITESTREAM_ENDPOINT"); e != "" { 45 | endpoint = e 46 | } 47 | 48 | if r := os.Getenv("LITESTREAM_REGION"); r != "" { 49 | region = r 50 | } 51 | 52 | if endpoint != "" { 53 | endpoint = scheme + "://" + endpoint 54 | } 55 | 56 | if fps := os.Getenv("LITESTREAM_FORCE_PATH_STYLE"); fps != "" { 57 | if b, err := strconv.ParseBool(fps); err != nil { 58 | return nil, fmt.Errorf("invalid LITESTREAM_FORCE_PATH_STYLE value: %q", fps) 59 | } else { 60 | forcePathStyle = b 61 | } 62 | } 63 | 64 | client := lss3.NewReplicaClient() 65 | client.Bucket = bucket 66 | client.Path = path 67 | client.Region = region 68 | client.Endpoint = endpoint 69 | client.ForcePathStyle = forcePathStyle 70 | 71 | replica := litestream.NewReplica(lsdb, lss3.ReplicaClientType) 72 | replica.Client = client 73 | 74 | lsdb.Replicas = append(lsdb.Replicas, replica) 75 | 76 | if err := restore(ctx, replica); err != nil { 77 | return nil, err 78 | } 79 | 80 | if err := lsdb.Open(); err != nil { 81 | return nil, err 82 | } 83 | 84 | if err := lsdb.Sync(ctx); err != nil { 85 | return nil, err 86 | } 87 | 88 | return lsdb, nil 89 | } 90 | 91 | func restore(ctx context.Context, replica *litestream.Replica) error { 92 | if _, err := os.Stat(replica.DB().Path()); err == nil { 93 | return nil 94 | } else if !os.IsNotExist(err) { 95 | return err 96 | } 97 | 98 | opt := litestream.NewRestoreOptions() 99 | opt.OutputPath = replica.DB().Path() 100 | 101 | var err error 102 | if opt.Generation, _, err = replica.CalcRestoreTarget(ctx, opt); err != nil { 103 | return err 104 | } 105 | 106 | if opt.Generation == "" { 107 | return nil 108 | } 109 | 110 | if err := replica.Restore(ctx, opt); err != nil { 111 | return err 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/main.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | _ "embed" 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "log/slog" 13 | "net/http" 14 | "os" 15 | "os/signal" 16 | "path/filepath" 17 | "runtime" 18 | "syscall" 19 | "time" 20 | 21 | "github.com/flowchartsman/swaggerui" 22 | "go.uber.org/automaxprocs/maxprocs" 23 | 24 | // database driver 25 | _ "github.com/mattn/go-sqlite3" 26 | 27 | "authors/internal/server/litefs" 28 | "authors/internal/server/litestream" 29 | ) 30 | 31 | //go:generate sqlc-http -m authors -migration-path sql/migrations -litefs -litestream -append 32 | 33 | const ( 34 | serviceName = "authors" 35 | forwardTimeout = 10 * time.Second 36 | ) 37 | 38 | var ( 39 | dev bool 40 | dbURL string 41 | port int 42 | replicationURL string 43 | 44 | litefsConfig litefs.Config 45 | liteFS *litefs.LiteFS 46 | //go:embed openapi.yml 47 | openAPISpec []byte 48 | ) 49 | 50 | func main() { 51 | flag.StringVar(&dbURL, "db", "", "The Database connection URL") 52 | flag.IntVar(&port, "port", 5000, "The server port") 53 | 54 | flag.BoolVar(&dev, "dev", false, "Set logger to development mode") 55 | 56 | flag.StringVar(&replicationURL, "replication", "", "S3 replication URL") 57 | litefs.SetFlags(&litefsConfig) 58 | flag.Parse() 59 | 60 | dbURL = filepath.Join(litefsConfig.MountDir, dbURL) 61 | 62 | initLogger() 63 | 64 | if err := run(); err != nil && !errors.Is(err, http.ErrServerClosed) { 65 | slog.Error("server error", "error", err) 66 | os.Exit(1) 67 | } 68 | } 69 | 70 | func run() error { 71 | _, err := maxprocs.Set() 72 | if err != nil { 73 | slog.Warn("startup", "error", err) 74 | } 75 | slog.Info("startup", "GOMAXPROCS", runtime.GOMAXPROCS(0)) 76 | 77 | db, err := sql.Open("sqlite3", dbURL) 78 | if err != nil { 79 | return err 80 | } 81 | defer db.Close() 82 | 83 | if replicationURL != "" { 84 | slog.Info("replication", "url", replicationURL) 85 | lsdb, err := litestream.Replicate(context.Background(), dbURL, replicationURL) 86 | if err != nil { 87 | return fmt.Errorf("init replication error: %w", err) 88 | } 89 | defer lsdb.Close() 90 | } 91 | if err := ensureSchema(db); err != nil { 92 | return fmt.Errorf("migration error: %w", err) 93 | } 94 | 95 | mux := http.NewServeMux() 96 | registerHandlers(mux, db) 97 | 98 | mux.Handle("GET /swagger/", http.StripPrefix("/swagger", swaggerui.Handler(openAPISpec))) 99 | 100 | var handler http.Handler = mux 101 | if litefsConfig.MountDir != "" { 102 | err := litefsConfig.Validate() 103 | if err != nil { 104 | return fmt.Errorf("liteFS parameters validation: %w", err) 105 | } 106 | 107 | liteFS, err = litefs.Start(litefsConfig) 108 | if err != nil { 109 | return fmt.Errorf("cannot start LiteFS: %w", err) 110 | } 111 | defer liteFS.Close() 112 | 113 | <-liteFS.ReadyCh() 114 | slog.Info("LiteFS cluster is ready") 115 | 116 | mux.HandleFunc("/nodes/", liteFS.ClusterHandler) 117 | handler = liteFS.ForwardToLeader(forwardTimeout, "POST", "PUT", "PATCH", "DELETE")(handler) 118 | handler = liteFS.ConsistentReader(forwardTimeout, "GET")(handler) 119 | } 120 | 121 | server := &http.Server{ 122 | Addr: fmt.Sprintf(":%d", port), 123 | Handler: handler, 124 | // Please, configure timeouts! 125 | } 126 | 127 | done := make(chan os.Signal, 1) 128 | signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 129 | go func() { 130 | sig := <-done 131 | slog.Warn("signal detected...", "signal", sig) 132 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 133 | defer cancel() 134 | server.Shutdown(ctx) 135 | }() 136 | slog.Info("Listening...", "port", port) 137 | return server.ListenAndServe() 138 | } 139 | 140 | func initLogger() { 141 | var handler slog.Handler 142 | opts := slog.HandlerOptions{ 143 | AddSource: true, 144 | } 145 | switch { 146 | case dev: 147 | handler = slog.NewTextHandler(os.Stderr, &opts) 148 | default: 149 | handler = slog.NewJSONHandler(os.Stderr, &opts) 150 | } 151 | 152 | logger := slog.New(handler) 153 | slog.SetDefault(logger) 154 | } 155 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/migration.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "embed" 8 | 9 | "github.com/pressly/goose/v3" 10 | ) 11 | 12 | //go:embed sql/migrations 13 | var migrations embed.FS 14 | 15 | func ensureSchema(db *sql.DB) error { 16 | goose.SetBaseFS(migrations) 17 | 18 | if err := goose.SetDialect("sqlite"); err != nil { 19 | return err 20 | } 21 | 22 | return goose.Up(db, "sql/migrations") 23 | 24 | } 25 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | description: authors Services 4 | title: authors 5 | version: 0.0.1 6 | contact: 7 | name: sqlc-http 8 | url: https://github.com/walterwanderley/sqlc-http 9 | tags: 10 | - authors 11 | 12 | paths: 13 | /authors: 14 | post: 15 | tags: 16 | - authors 17 | summary: CreateAuthor 18 | requestBody: 19 | content: 20 | application/json: 21 | schema: 22 | type: object 23 | properties: 24 | name: 25 | type: string 26 | bio: 27 | type: string 28 | application/x-www-form-urlencoded: 29 | schema: 30 | type: object 31 | properties: 32 | name: 33 | type: string 34 | bio: 35 | type: string 36 | 37 | responses: 38 | "200": 39 | description: OK 40 | content: 41 | application/json: 42 | schema: 43 | type: object 44 | properties: 45 | last_insert_id: 46 | type: integer 47 | format: int64 48 | rows_affected: 49 | type: integer 50 | format: int64 51 | 52 | "default": 53 | description: Error message 54 | content: 55 | text/plain: 56 | schema: 57 | type: string 58 | get: 59 | tags: 60 | - authors 61 | summary: ListAuthors 62 | 63 | responses: 64 | "200": 65 | description: OK 66 | content: 67 | application/json: 68 | schema: 69 | type: array 70 | items: 71 | $ref: "#/components/schemas/authorsAuthor" 72 | 73 | "default": 74 | description: Error message 75 | content: 76 | text/plain: 77 | schema: 78 | type: string 79 | 80 | /authors/{id}: 81 | delete: 82 | tags: 83 | - authors 84 | summary: DeleteAuthor 85 | parameters: 86 | - name: id 87 | in: path 88 | schema: 89 | type: integer 90 | format: int64 91 | 92 | responses: 93 | "200": 94 | description: OK 95 | 96 | "default": 97 | description: Error message 98 | content: 99 | text/plain: 100 | schema: 101 | type: string 102 | get: 103 | tags: 104 | - authors 105 | summary: GetAuthor 106 | parameters: 107 | - name: id 108 | in: path 109 | schema: 110 | type: integer 111 | format: int64 112 | 113 | responses: 114 | "200": 115 | description: OK 116 | content: 117 | application/json: 118 | schema: 119 | $ref: "#/components/schemas/authorsAuthor" 120 | 121 | "default": 122 | description: Error message 123 | content: 124 | text/plain: 125 | schema: 126 | type: string 127 | put: 128 | tags: 129 | - authors 130 | summary: UpdateAuthor 131 | parameters: 132 | - name: id 133 | in: path 134 | schema: 135 | type: integer 136 | format: int64 137 | requestBody: 138 | content: 139 | application/json: 140 | schema: 141 | type: object 142 | properties: 143 | name: 144 | type: string 145 | bio: 146 | type: string 147 | application/x-www-form-urlencoded: 148 | schema: 149 | type: object 150 | properties: 151 | name: 152 | type: string 153 | bio: 154 | type: string 155 | 156 | responses: 157 | "200": 158 | description: OK 159 | content: 160 | application/json: 161 | schema: 162 | type: object 163 | properties: 164 | last_insert_id: 165 | type: integer 166 | format: int64 167 | rows_affected: 168 | type: integer 169 | format: int64 170 | 171 | "default": 172 | description: Error message 173 | content: 174 | text/plain: 175 | schema: 176 | type: string 177 | 178 | /authors/{id}/bio: 179 | patch: 180 | tags: 181 | - authors 182 | summary: UpdateAuthorBio 183 | parameters: 184 | - name: id 185 | in: path 186 | schema: 187 | type: integer 188 | format: int64 189 | requestBody: 190 | content: 191 | application/json: 192 | schema: 193 | type: object 194 | properties: 195 | bio: 196 | type: string 197 | application/x-www-form-urlencoded: 198 | schema: 199 | type: object 200 | properties: 201 | bio: 202 | type: string 203 | 204 | responses: 205 | "200": 206 | description: OK 207 | content: 208 | application/json: 209 | schema: 210 | type: object 211 | properties: 212 | last_insert_id: 213 | type: integer 214 | format: int64 215 | rows_affected: 216 | type: integer 217 | format: int64 218 | 219 | "default": 220 | description: Error message 221 | content: 222 | text/plain: 223 | schema: 224 | type: string 225 | 226 | 227 | components: 228 | schemas: 229 | authorsAuthor: 230 | type: object 231 | properties: 232 | id: 233 | type: integer 234 | format: int64 235 | name: 236 | type: string 237 | bio: 238 | type: string 239 | 240 | 241 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/registry.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "net/http" 8 | 9 | authors_app "authors/internal/authors" 10 | ) 11 | 12 | func registerHandlers(mux *http.ServeMux, db *sql.DB) { 13 | authorsService := authors_app.NewService(authors_app.New(db)) 14 | authorsService.RegisterHandlers(mux) 15 | } 16 | -------------------------------------------------------------------------------- /_examples/authors/sqlite/sql/migrations/001_authors.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | CREATE TABLE IF NOT EXISTS authors ( 3 | id integer PRIMARY KEY AUTOINCREMENT, 4 | name text NOT NULL, 5 | bio text 6 | ); 7 | 8 | -- +goose Down 9 | DROP TABLE IF EXISTS authors; -------------------------------------------------------------------------------- /_examples/authors/sqlite/sql/queries.sql: -------------------------------------------------------------------------------- 1 | /* name: GetAuthor :one */ 2 | /* http: GET /authors/{id}*/ 3 | SELECT * FROM authors 4 | WHERE id = ? LIMIT 1; 5 | 6 | /* name: ListAuthors :many */ 7 | /* http: GET /authors */ 8 | SELECT * FROM authors 9 | ORDER BY name; 10 | 11 | /* name: CreateAuthor :execresult */ 12 | /* http: POST /authors */ 13 | INSERT INTO authors ( 14 | name, bio 15 | ) VALUES ( 16 | ?, ? 17 | ); 18 | 19 | /* name: UpdateAuthor :execresult */ 20 | /* http: PUT /authors/{id} */ 21 | UPDATE authors 22 | SET name = ?, 23 | bio = ? 24 | WHERE id = ?; 25 | 26 | /* name: UpdateAuthorBio :execresult */ 27 | /* http: PATCH /authors/{id}/bio */ 28 | UPDATE authors 29 | SET bio = ? 30 | WHERE id = ?; 31 | 32 | /* name: DeleteAuthor :exec */ 33 | /* http: DELETE /authors/{id} */ 34 | DELETE FROM authors 35 | WHERE id = ?; -------------------------------------------------------------------------------- /_examples/authors/sqlite/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - schema: "./sql/migrations" 4 | queries: "./sql/queries.sql" 5 | engine: "sqlite" 6 | gen: 7 | go: 8 | out: "internal/authors" 9 | emit_interface: false 10 | emit_exact_table_names: false 11 | emit_empty_slices: false 12 | emit_exported_queries: false 13 | emit_json_tags: false 14 | emit_result_struct_pointers: false 15 | emit_params_struct_pointers: false 16 | emit_methods_with_db_argument: false -------------------------------------------------------------------------------- /_examples/booktest/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 2 | 3 | RUN go install github.com/cespare/reflex@latest 4 | 5 | WORKDIR /app 6 | COPY go.mod . 7 | COPY go.sum . 8 | 9 | RUN go mod download -x 10 | 11 | COPY configs/reflex.conf / 12 | 13 | ENTRYPOINT ["reflex", "-c", "/reflex.conf"] -------------------------------------------------------------------------------- /_examples/booktest/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Booktest example taken from [sqlc][sqlc] Git repository [examples][sqlc-git]. 4 | 5 | [sqlc]: https://sqlc.dev 6 | [sqlc-git]: https://github.com/sqlc-dev/sqlc/tree/main/examples/booktest 7 | 8 | ## Running 9 | 10 | ```sh 11 | ./gen.sh 12 | docker compose up 13 | ``` 14 | 15 | ### Exploring the API 16 | 17 | http://localhost:8080/swagger 18 | 19 | 20 | -------------------------------------------------------------------------------- /_examples/booktest/configs/grafana/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'xo-grpc' 5 | folder: '' 6 | type: file 7 | disableDeletion: false 8 | editable: true 9 | updateIntervalSeconds: 10 10 | options: 11 | path: /etc/grafana/provisioning/dashboards 12 | -------------------------------------------------------------------------------- /_examples/booktest/configs/grafana/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | datasources: 2 | - access: 'proxy' 3 | editable: true 4 | is_default: true 5 | name: 'prom1' 6 | org_id: 1 7 | type: 'prometheus' 8 | url: 'http://prometheus:9090' 9 | version: 1 10 | -------------------------------------------------------------------------------- /_examples/booktest/configs/prometheus.yml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: 'api-server' 3 | scrape_interval: 1s 4 | static_configs: 5 | - targets: ['app:8081'] 6 | -------------------------------------------------------------------------------- /_examples/booktest/configs/reflex.conf: -------------------------------------------------------------------------------- 1 | -r '(\.go$|go\.mod)' -s -- sh -c 'go run . -db postgres://postgres:secret@postgres:5432/postgres?sslmode=disable -port 8080 -prometheus-port 8081 -otlp-endpoint jaeger:4317 -dev' 2 | -------------------------------------------------------------------------------- /_examples/booktest/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | ports: 6 | - "8080:8080" 7 | volumes: 8 | - .:/app 9 | depends_on: 10 | - postgres 11 | environment: 12 | - GODEBUG=httpmuxgo121=0 13 | 14 | postgres: 15 | image: postgres 16 | volumes: 17 | - ./sql/schema.sql:/docker-entrypoint-initdb.d/1-ddl.sql 18 | environment: 19 | - POSTGRES_USER=postgres 20 | - POSTGRES_PASSWORD=secret 21 | 22 | prometheus: 23 | image: prom/prometheus 24 | command: --config.file=/etc/config/prometheus.yml 25 | volumes: 26 | - ./configs/prometheus.yml:/etc/config/prometheus.yml 27 | 28 | grafana: 29 | image: grafana/grafana 30 | volumes: 31 | - ./configs/grafana/datasources:/etc/grafana/provisioning/datasources/ 32 | - ./configs/grafana/dashboards:/etc/grafana/provisioning/dashboards/ 33 | ports: 34 | - "3000:3000" 35 | 36 | jaeger: 37 | image: jaegertracing/all-in-one 38 | ports: 39 | - "16686:16686" 40 | -------------------------------------------------------------------------------- /_examples/booktest/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -u 3 | set -e 4 | set -x 5 | 6 | go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest 7 | 8 | rm -rf internal go.mod go.sum main.go registry.go openapi.yml 9 | 10 | sqlc generate 11 | sqlc-http -m booktest -tracing -metric 12 | -------------------------------------------------------------------------------- /_examples/booktest/go.mod: -------------------------------------------------------------------------------- 1 | module booktest 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/XSAM/otelsql v0.38.0 7 | github.com/flowchartsman/swaggerui v0.0.0-20221017034628-909ed4f3701b 8 | github.com/go-playground/form/v4 v4.2.1 9 | github.com/jackc/pgx/v5 v5.7.4 10 | github.com/lib/pq v1.10.9 11 | github.com/prometheus/client_golang v1.22.0 12 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 13 | go.opentelemetry.io/otel v1.35.0 14 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 15 | go.opentelemetry.io/otel/exporters/prometheus v0.57.0 16 | go.opentelemetry.io/otel/sdk v1.35.0 17 | go.opentelemetry.io/otel/sdk/metric v1.35.0 18 | go.uber.org/automaxprocs v1.6.0 19 | ) 20 | 21 | require ( 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 24 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 25 | github.com/felixge/httpsnoop v1.0.4 // indirect 26 | github.com/go-logr/logr v1.4.2 // indirect 27 | github.com/go-logr/stdr v1.2.2 // indirect 28 | github.com/google/uuid v1.6.0 // indirect 29 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect 30 | github.com/jackc/pgpassfile v1.0.0 // indirect 31 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 32 | github.com/jackc/puddle/v2 v2.2.2 // indirect 33 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 34 | github.com/prometheus/client_model v0.6.1 // indirect 35 | github.com/prometheus/common v0.62.0 // indirect 36 | github.com/prometheus/procfs v0.15.1 // indirect 37 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 38 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect 39 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 40 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 41 | go.opentelemetry.io/proto/otlp v1.5.0 // indirect 42 | golang.org/x/crypto v0.33.0 // indirect 43 | golang.org/x/net v0.35.0 // indirect 44 | golang.org/x/sync v0.11.0 // indirect 45 | golang.org/x/sys v0.31.0 // indirect 46 | golang.org/x/text v0.22.0 // indirect 47 | google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect 48 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 49 | google.golang.org/grpc v1.71.0 // indirect 50 | google.golang.org/protobuf v1.36.5 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /_examples/booktest/internal/books/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | 5 | package books 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /_examples/booktest/internal/books/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | 5 | package books 6 | 7 | import ( 8 | "database/sql/driver" 9 | "fmt" 10 | "time" 11 | ) 12 | 13 | type BookType string 14 | 15 | const ( 16 | BookTypeFICTION BookType = "FICTION" 17 | BookTypeNONFICTION BookType = "NONFICTION" 18 | ) 19 | 20 | func (e *BookType) Scan(src interface{}) error { 21 | switch s := src.(type) { 22 | case []byte: 23 | *e = BookType(s) 24 | case string: 25 | *e = BookType(s) 26 | default: 27 | return fmt.Errorf("unsupported scan type for BookType: %T", src) 28 | } 29 | return nil 30 | } 31 | 32 | type NullBookType struct { 33 | BookType BookType 34 | Valid bool // Valid is true if BookType is not NULL 35 | } 36 | 37 | // Scan implements the Scanner interface. 38 | func (ns *NullBookType) Scan(value interface{}) error { 39 | if value == nil { 40 | ns.BookType, ns.Valid = "", false 41 | return nil 42 | } 43 | ns.Valid = true 44 | return ns.BookType.Scan(value) 45 | } 46 | 47 | // Value implements the driver Valuer interface. 48 | func (ns NullBookType) Value() (driver.Value, error) { 49 | if !ns.Valid { 50 | return nil, nil 51 | } 52 | return string(ns.BookType), nil 53 | } 54 | 55 | type Author struct { 56 | AuthorID int32 57 | Name string 58 | } 59 | 60 | type Book struct { 61 | BookID int32 62 | AuthorID int32 63 | Isbn string 64 | BookType BookType 65 | Title string 66 | Year int32 67 | Available time.Time 68 | Tags []string 69 | } 70 | -------------------------------------------------------------------------------- /_examples/booktest/internal/books/queries.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.29.0 4 | // source: queries.sql 5 | 6 | package books 7 | 8 | import ( 9 | "context" 10 | "database/sql" 11 | "time" 12 | 13 | "github.com/lib/pq" 14 | ) 15 | 16 | const booksByTags = `-- name: BooksByTags :many 17 | SELECT 18 | book_id, 19 | title, 20 | name, 21 | isbn, 22 | tags 23 | FROM books 24 | LEFT JOIN authors ON books.author_id = authors.author_id 25 | WHERE tags && $1::varchar[] 26 | ` 27 | 28 | type BooksByTagsRow struct { 29 | BookID int32 30 | Title string 31 | Name sql.NullString 32 | Isbn string 33 | Tags []string 34 | } 35 | 36 | func (q *Queries) BooksByTags(ctx context.Context, dollar_1 []string) ([]BooksByTagsRow, error) { 37 | rows, err := q.db.QueryContext(ctx, booksByTags, pq.Array(dollar_1)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer rows.Close() 42 | var items []BooksByTagsRow 43 | for rows.Next() { 44 | var i BooksByTagsRow 45 | if err := rows.Scan( 46 | &i.BookID, 47 | &i.Title, 48 | &i.Name, 49 | &i.Isbn, 50 | pq.Array(&i.Tags), 51 | ); err != nil { 52 | return nil, err 53 | } 54 | items = append(items, i) 55 | } 56 | if err := rows.Close(); err != nil { 57 | return nil, err 58 | } 59 | if err := rows.Err(); err != nil { 60 | return nil, err 61 | } 62 | return items, nil 63 | } 64 | 65 | const booksByTitleYear = `-- name: BooksByTitleYear :many 66 | SELECT book_id, author_id, isbn, book_type, title, year, available, tags FROM books 67 | WHERE title = $1 AND year = $2 68 | ` 69 | 70 | type BooksByTitleYearParams struct { 71 | Title string 72 | Year int32 73 | } 74 | 75 | func (q *Queries) BooksByTitleYear(ctx context.Context, arg BooksByTitleYearParams) ([]Book, error) { 76 | rows, err := q.db.QueryContext(ctx, booksByTitleYear, arg.Title, arg.Year) 77 | if err != nil { 78 | return nil, err 79 | } 80 | defer rows.Close() 81 | var items []Book 82 | for rows.Next() { 83 | var i Book 84 | if err := rows.Scan( 85 | &i.BookID, 86 | &i.AuthorID, 87 | &i.Isbn, 88 | &i.BookType, 89 | &i.Title, 90 | &i.Year, 91 | &i.Available, 92 | pq.Array(&i.Tags), 93 | ); err != nil { 94 | return nil, err 95 | } 96 | items = append(items, i) 97 | } 98 | if err := rows.Close(); err != nil { 99 | return nil, err 100 | } 101 | if err := rows.Err(); err != nil { 102 | return nil, err 103 | } 104 | return items, nil 105 | } 106 | 107 | const createAuthor = `-- name: CreateAuthor :one 108 | INSERT INTO authors (name) VALUES ($1) 109 | RETURNING author_id, name 110 | ` 111 | 112 | func (q *Queries) CreateAuthor(ctx context.Context, name string) (Author, error) { 113 | row := q.db.QueryRowContext(ctx, createAuthor, name) 114 | var i Author 115 | err := row.Scan(&i.AuthorID, &i.Name) 116 | return i, err 117 | } 118 | 119 | const createBook = `-- name: CreateBook :one 120 | INSERT INTO books ( 121 | author_id, 122 | isbn, 123 | book_type, 124 | title, 125 | year, 126 | available, 127 | tags 128 | ) VALUES ( 129 | $1, 130 | $2, 131 | $3, 132 | $4, 133 | $5, 134 | $6, 135 | $7 136 | ) 137 | RETURNING book_id, author_id, isbn, book_type, title, year, available, tags 138 | ` 139 | 140 | type CreateBookParams struct { 141 | AuthorID int32 142 | Isbn string 143 | BookType BookType 144 | Title string 145 | Year int32 146 | Available time.Time 147 | Tags []string 148 | } 149 | 150 | func (q *Queries) CreateBook(ctx context.Context, arg CreateBookParams) (Book, error) { 151 | row := q.db.QueryRowContext(ctx, createBook, 152 | arg.AuthorID, 153 | arg.Isbn, 154 | arg.BookType, 155 | arg.Title, 156 | arg.Year, 157 | arg.Available, 158 | pq.Array(arg.Tags), 159 | ) 160 | var i Book 161 | err := row.Scan( 162 | &i.BookID, 163 | &i.AuthorID, 164 | &i.Isbn, 165 | &i.BookType, 166 | &i.Title, 167 | &i.Year, 168 | &i.Available, 169 | pq.Array(&i.Tags), 170 | ) 171 | return i, err 172 | } 173 | 174 | const deleteBook = `-- name: DeleteBook :exec 175 | DELETE FROM books 176 | WHERE book_id = $1 177 | ` 178 | 179 | func (q *Queries) DeleteBook(ctx context.Context, bookID int32) error { 180 | _, err := q.db.ExecContext(ctx, deleteBook, bookID) 181 | return err 182 | } 183 | 184 | const getAuthor = `-- name: GetAuthor :one 185 | SELECT author_id, name FROM authors 186 | WHERE author_id = $1 187 | ` 188 | 189 | func (q *Queries) GetAuthor(ctx context.Context, authorID int32) (Author, error) { 190 | row := q.db.QueryRowContext(ctx, getAuthor, authorID) 191 | var i Author 192 | err := row.Scan(&i.AuthorID, &i.Name) 193 | return i, err 194 | } 195 | 196 | const getBook = `-- name: GetBook :one 197 | SELECT book_id, author_id, isbn, book_type, title, year, available, tags FROM books 198 | WHERE book_id = $1 199 | ` 200 | 201 | func (q *Queries) GetBook(ctx context.Context, bookID int32) (Book, error) { 202 | row := q.db.QueryRowContext(ctx, getBook, bookID) 203 | var i Book 204 | err := row.Scan( 205 | &i.BookID, 206 | &i.AuthorID, 207 | &i.Isbn, 208 | &i.BookType, 209 | &i.Title, 210 | &i.Year, 211 | &i.Available, 212 | pq.Array(&i.Tags), 213 | ) 214 | return i, err 215 | } 216 | 217 | const updateBook = `-- name: UpdateBook :exec 218 | UPDATE books 219 | SET title = $1, tags = $2, book_type = $3 220 | WHERE book_id = $4 221 | ` 222 | 223 | type UpdateBookParams struct { 224 | Title string 225 | Tags []string 226 | BookType BookType 227 | BookID int32 228 | } 229 | 230 | func (q *Queries) UpdateBook(ctx context.Context, arg UpdateBookParams) error { 231 | _, err := q.db.ExecContext(ctx, updateBook, 232 | arg.Title, 233 | pq.Array(arg.Tags), 234 | arg.BookType, 235 | arg.BookID, 236 | ) 237 | return err 238 | } 239 | 240 | const updateBookISBN = `-- name: UpdateBookISBN :exec 241 | UPDATE books 242 | SET title = $1, tags = $2, isbn = $4 243 | WHERE book_id = $3 244 | ` 245 | 246 | type UpdateBookISBNParams struct { 247 | Title string 248 | Tags []string 249 | BookID int32 250 | Isbn string 251 | } 252 | 253 | func (q *Queries) UpdateBookISBN(ctx context.Context, arg UpdateBookISBNParams) error { 254 | _, err := q.db.ExecContext(ctx, updateBookISBN, 255 | arg.Title, 256 | pq.Array(arg.Tags), 257 | arg.BookID, 258 | arg.Isbn, 259 | ) 260 | return err 261 | } 262 | -------------------------------------------------------------------------------- /_examples/booktest/internal/books/routes.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package books 4 | 5 | import "net/http" 6 | 7 | func (s *Service) RegisterHandlers(mux *http.ServeMux) { 8 | mux.HandleFunc("POST /books-by-tags", s.handleBooksByTags()) 9 | mux.HandleFunc("GET /books-by-title-year", s.handleBooksByTitleYear()) 10 | mux.HandleFunc("POST /author", s.handleCreateAuthor()) 11 | mux.HandleFunc("POST /book", s.handleCreateBook()) 12 | mux.HandleFunc("DELETE /book/{book_id}", s.handleDeleteBook()) 13 | mux.HandleFunc("GET /author/{author_id}", s.handleGetAuthor()) 14 | mux.HandleFunc("GET /book/{book_id}", s.handleGetBook()) 15 | mux.HandleFunc("PUT /book", s.handleUpdateBook()) 16 | mux.HandleFunc("PUT /book-isbn", s.handleUpdateBookISBN()) 17 | } 18 | -------------------------------------------------------------------------------- /_examples/booktest/internal/books/service_factory.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package books 4 | 5 | // NewService is a constructor of a interface { func RegisterHandlers(*http.ServeMux) } implementation. 6 | // Use this function to customize the server by adding middlewares to it. 7 | func NewService(querier *Queries) *Service { 8 | return &Service{querier: querier} 9 | } 10 | -------------------------------------------------------------------------------- /_examples/booktest/internal/server/encoding.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package server 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/go-playground/form/v4" 11 | ) 12 | 13 | var formDecoder *form.Decoder 14 | 15 | func init() { 16 | formDecoder = form.NewDecoder() 17 | formDecoder.SetTagName("json") 18 | 19 | } 20 | 21 | func Decode[T any](r *http.Request) (T, error) { 22 | var v T 23 | if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" { 24 | if err := r.ParseForm(); err != nil { 25 | return v, fmt.Errorf("parse form: %w", err) 26 | } 27 | if err := formDecoder.Decode(&v, r.Form); err != nil { 28 | return v, fmt.Errorf("decode form: %w", err) 29 | } 30 | } else { 31 | if err := json.NewDecoder(r.Body).Decode(&v); err != nil { 32 | return v, fmt.Errorf("decode json: %w", err) 33 | } 34 | } 35 | return v, nil 36 | } 37 | 38 | func Encode[T any](w http.ResponseWriter, r *http.Request, status int, v T) error { 39 | 40 | w.Header().Set("Content-Type", "application/json") 41 | w.WriteHeader(status) 42 | if err := json.NewEncoder(w).Encode(v); err != nil { 43 | return fmt.Errorf("encode json: %w", err) 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /_examples/booktest/internal/server/instrumentation/metric/metric.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package metric 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "log" 9 | "log/slog" 10 | "net/http" 11 | "time" 12 | 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/exporters/prometheus" 16 | "go.opentelemetry.io/otel/sdk/metric" 17 | "go.opentelemetry.io/otel/sdk/resource" 18 | semconv "go.opentelemetry.io/otel/semconv/v1.21.0" 19 | ) 20 | 21 | func Init(port int, serviceName string) error { 22 | metricExporter, err := prometheus.New() 23 | if err != nil { 24 | return err 25 | } 26 | meterProvider := metric.NewMeterProvider( 27 | metric.WithReader(metricExporter), 28 | metric.WithResource(resource.NewWithAttributes( 29 | semconv.SchemaURL, 30 | semconv.ServiceNameKey.String(serviceName), 31 | )), 32 | ) 33 | otel.SetMeterProvider(meterProvider) 34 | 35 | mux := http.NewServeMux() 36 | mux.Handle("/metrics", promhttp.Handler()) 37 | httpServer := &http.Server{ 38 | Addr: fmt.Sprintf(":%d", port), 39 | ReadTimeout: 15 * time.Second, 40 | WriteTimeout: 15 * time.Second, 41 | IdleTimeout: 60 * time.Second, 42 | Handler: mux, 43 | } 44 | slog.Info("Metrics server running", "port", port) 45 | go func() { 46 | if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 47 | log.Fatal(err.Error()) 48 | } 49 | }() 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /_examples/booktest/internal/server/instrumentation/trace/tracing.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package trace 4 | 5 | import ( 6 | "context" 7 | "log" 8 | 9 | "go.opentelemetry.io/otel" 10 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 11 | "go.opentelemetry.io/otel/propagation" 12 | "go.opentelemetry.io/otel/sdk/resource" 13 | tracesdk "go.opentelemetry.io/otel/sdk/trace" 14 | semconv "go.opentelemetry.io/otel/semconv/v1.21.0" 15 | ) 16 | 17 | func Init(ctx context.Context, serviceName string, endpoint string) (func(), error) { 18 | exp, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithInsecure()) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | tp := tracesdk.NewTracerProvider( 24 | tracesdk.WithBatcher(exp), 25 | tracesdk.WithSampler(tracesdk.AlwaysSample()), 26 | tracesdk.WithResource(resource.NewWithAttributes( 27 | semconv.SchemaURL, 28 | semconv.ServiceNameKey.String(serviceName), 29 | )), 30 | ) 31 | 32 | otel.SetTracerProvider(tp) 33 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) 34 | 35 | return func() { 36 | if err := tp.Shutdown(ctx); err != nil { 37 | log.Fatal(err.Error()) 38 | } 39 | }, nil 40 | } 41 | -------------------------------------------------------------------------------- /_examples/booktest/main.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | _ "embed" 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "log/slog" 13 | "net/http" 14 | "os" 15 | "os/signal" 16 | "runtime" 17 | "syscall" 18 | "time" 19 | 20 | "github.com/XSAM/otelsql" 21 | "github.com/flowchartsman/swaggerui" 22 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 23 | semconv "go.opentelemetry.io/otel/semconv/v1.23.0" 24 | "go.uber.org/automaxprocs/maxprocs" 25 | 26 | // database driver 27 | _ "github.com/jackc/pgx/v5/stdlib" 28 | 29 | "booktest/internal/server/instrumentation/metric" 30 | "booktest/internal/server/instrumentation/trace" 31 | ) 32 | 33 | //go:generate sqlc-http -m booktest -tracing -metric -append 34 | 35 | const serviceName = "booktest" 36 | 37 | var ( 38 | dev bool 39 | dbURL string 40 | port, prometheusPort int 41 | 42 | otlpEndpoint string 43 | 44 | //go:embed openapi.yml 45 | openAPISpec []byte 46 | ) 47 | 48 | func main() { 49 | flag.StringVar(&dbURL, "db", "", "The Database connection URL") 50 | flag.IntVar(&port, "port", 5000, "The server port") 51 | flag.IntVar(&prometheusPort, "prometheus-port", 0, "The metrics server port") 52 | flag.BoolVar(&dev, "dev", false, "Set logger to development mode") 53 | flag.StringVar(&otlpEndpoint, "otlp-endpoint", "", "The Open Telemetry Protocol Endpoint (example: localhost:4317)") 54 | 55 | flag.Parse() 56 | 57 | initLogger() 58 | 59 | if err := run(); err != nil && !errors.Is(err, http.ErrServerClosed) { 60 | slog.Error("server error", "error", err) 61 | os.Exit(1) 62 | } 63 | } 64 | 65 | func run() error { 66 | _, err := maxprocs.Set() 67 | if err != nil { 68 | slog.Warn("startup", "error", err) 69 | } 70 | slog.Info("startup", "GOMAXPROCS", runtime.GOMAXPROCS(0)) 71 | 72 | var db *sql.DB 73 | if otlpEndpoint != "" { 74 | 75 | db, err = otelsql.Open("pgx", dbURL, otelsql.WithAttributes( 76 | semconv.DBSystemPostgreSQL, 77 | )) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | err = otelsql.RegisterDBStatsMetrics(db, otelsql.WithAttributes( 83 | semconv.DBSystemPostgreSQL, 84 | )) 85 | if err != nil { 86 | return err 87 | } 88 | } else { 89 | 90 | db, err = sql.Open("pgx", dbURL) 91 | if err != nil { 92 | return err 93 | } 94 | } 95 | defer db.Close() 96 | 97 | mux := http.NewServeMux() 98 | registerHandlers(mux, db) 99 | 100 | mux.Handle("GET /swagger/", http.StripPrefix("/swagger", swaggerui.Handler(openAPISpec))) 101 | 102 | var handler http.Handler = mux 103 | if otlpEndpoint != "" { 104 | handler = otelhttp.NewHandler(handler, serviceName) 105 | } 106 | server := &http.Server{ 107 | Addr: fmt.Sprintf(":%d", port), 108 | Handler: handler, 109 | // Please, configure timeouts! 110 | } 111 | 112 | if prometheusPort > 0 { 113 | err := metric.Init(prometheusPort, serviceName) 114 | if err != nil { 115 | return err 116 | } 117 | } 118 | 119 | if otlpEndpoint != "" { 120 | shutdown, err := trace.Init(context.Background(), serviceName, otlpEndpoint) 121 | if err != nil { 122 | return err 123 | } 124 | defer shutdown() 125 | } 126 | 127 | done := make(chan os.Signal, 1) 128 | signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 129 | go func() { 130 | sig := <-done 131 | slog.Warn("signal detected...", "signal", sig) 132 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 133 | defer cancel() 134 | server.Shutdown(ctx) 135 | }() 136 | slog.Info("Listening...", "port", port) 137 | return server.ListenAndServe() 138 | } 139 | 140 | func initLogger() { 141 | var handler slog.Handler 142 | opts := slog.HandlerOptions{ 143 | AddSource: true, 144 | } 145 | switch { 146 | case dev: 147 | handler = slog.NewTextHandler(os.Stderr, &opts) 148 | default: 149 | handler = slog.NewJSONHandler(os.Stderr, &opts) 150 | } 151 | 152 | logger := slog.New(handler) 153 | slog.SetDefault(logger) 154 | } 155 | -------------------------------------------------------------------------------- /_examples/booktest/registry.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "net/http" 8 | 9 | books_app "booktest/internal/books" 10 | ) 11 | 12 | func registerHandlers(mux *http.ServeMux, db *sql.DB) { 13 | booksService := books_app.NewService(books_app.New(db)) 14 | booksService.RegisterHandlers(mux) 15 | } 16 | -------------------------------------------------------------------------------- /_examples/booktest/sql/queries.sql: -------------------------------------------------------------------------------- 1 | -- name: GetAuthor :one 2 | SELECT * FROM authors 3 | WHERE author_id = $1; 4 | 5 | -- name: GetBook :one 6 | SELECT * FROM books 7 | WHERE book_id = $1; 8 | 9 | -- name: DeleteBook :exec 10 | DELETE FROM books 11 | WHERE book_id = $1; 12 | 13 | -- name: BooksByTitleYear :many 14 | SELECT * FROM books 15 | WHERE title = $1 AND year = $2; 16 | 17 | -- name: BooksByTags :many 18 | SELECT 19 | book_id, 20 | title, 21 | name, 22 | isbn, 23 | tags 24 | FROM books 25 | LEFT JOIN authors ON books.author_id = authors.author_id 26 | WHERE tags && $1::varchar[]; 27 | 28 | -- name: CreateAuthor :one 29 | INSERT INTO authors (name) VALUES ($1) 30 | RETURNING *; 31 | 32 | -- name: CreateBook :one 33 | INSERT INTO books ( 34 | author_id, 35 | isbn, 36 | book_type, 37 | title, 38 | year, 39 | available, 40 | tags 41 | ) VALUES ( 42 | $1, 43 | $2, 44 | $3, 45 | $4, 46 | $5, 47 | $6, 48 | $7 49 | ) 50 | RETURNING *; 51 | 52 | -- name: UpdateBook :exec 53 | UPDATE books 54 | SET title = $1, tags = $2, book_type = $3 55 | WHERE book_id = $4; 56 | 57 | -- name: UpdateBookISBN :exec 58 | UPDATE books 59 | SET title = $1, tags = $2, isbn = $4 60 | WHERE book_id = $3; -------------------------------------------------------------------------------- /_examples/booktest/sql/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE authors ( 2 | author_id SERIAL PRIMARY KEY, 3 | name text NOT NULL DEFAULT '' 4 | ); 5 | 6 | CREATE INDEX authors_name_idx ON authors(name); 7 | 8 | CREATE TYPE book_type AS ENUM ( 9 | 'FICTION', 10 | 'NONFICTION' 11 | ); 12 | 13 | CREATE TABLE books ( 14 | book_id SERIAL PRIMARY KEY, 15 | author_id integer NOT NULL REFERENCES authors(author_id), 16 | isbn text NOT NULL DEFAULT '' UNIQUE, 17 | book_type book_type NOT NULL DEFAULT 'FICTION', 18 | title text NOT NULL DEFAULT '', 19 | year integer NOT NULL DEFAULT 2000, 20 | available timestamp with time zone NOT NULL DEFAULT 'NOW()', 21 | tags varchar[] NOT NULL DEFAULT '{}' 22 | ); 23 | 24 | CREATE INDEX books_title_idx ON books(title, year); 25 | 26 | CREATE FUNCTION say_hello(text) RETURNS text AS $$ 27 | BEGIN 28 | RETURN CONCAT('hello ', $1); 29 | END; 30 | $$ LANGUAGE plpgsql; 31 | 32 | CREATE INDEX books_title_lower_idx ON books(title); -------------------------------------------------------------------------------- /_examples/booktest/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - schema: "./sql/schema.sql" 4 | queries: "./sql/queries.sql" 5 | engine: "postgresql" 6 | gen: 7 | go: 8 | package: "books" 9 | out: "internal/books" 10 | emit_interface: false 11 | emit_exact_table_names: false 12 | emit_empty_slices: false 13 | emit_exported_queries: false 14 | emit_json_tags: false 15 | emit_result_struct_pointers: false 16 | emit_params_struct_pointers: false 17 | emit_methods_with_db_argument: false -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/walterwanderley/sqlc-http 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/walterwanderley/sqlc-grpc v0.19.12 7 | golang.org/x/mod v0.24.0 8 | golang.org/x/tools v0.33.0 9 | gopkg.in/yaml.v3 v3.0.1 10 | ) 11 | 12 | require ( 13 | github.com/emicklei/proto v1.14.1 // indirect 14 | golang.org/x/sync v0.14.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/emicklei/proto v1.14.1 h1:fFq+Bj70XXZWXWikcVRvYZxrMS4KIIiPAqdJ8vPrenY= 2 | github.com/emicklei/proto v1.14.1/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= 3 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 4 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 5 | github.com/walterwanderley/sqlc-grpc v0.19.12 h1:mDforcvQMreGvKZd5FG1AnrTjWvRTtHozzLHhW+hs0c= 6 | github.com/walterwanderley/sqlc-grpc v0.19.12/go.mod h1:qEa+8Ktavwnb8zRQtUzU04sfa8u8a79YLpQvqr0NESo= 7 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 8 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 9 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 10 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 11 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 12 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "regexp" 12 | "sort" 13 | "strings" 14 | 15 | "golang.org/x/mod/modfile" 16 | 17 | "github.com/walterwanderley/sqlc-grpc/config" 18 | "github.com/walterwanderley/sqlc-grpc/metadata" 19 | ) 20 | 21 | var ( 22 | gomodPath string 23 | module string 24 | ignoreQueries string 25 | migrationPath string 26 | migrationLib string 27 | liteFS bool 28 | litestream bool 29 | metric bool 30 | distributedTracing bool 31 | generateFrontend bool 32 | appendMode bool 33 | showVersion bool 34 | help bool 35 | ) 36 | 37 | func main() { 38 | flag.BoolVar(&help, "h", false, "Help for this program") 39 | flag.BoolVar(&showVersion, "v", false, "Show version") 40 | flag.BoolVar(&appendMode, "append", false, "Enable append mode. Don't rewrite editable files") 41 | flag.StringVar(&gomodPath, "go.mod", "go.mod", "Path to go.mod file") 42 | flag.StringVar(&module, "m", "my-project", "Go module name if there are no go.mod") 43 | flag.StringVar(&ignoreQueries, "i", "", "Comma separated list (regex) of queries to ignore") 44 | flag.StringVar(&migrationPath, "migration-path", "", "Path to migration directory") 45 | flag.StringVar(&migrationLib, "migration-lib", "goose", "The migration library. goose or migrate") 46 | flag.BoolVar(&liteFS, "litefs", false, "Enable support to LiteFS") 47 | flag.BoolVar(&litestream, "litestream", false, "Enable support to Litestream") 48 | flag.BoolVar(&distributedTracing, "tracing", false, "Enable support to distributed tracing") 49 | flag.BoolVar(&metric, "metric", false, "Enable support to metrics") 50 | flag.BoolVar(&generateFrontend, "frontend", false, "Generate frontend") 51 | flag.Parse() 52 | 53 | if help { 54 | flag.PrintDefaults() 55 | fmt.Println("\nFor more information, please visit https://github.com/walterwanderley/sqlc-http") 56 | return 57 | } 58 | 59 | if showVersion { 60 | fmt.Println(version) 61 | return 62 | } 63 | 64 | if migrationPath != "" { 65 | fi, err := os.Stat(migrationPath) 66 | if err != nil { 67 | log.Fatal("invalid -migration-path: ", err.Error()) 68 | } 69 | if !fi.IsDir() { 70 | log.Fatal("-migration-path must be a directory") 71 | } 72 | } 73 | 74 | cfg, err := config.Load() 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | 79 | queriesToIgnore := make([]*regexp.Regexp, 0) 80 | for _, queryName := range strings.Split(ignoreQueries, ",") { 81 | s := strings.TrimSpace(queryName) 82 | if s == "" { 83 | continue 84 | } 85 | queriesToIgnore = append(queriesToIgnore, regexp.MustCompile(s)) 86 | } 87 | 88 | if gomodPath == "go.mod" { 89 | if m := moduleFromGoMod(); m != "" { 90 | log.Println("Using module path from go.mod:", m) 91 | module = m 92 | } 93 | } 94 | 95 | args := strings.Join(os.Args, " ") 96 | if !strings.Contains(args, " -append") { 97 | args += " -append" 98 | } 99 | 100 | def := metadata.Definition{ 101 | Args: args, 102 | GoModule: module, 103 | MigrationPath: migrationPath, 104 | MigrationLib: migrationLib, 105 | Packages: make([]*metadata.Package, 0), 106 | LiteFS: liteFS, 107 | Litestream: litestream, 108 | DistributedTracing: distributedTracing, 109 | Metric: metric, 110 | } 111 | 112 | for _, p := range cfg.Packages { 113 | pkg, err := metadata.ParsePackage(metadata.PackageOpts{ 114 | Path: p.Path, 115 | EmitInterface: p.EmitInterface, 116 | EmitParamsPointers: p.EmitParamsStructPointers, 117 | EmitResultPointers: p.EmitResultStructPointers, 118 | EmitDbArgument: p.EmitMethodsWithDBArgument, 119 | }, queriesToIgnore) 120 | if err != nil { 121 | log.Fatal("parser error:", err.Error()) 122 | } 123 | pkg.GoModule = module 124 | pkg.Engine = p.Engine 125 | if p.SqlPackage == "" { 126 | pkg.SqlPackage = "database/sql" 127 | } else { 128 | pkg.SqlPackage = p.SqlPackage 129 | } 130 | 131 | if len(pkg.Services) == 0 { 132 | log.Println("No services on package", pkg.Package) 133 | continue 134 | } 135 | 136 | def.Packages = append(def.Packages, pkg) 137 | } 138 | sort.SliceStable(def.Packages, func(i, j int) bool { 139 | return strings.Compare(def.Packages[i].Package, def.Packages[j].Package) < 0 140 | }) 141 | 142 | if err := def.Validate(); err != nil { 143 | log.Fatal(err.Error()) 144 | } 145 | 146 | err = process(&def, appendMode, generateFrontend) 147 | if err != nil { 148 | log.Fatal("unable to process templates:", err.Error()) 149 | } 150 | 151 | postProcess(&def) 152 | } 153 | 154 | func moduleFromGoMod() string { 155 | f, err := os.Open(gomodPath) 156 | if err != nil { 157 | return "" 158 | } 159 | defer f.Close() 160 | 161 | b, err := io.ReadAll(f) 162 | if err != nil { 163 | return "" 164 | } 165 | 166 | return modfile.ModulePath(b) 167 | } 168 | 169 | func postProcess(def *metadata.Definition) { 170 | log.Printf("Configuring project %s...\n", def.GoModule) 171 | modDir := filepath.Dir(gomodPath) 172 | if modDir != "." { 173 | wd, err := os.Getwd() 174 | if err != nil { 175 | fmt.Println("current working directory: ", err.Error()) 176 | os.Exit(-1) 177 | } 178 | if err := os.Chdir(modDir); err != nil { 179 | fmt.Println("change working directory: ", err.Error()) 180 | os.Exit(-1) 181 | } 182 | defer os.Chdir(wd) 183 | } 184 | execCommand("go mod init " + def.GoModule) 185 | execCommand("go mod tidy") 186 | log.Println("Finished!") 187 | } 188 | 189 | func execCommand(command string) error { 190 | line := strings.Split(command, " ") 191 | cmd := exec.Command(line[0], line[1:]...) 192 | cmd.Stderr = os.Stderr 193 | cmd.Stdout = os.Stdout 194 | if err := cmd.Run(); err != nil { 195 | return fmt.Errorf("[error] %q: %w", command, err) 196 | } 197 | return nil 198 | } 199 | -------------------------------------------------------------------------------- /templates/internal/server/encoding.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package server 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/go-playground/form/v4" 11 | 12 | "{{ .GoModule}}/view" 13 | ) 14 | 15 | var formDecoder *form.Decoder 16 | 17 | func init() { 18 | formDecoder = form.NewDecoder() 19 | formDecoder.SetTagName("json") 20 | {{if UI}} 21 | formDecoder.RegisterCustomTypeFunc(func(vals []string) (interface{}, error) { 22 | if vals[0] == "" { 23 | return time.Time{}, nil 24 | } 25 | return time.ParseInLocation(time.DateOnly, vals[0], time.Local) 26 | }, time.Time{}){{end}} 27 | } 28 | 29 | func Decode[T any](r *http.Request) (T, error) { 30 | var v T 31 | if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" { 32 | if err := r.ParseForm(); err != nil { 33 | return v, fmt.Errorf("parse form: %w", err) 34 | } 35 | if err := formDecoder.Decode(&v, r.Form) ; err != nil { 36 | return v, fmt.Errorf("decode form: %w", err) 37 | } 38 | } else { 39 | if err := json.NewDecoder(r.Body).Decode(&v); err != nil { 40 | return v, fmt.Errorf("decode json: %w", err) 41 | } 42 | } 43 | return v, nil 44 | } 45 | 46 | func Encode[T any](w http.ResponseWriter, r *http.Request, status int, v T) error { 47 | {{if UI}}if !strings.Contains(r.Header.Get("Accept"), "application/json") { 48 | if err := view.RenderHTML(w, r, v); err != nil { 49 | slog.Error("render html", "error", err) 50 | } 51 | return nil 52 | }{{end}} 53 | w.Header().Set("Content-Type", "application/json") 54 | w.WriteHeader(status) 55 | if err := json.NewEncoder(w).Encode(v); err != nil { 56 | return fmt.Errorf("encode json: %w", err) 57 | } 58 | return nil 59 | } 60 | {{if UI}} 61 | func Info(w http.ResponseWriter, r *http.Request, code int, text string) error { 62 | return view.InfoMessage(code, text).Render(w, r) 63 | } 64 | 65 | func Success(w http.ResponseWriter, r *http.Request, code int, text string) error { 66 | return view.SuccessMessage(code, text).Render(w, r) 67 | } 68 | 69 | func Error(w http.ResponseWriter, r *http.Request, code int, text string) error { 70 | return view.ErrorMessage(code, text).Render(w, r) 71 | } 72 | 73 | func Warning(w http.ResponseWriter, r *http.Request, code int, text string) error { 74 | return view.WarningMessage(code, text).Render(w, r) 75 | } 76 | {{end}} 77 | -------------------------------------------------------------------------------- /templates/internal/server/instrumentation/metric/metric.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package metric 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "log" 9 | "log/slog" 10 | "net/http" 11 | "time" 12 | 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/exporters/prometheus" 16 | "go.opentelemetry.io/otel/sdk/metric" 17 | "go.opentelemetry.io/otel/sdk/resource" 18 | semconv "go.opentelemetry.io/otel/semconv/v1.21.0" 19 | ) 20 | 21 | func Init(port int, serviceName string) error { 22 | metricExporter, err := prometheus.New() 23 | if err != nil { 24 | return err 25 | } 26 | meterProvider := metric.NewMeterProvider( 27 | metric.WithReader(metricExporter), 28 | metric.WithResource(resource.NewWithAttributes( 29 | semconv.SchemaURL, 30 | semconv.ServiceNameKey.String(serviceName), 31 | )), 32 | ) 33 | otel.SetMeterProvider(meterProvider) 34 | 35 | mux := http.NewServeMux() 36 | mux.Handle("/metrics", promhttp.Handler()) 37 | httpServer := &http.Server{ 38 | Addr: fmt.Sprintf(":%d", port), 39 | ReadTimeout: 15 * time.Second, 40 | WriteTimeout: 15 * time.Second, 41 | IdleTimeout: 60 * time.Second, 42 | Handler: mux, 43 | } 44 | slog.Info("Metrics server running", "port", port) 45 | go func() { 46 | if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 47 | log.Fatal(err.Error()) 48 | } 49 | }() 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /templates/internal/server/instrumentation/trace/tracing.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package trace 4 | 5 | import ( 6 | "context" 7 | "log" 8 | 9 | "go.opentelemetry.io/otel" 10 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 11 | "go.opentelemetry.io/otel/propagation" 12 | "go.opentelemetry.io/otel/sdk/resource" 13 | tracesdk "go.opentelemetry.io/otel/sdk/trace" 14 | semconv "go.opentelemetry.io/otel/semconv/v1.21.0" 15 | ) 16 | 17 | func Init(ctx context.Context, serviceName string, endpoint string) (func(), error) { 18 | exp, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithInsecure()) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | tp := tracesdk.NewTracerProvider( 24 | tracesdk.WithBatcher(exp), 25 | tracesdk.WithSampler(tracesdk.AlwaysSample()), 26 | tracesdk.WithResource(resource.NewWithAttributes( 27 | semconv.SchemaURL, 28 | semconv.ServiceNameKey.String(serviceName), 29 | )), 30 | ) 31 | 32 | otel.SetTracerProvider(tp) 33 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) 34 | 35 | return func() { 36 | if err := tp.Shutdown(ctx); err != nil { 37 | log.Fatal(err.Error()) 38 | } 39 | }, nil 40 | } 41 | -------------------------------------------------------------------------------- /templates/internal/server/litefs/forward.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package litefs 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "io" 10 | "log/slog" 11 | "net/http" 12 | "time" 13 | 14 | "github.com/superfly/ltx" 15 | ) 16 | 17 | const txCookieName = "_txid" 18 | 19 | type RedirectTarget func() string 20 | 21 | func (lfs *LiteFS) ForwardToLeader(timeout time.Duration, methods ...string) func(http.Handler) http.Handler { 22 | return func(h http.Handler) http.Handler { 23 | return http.HandlerFunc(lfs.ForwardToLeaderFunc(h.ServeHTTP, timeout, methods...)) 24 | } 25 | } 26 | 27 | func (lfs *LiteFS) ForwardToLeaderFunc(h http.HandlerFunc, timeout time.Duration, methods ...string) http.HandlerFunc { 28 | return func(w http.ResponseWriter, r *http.Request) { 29 | var match bool 30 | 31 | for _, method := range methods { 32 | if r.Method == method { 33 | match = true 34 | break 35 | } 36 | } 37 | 38 | if !match { 39 | h(w, r) 40 | return 41 | } 42 | 43 | isLeader := lfs.store.IsPrimary() 44 | if isLeader { 45 | h(&responseWriter{w: w, lfs: lfs}, r) 46 | return 47 | } 48 | 49 | target := lfs.redirectTarget() 50 | if target == "" { 51 | http.Error(w, "leader redirect URL not found", http.StatusInternalServerError) 52 | return 53 | } 54 | 55 | if r.URL.Query().Get("forward") == "false" { 56 | w.Header().Set("location", string(target)) 57 | w.WriteHeader(http.StatusMovedPermanently) 58 | return 59 | } 60 | 61 | resp, err := forwardTo(target, r, timeout) 62 | if err != nil { 63 | http.Error(w, err.Error(), http.StatusInternalServerError) 64 | return 65 | } 66 | defer resp.Body.Close() 67 | for k, v := range resp.Header { 68 | for i, value := range v { 69 | if i == 0 { 70 | w.Header().Set(k, value) 71 | continue 72 | } 73 | w.Header().Add(k, value) 74 | } 75 | } 76 | w.WriteHeader(resp.StatusCode) 77 | io.Copy(w, resp.Body) 78 | } 79 | } 80 | 81 | func (lfs *LiteFS) ConsistentReader(timeout time.Duration, methods ...string) func(http.Handler) http.Handler { 82 | return func(h http.Handler) http.Handler { 83 | return http.HandlerFunc(lfs.ConsistentReaderFunc(h.ServeHTTP, timeout, methods...)) 84 | } 85 | } 86 | 87 | func (lfs *LiteFS) ConsistentReaderFunc(h http.HandlerFunc, timeout time.Duration, methods ...string) http.HandlerFunc { 88 | return func(w http.ResponseWriter, r *http.Request) { 89 | var match bool 90 | 91 | for _, method := range methods { 92 | if r.Method == method { 93 | match = true 94 | break 95 | } 96 | } 97 | 98 | if !match || lfs.store.IsPrimary() { 99 | h(w, r) 100 | return 101 | } 102 | 103 | var txID ltx.TXID 104 | if cookie, _ := r.Cookie(txCookieName); cookie != nil { 105 | var err error 106 | txID, err = ltx.ParseTXID(cookie.Value) 107 | if err != nil { 108 | slog.Warn("invalid cookie", "name", txCookieName, "error", err) 109 | h(w, r) 110 | return 111 | } 112 | } 113 | 114 | ticker := time.NewTicker(time.Millisecond) 115 | defer ticker.Stop() 116 | 117 | ctx, cancel := context.WithTimeout(r.Context(), timeout) 118 | defer cancel() 119 | 120 | var pos ltx.Pos 121 | LOOP: 122 | for { 123 | if pos = lfs.store.DBs()[0].Pos(); pos.TXID >= txID { 124 | break LOOP 125 | } 126 | 127 | select { 128 | case <-ctx.Done(): 129 | if r.URL.Query().Get("forward") == "false" { 130 | http.Error(w, "cosistent reader timeout", http.StatusGatewayTimeout) 131 | return 132 | } 133 | target := lfs.redirectTarget() 134 | if target == "" { 135 | http.Error(w, "leader redirect URL not found", http.StatusInternalServerError) 136 | return 137 | } 138 | resp, err := forwardTo(target, r, timeout) 139 | if err != nil { 140 | http.Error(w, err.Error(), http.StatusInternalServerError) 141 | return 142 | } 143 | defer resp.Body.Close() 144 | for k, v := range resp.Header { 145 | for i, value := range v { 146 | if i == 0 { 147 | w.Header().Set(k, value) 148 | continue 149 | } 150 | w.Header().Add(k, value) 151 | } 152 | } 153 | w.WriteHeader(resp.StatusCode) 154 | io.Copy(w, resp.Body) 155 | return 156 | case <-ticker.C: 157 | } 158 | } 159 | h(w, r) 160 | } 161 | } 162 | 163 | func forwardTo(addr string, req *http.Request, timeout time.Duration) (*http.Response, error) { 164 | newURL := addr + req.URL.Path + "?" + req.URL.RawQuery 165 | 166 | var buf bytes.Buffer 167 | defer req.Body.Close() 168 | _, err := io.Copy(&buf, req.Body) 169 | if err != nil { 170 | return nil, err 171 | } 172 | ctx, cancel := context.WithTimeout(req.Context(), timeout) 173 | defer cancel() 174 | newReq, err := http.NewRequestWithContext(ctx, req.Method, newURL, &buf) 175 | if err != nil { 176 | return nil, err 177 | } 178 | for k, v := range req.Header { 179 | for i, value := range v { 180 | if i == 0 { 181 | newReq.Header.Set(k, value) 182 | continue 183 | } 184 | newReq.Header.Add(k, value) 185 | } 186 | } 187 | return http.DefaultClient.Do(newReq) 188 | } 189 | 190 | type responseWriter struct { 191 | w http.ResponseWriter 192 | lfs *LiteFS 193 | statusCode int 194 | } 195 | 196 | func (rw *responseWriter) Header() http.Header { 197 | return rw.w.Header() 198 | } 199 | 200 | func (rw *responseWriter) Write(b []byte) (int, error) { 201 | if rw.statusCode == 0 || (rw.statusCode >= 200 && rw.statusCode < 300) { 202 | http.SetCookie(rw.w, &http.Cookie{ 203 | Name: txCookieName, 204 | Value: fmt.Sprint(rw.lfs.store.DBs()[0].Pos().TXID.String()), 205 | Expires: time.Now().Add(5 * time.Minute), 206 | HttpOnly: true, 207 | }) 208 | } 209 | return rw.w.Write(b) 210 | } 211 | 212 | func (rw *responseWriter) WriteHeader(statusCode int) { 213 | rw.statusCode = statusCode 214 | rw.w.WriteHeader(statusCode) 215 | } 216 | -------------------------------------------------------------------------------- /templates/internal/server/litestream/litestream.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package litestream 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/url" 9 | "os" 10 | "path" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/benbjohnson/litestream" 15 | lss3 "github.com/benbjohnson/litestream/s3" 16 | ) 17 | 18 | func Replicate(ctx context.Context, dsn, replicaURL string) (*litestream.DB, error) { 19 | if i := strings.Index(dsn, "?"); i > 0 { 20 | dsn = dsn[0:i] 21 | } 22 | dsn = strings.TrimPrefix(dsn, "file:") 23 | 24 | lsdb := litestream.NewDB(dsn) 25 | 26 | u, err := url.Parse(replicaURL) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | scheme := "https" 32 | host := u.Host 33 | path := strings.TrimPrefix(path.Clean(u.Path), "/") 34 | bucket, region, endpoint, forcePathStyle := lss3.ParseHost(host) 35 | 36 | if s := os.Getenv("LITESTREAM_SCHEME"); s != "" { 37 | if s != "https" && s != "http" { 38 | return nil, fmt.Errorf("unsupported LITESTREAM_SCHEME value: %q", s) 39 | } else { 40 | scheme = s 41 | } 42 | } 43 | 44 | if e := os.Getenv("LITESTREAM_ENDPOINT"); e != "" { 45 | endpoint = e 46 | } 47 | 48 | if r := os.Getenv("LITESTREAM_REGION"); r != "" { 49 | region = r 50 | } 51 | 52 | if endpoint != "" { 53 | endpoint = scheme + "://" + endpoint 54 | } 55 | 56 | if fps := os.Getenv("LITESTREAM_FORCE_PATH_STYLE"); fps != "" { 57 | if b, err := strconv.ParseBool(fps); err != nil { 58 | return nil, fmt.Errorf("invalid LITESTREAM_FORCE_PATH_STYLE value: %q", fps) 59 | } else { 60 | forcePathStyle = b 61 | } 62 | } 63 | 64 | client := lss3.NewReplicaClient() 65 | client.Bucket = bucket 66 | client.Path = path 67 | client.Region = region 68 | client.Endpoint = endpoint 69 | client.ForcePathStyle = forcePathStyle 70 | 71 | replica := litestream.NewReplica(lsdb, lss3.ReplicaClientType) 72 | replica.Client = client 73 | 74 | lsdb.Replicas = append(lsdb.Replicas, replica) 75 | 76 | if err := restore(ctx, replica); err != nil { 77 | return nil, err 78 | } 79 | 80 | if err := lsdb.Open(); err != nil { 81 | return nil, err 82 | } 83 | 84 | if err := lsdb.Sync(ctx); err != nil { 85 | return nil, err 86 | } 87 | 88 | return lsdb, nil 89 | } 90 | 91 | func restore(ctx context.Context, replica *litestream.Replica) error { 92 | if _, err := os.Stat(replica.DB().Path()); err == nil { 93 | return nil 94 | } else if !os.IsNotExist(err) { 95 | return err 96 | } 97 | 98 | opt := litestream.NewRestoreOptions() 99 | opt.OutputPath = replica.DB().Path() 100 | 101 | var err error 102 | if opt.Generation, _, err = replica.CalcRestoreTarget(ctx, opt); err != nil { 103 | return err 104 | } 105 | 106 | if opt.Generation == "" { 107 | return nil 108 | } 109 | 110 | if err := replica.Restore(ctx, opt); err != nil { 111 | return err 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /templates/migration.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "embed" 8 | "fmt" 9 | 10 | "github.com/golang-migrate/migrate/v4" 11 | driver "github.com/golang-migrate/migrate/v4/database/{{if eq .Database "postgresql"}}pgx/v5{{else}}{{.DatabaseDriver}}{{end}}" 12 | "github.com/golang-migrate/migrate/v4/source/iofs" 13 | "github.com/pressly/goose/v3" 14 | ) 15 | 16 | //go:embed {{.MigrationPath}} 17 | var migrations embed.FS 18 | 19 | func ensureSchema(db *sql.DB) error { 20 | {{if eq .MigrationLib "migrate"}}source, err := iofs.New(migrations, "{{.MigrationPath}}") 21 | if err != nil { 22 | return err 23 | } 24 | target, err := driver.WithInstance(db, new(driver.Config)) 25 | if err != nil { 26 | return err 27 | } 28 | m, err := migrate.NewWithInstance("iofs", source, "{{.Database}}", target) 29 | if err != nil { 30 | return err 31 | } 32 | err = m.Up() 33 | if err != nil && err != migrate.ErrNoChange { 34 | return err 35 | } 36 | return source.Close(){{else}}goose.SetBaseFS(migrations) 37 | 38 | if err := goose.SetDialect("{{if eq .Database "postgresql"}}postgres{{else}}{{.Database}}{{end}}"); err != nil { 39 | return err 40 | } 41 | 42 | return goose.Up(db, "{{.MigrationPath}}") 43 | {{end}} 44 | } -------------------------------------------------------------------------------- /templates/openapi.yml.tmpl: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | {{if .Info}}{{range .Info}}{{.}} 4 | {{end}}{{else}}description: {{ .GoModule}} Services 5 | title: {{ .GoModule}} 6 | version: 0.0.1 7 | contact: 8 | name: sqlc-http 9 | url: https://github.com/walterwanderley/sqlc-http{{end}} 10 | tags: 11 | {{if .Tags}}{{range .Tags}}{{.}} 12 | {{end -}}{{else}}{{range .Packages}}- {{.Package}} 13 | {{end -}}{{end}} 14 | paths: 15 | {{if .UserDefinedPaths}}{{range .UserDefinedPaths}}{{.}} 16 | {{end}}{{end}}{{range .Packages}}{{$pkg := .Package}}{{range $key, $val := . | GroupByPath}}{{$key}}: 17 | {{range $val}}{{. | HttpMethod | LowerCase}}: 18 | {{if .CustomProtoOptions}}{{range .CustomProtoOptions}}{{.}} 19 | {{end}}{{else}}tags: 20 | - {{$pkg}} 21 | summary: {{.Name}}{{end}} 22 | {{range . | ApiParameters}}{{.}} 23 | {{end}} 24 | responses: 25 | "200": 26 | description: OK 27 | {{range ApiResponse . UI}}{{.}} 28 | {{end}} 29 | "default": 30 | description: Error message 31 | content: 32 | text/plain: 33 | schema: 34 | type: string 35 | {{end}} 36 | {{end -}}{{end}} 37 | components: 38 | schemas: 39 | {{if .UserDefinedSchemas}}{{range .UserDefinedSchemas}}{{.}} 40 | {{end}}{{end}}{{range .Packages}}{{range . | ApiComponentSchemas}}{{.}} 41 | {{end}}{{end}} 42 | {{if .UserDefinedComponents}}{{range .UserDefinedComponents}}{{.}} 43 | {{end}}{{end}} 44 | {{range .ExtraDefinitions}}{{.}} 45 | {{end}} -------------------------------------------------------------------------------- /templates/registry.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "net/http" 8 | 9 | "github.com/jackc/pgx/v5/pgxpool" 10 | 11 | {{range .Packages}}{{.Package}}_app "{{ .GoModule}}/{{.SrcPath}}" 12 | {{end}} 13 | ) 14 | 15 | 16 | func registerHandlers(mux *http.ServeMux, db {{if eq .SqlPackage "pgx/v5"}}*pgxpool.Pool{{else}}*sql.DB{{end}}) { 17 | {{range .Packages}}{{.Package}}Service := {{.Package}}_app.NewService({{if .EmitDbArgument}}{{.Package}}_app.New(), db{{else}}{{.Package}}_app.New(db){{end}}) 18 | {{.Package}}Service.RegisterHandlers(mux) 19 | {{end -}} 20 | } -------------------------------------------------------------------------------- /templates/routes.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package {{.Package}} 4 | 5 | import "net/http" 6 | 7 | {{$pkg := .Package}} 8 | func (s *Service) RegisterHandlers(mux *http.ServeMux) { 9 | {{ range .Services }}mux.HandleFunc("{{. | HttpMethod}} {{. | HttpPath}}", s.handle{{.Name | UpperFirstCharacter}}()) 10 | {{ end -}} 11 | } 12 | -------------------------------------------------------------------------------- /templates/service.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). DO NOT EDIT. 2 | 3 | package {{.Package}} 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | "encoding/json" 9 | "fmt" 10 | "log/slog" 11 | "net" 12 | "net/http" 13 | "strconv" 14 | "time" 15 | 16 | "github.com/google/uuid" 17 | "github.com/jackc/pgx/v5" 18 | "github.com/jackc/pgx/v5/pgconn" 19 | "github.com/jackc/pgx/v5/pgtype" 20 | "github.com/jackc/pgx/v5/pgxpool" 21 | 22 | "{{.GoModule}}/internal/server" 23 | "{{.GoModule}}/view" 24 | ) 25 | 26 | type Service struct { 27 | querier {{if .EmitInterface}}Querier{{else}}*Queries{{end}} 28 | {{if .EmitDbArgument}}db {{if eq .SqlPackage "pgx/v5"}}*pgxpool.Pool{{else}}*sql.DB{{end}}{{end}} 29 | } 30 | 31 | {{$emitDbArgument := .EmitDbArgument}} 32 | {{ range .Services }} 33 | func (s *Service) handle{{.Name | UpperFirstCharacter}}() http.HandlerFunc { 34 | {{ range HandlerTypes . UI}}{{ .}} 35 | {{end}} 36 | return func(w http.ResponseWriter, r *http.Request) { 37 | {{ range . | Input}}{{ .}} 38 | {{end}} 39 | {{if . | HasPagination}}if arg.Limit < 1 { arg.Limit = 10 } 40 | {{end}} 41 | {{if not .EmptyOutput}}result, err := {{else}}err {{if not (or .EmptyInput (and (ne (. | HttpMethod) "GET") (ne (. | HttpMethod) "DELETE"))) }}:{{end}}= {{end}}s.querier.{{ .Name}}(r.Context(){{if $emitDbArgument}}, s.db{{end}}{{ .ParamsCallDatabase}}) 42 | if err != nil { 43 | slog.Error("sql call failed", "error", err, "method", "{{.Name}}") 44 | http.Error(w, err.Error(), http.StatusInternalServerError) 45 | return 46 | } 47 | {{if UI}}{{if . | HasPagination}}r = r.WithContext(view.ContextWithPagination(r.Context(), &view.Pagination{ 48 | Limit: req.Limit, 49 | Offset: req.Offset, 50 | })){{end}} 51 | {{range . | OutputUI}}{{ .}} 52 | {{end -}}{{else}} 53 | {{ range . | Output}}{{ .}} 54 | {{end -}}{{end -}} 55 | } 56 | } 57 | {{ end }} 58 | -------------------------------------------------------------------------------- /templates/service_factory.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc-http (https://github.com/walterwanderley/sqlc-http). 2 | 3 | package {{.Package}} 4 | 5 | import ( 6 | "database/sql" 7 | 8 | "github.com/jackc/pgx/v5/pgxpool" 9 | ) 10 | 11 | // NewService is a constructor of a interface { func RegisterHandlers(*http.ServeMux) } implementation. 12 | // Use this function to customize the server by adding middlewares to it. 13 | func NewService(querier {{if .EmitInterface}}Querier{{else}}*Queries{{end}}{{if .EmitDbArgument}}, db {{if eq .SqlPackage "pgx/v5"}}*pgxpool.Pool{{else}}*sql.DB{{end}}{{end}}) *Service { 14 | return &Service{querier: querier{{if .EmitDbArgument}}, db: db{{end}}} 15 | } 16 | -------------------------------------------------------------------------------- /templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "embed" 5 | "html/template" 6 | "strings" 7 | 8 | "github.com/walterwanderley/sqlc-grpc/converter" 9 | 10 | "github.com/walterwanderley/sqlc-http/metadata" 11 | "github.com/walterwanderley/sqlc-http/metadata/frontend" 12 | ) 13 | 14 | //go:embed * 15 | var Files embed.FS 16 | 17 | var Funcs = template.FuncMap{ 18 | "AddSpace": frontend.AddSpace, 19 | "HasPagination": frontend.HasPagination, 20 | "OutputUI": frontend.OutputUI, 21 | 22 | "UpperCase": strings.ToUpper, 23 | "LowerCase": strings.ToLower, 24 | 25 | "UpperFirstCharacter": converter.UpperFirstCharacter, 26 | "SnakeCase": converter.ToSnakeCase, 27 | "KebabCase": converter.ToKebabCase, 28 | "PascalCase": converter.ToPascalCase, 29 | 30 | "HandlerTypes": metadata.HandlerTypes, 31 | "Input": metadata.InputHttp, 32 | "Output": metadata.OutputHttp, 33 | "GroupByPath": metadata.GroupByPath, 34 | "ApiParameters": metadata.ApiParameters, 35 | "ApiResponse": metadata.ApiResponse, 36 | "ApiComponentSchemas": metadata.ApiComponentSchemas, 37 | "HttpMethod": metadata.HttpMethod, 38 | "HttpPath": metadata.HttpPath, 39 | } 40 | -------------------------------------------------------------------------------- /templates/view/breadcrumbs.go.tmpl: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import "net/http" 4 | 5 | type breadCrumb struct { 6 | Name string 7 | Href string 8 | } 9 | 10 | func breadCrumbsFromRequest(r *http.Request) []breadCrumb { 11 | {{ range .BreadCrumbs}}{{ .}} 12 | {{end}} 13 | return nil 14 | } 15 | 16 | func breadCrumbsFromStrings(items ...string) []breadCrumb { 17 | breadcrumbs := make([]breadCrumb, 0) 18 | for i := 0; i < len(items); i = i + 2 { 19 | var bc breadCrumb 20 | bc.Name = items[i] 21 | j := i + 1 22 | if j < len(items) { 23 | bc.Href = items[j] 24 | } 25 | breadcrumbs = append(breadcrumbs, bc) 26 | } 27 | return breadcrumbs 28 | } 29 | -------------------------------------------------------------------------------- /templates/view/content.go.tmpl: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | type Content[T any] struct { 11 | Data T 12 | Request *http.Request 13 | } 14 | 15 | func (c Content[T]) HxRequest() bool { 16 | return HXRequest(c.Request) 17 | } 18 | 19 | func (c Content[T]) BreadCrumbsFromRequest() []breadCrumb { 20 | return breadCrumbsFromRequest(c.Request) 21 | } 22 | 23 | func (c Content[T]) Pagination() *Pagination { 24 | pagination, _ := c.Request.Context().Value(paginationContext).(*Pagination) 25 | if pagination != nil { 26 | pagination.request = c.Request 27 | } 28 | return pagination 29 | } 30 | 31 | func (c Content[T]) BaseHref() string { 32 | schema := "http" 33 | if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto != "" { 34 | schema = forwardedProto 35 | } else if c.Request.TLS != nil { 36 | schema = "https" 37 | } 38 | context := os.Getenv("WEB_CONTEXT") 39 | return fmt.Sprintf("%s://%s%s/", schema, c.Request.Host, strings.TrimSuffix(context, "/")) 40 | } 41 | 42 | func (c Content[T]) HasQuery(key string) bool { 43 | return c.Request.URL.Query().Has(key) 44 | } 45 | 46 | func (c Content[T]) Query(key string) string { 47 | return c.Request.URL.Query().Get(key) 48 | } 49 | 50 | func (c Content[T]) MessageContext() *Message { 51 | if msg, ok := c.Request.Context().Value(messageContext).(Message); ok { 52 | return &msg 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /templates/view/etag/etag.go: -------------------------------------------------------------------------------- 1 | package etag 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "io" 7 | "io/fs" 8 | "log" 9 | "net/http" 10 | ) 11 | 12 | func HandlerFunc(fileSystem fs.FS, stripPrefix string) http.HandlerFunc { 13 | etags := generateETags(fileSystem, stripPrefix) 14 | handler := http.FileServer(http.FS(fileSystem)) 15 | if stripPrefix != "" { 16 | handler = http.StripPrefix(stripPrefix, handler) 17 | } 18 | return func(w http.ResponseWriter, r *http.Request) { 19 | if sum, ok := etags[r.URL.Path]; ok { 20 | w.Header().Set("ETag", sum) 21 | if r.Header.Get("If-None-Match") == sum { 22 | w.WriteHeader(http.StatusNotModified) 23 | return 24 | } 25 | } 26 | handler.ServeHTTP(w, r) 27 | } 28 | } 29 | 30 | func Handler(fileSystem fs.FS, stripPrefix string) http.Handler { 31 | return http.HandlerFunc(HandlerFunc(fileSystem, stripPrefix)) 32 | } 33 | 34 | func generateETags(fileSystem fs.FS, stripPrefix string) map[string]string { 35 | etags := make(map[string]string) 36 | err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error { 37 | if err != nil { 38 | return err 39 | } 40 | 41 | if d.IsDir() { 42 | return nil 43 | } 44 | 45 | in, err := fileSystem.Open(path) 46 | if err != nil { 47 | return err 48 | } 49 | defer in.Close() 50 | 51 | hasher := sha1.New() 52 | if _, err := io.Copy(hasher, in); err != nil { 53 | return err 54 | } 55 | 56 | sum := hex.EncodeToString(hasher.Sum(nil)) 57 | etags[stripPrefix+"/"+path] = sum 58 | return nil 59 | }) 60 | if err != nil { 61 | log.Println("[error] ETags:", err.Error()) 62 | } 63 | return etags 64 | } 65 | -------------------------------------------------------------------------------- /templates/view/message.go.tmpl: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "encoding/json" 5 | "html/template" 6 | "log/slog" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | retarget = "#messages" 13 | reswap = "beforeend show:body:top" 14 | ) 15 | 16 | var ( 17 | messageTemplate = template.Must(template.New("message.html").ParseFS(templatesFS, "templates/components/message.html")) 18 | messagesContextTemplate = template.Must(template.New("messages-context.html").ParseFS(templatesFS, 19 | "templates/components/messages-context.html", "templates/components/message.html")) 20 | ) 21 | 22 | type MessageType string 23 | 24 | const ( 25 | TypeInfo = MessageType("info") 26 | TypeSuccess = MessageType("success") 27 | TypeError = MessageType("error") 28 | TypeWarning = MessageType("warning") 29 | ) 30 | 31 | func (t MessageType) Icon() string { 32 | switch t { 33 | case TypeInfo: 34 | return "info-fill" 35 | case TypeSuccess: 36 | return "check-circle-fill" 37 | default: 38 | return "exclamation-triangle-fill" 39 | } 40 | } 41 | 42 | func (t MessageType) Class() string { 43 | switch t { 44 | case TypeInfo: 45 | return "primary" 46 | case TypeSuccess: 47 | return "success" 48 | case TypeError: 49 | return "danger" 50 | default: 51 | return "warning" 52 | } 53 | } 54 | 55 | type Message struct { 56 | Code int `json:"code"` 57 | Text string `json:"text"` 58 | Type MessageType `json:"type"` 59 | } 60 | 61 | func NewMessage(code int, text string, typ MessageType) Message { 62 | return Message{Code: code, 63 | Text: text, 64 | Type: typ} 65 | } 66 | 67 | func ErrorMessage(code int, text string) Message { 68 | return NewMessage(code, text, TypeError) 69 | } 70 | 71 | func InfoMessage(code int, text string) Message { 72 | return NewMessage(code, text, TypeInfo) 73 | } 74 | 75 | func SuccessMessage(code int, text string) Message { 76 | return NewMessage(code, text, TypeSuccess) 77 | } 78 | 79 | func WarningMessage(code int, text string) Message { 80 | return NewMessage(code, text, TypeWarning) 81 | } 82 | 83 | func (m Message) Render(w http.ResponseWriter, r *http.Request) error { 84 | if strings.Contains(r.Header.Get("accept"), "application/json") { 85 | w.WriteHeader(m.Code) 86 | return json.NewEncoder(w).Encode(m) 87 | } 88 | 89 | if HXRequest(r) { 90 | if r.Method == http.MethodDelete { 91 | err := messagesContextTemplate.Execute(w, m) 92 | if err != nil { 93 | slog.Error("render messages-context", "err", err) 94 | } 95 | return err 96 | } 97 | w.Header().Set("HX-Retarget", retarget) 98 | w.Header().Set("HX-Reswap", reswap) 99 | } 100 | 101 | err := messageTemplate.Execute(w, m) 102 | if err != nil { 103 | slog.Error("render message", "err", err) 104 | } 105 | return err 106 | } 107 | -------------------------------------------------------------------------------- /templates/view/pagination.go.tmpl: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | const defaultLimit = 10 10 | 11 | type Pagination struct { 12 | request *http.Request 13 | Limit int64 14 | Offset int64 15 | } 16 | 17 | func (p *Pagination) From() int64 { 18 | return p.Offset + 1 19 | } 20 | 21 | func (p *Pagination) To() int64 { 22 | return p.Offset + p.validLimit() 23 | } 24 | 25 | func (p *Pagination) Next() int64 { 26 | return p.Offset + p.validLimit() 27 | } 28 | 29 | func (p *Pagination) Prev() int64 { 30 | limit := p.validLimit() 31 | offset := p.Offset - limit 32 | if offset < 0 { 33 | offset = 0 34 | } 35 | return offset 36 | } 37 | 38 | func (p *Pagination) URL(limit, offset int64) string { 39 | if p == nil { 40 | return "" 41 | } 42 | if offset < 0 { 43 | offset = 0 44 | } 45 | if limit == 0 { 46 | limit = defaultLimit 47 | } 48 | var url strings.Builder 49 | url.WriteString(p.request.URL.Path) 50 | url.WriteString("?") 51 | for k := range p.request.URL.Query() { 52 | if k == "limit" || k == "offset" { 53 | continue 54 | } 55 | url.WriteString(fmt.Sprintf("%s=%s&", k, p.request.URL.Query().Get(k))) 56 | } 57 | if limit > 0 { 58 | url.WriteString(fmt.Sprintf("limit=%d&offset=%d", limit, offset)) 59 | } else { 60 | url.WriteString(fmt.Sprintf("offset=%d", offset)) 61 | } 62 | return url.String() 63 | } 64 | 65 | func (p *Pagination) validLimit() int64 { 66 | limit := p.Limit 67 | if limit == 0 { 68 | limit = 10 69 | } 70 | return limit 71 | } 72 | -------------------------------------------------------------------------------- /templates/view/static/css/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterwanderley/sqlc-http/f9920c63dbd0a9b7b52a62f093bbd522ef628468/templates/view/static/css/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /templates/view/static/css/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterwanderley/sqlc-http/f9920c63dbd0a9b7b52a62f093bbd522ef628468/templates/view/static/css/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /templates/view/static/css/style.css: -------------------------------------------------------------------------------- 1 | tr.htmx-swapping td { 2 | opacity: 0; 3 | transition: opacity 1s ease-out; 4 | } 5 | 6 | footer { 7 | position: fixed; 8 | left: 0; 9 | bottom: 0; 10 | width: 100%; 11 | background-color: orange; 12 | color: white; 13 | text-align: center; 14 | } 15 | -------------------------------------------------------------------------------- /templates/view/static/js/init.js: -------------------------------------------------------------------------------- 1 | loadComponents(document); 2 | 3 | htmx.onLoad(function (content) { 4 | loadComponents(content); 5 | }); 6 | 7 | function loadComponents(content) { 8 | const alertList = content.querySelectorAll('.alert') 9 | const alerts = [...alertList].map(element => new bootstrap.Alert(element)) 10 | } 11 | 12 | function replacePathParams(event) { 13 | let pathWithParameters = event.detail.path.replace(/{([A-Za-z0-9_]+)}/g, function (_match, parameterName) { 14 | let parameterValue = event.detail.parameters[parameterName] 15 | delete event.detail.parameters[parameterName] 16 | return parameterValue 17 | }) 18 | event.detail.path = pathWithParameters 19 | } 20 | 21 | function showMessage(msg) { 22 | var msgIcon = 'exclamation-triangle-fill'; 23 | var msgClass = 'warning'; 24 | switch (msg.type) { 25 | case 'error': 26 | msgClass = 'danger'; 27 | break 28 | case 'info': 29 | msgClass = 'primary'; 30 | msgIcon = 'info-fill' 31 | break 32 | case 'success': 33 | msgClass = 'success'; 34 | msgIcon = 'check-circle-fill' 35 | break 36 | } 37 | 38 | const messageDiv = 39 | ``; 48 | 49 | var messages = htmx.find('#messages'); 50 | console.log('messages', messages); 51 | messages.innerHTML = messageDiv; 52 | } 53 | 54 | htmx.on('htmx:responseError', function (evt) { 55 | try { 56 | const msg = JSON.parse(evt.detail.xhr.response) 57 | showMessage(msg) 58 | } catch (e) { 59 | const msg = { 60 | type: 'error', 61 | text: evt.detail.xhr.response 62 | } 63 | showMessage(msg) 64 | } 65 | }); 66 | 67 | htmx.on('htmx:sendError', function () { 68 | const msg = { 69 | type: 'warning', 70 | text: 'Server unavailable. Try again in a few minutes.' 71 | } 72 | showMessage(msg) 73 | }); -------------------------------------------------------------------------------- /templates/view/static/swagger/index.html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{.GoModule | PascalCase | AddSpace}} - API 8 | 9 | 10 | 11 |
12 | 13 | 21 | 22 | -------------------------------------------------------------------------------- /templates/view/templates/app/request.html.tmpl: -------------------------------------------------------------------------------- 1 | {{"{{"}}define "content"{{"}}"}} 2 | {{"{{"}}template "hx-context.html" . {{"-}}"}} 3 |
{{.Title}}
4 | {{if .AutoSubmit}}
5 | {{else -}} 6 |
7 | {{range .HtmlInput}}{{.}} 8 | {{end}} 9 |
10 |
{{end}} 11 |
12 | {{if .HasPathParam}} 13 | 16 | {{end -}} 17 | {{"{{"}}end{{"}}"}} -------------------------------------------------------------------------------- /templates/view/templates/app/response.html.tmpl: -------------------------------------------------------------------------------- 1 | {{"{{"}}define "content"{{"}}"}} 2 | {{"{{"}}template "hx-context.html" .{{"}}"}} 3 | {{if .HasEditService}}{{"{{- "}}if .HasQuery "edit"{{"}}"}} 4 |
{{.EditName}}
5 |
6 | {{range .HtmlInputEdit}}{{.}} 7 | {{end}} 8 |
9 | 12 | {{"{{- "}}else{{" -}}"}} 13 | {{range .HtmlOutput}}{{.}} 14 | {{end -}} 15 | {{"{{- "}}end{{" -}}"}} 16 | {{else}} 17 | {{- if .Service | HasPagination}}{{"{{"}}$pagination := .Pagination{{"}}"}}{{"{{"}}if $pagination{{"}}"}} 18 |
19 | {{"{{"}}end{{"}}"}}{{- end -}} 20 | {{range .HtmlOutput}}{{.}} 21 | {{end -}} 22 | {{if .Service | HasPagination}}{{"{{"}}if $pagination{{"}}"}} 23 | {{range .HtmlPagination "#table_response"}}{{.}} 24 | {{end -}} 25 |
26 | {{"{{"}}end{{"}}"}} 27 | {{- end -}} 28 | {{- end -}} 29 | {{"{{"}}end{{"}}"}} -------------------------------------------------------------------------------- /templates/view/templates/components/breadcrumbs.html.tmpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/view/templates/components/hx-context.html: -------------------------------------------------------------------------------- 1 | {{if and .HxRequest (not (eq (.Query "nested") "true"))}} 2 | {{ template "messages-context.html" .MessageContext }} 3 | {{ template "breadcrumbs.html" . }} 4 | {{end -}} 5 | -------------------------------------------------------------------------------- /templates/view/templates/components/message.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/view/templates/components/messages-context.html: -------------------------------------------------------------------------------- 1 |
2 | {{if .}} 3 | {{ template "message.html" .}} 4 | {{end -}} 5 |
-------------------------------------------------------------------------------- /templates/view/templates/index.html.tmpl: -------------------------------------------------------------------------------- 1 | {{"{{"}}define "content"{{"}}"}} 2 |
3 | {{"{{"}}template "hx-context.html" .{{"}}"}} 4 |

5 |

{{.GoModule | PascalCase}}

6 |
Generated by SQLC-HTTP
7 |

8 |
9 | {{"{{"}}end{{"}}"}} -------------------------------------------------------------------------------- /templates/view/templates/layout/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{.Title}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {{ template "header.html" .}} 18 |
19 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 32 | 33 | 34 |
35 |
36 | Loading... 37 |
38 | {{ template "messages-context.html" .Content.MessageContext }} 39 | {{ template "breadcrumbs.html" .Content}} 40 |
41 | {{ template "content" .Content}} 42 |
43 |
44 |
45 | {{ template "footer.html" .}} 46 | {{if .DevMode}} 47 | 56 | {{end}} 57 | 58 | -------------------------------------------------------------------------------- /templates/view/templates/layout/content.html: -------------------------------------------------------------------------------- 1 | {{- template "content" . -}} -------------------------------------------------------------------------------- /templates/view/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/view/templates/layout/header.html.tmpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/view/watcher/watcher.go.tmpl: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "log/slog" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "time" 12 | 13 | "github.com/fsnotify/fsnotify" 14 | ) 15 | 16 | const defaultPingInterval = 15 * time.Second 17 | 18 | type client chan []byte 19 | 20 | type WatchStreamer struct { 21 | watcher *fsnotify.Watcher 22 | clients map[client]struct{} 23 | connecting chan client 24 | disconnecting chan client 25 | event chan []byte 26 | pingInterval time.Duration 27 | } 28 | 29 | func New(dirs ...string) (*WatchStreamer, error) { 30 | w, err := fsnotify.NewWatcher() 31 | if err != nil { 32 | return nil, err 33 | } 34 | for _, dir := range dirs { 35 | if err := addRecurssive(w, dir); err != nil { 36 | return nil, err 37 | } 38 | } 39 | ws := WatchStreamer{ 40 | watcher: w, 41 | clients: make(map[client]struct{}), 42 | connecting: make(chan client), 43 | disconnecting: make(chan client), 44 | event: make(chan []byte, 1), 45 | pingInterval: defaultPingInterval, 46 | } 47 | return &ws, nil 48 | } 49 | 50 | func (ws *WatchStreamer) Add(dir string) error { 51 | return addRecurssive(ws.watcher, dir) 52 | } 53 | 54 | func (ws *WatchStreamer) SetPingInterval(interval time.Duration) { 55 | ws.pingInterval = interval 56 | } 57 | 58 | func (ws *WatchStreamer) Start(ctx context.Context) { 59 | go ws.run(ctx) 60 | go ws.watch(ctx) 61 | } 62 | 63 | func (ws *WatchStreamer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 64 | if r.URL.Query().Has("watchList") { 65 | json.NewEncoder(w).Encode(ws.watcher.WatchList()) 66 | return 67 | } 68 | 69 | fl, ok := w.(http.Flusher) 70 | if !ok { 71 | http.Error(w, "Flushing not supported", http.StatusNotImplemented) 72 | return 73 | } 74 | 75 | w.Header().Set("Cache-Control", "no-cache") 76 | w.Header().Set("Connection", "keep-alive") 77 | w.Header().Set("Content-Type", "text/event-stream") 78 | 79 | cl := make(client, 2) 80 | ws.connecting <- cl 81 | 82 | for { 83 | select { 84 | case <-time.After(ws.pingInterval): 85 | if _, err := w.Write([]byte("event: ping\n\n")); err != nil { 86 | slog.Error("[watcher] send ping", "error", err.Error()) 87 | return 88 | } 89 | 90 | case <-r.Context().Done(): 91 | ws.disconnecting <- cl 92 | return 93 | 94 | case event := <-cl: 95 | if _, err := w.Write(formatData(event)); err != nil { 96 | slog.Error("[watcher] send message", "error", err.Error()) 97 | return 98 | } 99 | fl.Flush() 100 | } 101 | } 102 | } 103 | 104 | func formatData(data []byte) []byte { 105 | var buf bytes.Buffer 106 | buf.WriteString("data: ") 107 | buf.Write(data) 108 | buf.WriteString("\n\n") 109 | return buf.Bytes() 110 | } 111 | 112 | func (ws *WatchStreamer) run(ctx context.Context) { 113 | for { 114 | select { 115 | case <-ctx.Done(): 116 | return 117 | case cl := <-ws.connecting: 118 | slog.Debug("[watcher] connected", "client", cl) 119 | ws.clients[cl] = struct{}{} 120 | 121 | case cl := <-ws.disconnecting: 122 | slog.Debug("[watcher] disconnected", "client", cl) 123 | delete(ws.clients, cl) 124 | 125 | case event := <-ws.event: 126 | for cl := range ws.clients { 127 | cl <- event 128 | } 129 | } 130 | } 131 | } 132 | 133 | func (ws *WatchStreamer) watch(ctx context.Context) { 134 | for { 135 | select { 136 | case <-ctx.Done(): 137 | ws.watcher.Close() 138 | return 139 | case event, ok := <-ws.watcher.Events: 140 | if !ok { 141 | return 142 | } 143 | if !event.Has(fsnotify.Write) { 144 | continue 145 | } 146 | slog.Debug("[watcher] file changed", "name", event.Name) 147 | ws.event <- []byte(event.Name) 148 | 149 | case err, ok := <-ws.watcher.Errors: 150 | if !ok { 151 | return 152 | } 153 | slog.Error("[watcher] fsnotify", "error", err) 154 | } 155 | } 156 | } 157 | 158 | func addRecurssive(w *fsnotify.Watcher, dir string) error { 159 | err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 160 | if info.IsDir() { 161 | return w.Add(path) 162 | } 163 | return nil 164 | }) 165 | return err 166 | } 167 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const version = "v0.2.17" 4 | --------------------------------------------------------------------------------