1
0
Fork 0
This commit is contained in:
Vojtěch Mareš 2024-05-04 18:21:37 +02:00
parent 7ed1e05284
commit 49e05cac10
Signed by: vojtech.mares
GPG key ID: C6827B976F17240D
23 changed files with 613 additions and 253 deletions

View file

@ -29,10 +29,9 @@ const (
// NewTraining defines model for NewTraining.
type NewTraining struct {
Duration int32 `json:"duration"`
Length *int32 `json:"length,omitempty"`
Name string `json:"name"`
Price struct {
Days int32 `json:"days"`
Name string `json:"name"`
Price struct {
Corporate *Price `json:"corporate,omitempty"`
Open *Price `json:"open,omitempty"`
} `json:"price"`
@ -90,11 +89,10 @@ type ProblemDetails struct {
// Training defines model for Training.
type Training struct {
Duration int32 `json:"duration"`
Id training.ID `json:"id"`
Length *int32 `json:"length,omitempty"`
Name string `json:"name"`
Price struct {
Days int32 `json:"days"`
Id training.ID `json:"id"`
Name string `json:"name"`
Price struct {
Corporate *Price `json:"corporate,omitempty"`
Open *Price `json:"open,omitempty"`
} `json:"price"`
@ -1675,40 +1673,39 @@ func (sh *strictHandler) ListTrainingFeedback(ctx *fiber.Ctx, trainingID trainin
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/+xczXLjuBF+FRSSQ1KhRHlnUjvRKd51snFlN3Gt7Rwy8aFFNCXMkAAHAG2rXHqSnPIu",
"yXulAJAQRXEs0uMZyTu68Q+NxtfdX3dDpB5oIvNCChRG0+kD1ckCc3CHf8O7KwVccDG3p4WSBSrD0d1k",
"pQLDpbDHqVQ5GDqlXJhX39CImmWB/hTnqOgqohmKuVn0fFhAjvbR6o42yqqwimiheILbuiRSFVKBcbd+",
"rTClU/qreL2uuFpUfOHGryIqCxQ9H14FDeXsHSbGXVH4oeQKGZ2+9dpGa0BqNW+2BkZNSM8qfVtLgQIS",
"bpY9kWKVkPCouxBtI8dgqfuaSibBsh+3QD/kmjhVqoUV1sIq5RoT90Du1BgUDDsQxBx41qn7R9yq25xe",
"zA5N/ozIZpC839YChBTLXJY+VjCFMjN0mkKmMYicSZkhCCszkXmOwnSqzfXlAuzJaZbJO6tkH4nWGX3c",
"bpk8h3uelzmdnkwimnPhTybbvtBCphK51rYLnYvuEIVcln59QZ00k2BopwaizGfeGZNSKRSJiwcU9qm3",
"9Pt//pVG9E/XP9OIXl+eNdRomLRDMTnLMD9DAzyrzKITxQvv6/TSOS8xCzAkAaU4amIWSJgfQGRKQBBU",
"SirChT3+y9XVBVGoCyk0jsklIlkYU+hpHDMwYBQk71GNOZp0LNU8ZjKJFybPYpUm376ZfEtSqUguFRIu",
"PCpcivG/PINscK1TYVvlU7IocxAjhcBgliHB+yID4eQQXWDCU54QI4lZcE1kUoGJdi12aYWHZNxFGFxo",
"A8Kbsj3r9c/nRGGKXphDjDMUhqc1aGHyYZNqA6bssM3VAj3a/gGSSIZkjgIt6TMyWzrJUvE5F0SjukXl",
"wO297gb9GW4y7IG1LvMc1LIlk1iBnWvzF54C5g7RrSh1d+tlBEij2okalu0K32bChyz7e0qnbx8n+2aV",
"sIracc/ZRsyXJWdbK4jo/WguR9VFUwkbn58174x4XkjlGKQAW0nQOTcZzMY5KNSLD+NE5vHiQ2zpWKYp",
"T7BxOIKCx8X7eWyCpm3YOOvA46aBSJ2vB6PiBm4jA4wp1Lqb8p+Omp3tAJFr5usnIRgEbCNpB4NYPjeS",
"9YwHhmaz5hiMZBj8rJFaSz0IpFYueaXSe4YwkJhGWUg55H+8le8MJgunh9WC1rUh/Yd8Z/7772RBfgKF",
"//sPjWip7Kg6r9/d3Y23RtuamScotPPuStKFkoXiaEAtG1mFfhdWSE4vzmlEb1FpnwpOxq/GkxrAApL3",
"MEcP2qKcObAkFHxkc98cRaxKYbgrVe9HzRujnDOW4Z1Vz8L0UzilN1XnAwWnU/pqfOLms8ZxHhCwdmdz",
"dLhZF3EFxTmjU/oj1+YqPGXt4asfN+KbyaQGvaploSgy7ov6+J32LYX3SudxBnO9q5do5Jba1qAULL0/",
"tJNpxrVxGTnouIro7x/Vq8quv9vW7/EWZ6Oa7FDmXBhUAjJfMro0XZUMFY4EsqyhaEQNzJ3F1teswQqp",
"OwzxvUIwGMDxkYHafCfZcpAReqf2zfAzqsTVlv1Pnm3qzXlb1WB1jyQOBGZt/HpPNr6FjDPCRVGaA3U1",
"7ykEiMC74G4f8bZV1CCBuCwSmXMxH9nO/XFOuK4ebWZs7bhFQY4GlXZZilttP5SolmvKTZUj4DUWO/Yy",
"bObqkmPkc0jJeM7NhqDQattueZfUm+eixFYD2GNvq1VuBrsOiLSOfqA/5XonOXC6rV3aa+t6+h0kXK2r",
"HRsP9eH52cp7SYbeRpvBceauN3i6KyJcebR25CCZthm308EPqafa9v/XHd18gNaBU/H36z04TdBESENS",
"WQp2oA7s3YhAcFUyWxJnzO6ioZOof0DzFTvi5IvUJqfrFHv06l1e/QOa/i5tK6wtl74uGHxt9HoAdf7k",
"y9b5pbPyIdX5x8B+NLB9WPaN7Y9XVvHuzqNPx3FMa0/acgmdxC+rB1iX/o+1w6Hu77cDc+b7wWP6+bT0",
"411uP1tN67k3vcpe39xq+sMefNtpAZlCYEuC91wb/RI2nFweSKXqGWy7UkH8wNzvWwN67q8iMqPOFbH6",
"t8BnXs2X+I2x106CC4q97yI4LV7MDoKLyCHZb3ffdQyxlxtiB5LOnz9eeqX1Y2f5gigsdJXDKKx3TRFD",
"9cZH/47zNIw40t/LrzA+cy/deIVpQE+9dsoD76uDoj40BwRpY41D+u2A5zH2jqXH4+8M7m9HYVOHVpBX",
"9/a/uxA02dxhIL+B9g2Fc64NKmTrN6ytJ/72QOnplDECIjAMMfITuOkJpUT8AOE90oG7Fkd+e7H81r0i",
"aL5R/Myr+lJvK/falwl0sve9maDJS9mfaVDVJ5VRn0pVcdr8tm1gRRZeMz8y15G5Doq5PmvNuf66Yj/1",
"5ub8m+RT39t/rRk0qUtKXc5ybiqdDpCaL51+pCZE/3PW03g6jAnsOoSpm5Tca3vqyMPH3alhxDFkZypt",
"jDrkjakQuJ8vUAdF5lcTlUcf/kw+PNhzrVD3Wbz3s00NfpQJZIThLWayyFGY6hP6ja8fp3Gc2ecWUpvp",
"m8mbSXx74r4ibf2LgoG51aJDgp7GMRR8vHaasfZPb3hYp9gLJVmZ+P816CW5LfFm9f8AAAD//02V5uLp",
"RgAA",
"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",
}
// GetSwagger returns the content of the embedded swagger specification file

126
internal/api/server.go Normal file
View file

@ -0,0 +1,126 @@
package api
import (
"database/sql"
"fmt"
"github.com/gofiber/fiber/v2"
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/postgres"
"gitlab.mareshq.com/hq/backoffice/backoffice-api/pkg/training"
"go.uber.org/zap"
)
type APIServer struct {
logger *zap.SugaredLogger
trainingRepo *postgres.TrainingRepository
dateRepo *postgres.DateRepository
attendeeRepo *postgres.AttendeeRepository
}
func NewAPIServer(db *sql.DB, logger *zap.SugaredLogger) *APIServer {
return &APIServer{
logger: logger,
trainingRepo: postgres.NewTrainingRepository(db),
dateRepo: postgres.NewDateRepository(db),
attendeeRepo: postgres.NewAttendeeRepository(db),
}
}
func (s *APIServer) ListTrainings(c *fiber.Ctx) error {
all, err := s.trainingRepo.FindAll()
if err != nil {
s.logger.Error("ListTrainings", zap.Error(err))
c.JSON(ListTrainings500ApplicationProblemPlusJSONResponse{
Status: fiber.StatusInternalServerError,
Title: fmt.Sprintf("Internal server error: %s", "failed to list trainings"),
})
}
c.JSON(ListTrainings200JSONResponse{})
}
func (s *APIServer) CreateTraining(c *fiber.Ctx) error {
tr := &training.Training{}
if err := c.BodyParser(tr); err != nil {
s.logger.Error("CreateTraining", zap.Error(err))
c.JSON(CreateTraining400ApplicationProblemPlusJSONResponse{
Status: fiber.StatusBadRequest,
Title: fmt.Sprintf("Internal server error: %s", "failed to parse request body"),
})
}
if err := s.trainingRepo.Create(tr); err != nil {
s.logger.Error("CreateTraining", zap.Error(err))
c.JSON(CreateTraining500ApplicationProblemPlusJSONResponse{
Status: fiber.StatusInternalServerError,
Title: fmt.Sprintf("Internal server error: %s", "failed to save training"),
})
}
c.JSON(CreateTraining201JSONResponse{
Id: tr.ID,
Name: tr.Name,
Days: int32(tr.Days),
Price: tr.Price,
})
}
func (s *APIServer) UpdateTraining(c *fiber.Ctx) error {
s.logger.Info("UpdateTraining")
}
func (s *APIServer) DeleteTraining(c *fiber.Ctx, trainingID training.ID) error {
s.logger.Info("DeleteTraining")
}
func (s *APIServer) GetTraining(c *fiber.Ctx, trainingID training.ID) error {
s.logger.Info("GetTraining")
}
func (s *APIServer) ListTrainingDates(c *fiber.Ctx, trainingID training.ID) error {
s.logger.Info("ListTrainingDates")
}
func (s *APIServer) CreateTrainingDate(c *fiber.Ctx, trainingID training.ID) error {
s.logger.Info("CreateTrainingDate")
}
func (s *APIServer) UpdateTrainingDate(c *fiber.Ctx, trainingID training.ID, dateID training.DateID) error {
s.logger.Info("UpdateTrainingDate")
}
func (s *APIServer) DeleteTrainingDate(c *fiber.Ctx, trainingID training.ID, dateID training.DateID) error {
s.logger.Info("DeleteTrainingDate")
}
func (s *APIServer) ListUpcomingTrainingDates(c *fiber.Ctx, params ListUpcomingTrainingDatesParams) error {
s.logger.Info("ListUpcomingTrainingDates")
return nil
}
func (s *APIServer) ListTrainingDateAttendees(c *fiber.Ctx, trainingID training.ID, dateID training.DateID) error {
s.logger.Info("ListTrainingDateAttendees")
}
func (s *APIServer) CreateTrainingDateAttendee(c *fiber.Ctx, trainingID training.ID, dateID training.DateID) error {
s.logger.Info("CreateTrainingDateAttendee")
}
func (s *APIServer) DeleteTrainingDateAttendee(c *fiber.Ctx, trainingID training.ID, dateID training.DateID, attendeeID training.AttendeeID) error {
s.logger.Info("DeleteTrainingDateAttendee")
}
func (s *APIServer) CreateTrainingDateAttendeeFeedback(c *fiber.Ctx, trainingID training.ID, dateID training.DateID, attendeeID training.AttendeeID) error {
s.logger.Info("CreateTrainingDateAttendeeFeedback")
}
func (s *APIServer) ListTrainingDateFeedback(c *fiber.Ctx, trainingID training.ID, dateID training.DateID) error {
s.logger.Info("ListTrainingDateFeedback")
}
func (s *APIServer) ListTrainingFeedback(c *fiber.Ctx, trainingID training.ID) error {
s.logger.Info("ListTrainingFeedback")
}

View file

@ -0,0 +1,13 @@
package currency
type Currency string
var (
Currencies = []Currency{CZK, EUR, USD}
)
const (
CZK Currency = "CZK"
EUR Currency = "EUR"
USD Currency = "USD"
)

View file

@ -77,6 +77,7 @@ func (r *AttendeeRepository) CountForDate(dateID training.DateID) (int, error) {
}
func (r *AttendeeRepository) Create(a *training.Attendee) error {
a.ID = training.NewAttendeeID()
_, err := r.db.Exec("INSERT INTO attendee (id, date_id, name, email, company, role, is_student, has_attended, has_paid) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", a.ID, a.DateID, a.Name, a.Email, a.Company, a.Role, a.IsStudent, a.HasAttended, a.HasPaid)
return err
}

View file

@ -69,6 +69,7 @@ func (r *DateRepository) FindAllForTraining(id training.ID) ([]training.Date, er
}
func (r *DateRepository) Create(d *training.Date) error {
d.ID = training.NewDateID()
_, err := r.db.Exec("INSERT INTO date (id, date, training_id, start_time, days, price, is_online, location, address, capacity) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", d.ID, d.Date, d.TrainingID, d.StartTime, d.Days, d.Price, d.IsOnline, d.Location, d.Address, d.Capacity)
return err
}

View file

@ -15,6 +15,7 @@ func NewTrainingRepository(db *sql.DB) *TrainingRepository {
}
func (r *TrainingRepository) Create(t *training.Training) error {
t.ID = training.NewID()
_, err := r.db.Exec("INSERT INTO training (id, name, days, description, price) VALUES ($1, $2, $3)", t.ID, t.Name, t.Days, t.Description, t.Price)
return err
}

View file

@ -0,0 +1,7 @@
package server
type Config struct {
PostgresURL string
Port int
ShutdownGraceSeconds int
}

145
internal/server/server.go Normal file
View file

@ -0,0 +1,145 @@
package server
import (
"context"
"os"
"os/signal"
"sync"
"syscall"
"time"
fiberzap "github.com/gofiber/contrib/fiberzap/v2"
"github.com/gofiber/contrib/swagger"
"github.com/gofiber/fiber/v2"
middleware "github.com/oapi-codegen/fiber-middleware"
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/api"
"go.uber.org/zap"
)
type Server struct {
ready ready
}
func NewServer() *Server {
return &Server{}
}
func (s *Server) Run() {
shutdownCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
logger, err := zap.NewProduction()
if err != nil {
logger.Fatal("failed to initialize logger", zap.Error(err))
}
defer logger.Sync()
sugaredLogger := logger.Sugar()
fiberConfig := fiber.Config{
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Second,
DisableStartupMessage: true,
}
app := fiber.New(fiberConfig)
app.Get("/", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotFound)
})
app.Get("/livez", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})
app.Get("/readyz", func(c *fiber.Ctx) error {
if s.ready.isReady() {
return c.SendStatus(fiber.StatusOK)
}
return c.SendStatus(fiber.StatusServiceUnavailable)
})
// TODO: add /metrics endpoint
app.Use(fiberzap.New(fiberzap.Config{
Logger: logger,
}))
swaggerConfig := swagger.Config{
BasePath: "/",
FilePath: "./api/v1/openapi.yaml",
Path: "/swagger",
Title: "Swagger API Docs",
}
app.Use(swagger.New(swaggerConfig))
app = registerAPIHandlers(app, sugaredLogger)
<-shutdownCtx.Done()
stop()
s.ready.notReady()
// wait till Pod is signaling not ready (no new requests)
time.Sleep(5 * time.Second)
shutdownBegan := time.Now()
sugaredLogger.Info("Shutdown signal recieved. Shutting down gracefully...")
// * Gracefully shut down fiber server
// * If does not shutdown within `gracefulShutdownPeriod`, force shutdown
// ! Does not close keep-alive connections
// ! fiber.Config{ReadTimeout} must be set and non-zero and greater than the `gracefulShutdownPeriod`
// TODO: make shutdown timeout configurable
err = app.ShutdownWithTimeout(time.Second * 10)
if err != nil {
sugaredLogger.Errorw("Error during shutdown", "error", err)
}
sugaredLogger.Infof("Shutdown completed in %v", time.Since(shutdownBegan))
}
func registerAPIHandlers(fiberApp *fiber.App, logger *zap.SugaredLogger) *fiber.App {
swagger, err := api.GetSwagger()
if err != nil {
logger.Fatalf("Error getting swagger: %v", err)
}
// TODO: validate this. Copied from example
// See: https://github.com/deepmap/oapi-codegen/blob/master/examples/petstore-expanded/fiber/petstore.go#L41
// Clear out the servers array in the swagger spec, that skips validating
// that server names match. We don't know how this thing will be run.
swagger.Servers = nil
fiberApp.Use(middleware.OapiRequestValidator(swagger))
api.RegisterHandlers(fiberApp)
return fiberApp
}
type ready struct {
ready bool
lock sync.Mutex
}
func (r *ready) setReady() {
r.lock.Lock()
defer r.lock.Unlock()
r.ready = true
}
func (r *ready) isReady() bool {
r.lock.Lock()
defer r.lock.Unlock()
return r.ready
}
func (r *ready) notReady() {
r.lock.Lock()
defer r.lock.Unlock()
r.ready = false
}

View file

@ -0,0 +1,6 @@
package version
var (
Version = "dev"
Commit = "N/A"
)