1
0
Fork 0

feat: add postgres repository for training

This commit is contained in:
Vojtěch Mareš 2024-06-23 10:35:38 +02:00
parent 8c277ef692
commit ff7e320481
Signed by: vojtech.mares
GPG key ID: C6827B976F17240D
19 changed files with 1277 additions and 414 deletions

View file

@ -2,3 +2,30 @@
codegen: codegen:
@echo "Generating code..." @echo "Generating code..."
@oapi-codegen --config=./oapi-codegen.cli.yaml ./api/v1/openapi.yaml @oapi-codegen --config=./oapi-codegen.cli.yaml ./api/v1/openapi.yaml
POSTGRES_URL="pgx5://yggdrasil_user:yggdrasil@localhost:5432/yggdrasil?sslmode=disable"
.PHONY: create-migration
create-migration:
migrate create -ext sql -dir ./migrations -seq -digits 3 ${name}
.PHONY: local-migrate-up
local-migrate-up:
migrate -database ${POSTGRES_URL} -path ./migrations up
.PHONY: local-migrate-force
local-migrate-force:
migrate -database ${POSTGRES_URL} -path ./migrations force
.PHONY: local-migrate-down
local-migrate-down:
migrate -database ${POSTGRES_URL} -path ./migrations down
.PHONY: local-migrate-drop
local-migrate-drop:
migrate -database ${POSTGRES_URL} -path ./migrations drop -f
.PHONY: local-db-seed
local-db-seed:
@echo "Seeding database..."
@APP_ENV="development" DATABASE_URL_FILE="postgres_url" go run ./cmd/seed/main.go

67
cmd/seed/main.go Normal file
View file

@ -0,0 +1,67 @@
package main
import (
"context"
pgxDeciaml "github.com/jackc/pgx-shopspring-decimal"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
pgxUUID "github.com/vgarvardt/pgx-google-uuid/v5"
"gitlab.mareshq.com/hq/yggdrasil/internal/bootstrap"
"gitlab.mareshq.com/hq/yggdrasil/internal/faker"
"gitlab.mareshq.com/hq/yggdrasil/pkg/training"
"go.uber.org/zap"
"os"
"os/signal"
"strings"
"syscall"
)
func main() {
shutdownCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
logger := bootstrap.Logger()
databaseURLFile := os.Getenv("DATABASE_URL_FILE")
if databaseURLFile == "" {
logger.Fatal("DATABASE_URL_FILE is not set")
}
dat, err := os.ReadFile(databaseURLFile)
if err != nil {
logger.Fatal("Error reading DATABASE_URL_FILE", zap.Error(err))
}
databaseURL := string(dat)
// clean up url from invalid characters
databaseURL = strings.ReplaceAll(databaseURL, "\n", "")
databaseURL = strings.ReplaceAll(databaseURL, "\t", "")
pgxConfig, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
logger.Fatal("Error parsing database url with pgx (database driver)", zap.Error(err))
}
pgxConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
pgxUUID.Register(conn.TypeMap())
pgxDeciaml.Register(conn.TypeMap())
return nil
}
pool, err := pgxpool.NewWithConfig(shutdownCtx, pgxConfig)
if err != nil {
logger.Fatal("Error creating pgx pool", zap.Error(err))
}
trainingRepo := training.NewPostgresTrainingRepository(pool)
trainingDateRepo := training.NewPostgresTrainingDateRepository(pool)
//trainingDateAttendeeRepo := training.NewPostgresTrainingDateAttendeeRepository(pool)
f := faker.NewFaker(trainingRepo, trainingDateRepo)
if err := f.GenerateFakeData(); err != nil {
logger.Fatal("Error generating fake data", zap.Error(err))
}
logger.Info("Fake data generated successfully!")
}

View file

@ -2,36 +2,18 @@ package main
import ( import (
"context" "context"
"gitlab.mareshq.com/hq/yggdrasil/internal/bootstrap"
"os/signal" "os/signal"
"syscall" "syscall"
"gitlab.mareshq.com/hq/yggdrasil/internal/faker"
"gitlab.mareshq.com/hq/yggdrasil/internal/server"
"gitlab.mareshq.com/hq/yggdrasil/pkg/training"
"go.uber.org/zap"
) )
//go:generate go run github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen --config=../../oapi-codegen.yaml ../../api/v1/openapi.yaml //go:generate go run github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen --config=../../oapi-codegen.yaml ../../api/v1/openapi.yaml
var port = 3000
func main() { func main() {
shutdownCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) shutdownCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop() defer stop()
logger := zap.Must(zap.NewDevelopment()) logger := bootstrap.Logger()
defer logger.Sync() srv := bootstrap.Bootstrap(logger)
trainingRepository := training.NewInMemoryTrainingRepository()
trainingDateRepository := training.NewInMemoryTrainingDateRepository()
trainingDateAttendeeRepository := training.NewInMemoryTrainingDateAttendeeRepository()
f := faker.NewFaker(trainingRepository, trainingDateRepository)
if err := f.GenerateFakeData(); err != nil {
logger.Fatal("Error generating fake data", zap.Error(err))
}
apiHandlers := server.NewAPIHandlers(trainingRepository, trainingDateRepository, trainingDateAttendeeRepository)
srv := server.NewServer(apiHandlers, port, logger)
srv.Run(shutdownCtx) srv.Run(shutdownCtx)
} }

10
go.mod
View file

@ -8,10 +8,14 @@ require (
github.com/gofiber/contrib/swagger v1.1.2 github.com/gofiber/contrib/swagger v1.1.2
github.com/gofiber/fiber/v2 v2.52.4 github.com/gofiber/fiber/v2 v2.52.4
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e
github.com/jackc/pgx/v5 v5.5.5
github.com/oapi-codegen/fiber-middleware v1.0.2 github.com/oapi-codegen/fiber-middleware v1.0.2
github.com/oapi-codegen/runtime v1.1.1 github.com/oapi-codegen/runtime v1.1.1
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/vgarvardt/pgx-google-uuid/v5 v5.0.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
) )
@ -33,6 +37,9 @@ require (
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/invopop/yaml v0.3.1 // indirect github.com/invopop/yaml v0.3.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/compress v1.17.8 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
@ -50,7 +57,10 @@ require (
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
go.mongodb.org/mongo-driver v1.13.1 // indirect go.mongodb.org/mongo-driver v1.13.1 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

21
go.sum
View file

@ -64,6 +64,18 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e h1:i3gQ/Zo7sk4LUVbsAjTNeC4gIjoPNIZVzs4EXstssV4=
github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e/go.mod h1:zUHglCZ4mpDUPgIwqEKoba6+tcUQzRdb1+DPTuYe9pI=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
@ -123,6 +135,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@ -137,6 +150,8 @@ github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7g
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vgarvardt/pgx-google-uuid/v5 v5.0.0 h1:kIIQmW04MYKyRE2ZwREPl1NY4/Uxf5x48ABTQ+yFdFo=
github.com/vgarvardt/pgx-google-uuid/v5 v5.0.0/go.mod h1:fskJeXpJTJCU9JvsZQRgR4OhKKpciztvx4rdXWil7E0=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
@ -156,6 +171,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@ -164,6 +181,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -182,6 +201,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View file

@ -0,0 +1,95 @@
package bootstrap
import (
"context"
pgxDeciaml "github.com/jackc/pgx-shopspring-decimal"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
pgxUUID "github.com/vgarvardt/pgx-google-uuid/v5"
"gitlab.mareshq.com/hq/yggdrasil/internal/faker"
"gitlab.mareshq.com/hq/yggdrasil/internal/server"
"gitlab.mareshq.com/hq/yggdrasil/pkg/training"
"go.uber.org/zap"
"os"
"strconv"
"strings"
)
func Bootstrap(logger *zap.Logger) *server.Server {
portStr := os.Getenv("APP_PORT")
if portStr == "" {
logger.Fatal("APP_PORT is not set")
}
port, err := strconv.Atoi(portStr)
if err != nil {
logger.Fatal("Error parsing APP_PORT", zap.Error(err))
}
databaseURLFile := os.Getenv("DATABASE_URL_FILE")
if databaseURLFile == "" {
logger.Fatal("DATABASE_URL_FILE is not set")
}
dat, err := os.ReadFile(databaseURLFile)
if err != nil {
logger.Fatal("Error reading DATABASE_URL_FILE", zap.Error(err))
}
databaseURL := string(dat)
// clean up url from invalid characters
databaseURL = strings.ReplaceAll(databaseURL, "\n", "")
databaseURL = strings.ReplaceAll(databaseURL, "\t", "")
pgxConfig, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
logger.Fatal("Error parsing database url with pgx (database driver)", zap.Error(err))
}
pgxConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
pgxUUID.Register(conn.TypeMap())
pgxDeciaml.Register(conn.TypeMap())
return nil
}
pool, err := pgxpool.NewWithConfig(context.TODO(), pgxConfig)
if err != nil {
logger.Fatal("Error creating pgx pool", zap.Error(err))
}
trainingRepo := training.NewPostgresTrainingRepository(pool)
trainingDateRepo := training.NewPostgresTrainingDateRepository(pool)
trainingDateAttendeeRepo := training.NewPostgresTrainingDateAttendeeRepository(pool)
apiHandlers := server.NewAPIHandlers(trainingRepo, trainingDateRepo, trainingDateAttendeeRepo)
srv := server.NewServer(apiHandlers, port, logger, pool)
return srv
}
func BootstrapInMemory(logger *zap.Logger) *server.Server {
portStr := os.Getenv("APP_PORT")
if portStr == "" {
logger.Fatal("APP_PORT is not set")
}
port, err := strconv.Atoi(portStr)
if err != nil {
logger.Fatal("Error parsing APP_PORT", zap.Error(err))
}
trainingRepository := training.NewInMemoryTrainingRepository()
trainingDateRepository := training.NewInMemoryTrainingDateRepository()
trainingDateAttendeeRepository := training.NewInMemoryTrainingDateAttendeeRepository()
f := faker.NewFaker(trainingRepository, trainingDateRepository)
if err := f.GenerateFakeData(); err != nil {
logger.Fatal("Error generating fake data", zap.Error(err))
}
apiHandlers := server.NewAPIHandlers(trainingRepository, trainingDateRepository, trainingDateAttendeeRepository)
srv := server.NewServer(apiHandlers, port, logger, nil)
return srv
}

View file

@ -0,0 +1,23 @@
package bootstrap
import (
"go.uber.org/zap"
"os"
)
func Logger() *zap.Logger {
if os.Getenv("APP_ENV") == "development" {
logger, err := zap.NewDevelopment()
if err != nil {
return zap.NewNop()
}
return logger
}
logger, err := zap.NewProduction()
if err != nil {
return zap.NewNop()
}
return logger
}

View file

@ -207,11 +207,11 @@ func (f *Faker) GenerateFakeData() error {
} }
now := time.Now() now := time.Now()
min := time.Date(now.Year(), now.Month(), now.Day(), 8, 0, 0, 0, time.UTC) minT := time.Date(now.Year(), now.Month(), now.Day(), 8, 0, 0, 0, time.UTC)
max := time.Date(now.Year()+1, now.Month(), now.Day(), 8, 0, 0, 0, time.UTC) maxT := time.Date(now.Year()+1, now.Month(), now.Day(), 8, 0, 0, 0, time.UTC)
delta := max.Sub(min) delta := maxT.Sub(minT)
sec := rand.Int64N(int64(delta.Seconds())) + min.UnixNano()/1000000000 sec := rand.Int64N(int64(delta.Seconds())) + minT.UnixNano()/1000000000
date := time.Unix(sec, 0) date := time.Unix(sec, 0)
amount := decimal.NewFromInt(4900) amount := decimal.NewFromInt(4900)
@ -231,21 +231,19 @@ func (f *Faker) GenerateFakeData() error {
} }
td := training.TrainingDate{ td := training.TrainingDate{
Date: date, Date: date,
StartTime: date.Truncate(24 * time.Hour), StartTime: date.Truncate(24 * time.Hour),
Days: t.Days, Days: t.Days,
IsOnline: online, IsOnline: online,
Location: location, Location: location,
Address: "TBD", Address: "TBD",
Capacity: 12, Capacity: 12,
Price: money.Price{ PriceAmount: amount,
Amount: amount, PriceCurrency: cur,
Currency: cur,
},
} }
err := f.trainingDateRepository.Create(t.ID, &td) dateErr := f.trainingDateRepository.Create(t.ID, &td)
if err != nil { if dateErr != nil {
return err return dateErr
} }
} }
} }

8
internal/money/money.go Normal file
View file

@ -0,0 +1,8 @@
package money
import "github.com/shopspring/decimal"
type Money struct {
Amount decimal.Decimal `db:"amount"`
Currency Currency `db:"currency"`
}

View file

@ -1,8 +0,0 @@
package money
import "github.com/shopspring/decimal"
type Price struct {
Amount decimal.Decimal
Currency Currency
}

View file

@ -238,8 +238,8 @@ func (h *APIHandlers) ListTrainingDates(ctx context.Context, req ListTrainingDat
Address: td.Address, Address: td.Address,
Capacity: td.Capacity, Capacity: td.Capacity,
Price: Price{ Price: Price{
Amount: td.Price.Amount.String(), Amount: td.PriceAmount.String(),
Currency: td.Price.Currency, Currency: td.PriceCurrency,
}, },
} }
} }
@ -258,7 +258,7 @@ func (h *APIHandlers) CreateTrainingDate(ctx context.Context, req CreateTraining
}}, nil }}, nil
} }
price := money.Price{ price := money.Money{
Amount: amount, Amount: amount,
Currency: req.Body.Price.Currency, Currency: req.Body.Price.Currency,
} }
@ -274,14 +274,15 @@ func (h *APIHandlers) CreateTrainingDate(ctx context.Context, req CreateTraining
} }
td := training.TrainingDate{ td := training.TrainingDate{
Date: req.Body.Date.Time, Date: req.Body.Date.Time,
StartTime: startTime, StartTime: startTime,
Days: req.Body.Days, Days: req.Body.Days,
IsOnline: req.Body.IsOnline, IsOnline: req.Body.IsOnline,
Location: req.Body.Location, Location: req.Body.Location,
Address: req.Body.Address, Address: req.Body.Address,
Capacity: req.Body.Capacity, Capacity: req.Body.Capacity,
Price: price, PriceAmount: price.Amount,
PriceCurrency: price.Currency,
} }
err = h.trainingDateRepository.Create(req.TrainingID, &td) err = h.trainingDateRepository.Create(req.TrainingID, &td)
@ -304,8 +305,8 @@ func (h *APIHandlers) CreateTrainingDate(ctx context.Context, req CreateTraining
Address: td.Address, Address: td.Address,
Capacity: td.Capacity, Capacity: td.Capacity,
Price: Price{ Price: Price{
Amount: td.Price.Amount.String(), Amount: td.PriceAmount.String(),
Currency: td.Price.Currency, Currency: td.PriceCurrency,
}, },
}, nil }, nil
} }
@ -357,8 +358,8 @@ func (h *APIHandlers) GetTrainingDate(ctx context.Context, req GetTrainingDateRe
Address: td.Address, Address: td.Address,
Capacity: td.Capacity, Capacity: td.Capacity,
Price: Price{ Price: Price{
Amount: td.Price.Amount.String(), Amount: td.PriceAmount.String(),
Currency: td.Price.Currency, Currency: td.PriceCurrency,
}, },
}, nil }, nil
} }
@ -374,7 +375,7 @@ func (h *APIHandlers) UpdateTrainingDate(ctx context.Context, req UpdateTraining
}}, nil }}, nil
} }
price := money.Price{ price := money.Money{
Amount: amount, Amount: amount,
Currency: req.Body.Price.Currency, Currency: req.Body.Price.Currency,
} }
@ -390,15 +391,16 @@ func (h *APIHandlers) UpdateTrainingDate(ctx context.Context, req UpdateTraining
} }
td := training.TrainingDate{ td := training.TrainingDate{
ID: req.TrainingDateID, ID: req.TrainingDateID,
Date: req.Body.Date.Time, Date: req.Body.Date.Time,
StartTime: startTime, StartTime: startTime,
Days: req.Body.Days, Days: req.Body.Days,
IsOnline: req.Body.IsOnline, IsOnline: req.Body.IsOnline,
Location: req.Body.Location, Location: req.Body.Location,
Address: req.Body.Address, Address: req.Body.Address,
Capacity: req.Body.Capacity, Capacity: req.Body.Capacity,
Price: price, PriceAmount: price.Amount,
PriceCurrency: price.Currency,
} }
err = h.trainingDateRepository.Update(&td) err = h.trainingDateRepository.Update(&td)
@ -421,8 +423,8 @@ func (h *APIHandlers) UpdateTrainingDate(ctx context.Context, req UpdateTraining
Address: td.Address, Address: td.Address,
Capacity: td.Capacity, Capacity: td.Capacity,
Price: Price{ Price: Price{
Amount: td.Price.Amount.String(), Amount: td.PriceAmount.String(),
Currency: td.Price.Currency, Currency: td.PriceCurrency,
}, },
}, nil }, nil
} }
@ -463,8 +465,8 @@ func (h *APIHandlers) ListAllUpcomingTrainingDates(ctx context.Context, req List
Address: td.Address, Address: td.Address,
Capacity: td.Capacity, Capacity: td.Capacity,
Price: Price{ Price: Price{
Amount: td.Price.Amount.String(), Amount: td.PriceAmount.String(),
Currency: td.Price.Currency, Currency: td.PriceCurrency,
}, },
} }
} }
@ -496,8 +498,8 @@ func (h *APIHandlers) ListTrainingUpcomingDates(ctx context.Context, req ListTra
Address: td.Address, Address: td.Address,
Capacity: td.Capacity, Capacity: td.Capacity,
Price: Price{ Price: Price{
Amount: td.Price.Amount.String(), Amount: td.PriceAmount.String(),
Currency: td.Price.Currency, Currency: td.PriceCurrency,
}, },
} }
} }
@ -529,8 +531,8 @@ func (h *APIHandlers) ListTrainingDateAttendees(ctx context.Context, req ListTra
HasPaid: a.HasPaid, HasPaid: a.HasPaid,
HasAttended: a.HasAttended, HasAttended: a.HasAttended,
Bill: Price{ Bill: Price{
Amount: a.Bill.Amount.String(), Amount: a.BillAmount.String(),
Currency: a.Bill.Currency, Currency: a.BillCurrency,
}, },
} }
} }
@ -576,15 +578,16 @@ func (h *APIHandlers) CreateTrainingDateAttendee(ctx context.Context, req Create
} }
ta := training.TrainingDateAttendee{ ta := training.TrainingDateAttendee{
Name: req.Body.Name, Name: req.Body.Name,
Email: string(req.Body.Email), Email: string(req.Body.Email),
Phone: req.Body.Phone, Phone: req.Body.Phone,
Company: req.Body.Company, Company: req.Body.Company,
Position: req.Body.Position, Position: req.Body.Position,
IsStudent: *req.Body.IsStudent, IsStudent: *req.Body.IsStudent,
HasPaid: false, HasPaid: false,
HasAttended: false, HasAttended: false,
Bill: td.Price, BillAmount: td.PriceAmount,
BillCurrency: td.PriceCurrency,
} }
err = h.trainingDateAttendeeRepository.Create(req.TrainingDateID, &ta) err = h.trainingDateAttendeeRepository.Create(req.TrainingDateID, &ta)
@ -608,8 +611,8 @@ func (h *APIHandlers) CreateTrainingDateAttendee(ctx context.Context, req Create
HasAttended: ta.HasAttended, HasAttended: ta.HasAttended,
HasPaid: ta.HasPaid, HasPaid: ta.HasPaid,
Bill: Price{ Bill: Price{
Amount: ta.Bill.Amount.String(), Amount: ta.BillAmount.String(),
Currency: ta.Bill.Currency, Currency: ta.BillCurrency,
}, },
}, nil }, nil
} }
@ -714,8 +717,8 @@ func (h *APIHandlers) GetTrainingDateAttendee(ctx context.Context, req GetTraini
HasAttended: ta.HasAttended, HasAttended: ta.HasAttended,
HasPaid: ta.HasPaid, HasPaid: ta.HasPaid,
Bill: Price{ Bill: Price{
Amount: ta.Bill.Amount.String(), Amount: ta.BillAmount.String(),
Currency: ta.Bill.Currency, Currency: ta.BillCurrency,
}, },
}, nil }, nil
} }
@ -803,8 +806,8 @@ func (h *APIHandlers) UpdateTrainingDateAttendee(ctx context.Context, req Update
HasAttended: ta.HasAttended, HasAttended: ta.HasAttended,
HasPaid: ta.HasPaid, HasPaid: ta.HasPaid,
Bill: Price{ Bill: Price{
Amount: ta.Bill.Amount.String(), Amount: ta.BillAmount.String(),
Currency: ta.Bill.Currency, Currency: ta.BillCurrency,
}, },
}, nil }, nil
} }
@ -887,8 +890,8 @@ func (h *APIHandlers) UpdateTrainingDateAttendeePayment(ctx context.Context, req
HasAttended: ta.HasAttended, HasAttended: ta.HasAttended,
HasPaid: ta.HasPaid, HasPaid: ta.HasPaid,
Bill: Price{ Bill: Price{
Amount: ta.Bill.Amount.String(), Amount: ta.BillAmount.String(),
Currency: ta.Bill.Currency, Currency: ta.BillCurrency,
}, },
}, nil }, nil
} }
@ -971,8 +974,8 @@ func (h *APIHandlers) UpdateTrainingDateAttendeeAttendance(ctx context.Context,
HasAttended: ta.HasAttended, HasAttended: ta.HasAttended,
HasPaid: ta.HasPaid, HasPaid: ta.HasPaid,
Bill: Price{ Bill: Price{
Amount: ta.Bill.Amount.String(), Amount: ta.BillAmount.String(),
Currency: ta.Bill.Currency, Currency: ta.BillCurrency,
}, },
}, nil }, nil
} }

View file

@ -3,6 +3,7 @@ package server
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/jackc/pgx/v5/pgxpool"
"time" "time"
fiberzap "github.com/gofiber/contrib/fiberzap/v2" fiberzap "github.com/gofiber/contrib/fiberzap/v2"
@ -16,13 +17,15 @@ type Server struct {
port int port int
logger *zap.Logger logger *zap.Logger
apiHandlers *APIHandlers apiHandlers *APIHandlers
pool *pgxpool.Pool
} }
func NewServer(apiHandlers *APIHandlers, port int, logger *zap.Logger) *Server { func NewServer(apiHandlers *APIHandlers, port int, logger *zap.Logger, pool *pgxpool.Pool) *Server {
return &Server{ return &Server{
apiHandlers: apiHandlers, apiHandlers: apiHandlers,
port: port, port: port,
logger: logger, logger: logger,
pool: pool,
} }
} }
@ -54,6 +57,11 @@ func (s *Server) Run(ctx context.Context) {
panic(err) panic(err)
} }
// gracefully shutdown/close database pool
if s.pool != nil {
s.pool.Close()
}
s.logger.Info("HTTP server shut down gracefully.", zap.Duration("duration", time.Since(shutdownBegan))) s.logger.Info("HTTP server shut down gracefully.", zap.Duration("duration", time.Since(shutdownBegan)))
} }

View file

@ -338,7 +338,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -384,7 +384,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -447,7 +447,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -510,7 +510,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -584,7 +584,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -599,7 +599,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -614,7 +614,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -663,7 +663,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -705,7 +705,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -765,7 +765,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -782,7 +782,7 @@ func TestServer(t *testing.T) {
Position: "Software Engineer", Position: "Software Engineer",
Phone: "+420 123 456 789", Phone: "+420 123 456 789",
IsStudent: false, IsStudent: false,
Bill: money.Price{ Bill: money.Money{
Amount: td.Price.Amount, Amount: td.Price.Amount,
Currency: td.Price.Currency, Currency: td.Price.Currency,
}, },
@ -842,7 +842,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -856,7 +856,7 @@ func TestServer(t *testing.T) {
Position: "Software Engineer", Position: "Software Engineer",
Phone: "+420 123 456 789", Phone: "+420 123 456 789",
IsStudent: false, IsStudent: false,
Bill: money.Price{ Bill: money.Money{
Amount: td.Price.Amount, Amount: td.Price.Amount,
Currency: td.Price.Currency, Currency: td.Price.Currency,
}, },
@ -909,7 +909,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -923,7 +923,7 @@ func TestServer(t *testing.T) {
Position: "Software Engineer", Position: "Software Engineer",
Phone: "+420 123 456 789", Phone: "+420 123 456 789",
IsStudent: false, IsStudent: false,
Bill: money.Price{ Bill: money.Money{
Amount: td.Price.Amount, Amount: td.Price.Amount,
Currency: td.Price.Currency, Currency: td.Price.Currency,
}, },
@ -986,7 +986,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -1000,7 +1000,7 @@ func TestServer(t *testing.T) {
Position: "Software Engineer", Position: "Software Engineer",
Phone: "+420 123 456 789", Phone: "+420 123 456 789",
IsStudent: false, IsStudent: false,
Bill: money.Price{ Bill: money.Money{
Amount: td.Price.Amount, Amount: td.Price.Amount,
Currency: td.Price.Currency, Currency: td.Price.Currency,
}, },
@ -1046,7 +1046,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -1064,7 +1064,7 @@ func TestServer(t *testing.T) {
Position: "Software Engineer", Position: "Software Engineer",
Phone: "+420 123 456 789", Phone: "+420 123 456 789",
IsStudent: false, IsStudent: false,
Bill: money.Price{ Bill: money.Money{
Amount: td.Price.Amount, Amount: td.Price.Amount,
Currency: td.Price.Currency, Currency: td.Price.Currency,
}, },
@ -1123,7 +1123,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -1141,7 +1141,7 @@ func TestServer(t *testing.T) {
Position: "Software Engineer", Position: "Software Engineer",
Phone: "+420 123 456 789", Phone: "+420 123 456 789",
IsStudent: false, IsStudent: false,
Bill: money.Price{ Bill: money.Money{
Amount: td.Price.Amount, Amount: td.Price.Amount,
Currency: td.Price.Currency, Currency: td.Price.Currency,
}, },
@ -1200,7 +1200,7 @@ func TestServer(t *testing.T) {
IsOnline: false, IsOnline: false,
Location: "NYC", Location: "NYC",
StartTime: date, StartTime: date,
Price: money.Price{ Price: money.Money{
Amount: decimal.NewFromInt(200), Amount: decimal.NewFromInt(200),
Currency: "EUR", Currency: "EUR",
}, },
@ -1216,7 +1216,7 @@ func TestServer(t *testing.T) {
Position: "Software Engineer", Position: "Software Engineer",
Phone: "+420 123 456 789", Phone: "+420 123 456 789",
IsStudent: false, IsStudent: false,
Bill: money.Price{ Bill: money.Money{
Amount: td.Price.Amount, Amount: td.Price.Amount,
Currency: td.Price.Currency, Currency: td.Price.Currency,
}, },

View file

@ -0,0 +1,9 @@
DROP TABLE IF EXISTS training.date_attendees;
DROP TABLE IF EXISTS training.dates;
DROP TABLE IF EXISTS training.prices;
DROP TABLE IF EXISTS training.trainings;
DROP SCHEMA IF EXISTS training;

View file

@ -0,0 +1,56 @@
BEGIN;
CREATE SCHEMA IF NOT EXISTS training;
CREATE TABLE IF NOT EXISTS training.trainings (
id UUID PRIMARY KEY,
name varchar(255) NOT NULL,
description text NOT NULL,
days smallint NOT NULL
);
CREATE TABLE IF NOT EXISTS training.prices (
training_id UUID REFERENCES training.trainings(id),
amount NUMERIC(10,4) NOT NULL,
currency VARCHAR(3) NOT NULL,
CONSTRAINT positive_amount CHECK (amount >= 0),
CONSTRAINT allowed_currencies CHECK (currency IN ('USD', 'EUR', 'CZK')),
type VARCHAR(10) NOT NULL,
CHECK (type IN ('OPEN', 'CORPORATE')),
PRIMARY KEY (training_id, currency, type) -- composite primary key
);
CREATE TABLE IF NOT EXISTS training.dates (
ID UUID PRIMARY KEY,
training_id UUID REFERENCES training.trainings(id),
date DATE NOT NULL,
start_time TIME NOT NULL,
days SMALLINT NOT NULL,
is_online BOOLEAN NOT NULL,
location VARCHAR(255) NOT NULL,
address VARCHAR(500) NOT NULL,
capacity SMALLINT NOT NULL,
price_amount NUMERIC(10,4) NOT NULL,
price_currency VARCHAR(3) NOT NULL,
CONSTRAINT positive_amount CHECK (price_amount >= 0),
CONSTRAINT allowed_currencies CHECK (price_currency IN ('USD', 'EUR', 'CZK'))
);
CREATE TABLE IF NOT EXISTS training.date_attendees (
id UUID PRIMARY KEY,
date_id UUID REFERENCES training.dates(id),
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
phone VARCHAR(20) NOT NULL,
company VARCHAR(255) NOT NULL,
position VARCHAR(255) NOT NULL,
is_student BOOLEAN NOT NULL,
has_paid BOOLEAN NOT NULL,
has_attended BOOLEAN NOT NULL,
bill_amount NUMERIC(10,4) NOT NULL,
bill_currency VARCHAR(3) NOT NULL,
CONSTRAINT positive_amount CHECK (bill_amount >= 0),
CONSTRAINT allowed_currencies CHECK (bill_currency IN ('USD', 'EUR', 'CZK'))
);
COMMIT;

View file

@ -0,0 +1,270 @@
package training
import (
"sync"
"time"
)
type InMemoryTrainingRepository struct {
trainings map[TrainingID]Training
lock sync.RWMutex
}
func NewInMemoryTrainingRepository() *InMemoryTrainingRepository {
return &InMemoryTrainingRepository{
trainings: make(map[TrainingID]Training),
}
}
func (r *InMemoryTrainingRepository) Create(training *Training) error {
r.lock.Lock()
defer r.lock.Unlock()
training.ID = NewTrainingID()
r.trainings[training.ID] = *training
return nil
}
func (r *InMemoryTrainingRepository) FindByID(id TrainingID) (*Training, error) {
r.lock.RLock()
defer r.lock.RUnlock()
training, ok := r.trainings[id]
if !ok {
return nil, ErrTrainingNotFound
}
return &training, nil
}
func (r *InMemoryTrainingRepository) FindAll() ([]Training, error) {
r.lock.RLock()
defer r.lock.RUnlock()
trainings := make([]Training, 0, len(r.trainings))
for _, training := range r.trainings {
trainings = append(trainings, training)
}
return trainings, nil
}
func (r *InMemoryTrainingRepository) Update(training *Training) error {
r.lock.Lock()
defer r.lock.Unlock()
r.trainings[training.ID] = *training
return nil
}
func (r *InMemoryTrainingRepository) Delete(id TrainingID) error {
r.lock.Lock()
defer r.lock.Unlock()
_, ok := r.trainings[id]
if !ok {
return ErrTrainingNotFound
}
delete(r.trainings, id)
return nil
}
type InMemoryTrainingDateRepository struct {
trainingDates map[TrainingDateID]TrainingDate
trainingToDates map[TrainingID][]TrainingDateID
lock sync.RWMutex
}
func NewInMemoryTrainingDateRepository() *InMemoryTrainingDateRepository {
return &InMemoryTrainingDateRepository{
trainingDates: make(map[TrainingDateID]TrainingDate),
trainingToDates: make(map[TrainingID][]TrainingDateID),
}
}
func (r *InMemoryTrainingDateRepository) Create(trainingID TrainingID, trainingDate *TrainingDate) error {
r.lock.Lock()
defer r.lock.Unlock()
trainingDate.ID = NewTrainingDateID()
trainingDate.trainingID = trainingID
r.trainingDates[trainingDate.ID] = *trainingDate
r.trainingToDates[trainingID] = append(r.trainingToDates[trainingID], trainingDate.ID)
return nil
}
func (r *InMemoryTrainingDateRepository) FindByID(id TrainingDateID) (*TrainingDate, error) {
r.lock.RLock()
defer r.lock.RUnlock()
date, ok := r.trainingDates[id]
if !ok {
return nil, ErrTrainingDateNotFound
}
return &date, nil
}
func (r *InMemoryTrainingDateRepository) FindAll() ([]TrainingDate, error) {
r.lock.RLock()
defer r.lock.RUnlock()
dates := make([]TrainingDate, len(r.trainingDates))
for _, date := range r.trainingDates {
dates = append(dates, date)
}
return dates, nil
}
func (r *InMemoryTrainingDateRepository) FindAllByTrainingID(trainingID TrainingID) ([]TrainingDate, error) {
r.lock.RLock()
defer r.lock.RUnlock()
dates := make([]TrainingDate, 0)
for _, id := range r.trainingToDates[trainingID] {
dates = append(dates, r.trainingDates[id])
}
return dates, nil
}
func (r *InMemoryTrainingDateRepository) FindUpcomingByTrainingID(trainingID TrainingID) ([]TrainingDate, error) {
r.lock.RLock()
defer r.lock.RUnlock()
now := time.Now()
var dates []TrainingDate
for _, id := range r.trainingToDates[trainingID] {
date := r.trainingDates[id]
if date.Date.After(now) {
dates = append(dates, date)
}
}
return dates, nil
}
func (r *InMemoryTrainingDateRepository) Update(trainingDate *TrainingDate) error {
r.lock.Lock()
defer r.lock.Unlock()
oldDate := r.trainingDates[trainingDate.ID]
trainingDate.trainingID = oldDate.trainingID
r.trainingDates[trainingDate.ID] = *trainingDate
return nil
}
func (r *InMemoryTrainingDateRepository) Delete(id TrainingDateID) error {
r.lock.Lock()
defer r.lock.Unlock()
date, ok := r.trainingDates[id]
if !ok {
return ErrTrainingDateNotFound
}
delete(r.trainingDates, id)
dates := r.trainingToDates[date.trainingID]
for idx, d := range dates {
if d == id {
r.trainingToDates[date.trainingID] = append(dates[:idx], dates[idx+1:]...)
break
}
}
return nil
}
type InMemoryTrainingDateAttendeeRepository struct {
attendees map[TrainingDateAttendeeID]TrainingDateAttendee
dateToAttendees map[TrainingDateID][]TrainingDateAttendeeID
lock sync.RWMutex
}
func NewInMemoryTrainingDateAttendeeRepository() *InMemoryTrainingDateAttendeeRepository {
return &InMemoryTrainingDateAttendeeRepository{
attendees: make(map[TrainingDateAttendeeID]TrainingDateAttendee),
dateToAttendees: make(map[TrainingDateID][]TrainingDateAttendeeID),
}
}
func (r *InMemoryTrainingDateAttendeeRepository) Create(trainingDateID TrainingDateID, attendee *TrainingDateAttendee) error {
r.lock.Lock()
defer r.lock.Unlock()
attendee.ID = NewTrainingDateAttendeeID()
attendee.trainingDateID = trainingDateID
r.attendees[attendee.ID] = *attendee
r.dateToAttendees[trainingDateID] = append(r.dateToAttendees[trainingDateID], attendee.ID)
return nil
}
func (r *InMemoryTrainingDateAttendeeRepository) FindByID(id TrainingDateAttendeeID) (*TrainingDateAttendee, error) {
r.lock.RLock()
defer r.lock.RUnlock()
attendee, ok := r.attendees[id]
if !ok {
return nil, ErrTrainingDateAttendeeNotFound
}
return &attendee, nil
}
func (r *InMemoryTrainingDateAttendeeRepository) FindAll() ([]TrainingDateAttendee, error) {
r.lock.RLock()
defer r.lock.RUnlock()
attendees := make([]TrainingDateAttendee, len(r.attendees))
for _, attendee := range r.attendees {
attendees = append(attendees, attendee)
}
return attendees, nil
}
func (r *InMemoryTrainingDateAttendeeRepository) FindAllByTrainingDateID(trainingDateID TrainingDateID) ([]TrainingDateAttendee, error) {
r.lock.RLock()
defer r.lock.RUnlock()
attendees := make([]TrainingDateAttendee, 0)
for _, id := range r.dateToAttendees[trainingDateID] {
attendees = append(attendees, r.attendees[id])
}
return attendees, nil
}
func (r *InMemoryTrainingDateAttendeeRepository) Update(attendee *TrainingDateAttendee) error {
r.lock.Lock()
defer r.lock.Unlock()
oldAttendee := r.attendees[attendee.ID]
attendee.trainingDateID = oldAttendee.trainingDateID
r.attendees[attendee.ID] = *attendee
return nil
}
func (r *InMemoryTrainingDateAttendeeRepository) Delete(id TrainingDateAttendeeID) error {
r.lock.Lock()
defer r.lock.Unlock()
attendee, ok := r.attendees[id]
if !ok {
return ErrTrainingDateAttendeeNotFound
}
delete(r.attendees, id)
attendees := r.dateToAttendees[attendee.trainingDateID]
for idx, a := range attendees {
if a == id {
r.dateToAttendees[attendee.trainingDateID] = append(attendees[:idx], attendees[idx+1:]...)
break
}
}
return nil
}

View file

@ -1,11 +1,11 @@
package training package training
import ( import (
"github.com/shopspring/decimal"
"gitlab.mareshq.com/hq/yggdrasil/internal/money"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/shopspring/decimal"
"gitlab.mareshq.com/hq/yggdrasil/internal/money"
) )
type TrainingID = uuid.UUID type TrainingID = uuid.UUID
@ -19,7 +19,7 @@ type Training struct {
Name string Name string
Days int8 Days int8
Description string Description string
Pricing []TrainingPrice Pricing []TrainingPrice `db:"-"`
} }
type TrainingPrice struct { type TrainingPrice struct {
@ -44,15 +44,16 @@ func NewTrainingDateID() TrainingDateID {
type TrainingDate struct { type TrainingDate struct {
trainingID TrainingID trainingID TrainingID
ID TrainingDateID ID TrainingDateID
Date time.Time Date time.Time
StartTime time.Time StartTime time.Time
Days int8 Days int8
IsOnline bool IsOnline bool
Location string Location string
Address string Address string
Capacity int8 Capacity int8
Price money.Price PriceAmount decimal.Decimal `db:"price_amount"`
PriceCurrency money.Currency `db:"price_currency"`
} }
type TrainingDateAttendeeID = uuid.UUID type TrainingDateAttendeeID = uuid.UUID
@ -64,14 +65,15 @@ func NewTrainingDateAttendeeID() TrainingDateAttendeeID {
type TrainingDateAttendee struct { type TrainingDateAttendee struct {
trainingDateID TrainingDateID trainingDateID TrainingDateID
ID TrainingDateAttendeeID ID TrainingDateAttendeeID
Name string Name string
Email string Email string
Phone string Phone string
Company string Company string
Position string Position string
Bill money.Price IsStudent bool
IsStudent bool HasPaid bool
HasPaid bool HasAttended bool
HasAttended bool BillAmount decimal.Decimal `db:"bill_amount"`
BillCurrency money.Currency `db:"bill_currency"`
} }

View file

@ -0,0 +1,561 @@
package training
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"time"
)
type PostgresTrainingRepository struct {
pg *pgxpool.Pool
}
func NewPostgresTrainingRepository(pg *pgxpool.Pool) *PostgresTrainingRepository {
return &PostgresTrainingRepository{pg: pg}
}
func (r *PostgresTrainingRepository) Create(training *Training) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
training.ID = NewTrainingID()
tx, err := r.pg.Begin(ctx)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `
INSERT INTO training.trainings (id, name, description, days)
VALUES ($1, $2, $3, $4)
`, training.ID, training.Name, training.Description, training.Days)
if err != nil {
return err
}
queryBatch := &pgx.Batch{}
for _, price := range training.Pricing {
queryBatch.Queue(`
INSERT INTO training.prices (training_id, amount, currency, type)
VALUES ($1, $2, $3, $4)
`, training.ID, price.Amount, price.Currency, price.Type)
}
batch := tx.SendBatch(ctx, queryBatch)
defer func() {
if e := batch.Close(); e != nil {
err = e
}
if err != nil {
_ = tx.Rollback(ctx)
} else {
if e := tx.Commit(ctx); e != nil {
err = e
}
}
}()
_, err = batch.Exec()
if err != nil {
return err
}
return nil
}
func (r *PostgresTrainingRepository) FindByID(id TrainingID) (*Training, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var training Training
err := r.pg.QueryRow(ctx, `
SELECT id, name, description, days
FROM training.trainings
WHERE id = $1
`, id).Scan(&training.ID, &training.Name, &training.Description, &training.Days)
if err != nil {
return nil, err
}
rows, err := r.pg.Query(ctx, `
SELECT amount, currency, type
FROM training.prices
WHERE training_id = $1
`, id)
if err != nil {
return nil, err
}
defer rows.Close()
training.Pricing = make([]TrainingPrice, 0)
training.Pricing, err = pgx.CollectRows[TrainingPrice](rows, func(row pgx.CollectableRow) (TrainingPrice, error) {
var price TrainingPrice
err := row.Scan(&price.Amount, &price.Currency, &price.Type)
if err != nil {
return TrainingPrice{}, err
}
return price, nil
})
if err != nil {
return nil, err
}
return &training, nil
}
func (r *PostgresTrainingRepository) FindAll() ([]Training, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := r.pg.Query(ctx, `
SELECT *
FROM training.trainings
`)
if err != nil {
return nil, err
}
defer rows.Close()
trainings, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (Training, error) {
var training Training
scanErr := row.Scan(&training.ID, &training.Name, &training.Description, &training.Days)
if scanErr != nil {
return Training{}, scanErr
}
priceRows, queryErr := r.pg.Query(ctx, `SELECT amount, currency, type FROM training.prices WHERE training_id = $1`, training.ID)
if queryErr != nil {
return Training{}, queryErr
}
training.Pricing, scanErr = pgx.CollectRows(priceRows, pgx.RowToStructByName[TrainingPrice])
if scanErr != nil {
return Training{}, scanErr
}
return training, nil
})
if err != nil {
return nil, err
}
return trainings, nil
}
func (r *PostgresTrainingRepository) Update(training *Training) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tx, err := r.pg.Begin(ctx)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `
UPDATE training.trainings
SET name = $1, description = $2, days = $3
WHERE id = $4
`, training.Name, training.Description, training.Days, training.ID)
if err != nil {
return err
}
queryBatch := &pgx.Batch{}
for _, price := range training.Pricing {
queryBatch.Queue(`
INSERT INTO training.prices (training_id, amount, currency, type)
VALUES ($1, $2, $3, $4)
`, training.ID, price.Amount, price.Currency, price.Type)
}
batch := tx.SendBatch(ctx, queryBatch)
defer func() {
if e := batch.Close(); e != nil {
err = e
}
if err != nil {
_ = tx.Rollback(ctx)
} else {
if e := tx.Commit(ctx); e != nil {
err = e
}
}
}()
_, err = batch.Exec()
if err != nil {
return err
}
return nil
}
func (r *PostgresTrainingRepository) Delete(id TrainingID) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tx, err := r.pg.Begin(ctx)
if err != nil {
return err
}
_, err = tx.Exec(context.Background(), `
DELETE FROM training.prices
WHERE training_id = $1
`, id)
if err != nil {
return err
}
_, err = tx.Exec(context.Background(), `
DELETE FROM training.trainings
WHERE id = $1
`, id)
if err != nil {
return err
}
err = tx.Commit(ctx)
if err != nil {
return err
}
return nil
}
type PostgresTrainingDateRepository struct {
pg *pgxpool.Pool
}
func NewPostgresTrainingDateRepository(pg *pgxpool.Pool) *PostgresTrainingDateRepository {
return &PostgresTrainingDateRepository{pg: pg}
}
func (r *PostgresTrainingDateRepository) Create(trainingID TrainingID, trainingDate *TrainingDate) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
trainingDate.ID = NewTrainingDateID()
_, err := r.pg.Exec(ctx, `
INSERT INTO training.dates (id, training_id, date, start_time, days, is_online, location, address, capacity, price_amount, price_currency)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
trainingDate.ID,
trainingID,
trainingDate.Date,
trainingDate.StartTime,
trainingDate.Days,
trainingDate.IsOnline,
trainingDate.Location,
trainingDate.Address,
trainingDate.Capacity,
trainingDate.PriceAmount,
trainingDate.PriceCurrency,
)
if err != nil {
return err
}
return nil
}
func (r *PostgresTrainingDateRepository) FindByID(id TrainingDateID) (*TrainingDate, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var trainingDate TrainingDate
err := r.pg.QueryRow(ctx, `SELECT * FROM training.dates WHERE id = $1`, id).
Scan(
&trainingDate.ID,
&trainingDate.trainingID,
&trainingDate.Date,
&trainingDate.StartTime,
&trainingDate.Days,
&trainingDate.IsOnline,
&trainingDate.Location,
&trainingDate.Address,
&trainingDate.Capacity,
&trainingDate.PriceAmount,
&trainingDate.PriceCurrency,
)
if err != nil {
return nil, err
}
return &trainingDate, nil
}
func (r *PostgresTrainingDateRepository) FindAll() ([]TrainingDate, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := r.pg.Query(ctx, `SELECT * FROM training.dates`)
if err != nil {
return nil, err
}
defer rows.Close()
var trainingDates []TrainingDate
for rows.Next() {
var trainingDate TrainingDate
err := rows.Scan(
&trainingDate.ID,
&trainingDate.trainingID,
&trainingDate.Date,
&trainingDate.StartTime,
&trainingDate.Days,
&trainingDate.IsOnline,
&trainingDate.Location,
&trainingDate.Address,
&trainingDate.Capacity,
&trainingDate.PriceAmount,
&trainingDate.PriceCurrency,
)
if err != nil {
return nil, err
}
trainingDates = append(trainingDates, trainingDate)
}
return trainingDates, nil
}
func (r *PostgresTrainingDateRepository) FindAllByTrainingID(trainingID TrainingID) ([]TrainingDate, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := r.pg.Query(ctx, `SELECT * FROM training.dates WHERE training_id = $1`, trainingID)
if err != nil {
return nil, err
}
defer rows.Close()
var trainingDates []TrainingDate
for rows.Next() {
var trainingDate TrainingDate
err := rows.Scan(
&trainingDate.ID,
&trainingDate.trainingID,
&trainingDate.Date,
&trainingDate.StartTime,
&trainingDate.Days,
&trainingDate.IsOnline,
&trainingDate.Location,
&trainingDate.Address,
&trainingDate.Capacity,
&trainingDate.PriceAmount,
&trainingDate.PriceCurrency,
)
if err != nil {
return nil, err
}
trainingDates = append(trainingDates, trainingDate)
}
return trainingDates, nil
}
func (r *PostgresTrainingDateRepository) FindUpcomingByTrainingID(trainingID TrainingID) ([]TrainingDate, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := r.pg.Query(ctx, `
SELECT * FROM training.dates
WHERE training_id = $1 AND date > $2
`, trainingID, time.Now())
if err != nil {
return nil, err
}
defer rows.Close()
var trainingDates []TrainingDate
for rows.Next() {
var trainingDate TrainingDate
err := rows.Scan(
&trainingDate.ID,
&trainingDate.trainingID,
&trainingDate.Date,
&trainingDate.StartTime,
&trainingDate.Days,
&trainingDate.IsOnline,
&trainingDate.Location,
&trainingDate.Address,
&trainingDate.Capacity,
&trainingDate.PriceAmount,
&trainingDate.PriceCurrency,
)
if err != nil {
return nil, err
}
trainingDates = append(trainingDates, trainingDate)
}
return trainingDates, nil
}
func (r *PostgresTrainingDateRepository) Update(trainingDate *TrainingDate) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := r.pg.Exec(ctx, `
UPDATE training.dates
SET date = $1, start_time = $2, days = $3, is_online = $4, location = $5, address = $6, capacity = $7, price_amount = $8, price_currency = $9
WHERE id = $10
`, trainingDate.Date, trainingDate.StartTime, trainingDate.Days, trainingDate.IsOnline, trainingDate.Location, trainingDate.Address, trainingDate.Capacity, trainingDate.PriceAmount, trainingDate.PriceCurrency, trainingDate.ID)
if err != nil {
return err
}
return nil
}
func (r *PostgresTrainingDateRepository) Delete(id TrainingDateID) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := r.pg.Exec(ctx, `DELETE FROM training.dates WHERE id = $1`, id)
if err != nil {
return err
}
return nil
}
type PostgresTrainingDateAttendeeRepository struct {
pg *pgxpool.Pool
}
func NewPostgresTrainingDateAttendeeRepository(pg *pgxpool.Pool) *PostgresTrainingDateAttendeeRepository {
return &PostgresTrainingDateAttendeeRepository{pg: pg}
}
func (r *PostgresTrainingDateAttendeeRepository) Create(trainingDateID TrainingDateID, attendee *TrainingDateAttendee) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
attendee.ID = NewTrainingDateAttendeeID()
_, err := r.pg.Exec(ctx, `
INSERT INTO training.date_attendees (id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`, attendee.ID, trainingDateID, attendee.Name, attendee.Email, attendee.Phone, attendee.Company, attendee.Position, attendee.IsStudent, attendee.HasPaid, attendee.HasAttended, attendee.BillAmount, attendee.BillCurrency)
if err != nil {
return err
}
return nil
}
func (r *PostgresTrainingDateAttendeeRepository) FindByID(id TrainingDateAttendeeID) (*TrainingDateAttendee, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var attendee TrainingDateAttendee
err := r.pg.QueryRow(ctx, `
SELECT id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency
FROM training.date_attendees
WHERE id = $1
`, id).Scan(&attendee.ID, &attendee.trainingDateID, &attendee.Name, &attendee.Email, &attendee.Phone, &attendee.Company, &attendee.Position, &attendee.IsStudent, &attendee.HasPaid, &attendee.HasAttended, &attendee.BillAmount, &attendee.BillCurrency)
if err != nil {
return nil, err
}
return &attendee, nil
}
func (r *PostgresTrainingDateAttendeeRepository) FindAll() ([]TrainingDateAttendee, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := r.pg.Query(ctx, `
SELECT id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency
FROM training.date_attendees
`)
if err != nil {
return nil, err
}
defer rows.Close()
var attendees []TrainingDateAttendee
for rows.Next() {
var attendee TrainingDateAttendee
err := rows.Scan(&attendee.ID, &attendee.trainingDateID, &attendee.Name, &attendee.Email, &attendee.Phone, &attendee.Company, &attendee.Position, &attendee.IsStudent, &attendee.HasPaid, &attendee.HasAttended, &attendee.BillAmount, &attendee.BillCurrency)
if err != nil {
return nil, err
}
attendees = append(attendees, attendee)
}
return attendees, nil
}
func (r *PostgresTrainingDateAttendeeRepository) FindAllByTrainingDateID(trainingDateID TrainingDateID) ([]TrainingDateAttendee, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := r.pg.Query(ctx, `
SELECT id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency
FROM training.date_attendees
WHERE date_id = $1
`, trainingDateID)
if err != nil {
return nil, err
}
defer rows.Close()
var attendees []TrainingDateAttendee
for rows.Next() {
var attendee TrainingDateAttendee
err := rows.Scan(&attendee.ID, &attendee.trainingDateID, &attendee.Name, &attendee.Email, &attendee.Phone, &attendee.Company, &attendee.Position, &attendee.IsStudent, &attendee.HasPaid, &attendee.HasAttended, &attendee.BillAmount, &attendee.BillCurrency)
if err != nil {
return nil, err
}
attendees = append(attendees, attendee)
}
return attendees, nil
}
func (r *PostgresTrainingDateAttendeeRepository) Update(attendee *TrainingDateAttendee) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := r.pg.Exec(ctx, `
UPDATE training.date_attendees
SET name = $1, email = $2, phone = $3, company = $4, position = $5, is_student = $6, has_paid = $7, has_attended = $8, bill_amount = $9, bill_currency = $10
WHERE id = $10
`, attendee.Name, attendee.Email, attendee.Phone, attendee.Company, attendee.Position, attendee.IsStudent, attendee.HasPaid, attendee.HasAttended, attendee.BillAmount, attendee.BillCurrency, attendee.ID)
if err != nil {
return err
}
return nil
}
func (r *PostgresTrainingDateAttendeeRepository) Delete(id TrainingDateAttendeeID) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := r.pg.Exec(ctx, `DELETE FROM training.date_attendees WHERE id = $1`, id)
if err != nil {
return err
}
return nil
}

View file

@ -1,10 +1,5 @@
package training package training
import (
"sync"
"time"
)
type TrainingRepository interface { type TrainingRepository interface {
Create(training *Training) error Create(training *Training) error
FindByID(id TrainingID) (*Training, error) FindByID(id TrainingID) (*Training, error)
@ -13,69 +8,6 @@ type TrainingRepository interface {
Delete(id TrainingID) error Delete(id TrainingID) error
} }
type InMemoryTrainingRepository struct {
trainings map[TrainingID]Training
lock sync.RWMutex
}
func NewInMemoryTrainingRepository() *InMemoryTrainingRepository {
return &InMemoryTrainingRepository{
trainings: make(map[TrainingID]Training),
}
}
func (r *InMemoryTrainingRepository) Create(training *Training) error {
r.lock.Lock()
defer r.lock.Unlock()
training.ID = NewTrainingID()
r.trainings[training.ID] = *training
return nil
}
func (r *InMemoryTrainingRepository) FindByID(id TrainingID) (*Training, error) {
r.lock.RLock()
defer r.lock.RUnlock()
training, ok := r.trainings[id]
if !ok {
return nil, ErrTrainingNotFound
}
return &training, nil
}
func (r *InMemoryTrainingRepository) FindAll() ([]Training, error) {
r.lock.RLock()
defer r.lock.RUnlock()
trainings := make([]Training, 0, len(r.trainings))
for _, training := range r.trainings {
trainings = append(trainings, training)
}
return trainings, nil
}
func (r *InMemoryTrainingRepository) Update(training *Training) error {
r.lock.Lock()
defer r.lock.Unlock()
r.trainings[training.ID] = *training
return nil
}
func (r *InMemoryTrainingRepository) Delete(id TrainingID) error {
r.lock.Lock()
defer r.lock.Unlock()
_, ok := r.trainings[id]
if !ok {
return ErrTrainingNotFound
}
delete(r.trainings, id)
return nil
}
type TrainingDateRepository interface { type TrainingDateRepository interface {
Create(trainingID TrainingID, trainingDate *TrainingDate) error Create(trainingID TrainingID, trainingDate *TrainingDate) error
FindByID(id TrainingDateID) (*TrainingDate, error) FindByID(id TrainingDateID) (*TrainingDate, error)
@ -86,114 +18,6 @@ type TrainingDateRepository interface {
Delete(id TrainingDateID) error Delete(id TrainingDateID) error
} }
type InMemoryTrainingDateRepository struct {
trainingDates map[TrainingDateID]TrainingDate
trainingToDates map[TrainingID][]TrainingDateID
lock sync.RWMutex
}
func NewInMemoryTrainingDateRepository() *InMemoryTrainingDateRepository {
return &InMemoryTrainingDateRepository{
trainingDates: make(map[TrainingDateID]TrainingDate),
trainingToDates: make(map[TrainingID][]TrainingDateID),
}
}
func (r *InMemoryTrainingDateRepository) Create(trainingID TrainingID, trainingDate *TrainingDate) error {
r.lock.Lock()
defer r.lock.Unlock()
trainingDate.ID = NewTrainingDateID()
trainingDate.trainingID = trainingID
r.trainingDates[trainingDate.ID] = *trainingDate
r.trainingToDates[trainingID] = append(r.trainingToDates[trainingID], trainingDate.ID)
return nil
}
func (r *InMemoryTrainingDateRepository) FindByID(id TrainingDateID) (*TrainingDate, error) {
r.lock.RLock()
defer r.lock.RUnlock()
date, ok := r.trainingDates[id]
if !ok {
return nil, ErrTrainingDateNotFound
}
return &date, nil
}
func (r *InMemoryTrainingDateRepository) FindAll() ([]TrainingDate, error) {
r.lock.RLock()
defer r.lock.RUnlock()
dates := make([]TrainingDate, len(r.trainingDates))
for _, date := range r.trainingDates {
dates = append(dates, date)
}
return dates, nil
}
func (r *InMemoryTrainingDateRepository) FindAllByTrainingID(trainingID TrainingID) ([]TrainingDate, error) {
r.lock.RLock()
defer r.lock.RUnlock()
dates := make([]TrainingDate, 0)
for _, id := range r.trainingToDates[trainingID] {
dates = append(dates, r.trainingDates[id])
}
return dates, nil
}
func (r *InMemoryTrainingDateRepository) FindUpcomingByTrainingID(trainingID TrainingID) ([]TrainingDate, error) {
r.lock.RLock()
defer r.lock.RUnlock()
now := time.Now()
var dates []TrainingDate
for _, id := range r.trainingToDates[trainingID] {
date := r.trainingDates[id]
if date.Date.After(now) {
dates = append(dates, date)
}
}
return dates, nil
}
func (r *InMemoryTrainingDateRepository) Update(trainingDate *TrainingDate) error {
r.lock.Lock()
defer r.lock.Unlock()
oldDate := r.trainingDates[trainingDate.ID]
trainingDate.trainingID = oldDate.trainingID
r.trainingDates[trainingDate.ID] = *trainingDate
return nil
}
func (r *InMemoryTrainingDateRepository) Delete(id TrainingDateID) error {
r.lock.Lock()
defer r.lock.Unlock()
date, ok := r.trainingDates[id]
if !ok {
return ErrTrainingDateNotFound
}
delete(r.trainingDates, id)
dates := r.trainingToDates[date.trainingID]
for idx, d := range dates {
if d == id {
r.trainingToDates[date.trainingID] = append(dates[:idx], dates[idx+1:]...)
break
}
}
return nil
}
type TrainingDateAttendeeRepository interface { type TrainingDateAttendeeRepository interface {
Create(trainingDateID TrainingDateID, attendee *TrainingDateAttendee) error Create(trainingDateID TrainingDateID, attendee *TrainingDateAttendee) error
FindByID(id TrainingDateAttendeeID) (*TrainingDateAttendee, error) FindByID(id TrainingDateAttendeeID) (*TrainingDateAttendee, error)
@ -202,96 +26,3 @@ type TrainingDateAttendeeRepository interface {
Update(attendee *TrainingDateAttendee) error Update(attendee *TrainingDateAttendee) error
Delete(id TrainingDateAttendeeID) error Delete(id TrainingDateAttendeeID) error
} }
type InMemoryTrainingDateAttendeeRepository struct {
attendees map[TrainingDateAttendeeID]TrainingDateAttendee
dateToAttendees map[TrainingDateID][]TrainingDateAttendeeID
lock sync.RWMutex
}
func NewInMemoryTrainingDateAttendeeRepository() *InMemoryTrainingDateAttendeeRepository {
return &InMemoryTrainingDateAttendeeRepository{
attendees: make(map[TrainingDateAttendeeID]TrainingDateAttendee),
dateToAttendees: make(map[TrainingDateID][]TrainingDateAttendeeID),
}
}
func (r *InMemoryTrainingDateAttendeeRepository) Create(trainingDateID TrainingDateID, attendee *TrainingDateAttendee) error {
r.lock.Lock()
defer r.lock.Unlock()
attendee.ID = NewTrainingDateAttendeeID()
attendee.trainingDateID = trainingDateID
r.attendees[attendee.ID] = *attendee
r.dateToAttendees[trainingDateID] = append(r.dateToAttendees[trainingDateID], attendee.ID)
return nil
}
func (r *InMemoryTrainingDateAttendeeRepository) FindByID(id TrainingDateAttendeeID) (*TrainingDateAttendee, error) {
r.lock.RLock()
defer r.lock.RUnlock()
attendee, ok := r.attendees[id]
if !ok {
return nil, ErrTrainingDateAttendeeNotFound
}
return &attendee, nil
}
func (r *InMemoryTrainingDateAttendeeRepository) FindAll() ([]TrainingDateAttendee, error) {
r.lock.RLock()
defer r.lock.RUnlock()
attendees := make([]TrainingDateAttendee, len(r.attendees))
for _, attendee := range r.attendees {
attendees = append(attendees, attendee)
}
return attendees, nil
}
func (r *InMemoryTrainingDateAttendeeRepository) FindAllByTrainingDateID(trainingDateID TrainingDateID) ([]TrainingDateAttendee, error) {
r.lock.RLock()
defer r.lock.RUnlock()
attendees := make([]TrainingDateAttendee, 0)
for _, id := range r.dateToAttendees[trainingDateID] {
attendees = append(attendees, r.attendees[id])
}
return attendees, nil
}
func (r *InMemoryTrainingDateAttendeeRepository) Update(attendee *TrainingDateAttendee) error {
r.lock.Lock()
defer r.lock.Unlock()
oldAttendee := r.attendees[attendee.ID]
attendee.trainingDateID = oldAttendee.trainingDateID
r.attendees[attendee.ID] = *attendee
return nil
}
func (r *InMemoryTrainingDateAttendeeRepository) Delete(id TrainingDateAttendeeID) error {
r.lock.Lock()
defer r.lock.Unlock()
attendee, ok := r.attendees[id]
if !ok {
return ErrTrainingDateAttendeeNotFound
}
delete(r.attendees, id)
attendees := r.dateToAttendees[attendee.trainingDateID]
for idx, a := range attendees {
if a == id {
r.dateToAttendees[attendee.trainingDateID] = append(attendees[:idx], attendees[idx+1:]...)
break
}
}
return nil
}