add qualification repository and usecase

This commit is contained in:
Dawid Wysokiński 2021-02-27 14:46:00 +01:00
parent a9d7250623
commit 8fae4a2b03
11 changed files with 382 additions and 5 deletions

View File

@ -82,7 +82,7 @@ func createSchema(db *pg.DB) error {
}
}
total, err := db.Model(modelsToCreate[0]).Where("role = ?", models.RoleAdmin).Count()
total, err := tx.Model(modelsToCreate[0]).Where("role = ?", models.RoleAdmin).Count()
if err != nil {
return errors.Wrap(err, "createSchema")
}

View File

@ -16,7 +16,7 @@ type Profession struct {
ID int `json:"id,omitempty" xml:"id" gqlgen:"id"`
Slug string `json:"slug" pg:",unique" xml:"slug" gqlgen:"slug"`
Name string `json:"name,omitempty" pg:",unique" xml:"name" gqlgen:"name"`
Description string `json:"description,omitempty" pg:",use_zero" xml:"description" gqlgen:"description"`
Description string `json:"description,omitempty" xml:"description" gqlgen:"description"`
CreatedAt time.Time `json:"createdAt,omitempty" pg:"default:now()" xml:"createdAt" gqlgen:"createdAt"`
}

View File

@ -38,9 +38,9 @@ func (q *Qualification) BeforeUpdate(ctx context.Context) (context.Context, erro
}
type QualificationToProfession struct {
QualificationID int `pg:"on_delete:CASCADE" json:"qualificationID" xml:"qualificationID" gqlgen:"qualificationID"`
QualificationID int `pg:"on_delete:CASCADE,unique:group_1" json:"qualificationID" xml:"qualificationID" gqlgen:"qualificationID"`
Qualification *Qualification `pg:"rel:has-one" json:"qualification" xml:"qualification" gqlgen:"qualification"`
ProfessionID int `pg:"on_delete:CASCADE" json:"professionID" xml:"professionID" gqlgen:"professionID"`
ProfessionID int `pg:"on_delete:CASCADE,unique:group_1" json:"professionID" xml:"professionID" gqlgen:"professionID"`
Profession *Qualification `pg:"rel:has-one" json:"profession" xml:"profession" gqlgen:"profession"`
}
@ -53,6 +53,16 @@ type QualificationInput struct {
DissociateProfession []int `json:"dissociateProfession" xml:"dissociateProfession" gqlgen:"dissociateProfession"`
}
func (input *QualificationInput) IsEmpty() bool {
return input == nil &&
input.Name == nil &&
input.Code == nil &&
input.Formula == nil &&
input.Description == nil &&
len(input.AssociateProfession) == 0 &&
len(input.DissociateProfession) == 0
}
func (input *QualificationInput) ToQualification() *Qualification {
q := &Qualification{}
if input.Name != nil {
@ -70,6 +80,19 @@ func (input *QualificationInput) ToQualification() *Qualification {
return q
}
func (input *QualificationInput) ApplyUpdate(q *orm.Query) (*orm.Query, error) {
if !input.IsEmpty() {
if input.Name != nil {
q.Set("name = ?", *input.Name)
}
if input.Description != nil {
q.Set("description = ?", *input.Description)
}
}
return q, nil
}
type QualificationFilterOr struct {
NameMATCH string `json:"nameMATCH" xml:"nameMATCH" gqlgen:"nameMATCH"`
NameIEQ string `gqlgen:"nameIEQ" json:"nameIEQ" xml:"nameIEQ"`

View File

@ -4,6 +4,6 @@ const (
messageInvalidID = "Niepoprawne ID."
messageItemNotFound = "Nie znaleziono zawodu."
messageEmptyPayload = "Nie wprowadzono jakichkolwiek danych."
messageNameIsRequired = "Nazwa zawodu jest polem wymaganym."
messageNameIsRequired = "Nazwa zawodu jest wymagana."
messageNameIsTooLong = "Nazwa zawodu może się składać z maksymalnie %d znaków."
)

View File

@ -0,0 +1,6 @@
package qualification
const (
DefaultLimit = 100
MaxNameLength = 100
)

View File

@ -0,0 +1,22 @@
package qualification
import (
"context"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
)
type FetchConfig struct {
Filter *models.QualificationFilter
Offset int
Limit int
Sort []string
Count bool
}
type Repository interface {
Store(ctx context.Context, input *models.QualificationInput) (*models.Qualification, error)
UpdateMany(ctx context.Context, f *models.QualificationFilter, input *models.QualificationInput) ([]*models.Qualification, error)
Delete(ctx context.Context, f *models.QualificationFilter) ([]*models.Qualification, error)
Fetch(ctx context.Context, cfg *FetchConfig) ([]*models.Qualification, int, error)
}

View File

@ -0,0 +1,9 @@
package repository
const (
messageNameIsAlreadyTaken = "Istnieje już kwalifikacja o podanej nazwie."
messageCodeIsAlreadyTaken = "Istnieje już kwalifikacja o podanym oznaczeniu."
messageFailedToSaveModel = "Wystąpił błąd podczas zapisywania kwalifikacji, prosimy spróbować później."
messageFailedToDeleteModel = "Wystąpił błąd podczas usuwania kwalifikacji, prosimy spróbować później."
messageFailedToFetchModel = "Wystąpił błąd podczas pobierania kwalifikacji, prosimy spróbować później."
)

View File

@ -0,0 +1,152 @@
package repository
import (
"context"
"fmt"
"strings"
sqlutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/sql"
errorutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/error"
"github.com/go-pg/pg/v10"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
"github.com/zdam-egzamin-zawodowy/backend/internal/qualification"
)
type pgRepository struct {
*pg.DB
}
type PGRepositoryConfig struct {
DB *pg.DB
}
func NewPGRepository(cfg *PGRepositoryConfig) (qualification.Repository, error) {
if cfg == nil || cfg.DB == nil {
return nil, fmt.Errorf("qualification/pg_repository: *pg.DB is required")
}
return &pgRepository{
cfg.DB,
}, nil
}
func (repo *pgRepository) Store(ctx context.Context, input *models.QualificationInput) (*models.Qualification, error) {
item := input.ToQualification()
err := repo.RunInTransaction(ctx, func(tx *pg.Tx) error {
if _, err := tx.
Model(item).
Context(ctx).
Returning("*").
Insert(); err != nil {
if strings.Contains(err.Error(), "name") {
return errorutils.Wrap(err, messageNameIsAlreadyTaken)
} else if strings.Contains(err.Error(), "code") {
return errorutils.Wrap(err, messageCodeIsAlreadyTaken)
}
return errorutils.Wrap(err, messageFailedToSaveModel)
}
for _, professionID := range input.AssociateProfession {
tx.
Model(&models.QualificationToProfession{
QualificationID: item.ID,
ProfessionID: professionID,
}).
Insert()
}
return nil
})
return item, err
}
func (repo *pgRepository) UpdateMany(
ctx context.Context,
f *models.QualificationFilter,
input *models.QualificationInput,
) ([]*models.Qualification, error) {
items := []*models.Qualification{}
err := repo.RunInTransaction(ctx, func(tx *pg.Tx) error {
if _, err := tx.
Model(&items).
Context(ctx).
Returning("*").
Apply(input.ApplyUpdate).
Apply(f.Where).
Update(); err != nil && err != pg.ErrNoRows {
if strings.Contains(err.Error(), "name") {
return errorutils.Wrap(err, messageNameIsAlreadyTaken)
} else if strings.Contains(err.Error(), "code") {
return errorutils.Wrap(err, messageCodeIsAlreadyTaken)
}
return errorutils.Wrap(err, messageFailedToSaveModel)
}
qualificationIDs := make([]int, len(items))
for index, item := range items {
qualificationIDs[index] = item.ID
}
if len(qualificationIDs) > 0 {
if len(input.DissociateProfession) > 0 {
tx.
Model(&models.QualificationToProfession{}).
Where(sqlutils.BuildConditionArray("profession_id"), input.DissociateProfession).
Where(sqlutils.BuildConditionArray("qualification_id"), qualificationIDs).
Delete()
}
if len(input.AssociateProfession) > 0 {
toInsert := []*models.QualificationToProfession{}
for _, professionID := range input.AssociateProfession {
for _, qualificationID := range qualificationIDs {
toInsert = append(toInsert, &models.QualificationToProfession{
ProfessionID: professionID,
QualificationID: qualificationID,
})
}
}
tx.Model(&toInsert).Insert()
}
}
return nil
})
return items, err
}
func (repo *pgRepository) Delete(ctx context.Context, f *models.QualificationFilter) ([]*models.Qualification, error) {
items := []*models.Qualification{}
if _, err := repo.
Model(&items).
Context(ctx).
Returning("*").
Apply(f.Where).
Delete(); err != nil && err != pg.ErrNoRows {
return nil, errorutils.Wrap(err, messageFailedToDeleteModel)
}
return items, nil
}
func (repo *pgRepository) Fetch(ctx context.Context, cfg *qualification.FetchConfig) ([]*models.Qualification, int, error) {
var err error
items := []*models.Qualification{}
total := 0
query := repo.
Model(&items).
Context(ctx).
Limit(cfg.Limit).
Offset(cfg.Offset).
Apply(cfg.Filter.Where)
if cfg.Count {
total, err = query.SelectAndCount()
} else {
err = query.Select()
}
if err != nil && err != pg.ErrNoRows {
return nil, 0, errorutils.Wrap(err, messageFailedToFetchModel)
}
return items, total, nil
}

View File

@ -0,0 +1,16 @@
package qualification
import (
"context"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
)
type Usecase interface {
Store(ctx context.Context, input *models.QualificationInput) (*models.Qualification, error)
UpdateOne(ctx context.Context, id int, input *models.QualificationInput) (*models.Qualification, error)
Delete(ctx context.Context, f *models.QualificationFilter) ([]*models.Qualification, error)
Fetch(ctx context.Context, cfg *FetchConfig) ([]*models.Qualification, int, error)
GetByID(ctx context.Context, id int) (*models.Qualification, error)
GetBySlug(ctx context.Context, slug string) (*models.Qualification, error)
}

View File

@ -0,0 +1,10 @@
package usecase
const (
messageInvalidID = "Niepoprawne ID."
messageItemNotFound = "Nie znaleziono zawodu."
messageEmptyPayload = "Nie wprowadzono jakichkolwiek danych."
messageNameIsRequired = "Nazwa zawodu jest wymagana."
messageCodeIsRequired = "Oznaczenie kwalifikacji jest wymagane."
messageNameIsTooLong = "Nazwa zawodu może się składać z maksymalnie %d znaków."
)

View File

@ -0,0 +1,139 @@
package usecase
import (
"context"
"fmt"
"strings"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
"github.com/zdam-egzamin-zawodowy/backend/internal/qualification"
sqlutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/sql"
)
type usecase struct {
qualificationRepository qualification.Repository
}
type Config struct {
QualificationRepository qualification.Repository
}
func New(cfg *Config) (qualification.Usecase, error) {
if cfg == nil || cfg.QualificationRepository == nil {
return nil, fmt.Errorf("qualification/usecase: QualificationRepository is required")
}
return &usecase{
cfg.QualificationRepository,
}, nil
}
func (ucase *usecase) Store(ctx context.Context, input *models.QualificationInput) (*models.Qualification, error) {
if err := ucase.validateInput(input, validateOptions{false}); err != nil {
return nil, err
}
return ucase.qualificationRepository.Store(ctx, input)
}
func (ucase *usecase) UpdateOne(ctx context.Context, id int, input *models.QualificationInput) (*models.Qualification, error) {
if id <= 0 {
return nil, fmt.Errorf(messageInvalidID)
}
if err := ucase.validateInput(input, validateOptions{true}); err != nil {
return nil, err
}
items, err := ucase.qualificationRepository.UpdateMany(ctx,
&models.QualificationFilter{
ID: []int{id},
},
input)
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, fmt.Errorf(messageItemNotFound)
}
return items[0], nil
}
func (ucase *usecase) Delete(ctx context.Context, f *models.QualificationFilter) ([]*models.Qualification, error) {
return ucase.qualificationRepository.Delete(ctx, f)
}
func (ucase *usecase) Fetch(ctx context.Context, cfg *qualification.FetchConfig) ([]*models.Qualification, int, error) {
if cfg == nil {
cfg = &qualification.FetchConfig{
Limit: qualification.DefaultLimit,
Count: true,
}
}
cfg.Sort = sqlutils.SanitizeSortExpressions(cfg.Sort)
return ucase.qualificationRepository.Fetch(ctx, cfg)
}
func (ucase *usecase) GetByID(ctx context.Context, id int) (*models.Qualification, error) {
items, _, err := ucase.Fetch(ctx, &qualification.FetchConfig{
Limit: 1,
Count: false,
Filter: &models.QualificationFilter{
ID: []int{id},
},
})
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, fmt.Errorf(messageItemNotFound)
}
return items[0], nil
}
func (ucase *usecase) GetBySlug(ctx context.Context, slug string) (*models.Qualification, error) {
items, _, err := ucase.Fetch(ctx, &qualification.FetchConfig{
Limit: 1,
Count: false,
Filter: &models.QualificationFilter{
Slug: []string{slug},
},
})
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, fmt.Errorf(messageItemNotFound)
}
return items[0], nil
}
type validateOptions struct {
allowNilValues bool
}
func (ucase *usecase) validateInput(input *models.QualificationInput, opts validateOptions) error {
if input.IsEmpty() {
return fmt.Errorf(messageEmptyPayload)
}
if input.Name != nil {
trimmedName := strings.TrimSpace(*input.Name)
input.Name = &trimmedName
if trimmedName == "" {
return fmt.Errorf(messageNameIsRequired)
} else if len(trimmedName) > qualification.MaxNameLength {
return fmt.Errorf(messageNameIsTooLong, qualification.MaxNameLength)
}
} else if !opts.allowNilValues {
return fmt.Errorf(messageNameIsRequired)
}
if input.Code != nil {
trimmedCode := strings.TrimSpace(*input.Code)
input.Code = &trimmedCode
if trimmedCode == "" {
return fmt.Errorf(messageCodeIsRequired)
}
} else if !opts.allowNilValues {
return fmt.Errorf(messageCodeIsRequired)
}
return nil
}