feat!: add slug to training
BREAKING CHANGE: update init migration
This commit is contained in:
parent
556b4f4e79
commit
2d32c80182
12 changed files with 219 additions and 45 deletions
|
|
@ -529,6 +529,8 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/TrainingPrice"
|
$ref: "#/components/schemas/TrainingPrice"
|
||||||
|
slug:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- days
|
- days
|
||||||
|
|
@ -544,6 +546,7 @@ components:
|
||||||
$ref: "#/components/schemas/TrainingID"
|
$ref: "#/components/schemas/TrainingID"
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
|
- slug
|
||||||
|
|
||||||
TrainingID:
|
TrainingID:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -7,6 +7,7 @@ require (
|
||||||
github.com/gofiber/contrib/fiberzap/v2 v2.1.3
|
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/gosimple/unidecode v1.0.1
|
||||||
github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e
|
github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e
|
||||||
github.com/jackc/pgx/v5 v5.6.0
|
github.com/jackc/pgx/v5 v5.6.0
|
||||||
github.com/oapi-codegen/fiber-middleware v1.0.2
|
github.com/oapi-codegen/fiber-middleware v1.0.2
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -45,6 +45,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
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/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
|
||||||
|
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
||||||
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/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ type NewTraining struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Pricing []TrainingPrice `json:"pricing"`
|
Pricing []TrainingPrice `json:"pricing"`
|
||||||
|
Slug *string `json:"slug,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTrainingDate defines model for NewTrainingDate.
|
// NewTrainingDate defines model for NewTrainingDate.
|
||||||
|
|
@ -138,6 +139,7 @@ type Training struct {
|
||||||
Id TrainingID `json:"id"`
|
Id TrainingID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Pricing []TrainingPrice `json:"pricing"`
|
Pricing []TrainingPrice `json:"pricing"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrainingDate defines model for TrainingDate.
|
// TrainingDate defines model for TrainingDate.
|
||||||
|
|
@ -2336,44 +2338,45 @@ func (sh *strictHandler) ListTrainingUpcomingDates(ctx *fiber.Ctx, trainingID Tr
|
||||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"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",
|
||||||
|
"VQAA",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,14 @@ func (h *APIHandlers) CreateTraining(ctx context.Context, req CreateTrainingRequ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slug := ""
|
||||||
|
if req.Body.Slug != nil {
|
||||||
|
slug = *req.Body.Slug
|
||||||
|
}
|
||||||
|
|
||||||
t := training.Training{
|
t := training.Training{
|
||||||
Name: req.Body.Name,
|
Name: req.Body.Name,
|
||||||
|
Slug: slug,
|
||||||
Days: req.Body.Days,
|
Days: req.Body.Days,
|
||||||
Description: req.Body.Description,
|
Description: req.Body.Description,
|
||||||
Pricing: pricing,
|
Pricing: pricing,
|
||||||
|
|
@ -108,6 +114,7 @@ func (h *APIHandlers) CreateTraining(ctx context.Context, req CreateTrainingRequ
|
||||||
return CreateTraining201JSONResponse{
|
return CreateTraining201JSONResponse{
|
||||||
Id: t.ID,
|
Id: t.ID,
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
|
Slug: t.Slug,
|
||||||
Days: t.Days,
|
Days: t.Days,
|
||||||
Description: t.Description,
|
Description: t.Description,
|
||||||
Pricing: responsePricing,
|
Pricing: responsePricing,
|
||||||
|
|
@ -162,6 +169,7 @@ func (h *APIHandlers) GetTraining(ctx context.Context, req GetTrainingRequestObj
|
||||||
return GetTraining200JSONResponse{
|
return GetTraining200JSONResponse{
|
||||||
Id: t.ID,
|
Id: t.ID,
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
|
Slug: t.Slug,
|
||||||
Days: t.Days,
|
Days: t.Days,
|
||||||
Description: t.Description,
|
Description: t.Description,
|
||||||
Pricing: pricing,
|
Pricing: pricing,
|
||||||
|
|
@ -187,9 +195,16 @@ func (h *APIHandlers) UpdateTraining(ctx context.Context, req UpdateTrainingRequ
|
||||||
Type: training.PriceType(p.Type),
|
Type: training.PriceType(p.Type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slug := ""
|
||||||
|
if req.Body.Slug != nil {
|
||||||
|
slug = *req.Body.Slug
|
||||||
|
}
|
||||||
|
|
||||||
t := training.Training{
|
t := training.Training{
|
||||||
ID: req.TrainingID,
|
ID: req.TrainingID,
|
||||||
Name: req.Body.Name,
|
Name: req.Body.Name,
|
||||||
|
Slug: slug,
|
||||||
Days: req.Body.Days,
|
Days: req.Body.Days,
|
||||||
Description: req.Body.Description,
|
Description: req.Body.Description,
|
||||||
Pricing: pricing,
|
Pricing: pricing,
|
||||||
|
|
@ -208,6 +223,7 @@ func (h *APIHandlers) UpdateTraining(ctx context.Context, req UpdateTrainingRequ
|
||||||
return UpdateTraining200JSONResponse{
|
return UpdateTraining200JSONResponse{
|
||||||
Id: t.ID,
|
Id: t.ID,
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
|
Slug: t.Slug,
|
||||||
Days: t.Days,
|
Days: t.Days,
|
||||||
Description: t.Description,
|
Description: t.Description,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"gitlab.mareshq.com/hq/yggdrasil/internal/money"
|
"gitlab.mareshq.com/hq/yggdrasil/internal/money"
|
||||||
|
"gitlab.mareshq.com/hq/yggdrasil/pkg/slug"
|
||||||
"gitlab.mareshq.com/hq/yggdrasil/pkg/training"
|
"gitlab.mareshq.com/hq/yggdrasil/pkg/training"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
@ -115,6 +116,8 @@ func TestServer(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slugString := slug.NewString(newTraining.Name)
|
||||||
|
|
||||||
rr, _ := doPost(t, app, "/v1/trainings", newTraining)
|
rr, _ := doPost(t, app, "/v1/trainings", newTraining)
|
||||||
assert.Equal(t, http.StatusCreated, rr.StatusCode)
|
assert.Equal(t, http.StatusCreated, rr.StatusCode)
|
||||||
|
|
||||||
|
|
@ -122,6 +125,7 @@ func TestServer(t *testing.T) {
|
||||||
err := json.NewDecoder(rr.Body).Decode(&resultTraining)
|
err := json.NewDecoder(rr.Body).Decode(&resultTraining)
|
||||||
assert.NoError(t, err, "error unmarshalling response")
|
assert.NoError(t, err, "error unmarshalling response")
|
||||||
assert.Equal(t, newTraining.Name, resultTraining.Name)
|
assert.Equal(t, newTraining.Name, resultTraining.Name)
|
||||||
|
assert.Equal(t, slugString, resultTraining.Slug)
|
||||||
assert.Equal(t, newTraining.Description, resultTraining.Description)
|
assert.Equal(t, newTraining.Description, resultTraining.Description)
|
||||||
assert.Equal(t, newTraining.Days, resultTraining.Days)
|
assert.Equal(t, newTraining.Days, resultTraining.Days)
|
||||||
assert.Equal(t, newTraining.Pricing, resultTraining.Pricing)
|
assert.Equal(t, newTraining.Pricing, resultTraining.Pricing)
|
||||||
|
|
@ -177,8 +181,10 @@ func TestServer(t *testing.T) {
|
||||||
|
|
||||||
_ = handlers.trainingRepository.Create(tr)
|
_ = handlers.trainingRepository.Create(tr)
|
||||||
|
|
||||||
|
updatedSlug := "updated-training"
|
||||||
updTr := NewTraining{
|
updTr := NewTraining{
|
||||||
Name: "Updated Training",
|
Name: "Updated Training",
|
||||||
|
Slug: &updatedSlug,
|
||||||
Description: tr.Description,
|
Description: tr.Description,
|
||||||
Days: tr.Days,
|
Days: tr.Days,
|
||||||
Pricing: []TrainingPrice{
|
Pricing: []TrainingPrice{
|
||||||
|
|
@ -197,6 +203,7 @@ func TestServer(t *testing.T) {
|
||||||
err := json.NewDecoder(rr.Body).Decode(&trr)
|
err := json.NewDecoder(rr.Body).Decode(&trr)
|
||||||
assert.NoError(t, err, "error unmarshalling response")
|
assert.NoError(t, err, "error unmarshalling response")
|
||||||
assert.Equal(t, updTr.Name, trr.Name)
|
assert.Equal(t, updTr.Name, trr.Name)
|
||||||
|
assert.Equal(t, updatedSlug, trr.Slug)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Delete training", func(t *testing.T) {
|
t.Run("Delete training", func(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ CREATE SCHEMA IF NOT EXISTS training;
|
||||||
CREATE TABLE IF NOT EXISTS training.trainings (
|
CREATE TABLE IF NOT EXISTS training.trainings (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name varchar(255) NOT NULL,
|
name varchar(255) NOT NULL,
|
||||||
|
slug varchar(255) NOT NULL,
|
||||||
description text NOT NULL,
|
description text NOT NULL,
|
||||||
days smallint NOT NULL
|
days smallint NOT NULL
|
||||||
);
|
);
|
||||||
|
|
|
||||||
47
pkg/slug/slug.go
Normal file
47
pkg/slug/slug.go
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
package slug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/gosimple/unidecode"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Slug string
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidSlug = errors.New("invalid slug")
|
||||||
|
|
||||||
|
regexpNonAuthorizedChars = regexp.MustCompile("[^a-zA-Z0-9-_]")
|
||||||
|
regexpMultipleDashes = regexp.MustCompile("-+")
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(s string) Slug {
|
||||||
|
s = unidecode.Unidecode(s)
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
s = regexpNonAuthorizedChars.ReplaceAllString(s, "-")
|
||||||
|
s = regexpMultipleDashes.ReplaceAllString(s, "-")
|
||||||
|
s = strings.Trim(s, "-_")
|
||||||
|
|
||||||
|
return Slug(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewString(s string) string {
|
||||||
|
return New(s).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validate(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return ErrInvalidSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
if s == NewString(s) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrInvalidSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Slug) String() string {
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
59
pkg/slug/slug_test.go
Normal file
59
pkg/slug/slug_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package slug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
t.Run("Test New", func(t *testing.T) {
|
||||||
|
s := New("Hello World")
|
||||||
|
assert.Equal(t, "hello-world", s.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test New with !", func(t *testing.T) {
|
||||||
|
s := New("Hello World!")
|
||||||
|
assert.Equal(t, "hello-world", s.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test New with unicode", func(t *testing.T) {
|
||||||
|
s := New("Ahoj Světe!") // hello world in Czech
|
||||||
|
assert.Equal(t, "ahoj-svete", s.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
t.Run("Test Validate", func(t *testing.T) {
|
||||||
|
assert.NoError(t, Validate("hello-world"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test Validate with !", func(t *testing.T) {
|
||||||
|
assert.ErrorIs(t, Validate("hello-world!"), ErrInvalidSlug)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test Validate with unicode", func(t *testing.T) {
|
||||||
|
assert.ErrorIs(t, Validate("ahoj-světe"), ErrInvalidSlug)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Test Validate with empty string", func(t *testing.T) {
|
||||||
|
assert.ErrorIs(t, Validate(""), ErrInvalidSlug)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlug_String(t *testing.T) {
|
||||||
|
t.Run("Test String", func(t *testing.T) {
|
||||||
|
s := Slug("hello-world")
|
||||||
|
assert.Equal(t, "hello-world", s.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewString(t *testing.T) {
|
||||||
|
t.Run("Test NewString", func(t *testing.T) {
|
||||||
|
input := "Hello World"
|
||||||
|
expected := "hello-world"
|
||||||
|
stringSlug := NewString(input)
|
||||||
|
|
||||||
|
assert.Equal(t, expected, stringSlug)
|
||||||
|
assert.IsType(t, string(""), stringSlug)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package training
|
package training
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"gitlab.mareshq.com/hq/yggdrasil/pkg/slug"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -24,6 +25,16 @@ func (r *InMemoryTrainingRepository) Create(training *Training) error {
|
||||||
|
|
||||||
training.ID = r.ai
|
training.ID = r.ai
|
||||||
r.ai++
|
r.ai++
|
||||||
|
|
||||||
|
if training.Slug == "" {
|
||||||
|
training.Slug = slug.NewString(training.Name)
|
||||||
|
} else {
|
||||||
|
slugValidateErr := slug.Validate(training.Slug)
|
||||||
|
if slugValidateErr != nil {
|
||||||
|
return slugValidateErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r.trainings[training.ID] = *training
|
r.trainings[training.ID] = *training
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +65,15 @@ func (r *InMemoryTrainingRepository) Update(training *Training) error {
|
||||||
r.lock.Lock()
|
r.lock.Lock()
|
||||||
defer r.lock.Unlock()
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
if training.Slug == "" {
|
||||||
|
training.Slug = slug.NewString(training.Name)
|
||||||
|
} else {
|
||||||
|
slugValidateErr := slug.Validate(training.Slug)
|
||||||
|
if slugValidateErr != nil {
|
||||||
|
return slugValidateErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r.trainings[training.ID] = *training
|
r.trainings[training.ID] = *training
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ type ID = int
|
||||||
type Training struct {
|
type Training struct {
|
||||||
ID ID
|
ID ID
|
||||||
Name string
|
Name string
|
||||||
|
Slug string
|
||||||
Days int8
|
Days int8
|
||||||
Description string
|
Description string
|
||||||
Pricing []Price `db:"-"`
|
Pricing []Price `db:"-"`
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"gitlab.mareshq.com/hq/yggdrasil/pkg/slug"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -19,16 +20,20 @@ func (r *PostgresTrainingRepository) Create(training *Training) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
if training.Slug == "" {
|
||||||
|
training.Slug = slug.NewString(training.Name)
|
||||||
|
}
|
||||||
|
|
||||||
tx, txErr := r.pg.Begin(ctx)
|
tx, txErr := r.pg.Begin(ctx)
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
|
|
||||||
queryErr := tx.QueryRow(ctx, `
|
queryErr := tx.QueryRow(ctx, `
|
||||||
INSERT INTO training.trainings (name, description, days)
|
INSERT INTO training.trainings (name, slug, description, days)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, training.Name, training.Description, training.Days).Scan(&training.ID)
|
`, training.Name, training.Slug, training.Description, training.Days).Scan(&training.ID)
|
||||||
if queryErr != nil {
|
if queryErr != nil {
|
||||||
return queryErr
|
return queryErr
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +80,7 @@ func (r *PostgresTrainingRepository) FindByID(id ID) (*Training, error) {
|
||||||
SELECT id, name, description, days
|
SELECT id, name, description, days
|
||||||
FROM training.trainings
|
FROM training.trainings
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, id).Scan(&training.ID, &training.Name, &training.Description, &training.Days)
|
`, id).Scan(&training.ID, &training.Name, &training.Slug, &training.Description, &training.Days)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -122,7 +127,7 @@ func (r *PostgresTrainingRepository) FindAll() ([]Training, error) {
|
||||||
|
|
||||||
trainings, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (Training, error) {
|
trainings, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (Training, error) {
|
||||||
var training Training
|
var training Training
|
||||||
scanErr := row.Scan(&training.ID, &training.Name, &training.Description, &training.Days)
|
scanErr := row.Scan(&training.ID, &training.Name, &training.Slug, &training.Description, &training.Days)
|
||||||
if scanErr != nil {
|
if scanErr != nil {
|
||||||
return Training{}, scanErr
|
return Training{}, scanErr
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +156,15 @@ func (r *PostgresTrainingRepository) Update(training *Training) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
if training.Slug == "" {
|
||||||
|
training.Slug = slug.NewString(training.Name)
|
||||||
|
} else {
|
||||||
|
slugValidateErr := slug.Validate(training.Slug)
|
||||||
|
if slugValidateErr != nil {
|
||||||
|
return slugValidateErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tx, err := r.pg.Begin(ctx)
|
tx, err := r.pg.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -158,9 +172,9 @@ func (r *PostgresTrainingRepository) Update(training *Training) error {
|
||||||
|
|
||||||
_, err = tx.Exec(ctx, `
|
_, err = tx.Exec(ctx, `
|
||||||
UPDATE training.trainings
|
UPDATE training.trainings
|
||||||
SET name = $1, description = $2, days = $3
|
SET name = $1, slug = $2, description = $3, days = $4
|
||||||
WHERE id = $4
|
WHERE id = $4
|
||||||
`, training.Name, training.Description, training.Days, training.ID)
|
`, training.Name, training.Slug, training.Description, training.Days, training.ID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
Reference in a new issue