From 42bdf9c70227604cf370ccbeadc531b6cd25fc6f Mon Sep 17 00:00:00 2001 From: Denis Date: Sun, 17 May 2026 21:02:09 +0300 Subject: [PATCH 01/11] refactor(repos): move hook repository out of store --- internal/{store/repository.go => repos/hook.go} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename internal/{store/repository.go => repos/hook.go} (99%) diff --git a/internal/store/repository.go b/internal/repos/hook.go similarity index 99% rename from internal/store/repository.go rename to internal/repos/hook.go index fde950e..6e9b1de 100644 --- a/internal/store/repository.go +++ b/internal/repos/hook.go @@ -1,4 +1,4 @@ -package store +package repos import ( "context" From a6d8774aa16280a07772eb5f9d6713db85254bb2 Mon Sep 17 00:00:00 2001 From: Denis Date: Sun, 17 May 2026 21:02:18 +0300 Subject: [PATCH 02/11] feat(serve): add retention cleanup flow --- internal/cli/serve/command.go | 11 +------- internal/cli/serve/flags.go | 4 +++ internal/cli/serve/options.go | 9 +++++-- internal/cli/serve/runner.go | 13 ++++++++++ internal/core/serve_core.go | 44 ++++++++++++++++++++++++++++++++ internal/repos/serve.go | 32 +++++++++++++++++++++++ internal/store/query/hooks.sql | 9 +++++++ internal/store/sqlc/hooks.sql.go | 44 ++++++++++++++++++++++++++++++++ sqlc.yaml | 2 +- 9 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 internal/cli/serve/runner.go create mode 100644 internal/core/serve_core.go create mode 100644 internal/repos/serve.go diff --git a/internal/cli/serve/command.go b/internal/cli/serve/command.go index 307ae66..0e2e420 100644 --- a/internal/cli/serve/command.go +++ b/internal/cli/serve/command.go @@ -32,7 +32,7 @@ func NewCommand(ctx context.Context, cfg *config.Config) *cobra.Command { return err } - return run(ctx, app) + return run(ctx, app, opts) }, } @@ -40,12 +40,3 @@ func NewCommand(ctx context.Context, cfg *config.Config) *cobra.Command { return cmd } - -func run(ctx context.Context, app *app.App) error { - if err := app.Start(ctx); err != nil { - slog.Error("server", "err", err) - return err - } - - return nil -} diff --git a/internal/cli/serve/flags.go b/internal/cli/serve/flags.go index 9bb6c78..f46e010 100644 --- a/internal/cli/serve/flags.go +++ b/internal/cli/serve/flags.go @@ -15,6 +15,8 @@ const ( flagMaxBodySize = "max-body-size" flagTrustedProxies = "trusted-proxies" + + flagRetention = "retention" ) func RegisterFlags(cmd *cobra.Command, cfg *config.Config, opt *Options) { @@ -28,4 +30,6 @@ func RegisterFlags(cmd *cobra.Command, cfg *config.Config, opt *Options) { flags.StringVar(&cfg.SecretKey, flagSecretKey, cfg.SecretKey, "API secret key via X-Webhix-Key or Bearer (env: WEBHIX_SECRET_KEY)") flags.Int64Var(&cfg.MaxBodySize, flagMaxBodySize, cfg.MaxBodySize, "maximum webhook request body size in bytes") flags.StringSliceVar(&cfg.TrustedProxies, flagTrustedProxies, cfg.TrustedProxies, "trusted proxy CIDRs") + + flags.DurationVarP(&opt.Retention, flagRetention, "re", opt.Retention, "TODO") } diff --git a/internal/cli/serve/options.go b/internal/cli/serve/options.go index fba5fb1..d3c1702 100644 --- a/internal/cli/serve/options.go +++ b/internal/cli/serve/options.go @@ -4,14 +4,19 @@ import ( "fmt" "net/url" "strings" + "time" "github.com/GaIsBAX/Webhix/internal/config" ) -type Options struct{} +type Options struct { + Retention time.Duration +} func DefaultOptions() Options { - return Options{} + return Options{ + Retention: time.Hour * 24, + } } func (o *Options) Validate(cfg *config.Config) error { diff --git a/internal/cli/serve/runner.go b/internal/cli/serve/runner.go new file mode 100644 index 0000000..bd4f5a8 --- /dev/null +++ b/internal/cli/serve/runner.go @@ -0,0 +1,13 @@ +package serve + +import ( + "context" + + "github.com/GaIsBAX/Webhix/internal/app" +) + +func run(ctx context.Context, application *app.App, opts Options) error { + return application.RunServe(ctx, app.ServeOptions{ + Retention: opts.Retention, + }) +} diff --git a/internal/core/serve_core.go b/internal/core/serve_core.go new file mode 100644 index 0000000..20ef8bd --- /dev/null +++ b/internal/core/serve_core.go @@ -0,0 +1,44 @@ +package core + +import ( + "context" + "time" +) + +type ServeRepository interface { + DeleteWebhookRequestsOlderThan(ctx context.Context, retention time.Duration) (int64, error) +} + +type Serve struct { + repo ServeRepository +} + +func NewServe(repo ServeRepository) *Serve { + return &Serve{ + repo: repo, + } +} + +func (s *Serve) RetentionCleaner(ctx context.Context, retention time.Duration) (int64, error) { + cleanup := func() (int64, error) { + return s.repo.DeleteWebhookRequestsOlderThan(ctx, retention) + } + + _, err := cleanup() + if err != nil { + return 0, err + } + + ticker := time.NewTicker(time.Hour * 24) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + return cleanup() + + case <-ctx.Done(): + return 0, ctx.Err() + } + } +} diff --git a/internal/repos/serve.go b/internal/repos/serve.go new file mode 100644 index 0000000..be74f5c --- /dev/null +++ b/internal/repos/serve.go @@ -0,0 +1,32 @@ +package repos + +import ( + "context" + "time" + + "github.com/GaIsBAX/Webhix/internal/store/sqlc" +) + +type Serve struct { + q *sqlc.Queries +} + +func NewServe(db sqlc.DBTX) *Serve { + return &Serve{ + q: sqlc.New(db), + } +} + +func (r *Serve) DeleteWebhookRequestsOlderThan(ctx context.Context, retention time.Duration) (int64, error) { + res, err := r.q.DeleteWebhookRequestsOlderThan(ctx, retention) + if err != nil { + return 0, err + } + + affected, err := res.RowsAffected() + if err != nil { + return 0, err + } + + return affected, nil +} diff --git a/internal/store/query/hooks.sql b/internal/store/query/hooks.sql index b7e5d4f..a4c3880 100644 --- a/internal/store/query/hooks.sql +++ b/internal/store/query/hooks.sql @@ -29,6 +29,15 @@ FROM webhook_requests WHERE hook_id = ? ORDER BY received_at DESC, id DESC; +-- name: ListWebhookRequestsByTime :many +SELECT id, token, name, created_at, updated_at +FROM hooks +WHERE created_at <= datetime('now', ?); + +-- name: DeleteWebhookRequestsOlderThan :execresult +DELETE FROM hooks +WHERE created_at < datetime('now', ?); + -- name: UpsertHookResponse :one INSERT INTO hook_responses (hook_id, status_code, headers, body) VALUES (?, ?, ?, ?) diff --git a/internal/store/sqlc/hooks.sql.go b/internal/store/sqlc/hooks.sql.go index 23579b7..ecf6a75 100644 --- a/internal/store/sqlc/hooks.sql.go +++ b/internal/store/sqlc/hooks.sql.go @@ -91,6 +91,15 @@ func (q *Queries) CreateWebhookRequest(ctx context.Context, arg CreateWebhookReq return i, err } +const deleteWebhookRequestsOlderThan = `-- name: DeleteWebhookRequestsOlderThan :execresult +DELETE FROM hooks +WHERE created_at < datetime('now', ?) +` + +func (q *Queries) DeleteWebhookRequestsOlderThan(ctx context.Context, datetime interface{}) (sql.Result, error) { + return q.db.ExecContext(ctx, deleteWebhookRequestsOlderThan, datetime) +} + const getHookByToken = `-- name: GetHookByToken :one SELECT id, token, name, created_at, updated_at FROM hooks @@ -173,6 +182,41 @@ func (q *Queries) ListWebhookRequestsByHookID(ctx context.Context, hookID int64) return items, nil } +const listWebhookRequestsByTime = `-- name: ListWebhookRequestsByTime :many +SELECT id, token, name, created_at, updated_at +FROM hooks +WHERE created_at <= datetime('now', ?) +` + +func (q *Queries) ListWebhookRequestsByTime(ctx context.Context, datetime interface{}) ([]Hook, error) { + rows, err := q.db.QueryContext(ctx, listWebhookRequestsByTime, datetime) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Hook + for rows.Next() { + var i Hook + if err := rows.Scan( + &i.ID, + &i.Token, + &i.Name, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const upsertHookResponse = `-- name: UpsertHookResponse :one INSERT INTO hook_responses (hook_id, status_code, headers, body) VALUES (?, ?, ?, ?) diff --git a/sqlc.yaml b/sqlc.yaml index afb8637..56ca5a7 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -1,7 +1,7 @@ version: "2" sql: - engine: sqlite - schema: migrations + schema: internal/store/migrations queries: internal/store/query gen: go: From 45539058c48c2bd9bbd993b6bb8d9731da603ced Mon Sep 17 00:00:00 2001 From: Denis Date: Sun, 17 May 2026 21:02:29 +0300 Subject: [PATCH 03/11] refactor(app): split application wiring --- internal/app/app.go | 107 ++++++++++++++++---------------------- internal/app/deps.go | 66 ++++++++++++------------ internal/app/http.go | 108 +++++++++++++++++++++++++++++++++++++++ internal/app/services.go | 15 ++++++ 4 files changed, 200 insertions(+), 96 deletions(-) create mode 100644 internal/app/http.go create mode 100644 internal/app/services.go diff --git a/internal/app/app.go b/internal/app/app.go index 89cbc51..ab5337a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -2,122 +2,101 @@ package app import ( "context" - "fmt" - "io/fs" "log/slog" "net/http" "time" "github.com/GaIsBAX/Webhix/internal/config" - "github.com/GaIsBAX/Webhix/internal/core" "github.com/GaIsBAX/Webhix/internal/hub" - "github.com/GaIsBAX/Webhix/internal/server" - "github.com/GaIsBAX/Webhix/internal/server/middleware" - "github.com/GaIsBAX/Webhix/internal/store" - "github.com/GaIsBAX/Webhix/internal/web" ) +const shutdownTimeout = 10 * time.Second + type App struct { - srv *http.Server - cfg *config.Config - deps *Deps - hub *hub.Hub + server *http.Server + config *config.Config + deps *dependencies + events *hub.Hub + services *services } func New(ctx context.Context, cfg *config.Config) (*App, error) { - mux := http.NewServeMux() - - deps, err := NewDeps(ctx, cfg) + deps, err := newDependencies(ctx, cfg) if err != nil { return nil, err } - eventHub := hub.New() - - hookRepository := store.NewHookRepository(deps.DB.DB) - hookService := core.NewHookService(hookRepository) - hookHandler := server.NewHookHandler(mux, hookService, eventHub, server.HookHandlerOptions{ - BaseURL: cfg.BaseURL, - MaxBodySize: cfg.MaxBodySize, - }) + services := newServices(deps.repositories) + events := hub.New() - hookHandler.RegisterRoutes() - - staticSub, err := fs.Sub(web.Static, "static") + mux, err := newMux(cfg, services, events) if err != nil { return nil, err } - staticFS := http.FileServer(http.FS(staticSub)) - mux.Handle("/ui/", http.StripPrefix("/ui/", staticFS)) - mux.Handle("/", staticFS) - password, secretKey, err := resolveAuth(cfg) + handler, err := newHTTPHandler(cfg, mux) if err != nil { - return nil, fmt.Errorf("auth setup: %w", err) - } - - handler := http.Handler(mux) - handler = middleware.NewAuth(password, secretKey).Protect(handler) - - if len(cfg.TrustedProxies) > 0 { - trustedProxies := middleware.NewTrustedProxies(cfg.TrustedProxies) - if trustedProxies == nil { - return nil, fmt.Errorf("invalid trusted proxies") - } - - handler = trustedProxies.BehindProxy(handler) + return nil, err } return &App{ - srv: &http.Server{Addr: cfg.Addr, Handler: handler, ReadHeaderTimeout: 5 * time.Second}, - cfg: cfg, - deps: deps, - hub: eventHub, + server: newHTTPServer(cfg, handler), + config: cfg, + deps: deps, + events: events, + services: services, }, nil } func (a *App) Start(ctx context.Context) error { - errCh := make(chan error, 1) + serverErr := make(chan error, 1) go func() { - slog.Info("webhix started", "addr", a.cfg.Addr, "base_url", a.cfg.BaseURL) - if err := a.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - errCh <- err + slog.Info("webhix started", "addr", a.config.Addr, "base_url", a.config.BaseURL) + if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + serverErr <- err } }() select { - case err := <-errCh: + case err := <-serverErr: return err + case <-ctx.Done(): return a.Shutdown() } } -func resolveAuth(cfg *config.Config) (password, secretKey string, err error) { - if cfg.Password == "" && cfg.SecretKey == "" { - return "", "", fmt.Errorf("auth is required: set WEBHIX_PASSWORD or WEBHIX_SECRET_KEY") - } - - return cfg.Password, cfg.SecretKey, nil -} - func (a *App) Shutdown() error { slog.Info("shutting down") - a.hub.Close() + a.events.Close() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() - if err := a.srv.Shutdown(ctx); err != nil { + if err := a.server.Shutdown(ctx); err != nil { slog.Error("graceful shutdown failed, forcing close", "err", err) - if closeErr := a.srv.Close(); closeErr != nil { + if closeErr := a.server.Close(); closeErr != nil { slog.Error("server close failed", "err", closeErr) } } - if err := a.deps.teardownInfrastructure(); err != nil { + if err := a.deps.close(); err != nil { slog.Error("teardown error", "err", err) } return nil } + +type ServeOptions struct { + Retention time.Duration +} + +func (a *App) RunServe(ctx context.Context, opts ServeOptions) error { + go func() { + if _, err := a.services.serve.RetentionCleaner(ctx, opts.Retention); err != nil { + slog.Error("retention cleaner", "err", err) + } + }() + + return a.Start(ctx) +} diff --git a/internal/app/deps.go b/internal/app/deps.go index 00a1505..b9bf0d0 100644 --- a/internal/app/deps.go +++ b/internal/app/deps.go @@ -3,55 +3,57 @@ package app import ( "context" "errors" + "fmt" "github.com/GaIsBAX/Webhix/internal/config" + "github.com/GaIsBAX/Webhix/internal/repos" "github.com/GaIsBAX/Webhix/internal/store" ) -type Deps struct { - DB *store.Database - cfg *config.Config +type dependencies struct { + db *store.Database + repositories *repositories } -func NewDeps(ctx context.Context, cfg *config.Config) (*Deps, error) { - deps := &Deps{ - cfg: cfg, +func newDependencies(ctx context.Context, cfg *config.Config) (*dependencies, error) { + db, err := store.New(ctx, cfg.DBPath) + if err != nil { + return nil, fmt.Errorf("open database: %w", err) } - if err := deps.setupInfrastructure(ctx); err != nil { - return nil, err + if err := db.Migrate(); err != nil { + if closeErr := db.Close(); closeErr != nil { + return nil, errors.Join( + fmt.Errorf("migrate database: %w", err), + fmt.Errorf("close database after migration failure: %w", closeErr), + ) + } + + return nil, fmt.Errorf("migrate database: %w", err) } - return deps, nil + return &dependencies{ + db: db, + repositories: newRepositories(db), + }, nil } -func (d *Deps) setupInfrastructure(ctx context.Context) error { - var errs []error - - database, err := store.New(ctx, d.cfg.DBPath) - if err != nil { - errs = append(errs, err) - } - - d.DB = database +type repositories struct { + hook *repos.HookRepository + serve *repos.Serve +} - if d.DB != nil { - if err := d.DB.Migrate(); err != nil { - errs = append(errs, err) - } +func newRepositories(db *store.Database) *repositories { + return &repositories{ + hook: repos.NewHookRepository(db.DB), + serve: repos.NewServe(db.DB), } - - return errors.Join(errs...) } -func (d *Deps) teardownInfrastructure() error { - var errs []error - - if d.DB != nil { - if err := d.DB.Close(); err != nil { - errs = append(errs, err) - } +func (d *dependencies) close() error { + if d.db != nil { + return d.db.Close() } - return errors.Join(errs...) + return nil } diff --git a/internal/app/http.go b/internal/app/http.go new file mode 100644 index 0000000..052e45a --- /dev/null +++ b/internal/app/http.go @@ -0,0 +1,108 @@ +package app + +import ( + "fmt" + "io/fs" + "net/http" + "time" + + "github.com/GaIsBAX/Webhix/internal/config" + "github.com/GaIsBAX/Webhix/internal/hub" + "github.com/GaIsBAX/Webhix/internal/server" + "github.com/GaIsBAX/Webhix/internal/server/middleware" + "github.com/GaIsBAX/Webhix/internal/web" +) + +const readHeaderTimeout = 5 * time.Second + +func newMux(cfg *config.Config, services *services, events *hub.Hub) (*http.ServeMux, error) { + mux := http.NewServeMux() + + registerWebhookRoutes(mux, cfg, services.hook, events) + + if err := registerStaticRoutes(mux); err != nil { + return nil, err + } + + return mux, nil +} + +func registerWebhookRoutes( + mux *http.ServeMux, + cfg *config.Config, + hookService server.HookService, + events *hub.Hub, +) { + handler := server.NewHookHandler( + mux, + hookService, + events, + server.HookHandlerOptions{ + BaseURL: cfg.BaseURL, + MaxBodySize: cfg.MaxBodySize, + }, + ) + + handler.RegisterRoutes() +} + +func registerStaticRoutes(mux *http.ServeMux) error { + staticSub, err := fs.Sub(web.Static, "static") + if err != nil { + return err + } + + staticHandler := http.FileServer(http.FS(staticSub)) + + mux.Handle("/ui/", http.StripPrefix("/ui/", staticHandler)) + mux.Handle("/", staticHandler) + + return nil +} + +func newHTTPHandler(cfg *config.Config, mux *http.ServeMux) (http.Handler, error) { + handler := http.Handler(mux) + + auth, err := newAuthMiddleware(cfg) + if err != nil { + return nil, err + } + + handler = auth.Protect(handler) + + if len(cfg.TrustedProxies) > 0 { + trustedProxies := middleware.NewTrustedProxies(cfg.TrustedProxies) + if trustedProxies == nil { + return nil, fmt.Errorf("invalid trusted proxies") + } + + handler = trustedProxies.BehindProxy(handler) + } + + return handler, nil +} + +func newAuthMiddleware(cfg *config.Config) (*middleware.Auth, error) { + password, secretKey, err := authCredentials(cfg) + if err != nil { + return nil, fmt.Errorf("auth setup: %w", err) + } + + return middleware.NewAuth(password, secretKey), nil +} + +func newHTTPServer(cfg *config.Config, handler http.Handler) *http.Server { + return &http.Server{ + Addr: cfg.Addr, + Handler: handler, + ReadHeaderTimeout: readHeaderTimeout, + } +} + +func authCredentials(cfg *config.Config) (password, secretKey string, err error) { + if cfg.Password == "" && cfg.SecretKey == "" { + return "", "", fmt.Errorf("auth is required: set WEBHIX_PASSWORD or WEBHIX_SECRET_KEY") + } + + return cfg.Password, cfg.SecretKey, nil +} diff --git a/internal/app/services.go b/internal/app/services.go new file mode 100644 index 0000000..6154053 --- /dev/null +++ b/internal/app/services.go @@ -0,0 +1,15 @@ +package app + +import "github.com/GaIsBAX/Webhix/internal/core" + +type services struct { + hook *core.HookService + serve *core.Serve +} + +func newServices(repositories *repositories) *services { + return &services{ + hook: core.NewHookService(repositories.hook), + serve: core.NewServe(repositories.serve), + } +} From 88bffe18163df11c284dc82b9a9637ab682fe12f Mon Sep 17 00:00:00 2001 From: GaIsBAX Date: Mon, 18 May 2026 12:09:51 +0300 Subject: [PATCH 04/11] fix(app): pass context to Shutdown, return deps close error --- internal/app/app.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index ab5337a..7b80e0d 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -62,18 +62,18 @@ func (a *App) Start(ctx context.Context) error { return err case <-ctx.Done(): - return a.Shutdown() + return a.Shutdown(ctx) } } -func (a *App) Shutdown() error { +func (a *App) Shutdown(ctx context.Context) error { slog.Info("shutting down") a.events.Close() - ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + shutdownCtx, cancel := context.WithTimeout(ctx, shutdownTimeout) defer cancel() - if err := a.server.Shutdown(ctx); err != nil { + if err := a.server.Shutdown(shutdownCtx); err != nil { slog.Error("graceful shutdown failed, forcing close", "err", err) if closeErr := a.server.Close(); closeErr != nil { slog.Error("server close failed", "err", closeErr) @@ -82,6 +82,7 @@ func (a *App) Shutdown() error { if err := a.deps.close(); err != nil { slog.Error("teardown error", "err", err) + return err } return nil From 0a545d4aa3fb576f31a282f93fcc5e723a8ebe71 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 18 May 2026 11:51:32 +0300 Subject: [PATCH 05/11] fix(cli): RetentionCleaner --- internal/core/serve_core.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/core/serve_core.go b/internal/core/serve_core.go index 20ef8bd..b21ca46 100644 --- a/internal/core/serve_core.go +++ b/internal/core/serve_core.go @@ -35,7 +35,7 @@ func (s *Serve) RetentionCleaner(ctx context.Context, retention time.Duration) ( for { select { case <-ticker.C: - return cleanup() + cleanup() case <-ctx.Done(): return 0, ctx.Err() From a07d398f568245e29facca6d7e02deb6cfa14182 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 18 May 2026 16:46:15 +0300 Subject: [PATCH 06/11] feat(cli): add version command and app wiring --- cmd/webhix/main.go | 4 +-- internal/app/services.go | 28 ++++++++++++++- internal/cli/cli.go | 12 +++++-- internal/cli/root.go | 13 +++++-- internal/cli/serve/command.go | 22 +++++++++--- internal/cli/serve/runner.go | 11 ++++-- internal/cli/version/command.go | 60 ++++++++++++++++++++++++++++++++ internal/cli/version/flags.go | 9 +++++ internal/cli/version/options.go | 28 +++++++++++++++ internal/config/config.go | 2 ++ internal/core/serve_core.go | 34 ++++++++++++++++++ internal/core/version_core.go | 28 +++++++++++++++ internal/domain/hook.go | 8 +++++ internal/domain/version.go | 8 +++++ internal/repos/serve.go | 9 +++++ internal/store/query/hooks.sql | 4 +++ internal/store/sqlc/hooks.sql.go | 12 +++++++ 17 files changed, 278 insertions(+), 14 deletions(-) create mode 100644 internal/cli/version/command.go create mode 100644 internal/cli/version/flags.go create mode 100644 internal/cli/version/options.go create mode 100644 internal/core/version_core.go create mode 100644 internal/domain/version.go diff --git a/cmd/webhix/main.go b/cmd/webhix/main.go index e36899a..21433b5 100644 --- a/cmd/webhix/main.go +++ b/cmd/webhix/main.go @@ -7,7 +7,7 @@ import ( "os/signal" "syscall" - "github.com/GaIsBAX/Webhix/internal/cli" + "github.com/GaIsBAX/Webhix/internal/app" "github.com/GaIsBAX/Webhix/internal/config" _ "github.com/GaIsBAX/Webhix/pkg" ) @@ -26,7 +26,7 @@ func main() { os.Exit(1) } - if err := cli.Run(ctx, cfg, os.Args[1:]); err != nil { + if err := app.Start(ctx, cfg, os.Args[1:]); err != nil { slog.Error("app run", "err", err) os.Exit(1) } diff --git a/internal/app/services.go b/internal/app/services.go index 6154053..055b595 100644 --- a/internal/app/services.go +++ b/internal/app/services.go @@ -1,6 +1,14 @@ package app -import "github.com/GaIsBAX/Webhix/internal/core" +import ( + "context" + + "github.com/GaIsBAX/Webhix/internal/cli" + "github.com/GaIsBAX/Webhix/internal/cli/serve" + "github.com/GaIsBAX/Webhix/internal/config" + "github.com/GaIsBAX/Webhix/internal/core" + "github.com/GaIsBAX/Webhix/internal/domain" +) type services struct { hook *core.HookService @@ -13,3 +21,21 @@ func newServices(repositories *repositories) *services { serve: core.NewServe(repositories.serve), } } + +func NewVersionService() *core.Version { + return core.NewVersion() +} + +func Start(ctx context.Context, cfg *config.Config, args []string) error { + versionService := NewVersionService() + serveFactory := serve.ServiceFactoryFunc(func(ctx context.Context, cfg *config.Config) (serve.Service, domain.ServeStartFunc, error) { + application, err := New(ctx, cfg) + if err != nil { + return nil, nil, err + } + + return application.services.serve, application.Start, nil + }) + + return cli.Run(ctx, cfg, args, versionService, serveFactory) +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 86265b6..afe7cf6 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -3,11 +3,19 @@ package cli import ( "context" + "github.com/GaIsBAX/Webhix/internal/cli/serve" + "github.com/GaIsBAX/Webhix/internal/cli/version" "github.com/GaIsBAX/Webhix/internal/config" ) -func Run(ctx context.Context, cfg *config.Config, args []string) error { - root := NewRootCommand(ctx, cfg) +func Run( + ctx context.Context, + cfg *config.Config, + args []string, + versionService version.Service, + serveFactory serve.ServiceFactory, +) error { + root := NewRootCommand(ctx, cfg, versionService, serveFactory) root.SetArgs(args) if err := root.Execute(); err != nil { diff --git a/internal/cli/root.go b/internal/cli/root.go index 83a4708..90732ee 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -5,21 +5,30 @@ import ( "github.com/GaIsBAX/Webhix/internal/cli/forward" "github.com/GaIsBAX/Webhix/internal/cli/serve" + "github.com/GaIsBAX/Webhix/internal/cli/version" "github.com/GaIsBAX/Webhix/internal/config" "github.com/spf13/cobra" ) -func NewRootCommand(ctx context.Context, cfg *config.Config) *cobra.Command { +func NewRootCommand( + ctx context.Context, + cfg *config.Config, + versionService version.Service, + serveFactory serve.ServiceFactory, +) *cobra.Command { cmd := &cobra.Command{ Use: "webhix", SilenceUsage: true, SilenceErrors: true, + Version: versionService.Info().Version, } + cmd.SetVersionTemplate("webhix {{.Version}}\n") addGroup(cmd, serve.ServeGroup, serve.ServeTitle) - cmd.AddCommand(serve.NewCommand(ctx, cfg)) + cmd.AddCommand(serve.NewCommand(ctx, cfg, serveFactory)) cmd.AddCommand(forward.NewCommand(ctx, cfg)) + cmd.AddCommand(version.NewCommand(ctx, versionService)) return cmd } diff --git a/internal/cli/serve/command.go b/internal/cli/serve/command.go index 0e2e420..cfd16c0 100644 --- a/internal/cli/serve/command.go +++ b/internal/cli/serve/command.go @@ -4,8 +4,8 @@ import ( "context" "log/slog" - "github.com/GaIsBAX/Webhix/internal/app" "github.com/GaIsBAX/Webhix/internal/config" + "github.com/GaIsBAX/Webhix/internal/domain" "github.com/spf13/cobra" ) @@ -14,7 +14,21 @@ const ( ServeTitle = "" ) -func NewCommand(ctx context.Context, cfg *config.Config) *cobra.Command { +type Service interface { + Run(ctx context.Context, opts domain.ServeRunOptions, start domain.ServeStartFunc, onRetentionError func(error)) error +} + +type ServiceFactory interface { + New(ctx context.Context, cfg *config.Config) (Service, domain.ServeStartFunc, error) +} + +type ServiceFactoryFunc func(ctx context.Context, cfg *config.Config) (Service, domain.ServeStartFunc, error) + +func (f ServiceFactoryFunc) New(ctx context.Context, cfg *config.Config) (Service, domain.ServeStartFunc, error) { + return f(ctx, cfg) +} + +func NewCommand(ctx context.Context, cfg *config.Config, factory ServiceFactory) *cobra.Command { opts := DefaultOptions() cmd := &cobra.Command{ @@ -26,13 +40,13 @@ func NewCommand(ctx context.Context, cfg *config.Config) *cobra.Command { return err } - app, err := app.New(ctx, cfg) + service, start, err := factory.New(ctx, cfg) if err != nil { slog.Error("init app", "err", err) return err } - return run(ctx, app, opts) + return run(ctx, service, start, cfg, opts) }, } diff --git a/internal/cli/serve/runner.go b/internal/cli/serve/runner.go index bd4f5a8..32f77f2 100644 --- a/internal/cli/serve/runner.go +++ b/internal/cli/serve/runner.go @@ -2,12 +2,17 @@ package serve import ( "context" + "log/slog" - "github.com/GaIsBAX/Webhix/internal/app" + "github.com/GaIsBAX/Webhix/internal/config" + "github.com/GaIsBAX/Webhix/internal/domain" ) -func run(ctx context.Context, application *app.App, opts Options) error { - return application.RunServe(ctx, app.ServeOptions{ +func run(ctx context.Context, service Service, start domain.ServeStartFunc, cfg *config.Config, opts Options) error { + return service.Run(ctx, domain.ServeRunOptions{ Retention: opts.Retention, + ReadOnly: cfg.ReadOnly, + }, start, func(err error) { + slog.Error("retention cleaner", "err", err) }) } diff --git a/internal/cli/version/command.go b/internal/cli/version/command.go new file mode 100644 index 0000000..03af0fb --- /dev/null +++ b/internal/cli/version/command.go @@ -0,0 +1,60 @@ +package version + +import ( + "context" + "encoding/json" + "fmt" + "io" + + "github.com/GaIsBAX/Webhix/internal/domain" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +type Service interface { + Info() domain.VersionInfo +} + +func NewCommand(ctx context.Context, service Service) *cobra.Command { + opts := NewOptions() + + cmd := &cobra.Command{ + Use: "version", + Short: "Print version information", + RunE: func(cmd *cobra.Command, args []string) error { + if err := opts.Validate(); err != nil { + return err + } + + return print(cmd.OutOrStdout(), service.Info(), opts.Output) + }, + } + + RegisterFlags(cmd, opts) + + return cmd +} + +func print(w io.Writer, info domain.VersionInfo, output string) error { + switch output { + case outputJSON: + encoder := json.NewEncoder(w) + return encoder.Encode(info) + + case outputYAML: + encoder := yaml.NewEncoder(w) + defer encoder.Close() + return encoder.Encode(info) + + default: + _, err := fmt.Fprintf( + w, + "webhix %s\ncommit: %s\nbuilt: %s\ngo: %s\n", + info.Version, + info.Commit, + info.Built, + info.Go, + ) + return err + } +} diff --git a/internal/cli/version/flags.go b/internal/cli/version/flags.go new file mode 100644 index 0000000..510c544 --- /dev/null +++ b/internal/cli/version/flags.go @@ -0,0 +1,9 @@ +package version + +import "github.com/spf13/cobra" + +const flagOutput = "output" + +func RegisterFlags(cmd *cobra.Command, opts *Options) { + cmd.Flags().StringVar(&opts.Output, flagOutput, opts.Output, "output format: text, json, or yaml") +} diff --git a/internal/cli/version/options.go b/internal/cli/version/options.go new file mode 100644 index 0000000..cd870ee --- /dev/null +++ b/internal/cli/version/options.go @@ -0,0 +1,28 @@ +package version + +import "fmt" + +const ( + outputText = "text" + outputJSON = "json" + outputYAML = "yaml" +) + +type Options struct { + Output string +} + +func NewOptions() *Options { + return &Options{ + Output: outputText, + } +} + +func (o Options) Validate() error { + switch o.Output { + case outputText, outputJSON, outputYAML: + return nil + default: + return fmt.Errorf("unsupported output format %q (want text, json, or yaml)", o.Output) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index e4d25d4..d5bd993 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,6 +18,8 @@ type Config struct { SecretKey string `env:"WEBHIX_SECRET_KEY"` TrustedProxies []string `env:"WEBHIX_TRUSTED_PROXIES"` + MaxRequests int64 `env:"MAX_REQUESTS" env-default:"10000"` + ReadOnly bool `env:"WEBHIX_READONLY"` } func LoadConfig() (*Config, error) { diff --git a/internal/core/serve_core.go b/internal/core/serve_core.go index b21ca46..044eea4 100644 --- a/internal/core/serve_core.go +++ b/internal/core/serve_core.go @@ -2,11 +2,15 @@ package core import ( "context" + "errors" "time" + + "github.com/GaIsBAX/Webhix/internal/domain" ) type ServeRepository interface { DeleteWebhookRequestsOlderThan(ctx context.Context, retention time.Duration) (int64, error) + GetCountRequests(ctx context.Context) (int64, error) } type Serve struct { @@ -42,3 +46,33 @@ func (s *Serve) RetentionCleaner(ctx context.Context, retention time.Duration) ( } } } + +func (s *Serve) Run(ctx context.Context, opts domain.ServeRunOptions, start domain.ServeStartFunc, onRetentionError func(error)) error { + s.StartRetentionCleaner(ctx, opts, onRetentionError) + return start(ctx) +} + +func (s *Serve) StartRetentionCleaner(ctx context.Context, opts domain.ServeRunOptions, onError func(error)) { + if opts.Retention <= 0 || opts.ReadOnly { + return + } + + go func() { + if _, err := s.RetentionCleaner(ctx, opts.Retention); err != nil && onError != nil { + onError(err) + } + }() +} + +func (s *Serve) RequestLimitGuard(ctx context.Context, limit int64) error { + count, err := s.repo.GetCountRequests(ctx) + if err != nil { + return err + } + + if count > limit { + return errors.New("request: rate limit exceeded") + } + + return nil +} diff --git a/internal/core/version_core.go b/internal/core/version_core.go new file mode 100644 index 0000000..ae5b5e9 --- /dev/null +++ b/internal/core/version_core.go @@ -0,0 +1,28 @@ +package core + +import ( + "runtime" + + "github.com/GaIsBAX/Webhix/internal/domain" +) + +var ( + WebhixVersion = "unknown" + Commit = "unknown" + Built = "unknown" +) + +type Version struct{} + +func NewVersion() *Version { + return &Version{} +} + +func (v *Version) Info() domain.VersionInfo { + return domain.VersionInfo{ + Version: WebhixVersion, + Commit: Commit, + Built: Built, + Go: runtime.Version(), + } +} diff --git a/internal/domain/hook.go b/internal/domain/hook.go index 38096ed..599b797 100644 --- a/internal/domain/hook.go +++ b/internal/domain/hook.go @@ -1,6 +1,7 @@ package domain import ( + "context" "fmt" "time" ) @@ -55,6 +56,13 @@ type UpsertHookResponseParams struct { Body []byte } +type ServeRunOptions struct { + Retention time.Duration + ReadOnly bool +} + +type ServeStartFunc func(context.Context) error + func (p UpsertHookResponseParams) Validate() error { if p.StatusCode < 100 || p.StatusCode > 599 { return fmt.Errorf("statusCode must be between 100 and 599") diff --git a/internal/domain/version.go b/internal/domain/version.go new file mode 100644 index 0000000..93944d6 --- /dev/null +++ b/internal/domain/version.go @@ -0,0 +1,8 @@ +package domain + +type VersionInfo struct { + Version string `json:"version" yaml:"version"` + Commit string `json:"commit" yaml:"commit"` + Built string `json:"built" yaml:"built"` + Go string `json:"go" yaml:"go"` +} diff --git a/internal/repos/serve.go b/internal/repos/serve.go index be74f5c..96da35d 100644 --- a/internal/repos/serve.go +++ b/internal/repos/serve.go @@ -30,3 +30,12 @@ func (r *Serve) DeleteWebhookRequestsOlderThan(ctx context.Context, retention ti return affected, nil } + +func (r *Serve) GetCountRequests(ctx context.Context) (int64, error) { + count, err := r.q.GetCountRequests(ctx) + if err != nil { + return 0, err + } + + return count, nil +} diff --git a/internal/store/query/hooks.sql b/internal/store/query/hooks.sql index a4c3880..e56e26c 100644 --- a/internal/store/query/hooks.sql +++ b/internal/store/query/hooks.sql @@ -8,6 +8,10 @@ SELECT id, token, name, created_at, updated_at FROM hooks WHERE token = ?; +-- name: GetCountRequests :one +SELECT COUNT(*) +FROM webhook_requests; + -- name: CreateWebhookRequest :one INSERT INTO webhook_requests ( hook_id, diff --git a/internal/store/sqlc/hooks.sql.go b/internal/store/sqlc/hooks.sql.go index ecf6a75..3898907 100644 --- a/internal/store/sqlc/hooks.sql.go +++ b/internal/store/sqlc/hooks.sql.go @@ -100,6 +100,18 @@ func (q *Queries) DeleteWebhookRequestsOlderThan(ctx context.Context, datetime i return q.db.ExecContext(ctx, deleteWebhookRequestsOlderThan, datetime) } +const getCountRequests = `-- name: GetCountRequests :one +SELECT COUNT(*) +FROM webhook_requests +` + +func (q *Queries) GetCountRequests(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, getCountRequests) + var count int64 + err := row.Scan(&count) + return count, err +} + const getHookByToken = `-- name: GetHookByToken :one SELECT id, token, name, created_at, updated_at FROM hooks From 958684d3014b043eb08d6f1a773b3cc25e19b7fa Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 18 May 2026 16:47:04 +0300 Subject: [PATCH 07/11] feat(server): add read-only serve mode --- internal/app/app.go | 14 -------------- internal/app/http.go | 1 + internal/cli/serve/flags.go | 10 ++++++++-- internal/server/errors.go | 6 ++++++ internal/server/handler.go | 22 ++++++++++++++++++++++ 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 7b80e0d..f725b3e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -87,17 +87,3 @@ func (a *App) Shutdown(ctx context.Context) error { return nil } - -type ServeOptions struct { - Retention time.Duration -} - -func (a *App) RunServe(ctx context.Context, opts ServeOptions) error { - go func() { - if _, err := a.services.serve.RetentionCleaner(ctx, opts.Retention); err != nil { - slog.Error("retention cleaner", "err", err) - } - }() - - return a.Start(ctx) -} diff --git a/internal/app/http.go b/internal/app/http.go index 052e45a..cd7745c 100644 --- a/internal/app/http.go +++ b/internal/app/http.go @@ -40,6 +40,7 @@ func registerWebhookRoutes( server.HookHandlerOptions{ BaseURL: cfg.BaseURL, MaxBodySize: cfg.MaxBodySize, + ReadOnly: cfg.ReadOnly, }, ) diff --git a/internal/cli/serve/flags.go b/internal/cli/serve/flags.go index f46e010..c7125c1 100644 --- a/internal/cli/serve/flags.go +++ b/internal/cli/serve/flags.go @@ -16,7 +16,9 @@ const ( flagMaxBodySize = "max-body-size" flagTrustedProxies = "trusted-proxies" - flagRetention = "retention" + flagRetention = "retention" + flagMaxRequests = "max-requests" + flagReadonly = "readonly" ) func RegisterFlags(cmd *cobra.Command, cfg *config.Config, opt *Options) { @@ -28,8 +30,12 @@ func RegisterFlags(cmd *cobra.Command, cfg *config.Config, opt *Options) { flags.StringVar(&cfg.BaseURL, flagBaseURL, cfg.BaseURL, "public base URL used for endpoint links") flags.StringVar(&cfg.Password, flagPassword, cfg.Password, "basic auth password (env: WEBHIX_PASSWORD)") flags.StringVar(&cfg.SecretKey, flagSecretKey, cfg.SecretKey, "API secret key via X-Webhix-Key or Bearer (env: WEBHIX_SECRET_KEY)") + flags.Int64Var(&cfg.MaxBodySize, flagMaxBodySize, cfg.MaxBodySize, "maximum webhook request body size in bytes") + flags.Int64Var(&cfg.MaxRequests, flagMaxRequests, cfg.MaxRequests, "TODO") + flags.BoolVar(&cfg.ReadOnly, flagReadonly, cfg.ReadOnly, "start Webhix in read-only mode") + flags.StringSliceVar(&cfg.TrustedProxies, flagTrustedProxies, cfg.TrustedProxies, "trusted proxy CIDRs") - flags.DurationVarP(&opt.Retention, flagRetention, "re", opt.Retention, "TODO") + flags.DurationVar(&opt.Retention, flagRetention, opt.Retention, "TODO") } diff --git a/internal/server/errors.go b/internal/server/errors.go index f352282..37b812a 100644 --- a/internal/server/errors.go +++ b/internal/server/errors.go @@ -9,6 +9,7 @@ const ( ErrCodeInternal = "INTERNAL_ERROR" ErrCodeBadRequest = "BAD_REQUEST" ErrCodePayloadTooLarge = "PAYLOAD_TOO_LARGE" + ErrCodeReadOnly = "READ_ONLY" ) var ( @@ -51,4 +52,9 @@ var ( Code: ErrCodePayloadTooLarge, Message: "Payload too large", } + + ErrReadOnly = ErrorContract{ + Code: ErrCodeReadOnly, + Message: "Read-only mode is enabled", + } ) diff --git a/internal/server/handler.go b/internal/server/handler.go index d631154..b23cd18 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -26,6 +26,7 @@ type HookService interface { type HookHandlerOptions struct { BaseURL string MaxBodySize int64 + ReadOnly bool } type HookHandler struct { @@ -59,6 +60,10 @@ func (h *HookHandler) RegisterRoutes() { } func (h *HookHandler) CreateEndpoint(w http.ResponseWriter, r *http.Request) { + if h.readOnly(w) { + return + } + contract, err := DecodeContract[CreateEndpointRequestContract](r) if err != nil { slog.Error("create endpoint", "err", err) @@ -89,6 +94,10 @@ func (h *HookHandler) CreateEndpoint(w http.ResponseWriter, r *http.Request) { } func (h *HookHandler) ReceiveWebhook(w http.ResponseWriter, r *http.Request) { + if h.readOnly(w) { + return + } + token := r.PathValue("token") headersJSON, err := json.Marshal(r.Header) @@ -250,6 +259,10 @@ func (h *HookHandler) GetResponse(w http.ResponseWriter, r *http.Request) { } func (h *HookHandler) SetResponse(w http.ResponseWriter, r *http.Request) { + if h.readOnly(w) { + return + } + token := r.PathValue("token") contract, err := DecodeContract[SetHookResponseRequestContract](r) @@ -290,3 +303,12 @@ func (h *HookHandler) SetResponse(w http.ResponseWriter, r *http.Request) { SendSuccess(w, http.StatusOK, data) } + +func (h *HookHandler) readOnly(w http.ResponseWriter) bool { + if !h.opts.ReadOnly { + return false + } + + SendError(w, http.StatusForbidden, ErrReadOnly) + return true +} From d4f7a51ec81c5fbf1ca634da3d066928cf1495a3 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 18 May 2026 16:58:42 +0300 Subject: [PATCH 08/11] fix(cli): mark yaml as direct dependency --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e267381..992a03e 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/pressly/goose/v3 v3.27.1 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -23,6 +24,5 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) From 2bb6ead51e5f6ed6ff2fa716bbd4ae0176eb9c98 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 18 May 2026 17:04:36 +0300 Subject: [PATCH 09/11] fix(cli): format version output switch --- internal/cli/version/command.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cli/version/command.go b/internal/cli/version/command.go index 03af0fb..9659422 100644 --- a/internal/cli/version/command.go +++ b/internal/cli/version/command.go @@ -40,12 +40,12 @@ func print(w io.Writer, info domain.VersionInfo, output string) error { case outputJSON: encoder := json.NewEncoder(w) return encoder.Encode(info) - + case outputYAML: encoder := yaml.NewEncoder(w) defer encoder.Close() return encoder.Encode(info) - + default: _, err := fmt.Fprintf( w, From 17e8a0f5bd8fb5b31359e098c342c480e9789975 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 18 May 2026 17:08:51 +0300 Subject: [PATCH 10/11] fix(cli): handle linted errors --- internal/cli/version/command.go | 6 ++++-- internal/core/serve_core.go | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/cli/version/command.go b/internal/cli/version/command.go index 9659422..b711193 100644 --- a/internal/cli/version/command.go +++ b/internal/cli/version/command.go @@ -43,8 +43,10 @@ func print(w io.Writer, info domain.VersionInfo, output string) error { case outputYAML: encoder := yaml.NewEncoder(w) - defer encoder.Close() - return encoder.Encode(info) + if err := encoder.Encode(info); err != nil { + return err + } + return encoder.Close() default: _, err := fmt.Fprintf( diff --git a/internal/core/serve_core.go b/internal/core/serve_core.go index 044eea4..7f2c7d1 100644 --- a/internal/core/serve_core.go +++ b/internal/core/serve_core.go @@ -39,7 +39,9 @@ func (s *Serve) RetentionCleaner(ctx context.Context, retention time.Duration) ( for { select { case <-ticker.C: - cleanup() + if _, err := cleanup(); err != nil { + return 0, err + } case <-ctx.Done(): return 0, ctx.Err() From 3fe8794488a6f4e6f31ac1d939f6e094bff71898 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 18 May 2026 20:46:32 +0300 Subject: [PATCH 11/11] fix(sql): clean up old webhook requests instead of hooks --- internal/store/query/hooks.sql | 24 +++++++++++--- internal/store/sqlc/hooks.sql.go | 57 ++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/internal/store/query/hooks.sql b/internal/store/query/hooks.sql index e56e26c..5672d69 100644 --- a/internal/store/query/hooks.sql +++ b/internal/store/query/hooks.sql @@ -34,13 +34,27 @@ WHERE hook_id = ? ORDER BY received_at DESC, id DESC; -- name: ListWebhookRequestsByTime :many -SELECT id, token, name, created_at, updated_at -FROM hooks -WHERE created_at <= datetime('now', ?); +SELECT + wr.id, + wr.hook_id, + h.token, + h.name, + wr.method, + wr.path, + wr.query, + wr.headers, + wr.remote_addr, + wr.content_type, + wr.body_size, + wr.received_at +FROM webhook_requests wr +JOIN hooks h ON h.id = wr.hook_id +WHERE wr.received_at <= datetime('now', ?) +ORDER BY wr.received_at DESC; -- name: DeleteWebhookRequestsOlderThan :execresult -DELETE FROM hooks -WHERE created_at < datetime('now', ?); +DELETE FROM webhook_requests +WHERE received_at < datetime('now', ?); -- name: UpsertHookResponse :one INSERT INTO hook_responses (hook_id, status_code, headers, body) diff --git a/internal/store/sqlc/hooks.sql.go b/internal/store/sqlc/hooks.sql.go index 3898907..3d7a6c5 100644 --- a/internal/store/sqlc/hooks.sql.go +++ b/internal/store/sqlc/hooks.sql.go @@ -8,6 +8,7 @@ package sqlc import ( "context" "database/sql" + "time" ) const createHook = `-- name: CreateHook :one @@ -92,8 +93,8 @@ func (q *Queries) CreateWebhookRequest(ctx context.Context, arg CreateWebhookReq } const deleteWebhookRequestsOlderThan = `-- name: DeleteWebhookRequestsOlderThan :execresult -DELETE FROM hooks -WHERE created_at < datetime('now', ?) +DELETE FROM webhook_requests +WHERE received_at < datetime('now', ?) ` func (q *Queries) DeleteWebhookRequestsOlderThan(ctx context.Context, datetime interface{}) (sql.Result, error) { @@ -195,26 +196,62 @@ func (q *Queries) ListWebhookRequestsByHookID(ctx context.Context, hookID int64) } const listWebhookRequestsByTime = `-- name: ListWebhookRequestsByTime :many -SELECT id, token, name, created_at, updated_at -FROM hooks -WHERE created_at <= datetime('now', ?) +SELECT + wr.id, + wr.hook_id, + h.token, + h.name, + wr.method, + wr.path, + wr.query, + wr.headers, + wr.remote_addr, + wr.content_type, + wr.body_size, + wr.received_at +FROM webhook_requests wr +JOIN hooks h ON h.id = wr.hook_id +WHERE wr.received_at <= datetime('now', ?) +ORDER BY wr.received_at DESC ` -func (q *Queries) ListWebhookRequestsByTime(ctx context.Context, datetime interface{}) ([]Hook, error) { +type ListWebhookRequestsByTimeRow struct { + ID int64 `json:"id"` + HookID int64 `json:"hook_id"` + Token string `json:"token"` + Name sql.NullString `json:"name"` + Method string `json:"method"` + Path string `json:"path"` + Query string `json:"query"` + Headers string `json:"headers"` + RemoteAddr sql.NullString `json:"remote_addr"` + ContentType sql.NullString `json:"content_type"` + BodySize int64 `json:"body_size"` + ReceivedAt time.Time `json:"received_at"` +} + +func (q *Queries) ListWebhookRequestsByTime(ctx context.Context, datetime interface{}) ([]ListWebhookRequestsByTimeRow, error) { rows, err := q.db.QueryContext(ctx, listWebhookRequestsByTime, datetime) if err != nil { return nil, err } defer rows.Close() - var items []Hook + var items []ListWebhookRequestsByTimeRow for rows.Next() { - var i Hook + var i ListWebhookRequestsByTimeRow if err := rows.Scan( &i.ID, + &i.HookID, &i.Token, &i.Name, - &i.CreatedAt, - &i.UpdatedAt, + &i.Method, + &i.Path, + &i.Query, + &i.Headers, + &i.RemoteAddr, + &i.ContentType, + &i.BodySize, + &i.ReceivedAt, ); err != nil { return nil, err }