├── 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 | `
40 |
43 |
44 | ` + msg.text + `
45 |
46 |
47 |
`;
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 |
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 |
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 |
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 |
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 |
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}}
8 |
9 |
10 |
11 | ID |
12 | Name |
13 | Bio |
14 | Birth Date |
15 | |
16 |
17 |
18 | {{range .Data}}
19 | {{.ID}} |
20 | {{.Name}} |
21 | {{.Bio}} |
22 | {{if .BirthDate}}{{.BirthDate.Format "02/01/2006"}}{{end}} |
23 |
24 |
25 |
26 |
27 | |
28 |
{{end}}
29 |
30 |
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 |
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 |
2 | {{if .BreadCrumbsFromRequest}}
3 |
15 | {{end -}}
16 |
--------------------------------------------------------------------------------
/_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 |
2 |
5 |
6 | {{.Text}}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/_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 |
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 |
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 | `
40 |
43 |
44 | ` + msg.text + `
45 |
46 |
47 |
`;
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 |
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 |
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 |
2 | {{"{{"}}if .BreadCrumbsFromRequest{{"}}"}}
3 |
15 | {{"{{"}}end -{{"}}"}}
16 |
--------------------------------------------------------------------------------
/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 |
2 |
5 |
6 | {{.Text}}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
--------------------------------------------------------------------------------