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

34
.redocly.lint-ignore.yaml Normal file
View file

@ -0,0 +1,34 @@
# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API.
# See https://redoc.ly/docs/cli/ for more information.
api/v1/openapi.yaml:
info-license-url:
- '#/info/license/url'
no-server-example.com:
- '#/servers/0/url'
operation-4xx-response:
- '#/paths/~1trainings/get/responses'
- '#/paths/~1trainings~1{trainingID}~1dates/get/responses'
- >-
#/paths/~1trainings~1{trainingID}~1dates~1{dateID}~1attendees/get/responses
- '#/paths/~1trainings~1{trainingID}~1dates~1{dateID}~1feedback/get/responses'
- '#/paths/~1trainings~1{trainingID}~1feedback/get/responses'
- '#/paths/~1trainings~1upcoming-dates/get/responses'
security-defined:
- '#/paths/~1trainings/get'
- '#/paths/~1trainings/post'
- '#/paths/~1trainings~1{trainingID}/get'
- '#/paths/~1trainings~1{trainingID}/put'
- '#/paths/~1trainings~1{trainingID}/delete'
- '#/paths/~1trainings~1{trainingID}~1dates/get'
- '#/paths/~1trainings~1{trainingID}~1dates/post'
- '#/paths/~1trainings~1{trainingID}~1dates~1{dateID}/put'
- '#/paths/~1trainings~1{trainingID}~1dates~1{dateID}/delete'
- '#/paths/~1trainings~1{trainingID}~1dates~1{dateID}~1attendees/get'
- '#/paths/~1trainings~1{trainingID}~1dates~1{dateID}~1attendees/post'
- >-
#/paths/~1trainings~1{trainingID}~1dates~1{dateID}~1attendees~1{attendeeID}/delete
- >-
#/paths/~1trainings~1{trainingID}~1dates~1{dateID}~1attendees~1{attendeeID}~1feedback/post
- '#/paths/~1trainings~1{trainingID}~1dates~1{dateID}~1feedback/get'
- '#/paths/~1trainings~1{trainingID}~1feedback/get'
- '#/paths/~1trainings~1upcoming-dates/get'

View file

@ -1,3 +1,21 @@
.PHONY: generate
generate:
go generate .
POSTGRES_URL="postgres://official:backoffice@localhost:5432/backoffice_dev?sslmode=disable"
.PHONY: local-migrate-up
local-migrate-up:
migrate -database ${POSTGRES_URL} -path ./db/migrations up
.PHONY: local-migrate-force
local-migrate-force:
migrate -database ${POSTGRES_URL} -path ./db/migrations force
.PHONY: local-migrate-down
local-migrate-down:
migrate -database ${POSTGRES_URL} -path ./db/migrations down
.PHONY: local-migrate-drop
local-migrate-drop:
migrate -database ${POSTGRES_URL} -path ./db/migrations drop -f

View file

@ -493,139 +493,6 @@ paths:
schema:
$ref: "#/components/schemas/ProblemDetails"
/trainings/{trainingID}/dates/{dateID}/attendees/{attendeeID}/feedback:
post:
summary: Submit feedback for an attendee of a date of a training
operationId: createTrainingDateAttendeeFeedback
tags:
- training attendee feedback
parameters:
- name: trainingID
in: path
required: true
schema:
type: string
format: uuid
x-go-type: training.ID
x-go-type-import:
path: gitlab.mareshq.com/hq/backoffice/backoffice-api/pkg/training
- name: dateID
in: path
required: true
schema:
type: string
format: uuid
x-go-type: training.DateID
x-go-type-import:
path: gitlab.mareshq.com/hq/backoffice/backoffice-api/pkg/training
- name: attendeeID
in: path
required: true
schema:
type: string
format: uuid
x-go-type: training.AttendeeID
x-go-type-import:
path: gitlab.mareshq.com/hq/backoffice/backoffice-api/pkg/training
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewTrainingFeedback"
responses:
"201":
description: Feedback created
content:
application/json:
schema:
$ref: "#/components/schemas/TrainingFeedback"
"409":
description: Feedback already submitted
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
"500":
description: Internal error
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
/trainings/{trainingID}/dates/{dateID}/feedback:
get:
summary: List all feedback of a date of a training
operationId: listTrainingDateFeedback
tags:
- training attendee feedback
parameters:
- name: trainingID
in: path
required: true
schema:
type: string
format: uuid
x-go-type: training.ID
x-go-type-import:
path: gitlab.mareshq.com/hq/backoffice/backoffice-api/pkg/training
- name: dateID
in: path
required: true
schema:
type: string
format: uuid
x-go-type: training.DateID
x-go-type-import:
path: gitlab.mareshq.com/hq/backoffice/backoffice-api/pkg/training
responses:
"200":
description: A list of feedback
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/TrainingFeedback"
"500":
description: Internal error
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
/trainings/{trainingID}/feedback:
get:
summary: List all feedback of a training
operationId: listTrainingFeedback
tags:
- training attendee feedback
parameters:
- name: trainingID
in: path
required: true
schema:
type: string
format: uuid
x-go-type: training.ID
x-go-type-import:
path: gitlab.mareshq.com/hq/backoffice/backoffice-api/pkg/training
responses:
"200":
description: A list of feedback
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/TrainingFeedback"
"500":
description: Internal error
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
/trainings/upcoming-dates:
get:
summary: List all upcoming dates of all trainings
@ -679,22 +546,14 @@ components:
properties:
name:
type: string
duration:
days:
type: integer
format: int32
price:
type: object
properties:
open:
$ref: "#/components/schemas/Price"
corporate:
$ref: "#/components/schemas/Price"
length:
type: integer
format: int32
$ref: "#/components/schemas/TrainingPrice"
required:
- name
- duration
- days
- price
Training:
allOf:
@ -710,6 +569,31 @@ components:
required:
- id
TrainingPrice:
types: array
items:
type: object
required:
- type
- price
properties:
type:
type: string
enum: [open, corporate]
price:
type: object
required:
- currency
- amount
properties:
currency:
type: string
enum: [CZK, EUR, USD]
amount:
type: number
format: float64
minimum: 0
NewTrainingDate:
type: object
properties:
@ -776,40 +660,6 @@ components:
required:
- id
NewTrainingFeedback:
type: object
properties:
rating:
type: integer
format: int32
minimum: 0
maximum: 10
comment:
type: string
anonymous:
type: boolean
default: false
isSharingAllowed:
type: boolean
default: false
required:
- rating
- comment
TrainingFeedback:
allOf:
- $ref: "#/components/schemas/NewTrainingFeedback"
- type: object
properties:
id:
type: string
format: uuid
x-go-type: training.FeedbackID
x-go-type-import:
path: gitlab.mareshq.com/hq/backoffice/backoffice-api/pkg/training
required:
- id
Price:
type: object
properties:

20
cmd/migrate.go Normal file
View file

@ -0,0 +1,20 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Migrate database schema up/down",
Long: `All software has versions. This is backoffice backend's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Migrate database schema up/down")
},
}

26
cmd/migrate_up.go Normal file
View file

@ -0,0 +1,26 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
migrateCmd.AddCommand(migrateUpCmd)
}
var migrateUpCmd = &cobra.Command{
Use: "up",
Short: "Migrate database schema up",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
// TODO: Implement this
// TODO: get database URL
// TODO: run migrations
// TODO: handle errors
// TODO: gracefully shutdown
},
}

20
cmd/server.go Normal file
View file

@ -0,0 +1,20 @@
package cmd
import (
"github.com/spf13/cobra"
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/server"
)
func init() {
rootCmd.AddCommand(serverCmd)
}
var serverCmd = &cobra.Command{
Use: "server",
Short: "Starts backoffice backend API server",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
srv := server.NewServer()
srv.Run()
},
}

21
cmd/version.go Normal file
View file

@ -0,0 +1,21 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/version"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of backoffice backend",
Long: `All software has versions. This is backoffice backend's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Version: %s (commit: %s)\n", version.Version, version.Commit)
},
}

View file

@ -10,40 +10,52 @@ CREATE TABLE IF NOT EXISTS training.trainings (
price decimal NOT NULL
);
CREATE TABLE IF NOT EXISTS training.prices (
training_id UUID REFERENCES training.trainings(id),
amount NUMERIC(6, 4) NOT NULL,
currency VARCHAR(3) NOT NULL,
CHECK (currency IN ('USD', 'EUR', 'CZK'))
type VARCHAR(10) NOT NULL,
CHECK (type IN ('OPEN', 'CORPORATE', 'STUDENT', 'GOVERNMENT'))
PRIMARY KEY (training_id, currency, type) -- composite primary key
)
CREATE TABLE IF NOT EXISTS training.dates (
id UUID PRIMARY KEY,
training_id UUID NOT NULL,
date date NOT NULL,
start_time time NOT NULL,
days smallint NOT NULL,
price decimal NOT NULL,
is_online boolean NOT NULL,
location varchar(255) NOT NULL,
address varchar(255) NOT NULL,
capacity smallint NOT NULL,
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(255) NOT NULL,
capacity SMALLINT NOT NULL,
amount NUMERIC(6, 4) NOT NULL,
currency VARCHAR(3) NOT NULL,
CHECK (currency IN ('USD', 'EUR', 'CZK'))
FOREIGN KEY (training_id) REFERENCES training.trainings(id)
);
CREATE TABLE IF NOT EXISTS training.attendees (
id UUID PRIMARY KEY,
date_id UUID NOT NULL,
name varchar(255) NOT NULL,
email varchar(255) NOT NULL,
company varchar(255) NOT NULL,
role varchar(255) NOT NULL,
is_student boolean NOT NULL,
has_attended boolean NOT NULL,
has_paid boolean NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
company VARCHAR(255) NOT NULL,
role VARCHAR(255) NOT NULL,
is_student BOOLEAN NOT NULL,
has_attended BOOLEAN NOT NULL,
has_paid BOOLEAN NOT NULL,
FOREIGN KEY (date_id) REFERENCES training.dates(id)
);
CREATE TABLE IF NOT EXISTS training.feedback (
id UUID PRIMARY KEY,
attendee_id UUID NOT NULL,
rating smallint NOT NULL,
comment text NOT NULL,
is_anonymous boolean NOT NULL,
is_sharing_allowed boolean NOT NULL,
rating SMALLINT NOT NULL,
comment TEXT NOT NULL,
is_anonymous BOOLEAN NOT NULL,
is_sharing_allowed BOOLEAN NOT NULL,
FOREIGN KEY (attendee_id) REFERENCES training.attendees(id)
);

2
go.mod
View file

@ -5,12 +5,14 @@ go 1.22.0
require (
github.com/deepmap/oapi-codegen/v2 v2.1.0
github.com/getkin/kin-openapi v0.122.0
github.com/gofiber/contrib/fiberzap/v2 v2.1.3
github.com/gofiber/contrib/swagger v1.1.2
github.com/gofiber/fiber/v2 v2.52.4
github.com/google/uuid v1.5.0
github.com/oapi-codegen/fiber-middleware v1.0.1
github.com/oapi-codegen/runtime v1.1.1
github.com/spf13/cobra v1.8.0
go.uber.org/zap v1.27.0
)
require (

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{
"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",
"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"
)

View file

@ -4,6 +4,11 @@ import "github.com/google/uuid"
type AttendeeID uuid.UUID
func NewAttendeeID() AttendeeID {
id := uuid.Must(uuid.NewV7())
return AttendeeID(id)
}
type Attendee struct {
ID AttendeeID
DateID DateID

View file

@ -8,15 +8,21 @@ import (
type DateID uuid.UUID
type Date struct {
ID DateID
TrainingID ID
Date time.Time
StartTime time.Time
Days int8
Price Price
IsOnline bool
Location string // could be empty (null) for example: Prague, Brno, London, ...
Address string // could be empty (null)
Capacity int8
func NewDateID() DateID {
id := uuid.Must(uuid.NewV7())
return DateID(id)
}
type Date struct {
ID DateID
TrainingID ID
Date time.Time
StartTime time.Time
Days int8
IsOnline bool
Location string // could be empty (null) for example: Prague, Brno, London, ...
Address string // could be empty (null)
Capacity int8
PriceAmount float64
PriceCurrency string
}

View file

@ -1,3 +1,22 @@
package training
type Price float32
// type Price float32
type TrainingPrice struct {
Currency string `json:"currency"`
Amount float64 `json:"amount"`
Type PriceType `json:"type"` // open | corporate
}
type PriceType string
var (
PriceTypes = []PriceType{OpenPrice, CorporatePrice, StudentPrice, GovernmentPrice}
)
const (
OpenPrice PriceType = "OPEN"
CorporatePrice PriceType = "CORPORATE"
StudentPrice PriceType = "STUDENT"
GovernmentPrice PriceType = "GOVERNMENT"
)

View file

@ -6,10 +6,15 @@ import (
type ID uuid.UUID
func NewID() ID {
id := uuid.Must(uuid.NewV7())
return ID(id)
}
type Training struct {
ID ID
Days int8
Name string
Description string
Price Price
Price []TrainingPrice
}

25
redocly.yaml Normal file
View file

@ -0,0 +1,25 @@
# See https://redocly.com/docs/cli/configuration/
extends:
- recommended
apis:
backoffice@v1:
root: ./api/v1/openapi.yaml
rules:
no-ambiguous-paths: error
rules:
no-unused-components: error
operation-singular-tag: warn
boolean-parameter-prefixes:
severity: error
prefixes: ["can", "is", "has"]
theme:
openapi:
generateCodeSamples:
languages:
- lang: curl
- lang: JavaScript
- lang: Go