WIP: EOL
This commit is contained in:
parent
7ed1e05284
commit
49e05cac10
23 changed files with 613 additions and 253 deletions
34
.redocly.lint-ignore.yaml
Normal file
34
.redocly.lint-ignore.yaml
Normal 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'
|
||||||
18
Makefile
18
Makefile
|
|
@ -1,3 +1,21 @@
|
||||||
.PHONY: generate
|
.PHONY: generate
|
||||||
generate:
|
generate:
|
||||||
go 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
|
||||||
|
|
|
||||||
|
|
@ -493,139 +493,6 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ProblemDetails"
|
$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:
|
/trainings/upcoming-dates:
|
||||||
get:
|
get:
|
||||||
summary: List all upcoming dates of all trainings
|
summary: List all upcoming dates of all trainings
|
||||||
|
|
@ -679,22 +546,14 @@ components:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
duration:
|
days:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
price:
|
price:
|
||||||
type: object
|
$ref: "#/components/schemas/TrainingPrice"
|
||||||
properties:
|
|
||||||
open:
|
|
||||||
$ref: "#/components/schemas/Price"
|
|
||||||
corporate:
|
|
||||||
$ref: "#/components/schemas/Price"
|
|
||||||
length:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- duration
|
- days
|
||||||
- price
|
- price
|
||||||
Training:
|
Training:
|
||||||
allOf:
|
allOf:
|
||||||
|
|
@ -710,6 +569,31 @@ components:
|
||||||
required:
|
required:
|
||||||
- id
|
- 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:
|
NewTrainingDate:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -776,40 +660,6 @@ components:
|
||||||
required:
|
required:
|
||||||
- id
|
- 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:
|
Price:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
||||||
20
cmd/migrate.go
Normal file
20
cmd/migrate.go
Normal 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
26
cmd/migrate_up.go
Normal 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
20
cmd/server.go
Normal 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
21
cmd/version.go
Normal 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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -10,40 +10,52 @@ CREATE TABLE IF NOT EXISTS training.trainings (
|
||||||
price decimal NOT NULL
|
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 (
|
CREATE TABLE IF NOT EXISTS training.dates (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
training_id UUID NOT NULL,
|
training_id UUID NOT NULL,
|
||||||
date date NOT NULL,
|
date DATE NOT NULL,
|
||||||
start_time time NOT NULL,
|
start_time TIME NOT NULL,
|
||||||
days smallint NOT NULL,
|
days SMALLINT NOT NULL,
|
||||||
price decimal NOT NULL,
|
is_online BOOLEAN NOT NULL,
|
||||||
is_online boolean NOT NULL,
|
location VARCHAR(255) NOT NULL,
|
||||||
location varchar(255) NOT NULL,
|
address VARCHAR(255) NOT NULL,
|
||||||
address varchar(255) NOT NULL,
|
capacity SMALLINT 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)
|
FOREIGN KEY (training_id) REFERENCES training.trainings(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS training.attendees (
|
CREATE TABLE IF NOT EXISTS training.attendees (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
date_id UUID NOT NULL,
|
date_id UUID NOT NULL,
|
||||||
name varchar(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
email varchar(255) NOT NULL,
|
email VARCHAR(255) NOT NULL,
|
||||||
company varchar(255) NOT NULL,
|
company VARCHAR(255) NOT NULL,
|
||||||
role varchar(255) NOT NULL,
|
role VARCHAR(255) NOT NULL,
|
||||||
is_student boolean NOT NULL,
|
is_student BOOLEAN NOT NULL,
|
||||||
has_attended boolean NOT NULL,
|
has_attended BOOLEAN NOT NULL,
|
||||||
has_paid boolean NOT NULL,
|
has_paid BOOLEAN NOT NULL,
|
||||||
FOREIGN KEY (date_id) REFERENCES training.dates(id)
|
FOREIGN KEY (date_id) REFERENCES training.dates(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS training.feedback (
|
CREATE TABLE IF NOT EXISTS training.feedback (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
attendee_id UUID NOT NULL,
|
attendee_id UUID NOT NULL,
|
||||||
rating smallint NOT NULL,
|
rating SMALLINT NOT NULL,
|
||||||
comment text NOT NULL,
|
comment TEXT NOT NULL,
|
||||||
is_anonymous boolean NOT NULL,
|
is_anonymous BOOLEAN NOT NULL,
|
||||||
is_sharing_allowed boolean NOT NULL,
|
is_sharing_allowed BOOLEAN NOT NULL,
|
||||||
FOREIGN KEY (attendee_id) REFERENCES training.attendees(id)
|
FOREIGN KEY (attendee_id) REFERENCES training.attendees(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -5,12 +5,14 @@ go 1.22.0
|
||||||
require (
|
require (
|
||||||
github.com/deepmap/oapi-codegen/v2 v2.1.0
|
github.com/deepmap/oapi-codegen/v2 v2.1.0
|
||||||
github.com/getkin/kin-openapi v0.122.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/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.5.0
|
github.com/google/uuid v1.5.0
|
||||||
github.com/oapi-codegen/fiber-middleware v1.0.1
|
github.com/oapi-codegen/fiber-middleware v1.0.1
|
||||||
github.com/oapi-codegen/runtime v1.1.1
|
github.com/oapi-codegen/runtime v1.1.1
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,7 @@ const (
|
||||||
|
|
||||||
// NewTraining defines model for NewTraining.
|
// NewTraining defines model for NewTraining.
|
||||||
type NewTraining struct {
|
type NewTraining struct {
|
||||||
Duration int32 `json:"duration"`
|
Days int32 `json:"days"`
|
||||||
Length *int32 `json:"length,omitempty"`
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Price struct {
|
Price struct {
|
||||||
Corporate *Price `json:"corporate,omitempty"`
|
Corporate *Price `json:"corporate,omitempty"`
|
||||||
|
|
@ -90,9 +89,8 @@ type ProblemDetails struct {
|
||||||
|
|
||||||
// Training defines model for Training.
|
// Training defines model for Training.
|
||||||
type Training struct {
|
type Training struct {
|
||||||
Duration int32 `json:"duration"`
|
Days int32 `json:"days"`
|
||||||
Id training.ID `json:"id"`
|
Id training.ID `json:"id"`
|
||||||
Length *int32 `json:"length,omitempty"`
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Price struct {
|
Price struct {
|
||||||
Corporate *Price `json:"corporate,omitempty"`
|
Corporate *Price `json:"corporate,omitempty"`
|
||||||
|
|
@ -1675,40 +1673,39 @@ func (sh *strictHandler) ListTrainingFeedback(ctx *fiber.Ctx, trainingID trainin
|
||||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"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",
|
||||||
"RgAA",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|
|
||||||
126
internal/api/server.go
Normal file
126
internal/api/server.go
Normal 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")
|
||||||
|
}
|
||||||
13
internal/currency/currency.go
Normal file
13
internal/currency/currency.go
Normal 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"
|
||||||
|
)
|
||||||
|
|
@ -77,6 +77,7 @@ func (r *AttendeeRepository) CountForDate(dateID training.DateID) (int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AttendeeRepository) Create(a *training.Attendee) 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)
|
_, 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ func (r *DateRepository) FindAllForTraining(id training.ID) ([]training.Date, er
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DateRepository) Create(d *training.Date) error {
|
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)
|
_, 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ func NewTrainingRepository(db *sql.DB) *TrainingRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *TrainingRepository) Create(t *training.Training) error {
|
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)
|
_, 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
internal/server/config.go
Normal file
7
internal/server/config.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PostgresURL string
|
||||||
|
Port int
|
||||||
|
ShutdownGraceSeconds int
|
||||||
|
}
|
||||||
145
internal/server/server.go
Normal file
145
internal/server/server.go
Normal 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
|
||||||
|
}
|
||||||
6
internal/version/version.go
Normal file
6
internal/version/version.go
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package version
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
Commit = "N/A"
|
||||||
|
)
|
||||||
|
|
@ -4,6 +4,11 @@ import "github.com/google/uuid"
|
||||||
|
|
||||||
type AttendeeID uuid.UUID
|
type AttendeeID uuid.UUID
|
||||||
|
|
||||||
|
func NewAttendeeID() AttendeeID {
|
||||||
|
id := uuid.Must(uuid.NewV7())
|
||||||
|
return AttendeeID(id)
|
||||||
|
}
|
||||||
|
|
||||||
type Attendee struct {
|
type Attendee struct {
|
||||||
ID AttendeeID
|
ID AttendeeID
|
||||||
DateID DateID
|
DateID DateID
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,21 @@ import (
|
||||||
|
|
||||||
type DateID uuid.UUID
|
type DateID uuid.UUID
|
||||||
|
|
||||||
|
func NewDateID() DateID {
|
||||||
|
id := uuid.Must(uuid.NewV7())
|
||||||
|
return DateID(id)
|
||||||
|
}
|
||||||
|
|
||||||
type Date struct {
|
type Date struct {
|
||||||
ID DateID
|
ID DateID
|
||||||
TrainingID ID
|
TrainingID ID
|
||||||
Date time.Time
|
Date time.Time
|
||||||
StartTime time.Time
|
StartTime time.Time
|
||||||
Days int8
|
Days int8
|
||||||
Price Price
|
|
||||||
IsOnline bool
|
IsOnline bool
|
||||||
Location string // could be empty (null) for example: Prague, Brno, London, ...
|
Location string // could be empty (null) for example: Prague, Brno, London, ...
|
||||||
Address string // could be empty (null)
|
Address string // could be empty (null)
|
||||||
Capacity int8
|
Capacity int8
|
||||||
|
PriceAmount float64
|
||||||
|
PriceCurrency string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,22 @@
|
||||||
package training
|
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"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,15 @@ import (
|
||||||
|
|
||||||
type ID uuid.UUID
|
type ID uuid.UUID
|
||||||
|
|
||||||
|
func NewID() ID {
|
||||||
|
id := uuid.Must(uuid.NewV7())
|
||||||
|
return ID(id)
|
||||||
|
}
|
||||||
|
|
||||||
type Training struct {
|
type Training struct {
|
||||||
ID ID
|
ID ID
|
||||||
Days int8
|
Days int8
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
Price Price
|
Price []TrainingPrice
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
redocly.yaml
Normal file
25
redocly.yaml
Normal 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
|
||||||
Reference in a new issue