mirror of
https://github.com/jwetzell/showbridge-go.git
synced 2026-09-06 05:38:58 +00:00
Merge pull request #238 from jwetzell/feat/postgres-module
add support for connecting to postgres database
This commit is contained in:
@@ -8,6 +8,7 @@ require (
|
||||
github.com/extism/go-sdk v1.7.1
|
||||
github.com/google/jsonschema-go v0.4.3
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
github.com/jwetzell/artnet-go v0.3.0
|
||||
github.com/jwetzell/free-d-go v0.2.0
|
||||
github.com/jwetzell/osc-go v0.3.1
|
||||
@@ -33,6 +34,9 @@ require (
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.19.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/minio/highwayhash v1.0.4 // indirect
|
||||
@@ -52,8 +56,8 @@ require (
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
modernc.org/libc v1.75.6 // indirect
|
||||
modernc.org/libquickjs v0.13.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
|
||||
@@ -6,6 +6,7 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -36,6 +37,14 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f h1:NW3E2QSchEk63/fjeEvWOa2cE02FSv9ox//VE/N4c8g=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jwetzell/artnet-go v0.3.0 h1:MDHElOSbpgmIbYGHO9SoxsT7ynrYtV5/UHGT82geHk0=
|
||||
github.com/jwetzell/artnet-go v0.3.0/go.mod h1:zqRsSsrd6rPwJacwscPteDqGIqevjZ3Z6dpp092VJ1w=
|
||||
github.com/jwetzell/free-d-go v0.2.0 h1:8WQW4du8Sf3d18aUzk9n5jZtHE6WdDQHDFjkFffvycc=
|
||||
@@ -48,11 +57,8 @@ github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
@@ -79,6 +85,9 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
|
||||
@@ -112,6 +121,8 @@ golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
@@ -121,6 +132,7 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/google/jsonschema-go/jsonschema"
|
||||
"github.com/jwetzell/showbridge-go/internal/common"
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "db.postgres",
|
||||
Title: "PostgreSQL Database",
|
||||
ParamsSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"url": {
|
||||
Title: "Database URL",
|
||||
Description: "the connection URL for the PostgreSQL database",
|
||||
Type: "string",
|
||||
MinLength: new(1),
|
||||
},
|
||||
},
|
||||
Required: []string{"url"},
|
||||
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
|
||||
},
|
||||
New: func(config config.ModuleConfig) (common.Module, error) {
|
||||
params := config.Params
|
||||
|
||||
urlString, err := params.GetString("url")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db.postgres url error: %w", err)
|
||||
}
|
||||
|
||||
return &DbPostgres{Url: urlString, config: config, logger: CreateLogger(config)}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type DbPostgres struct {
|
||||
config config.ModuleConfig
|
||||
Url string
|
||||
ctx context.Context
|
||||
inputHandler common.InputHandler
|
||||
db *sql.DB
|
||||
logger *slog.Logger
|
||||
dbMu sync.Mutex
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (dbs *DbPostgres) Id() string {
|
||||
return dbs.config.Id
|
||||
}
|
||||
|
||||
func (dbs *DbPostgres) Type() string {
|
||||
return dbs.config.Type
|
||||
}
|
||||
|
||||
func (dbs *DbPostgres) Start(ctx context.Context, inputHandler common.InputHandler) error {
|
||||
dbs.logger.Debug("running")
|
||||
dbs.inputHandler = inputHandler
|
||||
moduleContext, cancel := context.WithCancel(ctx)
|
||||
dbs.ctx = moduleContext
|
||||
dbs.cancel = cancel
|
||||
|
||||
db, err := sql.Open("pgx", dbs.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("db.postgres error connecting to database: %w", err)
|
||||
}
|
||||
dbs.dbMu.Lock()
|
||||
dbs.db = db
|
||||
dbs.dbMu.Unlock()
|
||||
<-dbs.ctx.Done()
|
||||
dbs.logger.Debug("done")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbs *DbPostgres) Stop() {
|
||||
if dbs.cancel != nil {
|
||||
defer dbs.cancel()
|
||||
}
|
||||
dbs.dbMu.Lock()
|
||||
defer dbs.dbMu.Unlock()
|
||||
if dbs.db != nil {
|
||||
dbs.db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (dbs *DbPostgres) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
|
||||
dbs.dbMu.Lock()
|
||||
defer dbs.dbMu.Unlock()
|
||||
if dbs.db == nil {
|
||||
return nil, fmt.Errorf("database not initialized")
|
||||
}
|
||||
return dbs.db.QueryContext(ctx, query, args...)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package module_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/module"
|
||||
)
|
||||
|
||||
func TestDbPostgresFromRegistry(t *testing.T) {
|
||||
registration, ok := module.GetModuleRegistration("db.postgres")
|
||||
if !ok {
|
||||
t.Fatalf("db.postgres module not registered")
|
||||
}
|
||||
|
||||
moduleInstance, err := registration.New(config.ModuleConfig{
|
||||
Id: "test",
|
||||
Type: "db.postgres",
|
||||
Params: map[string]any{
|
||||
"url": "postgres://localhost:5432",
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create db.postgres module: %s", err)
|
||||
}
|
||||
|
||||
if moduleInstance.Id() != "test" {
|
||||
t.Fatalf("db.postgres module has wrong id: %s", moduleInstance.Id())
|
||||
}
|
||||
|
||||
if moduleInstance.Type() != "db.postgres" {
|
||||
t.Fatalf("db.postgres module has wrong type: %s", moduleInstance.Type())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoodDbPostgres(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
params map[string]any
|
||||
}{}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
registration, ok := module.GetModuleRegistration("db.postgres")
|
||||
if !ok {
|
||||
t.Fatalf("db.postgres module not registered")
|
||||
}
|
||||
|
||||
moduleInstance, err := registration.New(config.ModuleConfig{
|
||||
Id: "test",
|
||||
Type: "db.postgres",
|
||||
Params: test.params,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("db.postgres failed to create module: %s", err)
|
||||
}
|
||||
// TODO(jwetzell) this is kind of hacky
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
moduleInstance.Stop()
|
||||
}()
|
||||
err = moduleInstance.Start(t.Context(), nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("db.postgres failed to start: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadDbPostgres(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params map[string]any
|
||||
errorString string
|
||||
}{
|
||||
{
|
||||
name: "no url param",
|
||||
params: map[string]any{},
|
||||
errorString: "db.postgres url error: not found",
|
||||
},
|
||||
{
|
||||
name: "non-string url",
|
||||
params: map[string]any{"url": 123},
|
||||
errorString: "db.postgres url error: not a string",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
registration, ok := module.GetModuleRegistration("db.postgres")
|
||||
if !ok {
|
||||
t.Fatalf("db.postgres module not registered")
|
||||
}
|
||||
|
||||
moduleInstance, err := registration.New(config.ModuleConfig{
|
||||
Id: "test",
|
||||
Type: "db.postgres",
|
||||
Params: test.params,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if test.errorString != err.Error() {
|
||||
t.Fatalf("db.postgres got error '%s', expected '%s'", err.Error(), test.errorString)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = moduleInstance.Start(t.Context(), nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("db.postgres expected to fail")
|
||||
}
|
||||
|
||||
if err.Error() != test.errorString {
|
||||
t.Fatalf("db.postgres got error '%s', expected '%s'", err.Error(), test.errorString)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user