From 8fae4a2b0392f52b88abfcd1c9b034913fe4c7d0 Mon Sep 17 00:00:00 2001 From: Kichiyaki Date: Sat, 27 Feb 2021 14:46:00 +0100 Subject: [PATCH] add qualification repository and usecase --- internal/db/db.go | 2 +- internal/models/profession.go | 2 +- internal/models/qualification.go | 27 +++- internal/profession/usecase/message.go | 2 +- internal/qualification/constants.go | 6 + internal/qualification/repository.go | 22 +++ internal/qualification/repository/message.go | 9 ++ .../qualification/repository/pg_repository.go | 152 ++++++++++++++++++ internal/qualification/usecase.go | 16 ++ internal/qualification/usecase/message.go | 10 ++ internal/qualification/usecase/usecase.go | 139 ++++++++++++++++ 11 files changed, 382 insertions(+), 5 deletions(-) create mode 100644 internal/qualification/constants.go create mode 100644 internal/qualification/repository.go create mode 100644 internal/qualification/repository/message.go create mode 100644 internal/qualification/repository/pg_repository.go create mode 100644 internal/qualification/usecase.go create mode 100644 internal/qualification/usecase/message.go create mode 100644 internal/qualification/usecase/usecase.go diff --git a/internal/db/db.go b/internal/db/db.go index 2dacc25..5a73938 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -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") } diff --git a/internal/models/profession.go b/internal/models/profession.go index c2afc6a..119e6e0 100644 --- a/internal/models/profession.go +++ b/internal/models/profession.go @@ -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"` } diff --git a/internal/models/qualification.go b/internal/models/qualification.go index 2ecfa90..ae5ce20 100644 --- a/internal/models/qualification.go +++ b/internal/models/qualification.go @@ -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"` diff --git a/internal/profession/usecase/message.go b/internal/profession/usecase/message.go index aced53e..5932601 100644 --- a/internal/profession/usecase/message.go +++ b/internal/profession/usecase/message.go @@ -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." ) diff --git a/internal/qualification/constants.go b/internal/qualification/constants.go new file mode 100644 index 0000000..51d9e5c --- /dev/null +++ b/internal/qualification/constants.go @@ -0,0 +1,6 @@ +package qualification + +const ( + DefaultLimit = 100 + MaxNameLength = 100 +) diff --git a/internal/qualification/repository.go b/internal/qualification/repository.go new file mode 100644 index 0000000..be19534 --- /dev/null +++ b/internal/qualification/repository.go @@ -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) +} diff --git a/internal/qualification/repository/message.go b/internal/qualification/repository/message.go new file mode 100644 index 0000000..cd7fe11 --- /dev/null +++ b/internal/qualification/repository/message.go @@ -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." +) diff --git a/internal/qualification/repository/pg_repository.go b/internal/qualification/repository/pg_repository.go new file mode 100644 index 0000000..0dc0171 --- /dev/null +++ b/internal/qualification/repository/pg_repository.go @@ -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 +} diff --git a/internal/qualification/usecase.go b/internal/qualification/usecase.go new file mode 100644 index 0000000..8c7dedc --- /dev/null +++ b/internal/qualification/usecase.go @@ -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) +} diff --git a/internal/qualification/usecase/message.go b/internal/qualification/usecase/message.go new file mode 100644 index 0000000..c4d9a7a --- /dev/null +++ b/internal/qualification/usecase/message.go @@ -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." +) diff --git a/internal/qualification/usecase/usecase.go b/internal/qualification/usecase/usecase.go new file mode 100644 index 0000000..312c34d --- /dev/null +++ b/internal/qualification/usecase/usecase.go @@ -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 +}