Merge pull request #3 from zdam-egzamin-zawodowy/usecases-and-repositories
add usecases and repositories
This commit is contained in:
commit
acbc4defe2
3
go.mod
3
go.mod
|
@ -4,12 +4,15 @@ go 1.16
|
|||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.13.0
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/gin-contrib/cors v1.3.1
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/go-pg/pg/v10 v10.7.7
|
||||
github.com/google/uuid v1.2.0
|
||||
github.com/gosimple/slug v1.9.0
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sethvargo/go-password v0.2.0
|
||||
github.com/sirupsen/logrus v1.8.0
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
|
||||
)
|
||||
|
|
6
go.sum
6
go.sum
|
@ -13,6 +13,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
|
@ -64,6 +66,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
|
@ -125,6 +129,8 @@ github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
|||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
|
||||
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Credentials struct {
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
StaySignedIn bool
|
||||
Credentials Credentials
|
||||
}
|
||||
|
||||
func (opts Metadata) ToMapClaims() jwt.MapClaims {
|
||||
mClaims := jwt.MapClaims{}
|
||||
mClaims["email"] = opts.Credentials.Email
|
||||
mClaims["password"] = opts.Credentials.Password
|
||||
mClaims["stay_signed_in"] = opts.StaySignedIn
|
||||
return mClaims
|
||||
}
|
||||
|
||||
type TokenGenerator interface {
|
||||
Generate(metadata Metadata) (string, error)
|
||||
ExtractAccessTokenMetadata(token string) (*Metadata, error)
|
||||
}
|
||||
|
||||
type tokenGenerator struct {
|
||||
accessSecret string
|
||||
}
|
||||
|
||||
func NewTokenGenerator(accessSecret string) TokenGenerator {
|
||||
return &tokenGenerator{
|
||||
accessSecret: accessSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *tokenGenerator) Generate(metadata Metadata) (string, error) {
|
||||
atClaims := metadata.ToMapClaims()
|
||||
if !metadata.StaySignedIn {
|
||||
atClaims["exp"] = time.Now().Add(time.Hour * 24).Unix()
|
||||
}
|
||||
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
|
||||
accessToken, err := at.SignedString([]byte(g.accessSecret))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "TokenGenerator.Generate")
|
||||
}
|
||||
|
||||
return accessToken, nil
|
||||
}
|
||||
|
||||
func (g *tokenGenerator) ExtractAccessTokenMetadata(token string) (*Metadata, error) {
|
||||
return extractTokenMetadata(g.accessSecret, token)
|
||||
}
|
||||
|
||||
func verifyToken(secret string, tokenString string) (*jwt.Token, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
//Make sure that the token method conform to "SigningMethodHMAC"
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "verifyToken")
|
||||
}
|
||||
if !token.Valid {
|
||||
return nil, fmt.Errorf("Token is invalid")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func extractTokenMetadata(secret, tokenString string) (*Metadata, error) {
|
||||
token, err := verifyToken(secret, tokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if ok {
|
||||
staySignedIn, ok := claims["stay_signed_in"].(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Invalid token payload (staySignedIn should be a boolean)")
|
||||
}
|
||||
email, ok := claims["email"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Invalid token payload (email should be a string)")
|
||||
}
|
||||
password, ok := claims["password"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Invalid token payload (password should be a string)")
|
||||
}
|
||||
return &Metadata{
|
||||
StaySignedIn: staySignedIn,
|
||||
Credentials: Credentials{
|
||||
Email: email,
|
||||
Password: password,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Cannot extract token metadata")
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
)
|
||||
|
||||
type Usecase interface {
|
||||
SignIn(ctx context.Context, email, password string, staySignedIn bool) (*models.User, string, error)
|
||||
ExtractAccessTokenMetadata(ctx context.Context, accessToken string) (*models.User, error)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package usecase
|
||||
|
||||
const (
|
||||
messageInvalidCredentials = "Niepoprawny email/hasło."
|
||||
messageInvalidAccessToken = "Niepoprawny token."
|
||||
)
|
|
@ -0,0 +1,84 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/auth"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/auth/jwt"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/user"
|
||||
errorutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/error"
|
||||
)
|
||||
|
||||
type usecase struct {
|
||||
userRepository user.Repository
|
||||
tokenGenerator jwt.TokenGenerator
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
UserRepository user.Repository
|
||||
TokenGenerator jwt.TokenGenerator
|
||||
}
|
||||
|
||||
func New(cfg *Config) (auth.Usecase, error) {
|
||||
if cfg == nil || cfg.UserRepository == nil {
|
||||
return nil, fmt.Errorf("user/usecase: UserRepository is required")
|
||||
}
|
||||
return &usecase{
|
||||
cfg.UserRepository,
|
||||
cfg.TokenGenerator,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ucase *usecase) SignIn(ctx context.Context, email, password string, staySignedIn bool) (*models.User, string, error) {
|
||||
user, err := ucase.GetUserByCredentials(ctx, email, password)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
token, err := ucase.tokenGenerator.Generate(jwt.Metadata{
|
||||
StaySignedIn: staySignedIn,
|
||||
Credentials: jwt.Credentials{
|
||||
Email: user.Email,
|
||||
Password: user.Password,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", errorutils.Wrap(err, messageInvalidCredentials)
|
||||
}
|
||||
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func (ucase *usecase) ExtractAccessTokenMetadata(ctx context.Context, accessToken string) (*models.User, error) {
|
||||
metadata, err := ucase.tokenGenerator.ExtractAccessTokenMetadata(accessToken)
|
||||
if err != nil {
|
||||
return nil, errorutils.Wrap(err, messageInvalidAccessToken)
|
||||
}
|
||||
|
||||
return ucase.GetUserByCredentials(ctx, metadata.Credentials.Email, metadata.Credentials.Password)
|
||||
}
|
||||
|
||||
func (ucase *usecase) GetUserByCredentials(ctx context.Context, email, password string) (*models.User, error) {
|
||||
users, _, err := ucase.userRepository.Fetch(ctx, &user.FetchConfig{
|
||||
Limit: 1,
|
||||
Count: false,
|
||||
Filter: &models.UserFilter{
|
||||
Email: []string{email},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(users) <= 0 {
|
||||
return nil, fmt.Errorf(messageInvalidCredentials)
|
||||
}
|
||||
|
||||
user := users[0]
|
||||
if err := user.CompareHashAndPassword(password); err != nil {
|
||||
return nil, fmt.Errorf(messageInvalidCredentials)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
|
@ -10,37 +10,35 @@ import (
|
|||
"github.com/go-pg/pg/v10"
|
||||
"github.com/go-pg/pg/v10/orm"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
extensions = `
|
||||
CREATE EXTENSION IF NOT EXISTS tsm_system_rows;
|
||||
`
|
||||
)
|
||||
|
||||
func init() {
|
||||
orm.RegisterTable((*models.QualificationToProfession)(nil))
|
||||
}
|
||||
var log = logrus.WithField("package", "internal/db")
|
||||
|
||||
type Config struct {
|
||||
DebugHook bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterTable((*models.QualificationToProfession)(nil))
|
||||
}
|
||||
|
||||
func New(cfg *Config) (*pg.DB, error) {
|
||||
db := pg.Connect(prepareOptions())
|
||||
if err := createSchema(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg != nil {
|
||||
if cfg.DebugHook {
|
||||
db.AddQueryHook(DebugHook{
|
||||
Entry: logrus.WithField("package", "internal/db"),
|
||||
Entry: log,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := createSchema(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
|
@ -56,11 +54,7 @@ func prepareOptions() *pg.Options {
|
|||
|
||||
func createSchema(db *pg.DB) error {
|
||||
return db.RunInTransaction(context.Background(), func(tx *pg.Tx) error {
|
||||
if _, err := tx.Exec(extensions); err != nil {
|
||||
return errors.Wrap(err, "createSchema")
|
||||
}
|
||||
|
||||
models := []interface{}{
|
||||
modelsToCreate := []interface{}{
|
||||
(*models.User)(nil),
|
||||
(*models.Profession)(nil),
|
||||
(*models.Qualification)(nil),
|
||||
|
@ -68,7 +62,7 @@ func createSchema(db *pg.DB) error {
|
|||
(*models.Question)(nil),
|
||||
}
|
||||
|
||||
for _, model := range models {
|
||||
for _, model := range modelsToCreate {
|
||||
err := tx.Model(model).CreateTable(&orm.CreateTableOptions{
|
||||
IfNotExists: true,
|
||||
FKConstraints: true,
|
||||
|
@ -78,6 +72,35 @@ func createSchema(db *pg.DB) error {
|
|||
}
|
||||
}
|
||||
|
||||
total, err := tx.Model(modelsToCreate[0]).Where("role = ?", models.RoleAdmin).Count()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "createSchema")
|
||||
}
|
||||
if total == 0 {
|
||||
activated := true
|
||||
pswd, err := password.Generate(15, 4, 2, true, false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "createSchema")
|
||||
}
|
||||
email := "admin@admin.com"
|
||||
_, err = tx.
|
||||
Model(&models.User{
|
||||
DisplayName: "admin",
|
||||
Email: email,
|
||||
Role: models.RoleAdmin,
|
||||
Activated: &activated,
|
||||
Password: pswd,
|
||||
}).
|
||||
Insert()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "createSchema")
|
||||
}
|
||||
log.
|
||||
WithField("email", email).
|
||||
WithField("password", pswd).
|
||||
Info("Admin account has been created")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package db
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -22,7 +23,7 @@ func (logger DebugHook) BeforeQuery(ctx context.Context, evt *pg.QueryEvent) (co
|
|||
if evt.Err != nil {
|
||||
logger.Entry.Errorf("%s executing a query:\n%s\n", evt.Err, q)
|
||||
} else {
|
||||
logger.Entry.Info(string(q))
|
||||
logger.Entry.Info(strings.TrimSpace(string(q)))
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-pg/pg/v10/orm"
|
||||
)
|
||||
|
||||
type Sort struct {
|
||||
Relationships map[string]string
|
||||
Orders []string
|
||||
}
|
||||
|
||||
func (s Sort) Apply(q *orm.Query) (*orm.Query, error) {
|
||||
for _, order := range s.Orders {
|
||||
if alias := s.extractAlias(order); alias != "" && s.Relationships[alias] != "" {
|
||||
q = q.Relation(s.Relationships[alias])
|
||||
}
|
||||
}
|
||||
return q.Order(s.Orders...), nil
|
||||
}
|
||||
|
||||
func (s Sort) extractAlias(order string) string {
|
||||
if strings.Contains(order, ".") {
|
||||
return strings.Split(order, ".")[0]
|
||||
}
|
||||
return ""
|
||||
}
|
|
@ -2,6 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
|
@ -16,7 +17,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"`
|
||||
}
|
||||
|
||||
|
@ -40,6 +41,23 @@ type ProfessionInput struct {
|
|||
Description *string `json:"description,omitempty" xml:"description" gqlgen:"description"`
|
||||
}
|
||||
|
||||
func (input *ProfessionInput) IsEmpty() bool {
|
||||
return input == nil && input.Name == nil && input.Description == nil
|
||||
}
|
||||
|
||||
func (input *ProfessionInput) Sanitize() *ProfessionInput {
|
||||
if input.Name != nil {
|
||||
trimmed := strings.TrimSpace(*input.Name)
|
||||
input.Name = &trimmed
|
||||
}
|
||||
if input.Description != nil {
|
||||
trimmed := strings.TrimSpace(*input.Description)
|
||||
input.Description = &trimmed
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
func (input *ProfessionInput) ToProfession() *Profession {
|
||||
p := &Profession{}
|
||||
if input.Name != nil {
|
||||
|
@ -51,6 +69,19 @@ func (input *ProfessionInput) ToProfession() *Profession {
|
|||
return p
|
||||
}
|
||||
|
||||
func (input *ProfessionInput) 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 ProfessionFilter struct {
|
||||
ID []int `gqlgen:"id" json:"id" xml:"id"`
|
||||
IDNEQ []int `gqlgen:"idNEQ" json:"idNEQ" xml:"idNEQ"`
|
||||
|
@ -74,6 +105,10 @@ type ProfessionFilter struct {
|
|||
}
|
||||
|
||||
func (f *ProfessionFilter) WhereWithAlias(q *orm.Query, alias string) (*orm.Query, error) {
|
||||
if f == nil {
|
||||
return q, nil
|
||||
}
|
||||
|
||||
if !isZero(f.ID) {
|
||||
q = q.Where(sqlutils.BuildConditionArray(sqlutils.AddAliasToColumnName("id", alias)), pg.Array(f.ID))
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
|
@ -38,9 +39,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 +54,37 @@ 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) Sanitize() *QualificationInput {
|
||||
if input.Name != nil {
|
||||
trimmed := strings.TrimSpace(*input.Name)
|
||||
input.Name = &trimmed
|
||||
}
|
||||
if input.Description != nil {
|
||||
trimmed := strings.TrimSpace(*input.Description)
|
||||
input.Description = &trimmed
|
||||
}
|
||||
if input.Code != nil {
|
||||
trimmed := strings.TrimSpace(*input.Code)
|
||||
input.Code = &trimmed
|
||||
}
|
||||
if input.Formula != nil {
|
||||
trimmed := strings.TrimSpace(*input.Formula)
|
||||
input.Formula = &trimmed
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
func (input *QualificationInput) ToQualification() *Qualification {
|
||||
q := &Qualification{}
|
||||
if input.Name != nil {
|
||||
|
@ -70,6 +102,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"`
|
||||
|
@ -79,6 +124,10 @@ type QualificationFilterOr struct {
|
|||
}
|
||||
|
||||
func (f *QualificationFilterOr) WhereWithAlias(q *orm.Query, alias string) *orm.Query {
|
||||
if f == nil {
|
||||
return q
|
||||
}
|
||||
|
||||
q = q.WhereGroup(func(q *orm.Query) (*orm.Query, error) {
|
||||
if !isZero(f.NameMATCH) {
|
||||
q = q.Where(sqlutils.BuildConditionMatch(sqlutils.AddAliasToColumnName("name", alias)), f.NameMATCH)
|
||||
|
@ -134,6 +183,10 @@ type QualificationFilter struct {
|
|||
}
|
||||
|
||||
func (f *QualificationFilter) WhereWithAlias(q *orm.Query, alias string) (*orm.Query, error) {
|
||||
if f == nil {
|
||||
return q, nil
|
||||
}
|
||||
|
||||
if !isZero(f.ID) {
|
||||
q = q.Where(sqlutils.BuildConditionArray(sqlutils.AddAliasToColumnName("id", alias)), pg.Array(f.ID))
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
|
@ -30,33 +31,71 @@ type Question struct {
|
|||
QualificationID int `pg:",unique:group_1,on_delete:CASCADE" json:"qualificationID" xml:"qualificationID" gqlgen:"qualificationID"`
|
||||
Qualification *Qualification `pg:"rel:has-one" json:"qualification" xml:"qualification" gqlgen:"qualification"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty" pg:"default:now()" xml:"createdAt" gqlgen:"createdAt"`
|
||||
UpdatedAt time.Time `pg:"default:now()" json:"updatedAt" xml:"updatedAt" gqlgen:"updatedAt"`
|
||||
}
|
||||
|
||||
func (q *Question) BeforeInsert(ctx context.Context) (context.Context, error) {
|
||||
q.CreatedAt = time.Now()
|
||||
q.UpdatedAt = time.Now()
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
type QuestionInput struct {
|
||||
Content *string `json:"content" xml:"content" gqlgen:"content"`
|
||||
From *string `json:"from" xml:"from" gqlgen:"from"`
|
||||
Explanation *string `json:"explanation" xml:"explanation" gqlgen:"explanation"`
|
||||
CorrectAnswer *Answer `json:"correctAnswer" xml:"correctAnswer" gqlgen:"correctAnswer"`
|
||||
AnswerA *Answer `gqlgen:"answerA" json:"answerA" xml:"answerA"`
|
||||
AnswerB *Answer `gqlgen:"answerB" json:"answerB" xml:"answerB"`
|
||||
AnswerC *Answer `gqlgen:"answerC" json:"answerC" xml:"answerC"`
|
||||
AnswerD *Answer `gqlgen:"answerD" json:"answerD" xml:"answerD"`
|
||||
QualificationID *int `gqlgen:"qualificationID" json:"qualificationID" xml:"qualificationID"`
|
||||
Image *graphql.Upload `json:"image" xml:"image" gqlgen:"image"`
|
||||
AnswerAImage *graphql.Upload `json:"answerAImage" gqlgen:"answerAImage" xml:"answerAImage"`
|
||||
AnswerAImagePath *string `json:"answerAImagePath" gqlgen:"answerAImagePath" xml:"answerAImagePath"`
|
||||
AnswerBImage *graphql.Upload `json:"answerBImage" gqlgen:"answerBImage" xml:"answerBImage"`
|
||||
AnswerBImagePath *string `json:"answerBImagePath" gqlgen:"answerBImagePath" xml:"answerBImagePath"`
|
||||
AnswerCImage *graphql.Upload `json:"answerCImage" gqlgen:"answerCImage" xml:"answerCImage"`
|
||||
AnswerCImagePath *string `json:"answerCImagePath" gqlgen:"answerCImagePath" xml:"answerCImagePath"`
|
||||
AnswerDImage *graphql.Upload `json:"answerDImage" gqlgen:"answerDImage" xml:"answerDImage"`
|
||||
AnswerDImagePath *string `json:"answerDImagePath" gqlgen:"answerDImagePath" xml:"answerDImagePath"`
|
||||
Content *string `json:"content" xml:"content" gqlgen:"content"`
|
||||
From *string `json:"from" xml:"from" gqlgen:"from"`
|
||||
Explanation *string `json:"explanation" xml:"explanation" gqlgen:"explanation"`
|
||||
CorrectAnswer *Answer `json:"correctAnswer" xml:"correctAnswer" gqlgen:"correctAnswer"`
|
||||
AnswerA *Answer `gqlgen:"answerA" json:"answerA" xml:"answerA"`
|
||||
AnswerB *Answer `gqlgen:"answerB" json:"answerB" xml:"answerB"`
|
||||
AnswerC *Answer `gqlgen:"answerC" json:"answerC" xml:"answerC"`
|
||||
AnswerD *Answer `gqlgen:"answerD" json:"answerD" xml:"answerD"`
|
||||
QualificationID *int `gqlgen:"qualificationID" json:"qualificationID" xml:"qualificationID"`
|
||||
Image *graphql.Upload `json:"image" xml:"image" gqlgen:"image"`
|
||||
DeleteImage *bool `json:"deleteImage" xml:"deleteImage" gqlgen:"deleteImage"`
|
||||
AnswerAImage *graphql.Upload `json:"answerAImage" gqlgen:"answerAImage" xml:"answerAImage"`
|
||||
DeleteAnswerAImage *bool `json:"deleteAnswerAImage" xml:"deleteAnswerAImage" gqlgen:"deleteAnswerAImage"`
|
||||
AnswerBImage *graphql.Upload `json:"answerBImage" gqlgen:"answerBImage" xml:"answerBImage"`
|
||||
DeleteAnswerBImage *bool `json:"deleteAnswerBImage" xml:"deleteAnswerBImage" gqlgen:"deleteAnswerBImage"`
|
||||
AnswerCImage *graphql.Upload `json:"answerCImage" gqlgen:"answerCImage" xml:"answerCImage"`
|
||||
DeleteAnswerCImage *bool `json:"deleteAnswerCImage" xml:"deleteAnswerCImage" gqlgen:"deleteAnswerCImage"`
|
||||
AnswerDImage *graphql.Upload `json:"answerDImage" gqlgen:"answerDImage" xml:"answerDImage"`
|
||||
DeleteAnswerDImage *bool `json:"deleteAnswerDImage" xml:"deleteAnswerDImage" gqlgen:"deleteAnswerDImage"`
|
||||
}
|
||||
|
||||
func (input *QuestionInput) IsEmpty() bool {
|
||||
return input == nil &&
|
||||
input.Content == nil &&
|
||||
input.From == nil &&
|
||||
input.Explanation == nil &&
|
||||
input.CorrectAnswer == nil &&
|
||||
input.AnswerA == nil &&
|
||||
input.AnswerAImage == nil &&
|
||||
input.AnswerB == nil &&
|
||||
input.AnswerBImage == nil &&
|
||||
input.AnswerC == nil &&
|
||||
input.AnswerCImage == nil &&
|
||||
input.AnswerD == nil &&
|
||||
input.AnswerDImage == nil &&
|
||||
input.Image == nil &&
|
||||
input.QualificationID == nil
|
||||
}
|
||||
|
||||
func (input *QuestionInput) Sanitize() *QuestionInput {
|
||||
if input.Content != nil {
|
||||
trimmed := strings.TrimSpace(*input.Content)
|
||||
input.Content = &trimmed
|
||||
}
|
||||
if input.From != nil {
|
||||
trimmed := strings.TrimSpace(*input.From)
|
||||
input.From = &trimmed
|
||||
}
|
||||
if input.Explanation != nil {
|
||||
trimmed := strings.TrimSpace(*input.Explanation)
|
||||
input.Explanation = &trimmed
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
func (input *QuestionInput) ToQuestion() *Question {
|
||||
|
@ -76,33 +115,55 @@ func (input *QuestionInput) ToQuestion() *Question {
|
|||
if input.AnswerA != nil {
|
||||
q.AnswerA = *input.AnswerA
|
||||
}
|
||||
if input.AnswerAImagePath != nil {
|
||||
q.AnswerAImage = *input.AnswerAImagePath
|
||||
}
|
||||
if input.AnswerB != nil {
|
||||
q.AnswerB = *input.AnswerB
|
||||
}
|
||||
if input.AnswerBImagePath != nil {
|
||||
q.AnswerBImage = *input.AnswerBImagePath
|
||||
}
|
||||
if input.AnswerC != nil {
|
||||
q.AnswerC = *input.AnswerC
|
||||
}
|
||||
if input.AnswerCImagePath != nil {
|
||||
q.AnswerCImage = *input.AnswerCImagePath
|
||||
}
|
||||
if input.AnswerD != nil {
|
||||
q.AnswerD = *input.AnswerD
|
||||
}
|
||||
if input.AnswerDImagePath != nil {
|
||||
q.AnswerDImage = *input.AnswerDImagePath
|
||||
}
|
||||
if input.QualificationID != nil {
|
||||
q.QualificationID = *input.QualificationID
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func (input *QuestionInput) ApplyUpdate(q *orm.Query) (*orm.Query, error) {
|
||||
if !input.IsEmpty() {
|
||||
if input.Content != nil {
|
||||
q.Set("content = ?", *input.Content)
|
||||
}
|
||||
if input.From != nil {
|
||||
q.Set("from = ?", *input.From)
|
||||
}
|
||||
if input.Explanation != nil {
|
||||
q.Set("explanation = ?", *input.Explanation)
|
||||
}
|
||||
if input.CorrectAnswer != nil {
|
||||
q.Set("correct_answer = ?", *input.CorrectAnswer)
|
||||
}
|
||||
if input.AnswerA != nil {
|
||||
q.Set("answer_a = ?", *input.AnswerA)
|
||||
}
|
||||
if input.AnswerB != nil {
|
||||
q.Set("answer_b = ?", *input.AnswerB)
|
||||
}
|
||||
if input.AnswerC != nil {
|
||||
q.Set("answer_c = ?", *input.AnswerC)
|
||||
}
|
||||
if input.AnswerD != nil {
|
||||
q.Set("answer_d = ?", *input.AnswerD)
|
||||
}
|
||||
if input.QualificationID != nil {
|
||||
q.Set("qualification_id = ?", *input.QualificationID)
|
||||
}
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
|
||||
type QuestionFilter struct {
|
||||
ID []int `gqlgen:"id" json:"id" xml:"id"`
|
||||
IDNEQ []int `gqlgen:"idNEQ" json:"idNEQ" xml:"idNEQ"`
|
||||
|
@ -124,6 +185,10 @@ type QuestionFilter struct {
|
|||
}
|
||||
|
||||
func (f *QuestionFilter) WhereWithAlias(q *orm.Query, alias string) (*orm.Query, error) {
|
||||
if f == nil {
|
||||
return q, nil
|
||||
}
|
||||
|
||||
if !isZero(f.ID) {
|
||||
q = q.Where(sqlutils.BuildConditionArray(sqlutils.AddAliasToColumnName("id", alias)), pg.Array(f.ID))
|
||||
}
|
||||
|
|
|
@ -2,13 +2,13 @@ package models
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sqlutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/sql"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/go-pg/pg/v10/orm"
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
@ -17,8 +17,7 @@ type User struct {
|
|||
tableName struct{} `pg:"alias:user"`
|
||||
|
||||
ID int `json:"id" pg:",pk" xml:"id" gqlgen:"id"`
|
||||
Slug string `json:"slug" pg:",unique" xml:"slug" gqlgen:"slug"`
|
||||
DisplayName string `json:"displayName" pg:",use_zero" xml:"displayName" gqlgen:"displayName"`
|
||||
DisplayName string `json:"displayName" pg:",use_zero,notnull" xml:"displayName" gqlgen:"displayName"`
|
||||
Password string `json:"-" gqlgen:"-" xml:"password"`
|
||||
Email string `json:"email" pg:",unique" xml:"email" gqlgen:"email"`
|
||||
CreatedAt time.Time `json:"createdAt" pg:"default:now()" xml:"createdAt" gqlgen:"createdAt"`
|
||||
|
@ -38,19 +37,6 @@ func (u *User) BeforeInsert(ctx context.Context) (context.Context, error) {
|
|||
return ctx, err
|
||||
}
|
||||
u.Password = string(hashedPassword)
|
||||
u.Slug = slug.Make(u.DisplayName)
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (u *User) BeforeUpdate(ctx context.Context) (context.Context, error) {
|
||||
if cost, _ := bcrypt.Cost([]byte(u.Password)); u.Password != "" && cost == 0 {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
u.Password = string(hashedPassword)
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
@ -73,6 +59,15 @@ type UserInput struct {
|
|||
Activated *bool `json:"activated" xml:"activated" gqlgen:"activated"`
|
||||
}
|
||||
|
||||
func (input *UserInput) IsEmpty() bool {
|
||||
return input == nil &&
|
||||
input.DisplayName == nil &&
|
||||
input.Password == nil &&
|
||||
input.Email == nil &&
|
||||
input.Role == nil &&
|
||||
input.Activated == nil
|
||||
}
|
||||
|
||||
func (input *UserInput) ToUser() *User {
|
||||
u := &User{
|
||||
Activated: input.Activated,
|
||||
|
@ -92,13 +87,59 @@ func (input *UserInput) ToUser() *User {
|
|||
return u
|
||||
}
|
||||
|
||||
func (input *UserInput) Sanitize() *UserInput {
|
||||
if input.DisplayName != nil {
|
||||
trimmed := strings.TrimSpace(*input.DisplayName)
|
||||
input.DisplayName = &trimmed
|
||||
}
|
||||
|
||||
if input.Password != nil {
|
||||
sanitized := strings.ToLower(strings.TrimSpace(*input.Password))
|
||||
input.Password = &sanitized
|
||||
}
|
||||
|
||||
if input.Email != nil {
|
||||
sanitized := strings.ToLower(strings.TrimSpace(*input.Email))
|
||||
input.Email = &sanitized
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
func (input *UserInput) ApplyUpdate(q *orm.Query) (*orm.Query, error) {
|
||||
if !input.IsEmpty() {
|
||||
if input.DisplayName != nil {
|
||||
q.Set("display_name = ?", *input.DisplayName)
|
||||
}
|
||||
|
||||
if input.Password != nil {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*input.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return q, err
|
||||
}
|
||||
q.Set("password = ?", string(hashedPassword))
|
||||
}
|
||||
|
||||
if input.Email != nil {
|
||||
q.Set("name = ?", *input.Email)
|
||||
}
|
||||
|
||||
if input.Role != nil {
|
||||
q.Set("role = ?", *input.Role)
|
||||
}
|
||||
|
||||
if input.Activated != nil {
|
||||
q.Set("activated = ?", *input.Activated)
|
||||
}
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
|
||||
type UserFilter struct {
|
||||
ID []int `json:"id" xml:"id" gqlgen:"id"`
|
||||
IDNEQ []int `json:"idNEQ" xml:"idNEQ" gqlgen:"idNEQ"`
|
||||
|
||||
Slug []string `json:"slug" xml:"slug" gqlgen:"slug"`
|
||||
SlugNEQ []string `json:"slugNEQ" xml:"slugNEQ" gqlgen:"slugNEQ"`
|
||||
|
||||
Activated *bool `json:"activated" xml:"activated" gqlgen:"activated"`
|
||||
|
||||
DisplayName []string `json:"displayName" xml:"displayName" gqlgen:"displayName"`
|
||||
|
@ -122,6 +163,10 @@ type UserFilter struct {
|
|||
}
|
||||
|
||||
func (f *UserFilter) WhereWithAlias(q *orm.Query, alias string) (*orm.Query, error) {
|
||||
if f == nil {
|
||||
return q, nil
|
||||
}
|
||||
|
||||
if !isZero(f.ID) {
|
||||
q = q.Where(sqlutils.BuildConditionArray(sqlutils.AddAliasToColumnName("id", alias)), pg.Array(f.ID))
|
||||
}
|
||||
|
@ -129,13 +174,6 @@ func (f *UserFilter) WhereWithAlias(q *orm.Query, alias string) (*orm.Query, err
|
|||
q = q.Where(sqlutils.BuildConditionNotInArray(sqlutils.AddAliasToColumnName("id", alias)), pg.Array(f.IDNEQ))
|
||||
}
|
||||
|
||||
if !isZero(f.Slug) {
|
||||
q = q.Where(sqlutils.BuildConditionArray(sqlutils.AddAliasToColumnName("slug", alias)), pg.Array(f.Slug))
|
||||
}
|
||||
if !isZero(f.SlugNEQ) {
|
||||
q = q.Where(sqlutils.BuildConditionNotInArray(sqlutils.AddAliasToColumnName("slug", alias)), pg.Array(f.SlugNEQ))
|
||||
}
|
||||
|
||||
if !isZero(f.Activated) {
|
||||
q = q.Where(sqlutils.BuildConditionEquals(sqlutils.AddAliasToColumnName("activated", alias)), f.Activated)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package profession
|
||||
|
||||
const (
|
||||
DefaultLimit = 100
|
||||
MaxNameLength = 100
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
package profession
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
)
|
||||
|
||||
type FetchConfig struct {
|
||||
Filter *models.ProfessionFilter
|
||||
Offset int
|
||||
Limit int
|
||||
Sort []string
|
||||
Count bool
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
Store(ctx context.Context, input *models.ProfessionInput) (*models.Profession, error)
|
||||
UpdateMany(ctx context.Context, f *models.ProfessionFilter, input *models.ProfessionInput) ([]*models.Profession, error)
|
||||
Delete(ctx context.Context, f *models.ProfessionFilter) ([]*models.Profession, error)
|
||||
Fetch(ctx context.Context, cfg *FetchConfig) ([]*models.Profession, int, error)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package repository
|
||||
|
||||
const (
|
||||
messageNameIsAlreadyTaken = "Istnieje już zawód o podanej nazwie."
|
||||
messageFailedToSaveModel = "Wystąpił błąd podczas zapisywania zawodu, prosimy spróbować później."
|
||||
messageFailedToDeleteModel = "Wystąpił błąd podczas usuwania zawodu, prosimy spróbować później."
|
||||
messageFailedToFetchModel = "Wystąpił błąd podczas pobierania zawodów, prosimy spróbować później."
|
||||
)
|
|
@ -0,0 +1,97 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
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/profession"
|
||||
)
|
||||
|
||||
type pgRepository struct {
|
||||
*pg.DB
|
||||
}
|
||||
|
||||
type PGRepositoryConfig struct {
|
||||
DB *pg.DB
|
||||
}
|
||||
|
||||
func NewPGRepository(cfg *PGRepositoryConfig) (profession.Repository, error) {
|
||||
if cfg == nil || cfg.DB == nil {
|
||||
return nil, fmt.Errorf("profession/pg_repository: *pg.DB is required")
|
||||
}
|
||||
return &pgRepository{
|
||||
cfg.DB,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *pgRepository) Store(ctx context.Context, input *models.ProfessionInput) (*models.Profession, error) {
|
||||
item := input.ToProfession()
|
||||
if _, err := repo.
|
||||
Model(item).
|
||||
Context(ctx).
|
||||
Returning("*").
|
||||
Insert(); err != nil {
|
||||
if strings.Contains(err.Error(), "name") {
|
||||
return nil, errorutils.Wrap(err, messageNameIsAlreadyTaken)
|
||||
}
|
||||
return nil, errorutils.Wrap(err, messageFailedToSaveModel)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (repo *pgRepository) UpdateMany(ctx context.Context, f *models.ProfessionFilter, input *models.ProfessionInput) ([]*models.Profession, error) {
|
||||
items := []*models.Profession{}
|
||||
if _, err := repo.
|
||||
Model(&items).
|
||||
Context(ctx).
|
||||
Returning("*").
|
||||
Apply(input.ApplyUpdate).
|
||||
Apply(f.Where).
|
||||
Update(); err != nil && err != pg.ErrNoRows {
|
||||
if strings.Contains(err.Error(), "name") {
|
||||
return nil, errorutils.Wrap(err, messageNameIsAlreadyTaken)
|
||||
}
|
||||
return nil, errorutils.Wrap(err, messageFailedToSaveModel)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (repo *pgRepository) Delete(ctx context.Context, f *models.ProfessionFilter) ([]*models.Profession, error) {
|
||||
items := []*models.Profession{}
|
||||
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 *profession.FetchConfig) ([]*models.Profession, int, error) {
|
||||
var err error
|
||||
items := []*models.Profession{}
|
||||
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
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package profession
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
)
|
||||
|
||||
type Usecase interface {
|
||||
Store(ctx context.Context, input *models.ProfessionInput) (*models.Profession, error)
|
||||
UpdateOneByID(ctx context.Context, id int, input *models.ProfessionInput) (*models.Profession, error)
|
||||
Delete(ctx context.Context, f *models.ProfessionFilter) ([]*models.Profession, error)
|
||||
Fetch(ctx context.Context, cfg *FetchConfig) ([]*models.Profession, int, error)
|
||||
GetByID(ctx context.Context, id int) (*models.Profession, error)
|
||||
GetBySlug(ctx context.Context, slug string) (*models.Profession, error)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package usecase
|
||||
|
||||
const (
|
||||
messageInvalidID = "Niepoprawne ID."
|
||||
messageItemNotFound = "Nie znaleziono zawodu."
|
||||
messageEmptyPayload = "Nie wprowadzono jakichkolwiek danych."
|
||||
messageNameIsRequired = "Nazwa zawodu jest wymagana."
|
||||
messageNameIsTooLong = "Nazwa zawodu może się składać z maksymalnie %d znaków."
|
||||
)
|
|
@ -0,0 +1,126 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/profession"
|
||||
sqlutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/sql"
|
||||
)
|
||||
|
||||
type usecase struct {
|
||||
professionRepository profession.Repository
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ProfessionRepository profession.Repository
|
||||
}
|
||||
|
||||
func New(cfg *Config) (profession.Usecase, error) {
|
||||
if cfg == nil || cfg.ProfessionRepository == nil {
|
||||
return nil, fmt.Errorf("profession/usecase: ProfessionRepository is required")
|
||||
}
|
||||
return &usecase{
|
||||
cfg.ProfessionRepository,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ucase *usecase) Store(ctx context.Context, input *models.ProfessionInput) (*models.Profession, error) {
|
||||
if err := ucase.validateInput(input.Sanitize(), validateOptions{false}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ucase.professionRepository.Store(ctx, input)
|
||||
}
|
||||
|
||||
func (ucase *usecase) UpdateOneByID(ctx context.Context, id int, input *models.ProfessionInput) (*models.Profession, error) {
|
||||
if id <= 0 {
|
||||
return nil, fmt.Errorf(messageInvalidID)
|
||||
}
|
||||
if err := ucase.validateInput(input.Sanitize(), validateOptions{true}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := ucase.professionRepository.UpdateMany(ctx,
|
||||
&models.ProfessionFilter{
|
||||
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.ProfessionFilter) ([]*models.Profession, error) {
|
||||
return ucase.professionRepository.Delete(ctx, f)
|
||||
}
|
||||
|
||||
func (ucase *usecase) Fetch(ctx context.Context, cfg *profession.FetchConfig) ([]*models.Profession, int, error) {
|
||||
if cfg == nil {
|
||||
cfg = &profession.FetchConfig{
|
||||
Limit: profession.DefaultLimit,
|
||||
Count: true,
|
||||
}
|
||||
}
|
||||
cfg.Sort = sqlutils.SanitizeSorts(cfg.Sort)
|
||||
return ucase.professionRepository.Fetch(ctx, cfg)
|
||||
}
|
||||
|
||||
func (ucase *usecase) GetByID(ctx context.Context, id int) (*models.Profession, error) {
|
||||
items, _, err := ucase.Fetch(ctx, &profession.FetchConfig{
|
||||
Limit: 1,
|
||||
Count: false,
|
||||
Filter: &models.ProfessionFilter{
|
||||
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.Profession, error) {
|
||||
items, _, err := ucase.Fetch(ctx, &profession.FetchConfig{
|
||||
Limit: 1,
|
||||
Count: false,
|
||||
Filter: &models.ProfessionFilter{
|
||||
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 {
|
||||
nameCanBeNil bool
|
||||
}
|
||||
|
||||
func (ucase *usecase) validateInput(input *models.ProfessionInput, opts validateOptions) error {
|
||||
if input.IsEmpty() {
|
||||
return fmt.Errorf(messageEmptyPayload)
|
||||
}
|
||||
|
||||
if input.Name != nil {
|
||||
if *input.Name == "" {
|
||||
return fmt.Errorf(messageNameIsRequired)
|
||||
} else if len(*input.Name) > profession.MaxNameLength {
|
||||
return fmt.Errorf(messageNameIsTooLong, profession.MaxNameLength)
|
||||
}
|
||||
} else if !opts.nameCanBeNil {
|
||||
return fmt.Errorf(messageNameIsRequired)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package qualification
|
||||
|
||||
const (
|
||||
DefaultLimit = 100
|
||||
MaxNameLength = 100
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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."
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
UpdateOneByID(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)
|
||||
}
|
|
@ -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."
|
||||
)
|
|
@ -0,0 +1,134 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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.Sanitize(), validateOptions{false}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ucase.qualificationRepository.Store(ctx, input)
|
||||
}
|
||||
|
||||
func (ucase *usecase) UpdateOneByID(ctx context.Context, id int, input *models.QualificationInput) (*models.Qualification, error) {
|
||||
if id <= 0 {
|
||||
return nil, fmt.Errorf(messageInvalidID)
|
||||
}
|
||||
if err := ucase.validateInput(input.Sanitize(), 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.SanitizeSorts(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 {
|
||||
if *input.Name == "" {
|
||||
return fmt.Errorf(messageNameIsRequired)
|
||||
} else if len(*input.Name) > qualification.MaxNameLength {
|
||||
return fmt.Errorf(messageNameIsTooLong, qualification.MaxNameLength)
|
||||
}
|
||||
} else if !opts.allowNilValues {
|
||||
return fmt.Errorf(messageNameIsRequired)
|
||||
}
|
||||
|
||||
if input.Code != nil {
|
||||
if *input.Code == "" {
|
||||
return fmt.Errorf(messageCodeIsRequired)
|
||||
}
|
||||
} else if !opts.allowNilValues {
|
||||
return fmt.Errorf(messageCodeIsRequired)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package question
|
||||
|
||||
const (
|
||||
DefaultLimit = 100
|
||||
TestMaxLimit = 40
|
||||
)
|
|
@ -0,0 +1,28 @@
|
|||
package question
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
)
|
||||
|
||||
type FetchConfig struct {
|
||||
Filter *models.QuestionFilter
|
||||
Offset int
|
||||
Limit int
|
||||
Sort []string
|
||||
Count bool
|
||||
}
|
||||
|
||||
type GenerateTestConfig struct {
|
||||
Qualifications []int
|
||||
Limit int
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
Store(ctx context.Context, input *models.QuestionInput) (*models.Question, error)
|
||||
UpdateOneByID(ctx context.Context, id int, input *models.QuestionInput) (*models.Question, error)
|
||||
Delete(ctx context.Context, f *models.QuestionFilter) ([]*models.Question, error)
|
||||
Fetch(ctx context.Context, cfg *FetchConfig) ([]*models.Question, int, error)
|
||||
GenerateTest(ctx context.Context, cfg *GenerateTestConfig) ([]*models.Question, error)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package repository
|
||||
|
||||
const (
|
||||
messageSimilarRecordExists = "Istnieje już podobne pytanie."
|
||||
messageFailedToSaveModel = "Wystąpił błąd podczas zapisywania pytania, prosimy spróbować później."
|
||||
messageFailedToDeleteModel = "Wystąpił błąd podczas usuwania pytania, prosimy spróbować później."
|
||||
messageFailedToFetchModel = "Wystąpił błąd podczas pobierania pytań, prosimy spróbować później."
|
||||
messageItemNotFound = "Nie znaleziono pytania."
|
||||
)
|
|
@ -0,0 +1,154 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/pkg/filestorage"
|
||||
errorutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/error"
|
||||
sqlutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/sql"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/db"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/question"
|
||||
)
|
||||
|
||||
type pgRepository struct {
|
||||
*pg.DB
|
||||
*repository
|
||||
}
|
||||
|
||||
type PGRepositoryConfig struct {
|
||||
DB *pg.DB
|
||||
FileStorage filestorage.FileStorage
|
||||
}
|
||||
|
||||
func NewPGRepository(cfg *PGRepositoryConfig) (question.Repository, error) {
|
||||
if cfg == nil || cfg.DB == nil || cfg.FileStorage == nil {
|
||||
return nil, fmt.Errorf("question/pg_repository: *pg.DB and filestorage.FileStorage are required")
|
||||
}
|
||||
return &pgRepository{
|
||||
cfg.DB,
|
||||
&repository{
|
||||
fileStorage: cfg.FileStorage,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *pgRepository) Store(ctx context.Context, input *models.QuestionInput) (*models.Question, error) {
|
||||
item := input.ToQuestion()
|
||||
if _, err := repo.
|
||||
Model(item).
|
||||
Context(ctx).
|
||||
Returning("*").
|
||||
Insert(); err != nil {
|
||||
if strings.Contains(err.Error(), "questions_from_content_correct_answer_qualification_id_key") {
|
||||
return nil, errorutils.Wrap(err, messageSimilarRecordExists)
|
||||
}
|
||||
return nil, errorutils.Wrap(err, messageFailedToSaveModel)
|
||||
}
|
||||
|
||||
repo.saveImages(item, input)
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (repo *pgRepository) UpdateOneByID(ctx context.Context, id int, input *models.QuestionInput) (*models.Question, error) {
|
||||
item := &models.Question{}
|
||||
baseQuery := repo.
|
||||
Model(item).
|
||||
Context(ctx).
|
||||
Returning("*").
|
||||
Where(sqlutils.BuildConditionEquals(sqlutils.AddAliasToColumnName("id", "question")), id).
|
||||
Set("updated_at = ?", time.Now())
|
||||
|
||||
if _, err := baseQuery.
|
||||
Clone().
|
||||
Apply(input.ApplyUpdate).
|
||||
Update(); err != nil && err != pg.ErrNoRows {
|
||||
if strings.Contains(err.Error(), "questions_from_content_correct_answer_qualification_id_key") {
|
||||
return nil, errorutils.Wrap(err, messageSimilarRecordExists)
|
||||
}
|
||||
return nil, errorutils.Wrap(err, messageFailedToSaveModel)
|
||||
}
|
||||
|
||||
repo.saveImages(item, input)
|
||||
repo.deleteImagesBasedOnInput(item, input)
|
||||
|
||||
if _, err := baseQuery.
|
||||
Clone().
|
||||
Set("image = ?", item.Image).
|
||||
Set("answer_a_image = ?", item.AnswerAImage).
|
||||
Set("answer_b_image = ?", item.AnswerBImage).
|
||||
Set("answer_c_image = ?", item.AnswerCImage).
|
||||
Set("answer_d_image = ?", item.AnswerDImage).
|
||||
Update(); err != nil && err != pg.ErrNoRows {
|
||||
return nil, errorutils.Wrap(err, messageFailedToSaveModel)
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (repo *pgRepository) Delete(ctx context.Context, f *models.QuestionFilter) ([]*models.Question, error) {
|
||||
items := []*models.Question{}
|
||||
if _, err := repo.
|
||||
Model(&items).
|
||||
Context(ctx).
|
||||
Returning("*").
|
||||
Apply(f.Where).
|
||||
Delete(); err != nil && err != pg.ErrNoRows {
|
||||
return nil, errorutils.Wrap(err, messageFailedToDeleteModel)
|
||||
}
|
||||
|
||||
go repo.getAllImagesAndDelete(items)
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (repo *pgRepository) Fetch(ctx context.Context, cfg *question.FetchConfig) ([]*models.Question, int, error) {
|
||||
var err error
|
||||
items := []*models.Question{}
|
||||
total := 0
|
||||
query := repo.
|
||||
Model(&items).
|
||||
Context(ctx).
|
||||
Limit(cfg.Limit).
|
||||
Offset(cfg.Offset).
|
||||
Apply(db.Sort{
|
||||
Relationships: map[string]string{
|
||||
"qualification": "qualification",
|
||||
},
|
||||
Orders: cfg.Sort,
|
||||
}.Apply).
|
||||
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
|
||||
}
|
||||
|
||||
func (repo *pgRepository) GenerateTest(ctx context.Context, cfg *question.GenerateTestConfig) ([]*models.Question, error) {
|
||||
subquery := repo.
|
||||
Model(&models.Question{}).
|
||||
Column("id").
|
||||
Where(sqlutils.BuildConditionArray("qualification_id"), cfg.Qualifications).
|
||||
Limit(cfg.Limit)
|
||||
items := []*models.Question{}
|
||||
if err := repo.
|
||||
Model(&items).
|
||||
Context(ctx).
|
||||
Where(sqlutils.BuildConditionIn("id"), subquery).
|
||||
Select(); err != nil && err != pg.ErrNoRows {
|
||||
return nil, errorutils.Wrap(err, messageFailedToFetchModel)
|
||||
}
|
||||
return items, nil
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/pkg/filestorage"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/pkg/utils"
|
||||
)
|
||||
|
||||
type repository struct {
|
||||
fileStorage filestorage.FileStorage
|
||||
}
|
||||
|
||||
func (repo *repository) saveImages(destination *models.Question, input *models.QuestionInput) {
|
||||
images := [...]*graphql.Upload{
|
||||
input.Image,
|
||||
input.AnswerAImage,
|
||||
input.AnswerBImage,
|
||||
input.AnswerCImage,
|
||||
input.AnswerDImage,
|
||||
}
|
||||
filenames := [...]string{
|
||||
destination.Image,
|
||||
destination.AnswerAImage,
|
||||
destination.AnswerBImage,
|
||||
destination.AnswerCImage,
|
||||
destination.AnswerDImage,
|
||||
}
|
||||
|
||||
for index, file := range images {
|
||||
if file != nil {
|
||||
generated := false
|
||||
if filenames[index] == "" {
|
||||
generated = true
|
||||
filenames[index] = utils.GenerateFilename(filepath.Ext(file.Filename))
|
||||
}
|
||||
err := repo.fileStorage.Put(file.File, filenames[index])
|
||||
if err != nil && generated {
|
||||
filenames[index] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destination.Image = filenames[0]
|
||||
destination.AnswerAImage = filenames[1]
|
||||
destination.AnswerBImage = filenames[2]
|
||||
destination.AnswerCImage = filenames[3]
|
||||
destination.AnswerDImage = filenames[4]
|
||||
}
|
||||
|
||||
func (repo *repository) deleteImages(images []string) {
|
||||
for _, image := range images {
|
||||
repo.fileStorage.Remove(image)
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *repository) deleteImagesBasedOnInput(question *models.Question, input *models.QuestionInput) {
|
||||
images := []string{}
|
||||
|
||||
if input.DeleteImage != nil &&
|
||||
*input.DeleteImage &&
|
||||
input.Image == nil &&
|
||||
question.Image != "" {
|
||||
images = append(images, question.Image)
|
||||
question.Image = ""
|
||||
}
|
||||
|
||||
if input.DeleteAnswerAImage != nil &&
|
||||
*input.DeleteAnswerAImage &&
|
||||
input.AnswerAImage == nil &&
|
||||
question.AnswerAImage != "" {
|
||||
images = append(images, question.AnswerAImage)
|
||||
question.AnswerAImage = ""
|
||||
}
|
||||
|
||||
if input.DeleteAnswerBImage != nil &&
|
||||
*input.DeleteAnswerBImage &&
|
||||
input.AnswerBImage == nil &&
|
||||
question.AnswerBImage != "" {
|
||||
images = append(images, question.AnswerBImage)
|
||||
question.AnswerBImage = ""
|
||||
}
|
||||
|
||||
if input.DeleteAnswerCImage != nil &&
|
||||
*input.DeleteAnswerCImage &&
|
||||
input.AnswerCImage == nil &&
|
||||
question.AnswerCImage != "" {
|
||||
images = append(images, question.AnswerCImage)
|
||||
question.AnswerCImage = ""
|
||||
}
|
||||
|
||||
if input.DeleteAnswerDImage != nil &&
|
||||
*input.DeleteAnswerDImage &&
|
||||
input.AnswerDImage == nil &&
|
||||
question.AnswerDImage != "" {
|
||||
images = append(images, question.AnswerDImage)
|
||||
question.AnswerDImage = ""
|
||||
}
|
||||
|
||||
repo.deleteImages(images)
|
||||
}
|
||||
|
||||
func (repo *repository) getAllImagesAndDelete(questions []*models.Question) {
|
||||
images := []string{}
|
||||
|
||||
for _, question := range questions {
|
||||
if question.Image != "" {
|
||||
images = append(images, question.Image)
|
||||
}
|
||||
if question.AnswerAImage != "" {
|
||||
images = append(images, question.AnswerAImage)
|
||||
}
|
||||
if question.AnswerBImage != "" {
|
||||
images = append(images, question.AnswerBImage)
|
||||
}
|
||||
if question.AnswerCImage != "" {
|
||||
images = append(images, question.AnswerCImage)
|
||||
}
|
||||
if question.AnswerDImage != "" {
|
||||
images = append(images, question.AnswerDImage)
|
||||
}
|
||||
}
|
||||
|
||||
repo.deleteImages(images)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package question
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
)
|
||||
|
||||
type Usecase interface {
|
||||
Store(ctx context.Context, input *models.QuestionInput) (*models.Question, error)
|
||||
UpdateOneByID(ctx context.Context, id int, input *models.QuestionInput) (*models.Question, error)
|
||||
Delete(ctx context.Context, f *models.QuestionFilter) ([]*models.Question, error)
|
||||
Fetch(ctx context.Context, cfg *FetchConfig) ([]*models.Question, int, error)
|
||||
GetByID(ctx context.Context, id int) (*models.Question, error)
|
||||
GenerateTest(ctx context.Context, cfg *GenerateTestConfig) ([]*models.Question, error)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package usecase
|
||||
|
||||
const (
|
||||
messageInvalidID = "Niepoprawne ID."
|
||||
messageItemNotFound = "Nie znaleziono pytania."
|
||||
messageEmptyPayload = "Nie wprowadzono jakichkolwiek danych."
|
||||
messageContentIsRequired = "Treść pytania jest wymagana."
|
||||
messageCorrectAnswerIsInvalid = "Odpowiedź poprawna na pytanie jest nieprawidłowa."
|
||||
messageQualificationIDIsRequired = "ID kwalifikacji jest wymagane."
|
||||
messageAnswerIsRequired = "Odpowiedź %s jest wymagana."
|
||||
messageAnswerIsInvalid = "Odpowiedź %s jest nieprawidłowa."
|
||||
messageImageNotAcceptableMIMEType = "%s: Oczekiwany jest obrazek w formacie png or jpg."
|
||||
messageCannotDeleteImageWithoutNewAnswer = "%s: Nie możesz usunąć obrazka i nie wprowadzić żadnej odpowiedzi."
|
||||
)
|
|
@ -0,0 +1,230 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/question"
|
||||
sqlutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/sql"
|
||||
)
|
||||
|
||||
var (
|
||||
imageAcceptedMIMETypes = map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/jpg": true,
|
||||
"image/png": true,
|
||||
}
|
||||
)
|
||||
|
||||
type usecase struct {
|
||||
questionRepository question.Repository
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
QuestionRepository question.Repository
|
||||
}
|
||||
|
||||
func New(cfg *Config) (question.Usecase, error) {
|
||||
if cfg == nil || cfg.QuestionRepository == nil {
|
||||
return nil, fmt.Errorf("question/usecase: cfg.QuestionRepository is required")
|
||||
}
|
||||
return &usecase{
|
||||
cfg.QuestionRepository,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ucase *usecase) Store(ctx context.Context, input *models.QuestionInput) (*models.Question, error) {
|
||||
if err := ucase.validateInput(input.Sanitize(), validateOptions{false}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ucase.questionRepository.Store(ctx, input)
|
||||
}
|
||||
|
||||
func (ucase *usecase) UpdateOneByID(ctx context.Context, id int, input *models.QuestionInput) (*models.Question, error) {
|
||||
if id <= 0 {
|
||||
return nil, fmt.Errorf(messageInvalidID)
|
||||
}
|
||||
if err := ucase.validateInput(input.Sanitize(), validateOptions{true}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item, err := ucase.questionRepository.UpdateOneByID(ctx,
|
||||
id,
|
||||
input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item == nil {
|
||||
return nil, fmt.Errorf(messageItemNotFound)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (ucase *usecase) Delete(ctx context.Context, f *models.QuestionFilter) ([]*models.Question, error) {
|
||||
return ucase.questionRepository.Delete(ctx, f)
|
||||
}
|
||||
|
||||
func (ucase *usecase) Fetch(ctx context.Context, cfg *question.FetchConfig) ([]*models.Question, int, error) {
|
||||
if cfg == nil {
|
||||
cfg = &question.FetchConfig{
|
||||
Limit: question.DefaultLimit,
|
||||
Count: true,
|
||||
}
|
||||
}
|
||||
cfg.Sort = sqlutils.SanitizeSorts(cfg.Sort)
|
||||
return ucase.questionRepository.Fetch(ctx, cfg)
|
||||
}
|
||||
|
||||
func (ucase *usecase) GetByID(ctx context.Context, id int) (*models.Question, error) {
|
||||
items, _, err := ucase.Fetch(ctx, &question.FetchConfig{
|
||||
Limit: 1,
|
||||
Count: false,
|
||||
Filter: &models.QuestionFilter{
|
||||
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) GenerateTest(ctx context.Context, cfg *question.GenerateTestConfig) ([]*models.Question, error) {
|
||||
if cfg == nil {
|
||||
cfg = &question.GenerateTestConfig{
|
||||
Limit: question.TestMaxLimit,
|
||||
}
|
||||
}
|
||||
if cfg.Limit > question.TestMaxLimit {
|
||||
cfg.Limit = question.TestMaxLimit
|
||||
}
|
||||
return ucase.questionRepository.GenerateTest(ctx, cfg)
|
||||
}
|
||||
|
||||
type validateOptions struct {
|
||||
allowNilValues bool
|
||||
}
|
||||
|
||||
func (ucase *usecase) validateInput(input *models.QuestionInput, opts validateOptions) error {
|
||||
if input.IsEmpty() {
|
||||
return fmt.Errorf(messageEmptyPayload)
|
||||
}
|
||||
|
||||
if input.Content != nil {
|
||||
if *input.Content == "" {
|
||||
return fmt.Errorf(messageContentIsRequired)
|
||||
}
|
||||
} else if !opts.allowNilValues {
|
||||
return fmt.Errorf(messageContentIsRequired)
|
||||
}
|
||||
|
||||
if input.CorrectAnswer != nil {
|
||||
if !input.CorrectAnswer.IsValid() {
|
||||
return fmt.Errorf(messageCorrectAnswerIsInvalid)
|
||||
}
|
||||
} else if !opts.allowNilValues {
|
||||
return fmt.Errorf(messageCorrectAnswerIsInvalid)
|
||||
}
|
||||
|
||||
if input.QualificationID != nil {
|
||||
if *input.QualificationID <= 0 {
|
||||
return fmt.Errorf(messageQualificationIDIsRequired)
|
||||
}
|
||||
} else if !opts.allowNilValues {
|
||||
return fmt.Errorf(messageQualificationIDIsRequired)
|
||||
}
|
||||
|
||||
if input.AnswerA != nil {
|
||||
if !input.AnswerA.IsValid() {
|
||||
return fmt.Errorf(messageAnswerIsInvalid, "A")
|
||||
}
|
||||
}
|
||||
|
||||
if input.AnswerB != nil {
|
||||
if !input.AnswerB.IsValid() {
|
||||
return fmt.Errorf(messageAnswerIsInvalid, "B")
|
||||
}
|
||||
}
|
||||
|
||||
if input.AnswerC != nil {
|
||||
if !input.AnswerC.IsValid() {
|
||||
return fmt.Errorf(messageAnswerIsInvalid, "C")
|
||||
}
|
||||
}
|
||||
|
||||
if input.AnswerD != nil {
|
||||
if !input.AnswerD.IsValid() {
|
||||
return fmt.Errorf(messageAnswerIsInvalid, "D")
|
||||
}
|
||||
}
|
||||
|
||||
if input.Image != nil {
|
||||
if !imageAcceptedMIMETypes[input.Image.ContentType] {
|
||||
return fmt.Errorf(messageImageNotAcceptableMIMEType, "Obrazek pytanie")
|
||||
}
|
||||
}
|
||||
|
||||
if input.AnswerAImage != nil {
|
||||
if !imageAcceptedMIMETypes[input.AnswerAImage.ContentType] {
|
||||
return fmt.Errorf(messageAnswerIsInvalid, "Obrazek odpowiedź A")
|
||||
}
|
||||
}
|
||||
|
||||
if input.AnswerBImage != nil {
|
||||
if !imageAcceptedMIMETypes[input.AnswerBImage.ContentType] {
|
||||
return fmt.Errorf(messageAnswerIsInvalid, "Obrazek odpowiedź B")
|
||||
}
|
||||
}
|
||||
|
||||
if input.AnswerCImage != nil {
|
||||
if !imageAcceptedMIMETypes[input.AnswerCImage.ContentType] {
|
||||
return fmt.Errorf(messageAnswerIsInvalid, "Obrazek odpowiedź C")
|
||||
}
|
||||
}
|
||||
|
||||
if input.AnswerDImage != nil {
|
||||
if !imageAcceptedMIMETypes[input.AnswerDImage.ContentType] {
|
||||
return fmt.Errorf(messageAnswerIsInvalid, "Obrazek odpowiedź D")
|
||||
}
|
||||
}
|
||||
|
||||
if input.DeleteAnswerAImage != nil && input.AnswerA == nil && input.AnswerAImage == nil {
|
||||
return fmt.Errorf(messageCannotDeleteImageWithoutNewAnswer, "Obrazek odpowiedź A")
|
||||
}
|
||||
|
||||
if input.DeleteAnswerBImage != nil && input.AnswerB == nil && input.AnswerBImage == nil {
|
||||
return fmt.Errorf(messageCannotDeleteImageWithoutNewAnswer, "Obrazek odpowiedź B")
|
||||
}
|
||||
|
||||
if input.DeleteAnswerCImage != nil && input.AnswerC == nil && input.AnswerCImage == nil {
|
||||
return fmt.Errorf(messageCannotDeleteImageWithoutNewAnswer, "Obrazek odpowiedź C")
|
||||
}
|
||||
|
||||
if input.DeleteAnswerDImage != nil && input.AnswerD == nil && input.AnswerDImage == nil {
|
||||
return fmt.Errorf(messageCannotDeleteImageWithoutNewAnswer, "Obrazek odpowiedź D")
|
||||
|
||||
}
|
||||
|
||||
if !opts.allowNilValues {
|
||||
if input.AnswerA == nil && input.AnswerAImage == nil {
|
||||
return fmt.Errorf(messageAnswerIsRequired, "A")
|
||||
}
|
||||
|
||||
if input.AnswerB == nil && input.AnswerBImage == nil {
|
||||
return fmt.Errorf(messageAnswerIsRequired, "B")
|
||||
}
|
||||
|
||||
if input.AnswerC == nil && input.AnswerCImage == nil {
|
||||
return fmt.Errorf(messageAnswerIsRequired, "C")
|
||||
}
|
||||
|
||||
if input.AnswerD == nil && input.AnswerDImage == nil {
|
||||
return fmt.Errorf(messageAnswerIsRequired, "D")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package user
|
||||
|
||||
const (
|
||||
DefaultLimit = 100
|
||||
MinDisplayNameLength = 2
|
||||
MaxDisplayNameLength = 32
|
||||
MinPasswordLength = 6
|
||||
MaxPasswordLength = 64
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
)
|
||||
|
||||
type FetchConfig struct {
|
||||
Filter *models.UserFilter
|
||||
Offset int
|
||||
Limit int
|
||||
Sort []string
|
||||
Count bool
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
Store(ctx context.Context, input *models.UserInput) (*models.User, error)
|
||||
UpdateMany(ctx context.Context, f *models.UserFilter, input *models.UserInput) ([]*models.User, error)
|
||||
Delete(ctx context.Context, f *models.UserFilter) ([]*models.User, error)
|
||||
Fetch(ctx context.Context, cfg *FetchConfig) ([]*models.User, int, error)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package repository
|
||||
|
||||
const (
|
||||
messageEmailIsAlreadyTaken = "Istnieje już użytkownik o podanym adresie e-mail."
|
||||
messageFailedToSaveModel = "Wystąpił błąd podczas zapisywania użytkownika, prosimy spróbować później."
|
||||
messageFailedToDeleteModel = "Wystąpił błąd podczas usuwania użytkownika, prosimy spróbować później."
|
||||
messageFailedToFetchModel = "Wystąpił błąd podczas pobierania użytkowników, prosimy spróbować później."
|
||||
)
|
|
@ -0,0 +1,97 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
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/user"
|
||||
)
|
||||
|
||||
type pgRepository struct {
|
||||
*pg.DB
|
||||
}
|
||||
|
||||
type PGRepositoryConfig struct {
|
||||
DB *pg.DB
|
||||
}
|
||||
|
||||
func NewPGRepository(cfg *PGRepositoryConfig) (user.Repository, error) {
|
||||
if cfg == nil || cfg.DB == nil {
|
||||
return nil, fmt.Errorf("user/pg_repository: *pg.DB is required")
|
||||
}
|
||||
return &pgRepository{
|
||||
cfg.DB,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *pgRepository) Store(ctx context.Context, input *models.UserInput) (*models.User, error) {
|
||||
item := input.ToUser()
|
||||
if _, err := repo.
|
||||
Model(item).
|
||||
Context(ctx).
|
||||
Returning("*").
|
||||
Insert(); err != nil {
|
||||
if strings.Contains(err.Error(), "email") {
|
||||
return nil, errorutils.Wrap(err, messageEmailIsAlreadyTaken)
|
||||
}
|
||||
return nil, errorutils.Wrap(err, messageFailedToSaveModel)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (repo *pgRepository) UpdateMany(ctx context.Context, f *models.UserFilter, input *models.UserInput) ([]*models.User, error) {
|
||||
items := []*models.User{}
|
||||
if _, err := repo.
|
||||
Model(&items).
|
||||
Context(ctx).
|
||||
Returning("*").
|
||||
Apply(input.ApplyUpdate).
|
||||
Apply(f.Where).
|
||||
Update(); err != nil && err != pg.ErrNoRows {
|
||||
if strings.Contains(err.Error(), "email") {
|
||||
return nil, errorutils.Wrap(err, messageEmailIsAlreadyTaken)
|
||||
}
|
||||
return nil, errorutils.Wrap(err, messageFailedToSaveModel)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (repo *pgRepository) Delete(ctx context.Context, f *models.UserFilter) ([]*models.User, error) {
|
||||
items := []*models.User{}
|
||||
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 *user.FetchConfig) ([]*models.User, int, error) {
|
||||
var err error
|
||||
items := []*models.User{}
|
||||
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
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
)
|
||||
|
||||
type Usecase interface {
|
||||
Store(ctx context.Context, input *models.UserInput) (*models.User, error)
|
||||
UpdateOneByID(ctx context.Context, id int, input *models.UserInput) (*models.User, error)
|
||||
UpdateMany(ctx context.Context, f *models.UserFilter, input *models.UserInput) ([]*models.User, error)
|
||||
Delete(ctx context.Context, f *models.UserFilter) ([]*models.User, error)
|
||||
Fetch(ctx context.Context, cfg *FetchConfig) ([]*models.User, int, error)
|
||||
GetByID(ctx context.Context, id int) (*models.User, error)
|
||||
GetByCredentials(ctx context.Context, email, password string) (*models.User, error)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package usecase
|
||||
|
||||
const (
|
||||
messageInvalidID = "Niepoprawne ID."
|
||||
messageItemNotFound = "Nie znaleziono użytkownika."
|
||||
messageEmptyPayload = "Nie wprowadzono jakichkolwiek danych."
|
||||
messageInvalidCredentials = "Niepoprawny email/hasło."
|
||||
messageDisplayNameIsRequired = "Wymagane jest wprowadzenie nazwy użytkownika."
|
||||
messageDisplayNameIsTooLong = "Nazwa użytkownika może się składać z maksymalnie %d znaków."
|
||||
messageEmailIsRequired = "Wymagane jest wprowadzenie adresu e-mail"
|
||||
messageEmailIsInvalid = "Wprowadzony adres e-mail jest nieprawidłowy."
|
||||
messagePasswordIsRequired = "Wymagane jest wprowadzenie hasła."
|
||||
messagePasswordInvalidLength = "Długość hasła powinna wynosić %d-%d znaków."
|
||||
messageInvalidRole = "Nieznana rola użytkownika."
|
||||
)
|
|
@ -0,0 +1,169 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/internal/user"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/pkg/utils"
|
||||
sqlutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/sql"
|
||||
)
|
||||
|
||||
type usecase struct {
|
||||
userRepository user.Repository
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
UserRepository user.Repository
|
||||
}
|
||||
|
||||
func New(cfg *Config) (user.Usecase, error) {
|
||||
if cfg == nil || cfg.UserRepository == nil {
|
||||
return nil, fmt.Errorf("user/usecase: UserRepository is required")
|
||||
}
|
||||
return &usecase{
|
||||
cfg.UserRepository,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ucase *usecase) Store(ctx context.Context, input *models.UserInput) (*models.User, error) {
|
||||
if err := ucase.validateInput(input.Sanitize(), validateOptions{false}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ucase.userRepository.Store(ctx, input)
|
||||
}
|
||||
|
||||
func (ucase *usecase) UpdateOneByID(ctx context.Context, id int, input *models.UserInput) (*models.User, error) {
|
||||
if id <= 0 {
|
||||
return nil, fmt.Errorf(messageInvalidID)
|
||||
}
|
||||
items, err := ucase.UpdateMany(
|
||||
ctx,
|
||||
&models.UserFilter{
|
||||
ID: []int{id},
|
||||
},
|
||||
input.Sanitize(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, fmt.Errorf(messageItemNotFound)
|
||||
}
|
||||
return items[0], nil
|
||||
}
|
||||
|
||||
func (ucase *usecase) UpdateMany(ctx context.Context, f *models.UserFilter, input *models.UserInput) ([]*models.User, error) {
|
||||
if f == nil {
|
||||
return []*models.User{}, nil
|
||||
}
|
||||
if err := ucase.validateInput(input.Sanitize(), validateOptions{true}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := ucase.userRepository.UpdateMany(ctx, f, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (ucase *usecase) Delete(ctx context.Context, f *models.UserFilter) ([]*models.User, error) {
|
||||
return ucase.userRepository.Delete(ctx, f)
|
||||
}
|
||||
|
||||
func (ucase *usecase) Fetch(ctx context.Context, cfg *user.FetchConfig) ([]*models.User, int, error) {
|
||||
if cfg == nil {
|
||||
cfg = &user.FetchConfig{
|
||||
Limit: user.DefaultLimit,
|
||||
Count: true,
|
||||
}
|
||||
}
|
||||
cfg.Sort = sqlutils.SanitizeSorts(cfg.Sort)
|
||||
return ucase.userRepository.Fetch(ctx, cfg)
|
||||
}
|
||||
|
||||
func (ucase *usecase) GetByID(ctx context.Context, id int) (*models.User, error) {
|
||||
items, _, err := ucase.Fetch(ctx, &user.FetchConfig{
|
||||
Limit: 1,
|
||||
Count: false,
|
||||
Filter: &models.UserFilter{
|
||||
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) GetByCredentials(ctx context.Context, email, password string) (*models.User, error) {
|
||||
items, _, err := ucase.Fetch(ctx, &user.FetchConfig{
|
||||
Limit: 1,
|
||||
Count: false,
|
||||
Filter: &models.UserFilter{
|
||||
Email: []string{email},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, fmt.Errorf(messageInvalidCredentials)
|
||||
}
|
||||
if err := items[0].CompareHashAndPassword(password); err != nil {
|
||||
return nil, fmt.Errorf(messageInvalidCredentials)
|
||||
}
|
||||
return items[0], nil
|
||||
}
|
||||
|
||||
type validateOptions struct {
|
||||
acceptNilValues bool
|
||||
}
|
||||
|
||||
func (ucase *usecase) validateInput(input *models.UserInput, opts validateOptions) error {
|
||||
if input.IsEmpty() {
|
||||
return fmt.Errorf(messageEmptyPayload)
|
||||
}
|
||||
|
||||
if input.DisplayName != nil {
|
||||
displayNameLength := len(*input.DisplayName)
|
||||
if displayNameLength < user.MinDisplayNameLength {
|
||||
return fmt.Errorf(messageDisplayNameIsRequired)
|
||||
} else if displayNameLength > user.MaxDisplayNameLength {
|
||||
return fmt.Errorf(messageDisplayNameIsTooLong, user.MaxDisplayNameLength)
|
||||
}
|
||||
} else if !opts.acceptNilValues {
|
||||
return fmt.Errorf(messageDisplayNameIsRequired)
|
||||
}
|
||||
|
||||
if input.Email != nil {
|
||||
if !utils.IsEmailValid(*input.Email) {
|
||||
return fmt.Errorf(messageEmailIsInvalid)
|
||||
}
|
||||
} else if !opts.acceptNilValues {
|
||||
return fmt.Errorf(messageEmailIsRequired)
|
||||
}
|
||||
|
||||
if input.Password != nil {
|
||||
passwordLength := len(*input.Password)
|
||||
if passwordLength > user.MaxPasswordLength || passwordLength < user.MinPasswordLength {
|
||||
return fmt.Errorf(messagePasswordInvalidLength, user.MinPasswordLength, user.MaxPasswordLength)
|
||||
}
|
||||
} else if !opts.acceptNilValues {
|
||||
return fmt.Errorf(messagePasswordIsRequired)
|
||||
}
|
||||
|
||||
if input.Role != nil {
|
||||
if !input.Role.IsValid() {
|
||||
return fmt.Errorf(messageInvalidRole)
|
||||
}
|
||||
} else if !opts.acceptNilValues {
|
||||
return fmt.Errorf(messagePasswordIsRequired)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package filestorage
|
||||
|
||||
import "os"
|
||||
|
||||
type Config struct {
|
||||
BasePath string
|
||||
}
|
||||
|
||||
func getDefaultConfig() *Config {
|
||||
path, _ := os.Getwd()
|
||||
return &Config{
|
||||
BasePath: path,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package filestorage
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type fileStorage struct {
|
||||
basePath string
|
||||
}
|
||||
|
||||
func New(cfg *Config) FileStorage {
|
||||
if cfg == nil {
|
||||
cfg = getDefaultConfig()
|
||||
}
|
||||
|
||||
return &fileStorage{
|
||||
cfg.BasePath,
|
||||
}
|
||||
}
|
||||
|
||||
func (storage *fileStorage) Put(file io.Reader, filename string) error {
|
||||
fullPath := path.Join(storage.basePath, filename)
|
||||
f, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "FileStorage.Put")
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, file)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "FileStorage.Put")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *fileStorage) Remove(filename string) error {
|
||||
fullPath := path.Join(storage.basePath, filename)
|
||||
err := os.Remove(fullPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "FileStorage.Remove")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package filestorage
|
||||
|
||||
import "io"
|
||||
|
||||
type FileStorage interface {
|
||||
Put(file io.Reader, filename string) error
|
||||
Remove(filename string) error
|
||||
}
|
|
@ -5,7 +5,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
EnvMode = "MODE"
|
||||
EnvKey = "MODE"
|
||||
DevelopmentMode = "development"
|
||||
ProductionMode = "production"
|
||||
TestMode = "test"
|
||||
|
@ -14,7 +14,7 @@ const (
|
|||
var mode = DevelopmentMode
|
||||
|
||||
func init() {
|
||||
Set(os.Getenv(EnvMode))
|
||||
Set(os.Getenv(EnvKey))
|
||||
}
|
||||
|
||||
func Set(value string) {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package errorutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zdam-egzamin-zawodowy/backend/pkg/mode"
|
||||
)
|
||||
|
||||
func Wrap(details error, message string) error {
|
||||
return Wrapf(details, message)
|
||||
}
|
||||
|
||||
func Wrapf(details error, message string, args ...interface{}) error {
|
||||
if mode.Get() != mode.ProductionMode {
|
||||
return errors.Wrapf(details, message, args...)
|
||||
}
|
||||
return fmt.Errorf(message)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func GenerateFilename(ext string) string {
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
ext = "." + ext
|
||||
}
|
||||
return uuid.New().String() + ext
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
|
||||
func IsEmailValid(e string) bool {
|
||||
if len(e) < 3 && len(e) > 254 {
|
||||
return false
|
||||
}
|
||||
if !emailRegex.MatchString(e) {
|
||||
return false
|
||||
}
|
||||
parts := strings.Split(e, "@")
|
||||
mx, err := net.LookupMX(parts[1])
|
||||
if err != nil || len(mx) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package sqlutils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
sortRegex = regexp.MustCompile(`^[\p{L}\_\.]+$`)
|
||||
)
|
||||
|
||||
func SanitizeSort(sort string) string {
|
||||
trimmed := strings.TrimSpace(sort)
|
||||
splitted := strings.Split(trimmed, " ")
|
||||
length := len(splitted)
|
||||
|
||||
if length != 2 || !sortRegex.Match([]byte(splitted[0])) {
|
||||
return ""
|
||||
}
|
||||
|
||||
table := ""
|
||||
column := splitted[0]
|
||||
if strings.Contains(splitted[0], ".") {
|
||||
columnAndTable := strings.Split(splitted[0], ".")
|
||||
table = underscore(columnAndTable[0]) + "."
|
||||
column = columnAndTable[1]
|
||||
}
|
||||
|
||||
direction := "ASC"
|
||||
if strings.ToUpper(splitted[1]) == "DESC" {
|
||||
direction = "DESC"
|
||||
}
|
||||
|
||||
return strings.ToLower(table+underscore(column)) + " " + direction
|
||||
}
|
||||
|
||||
func SanitizeSorts(sorts []string) []string {
|
||||
sanitizedSorts := []string{}
|
||||
for _, sort := range sorts {
|
||||
sanitized := SanitizeSort(sort)
|
||||
if sanitized != "" {
|
||||
sanitizedSorts = append(sanitizedSorts, sanitized)
|
||||
}
|
||||
}
|
||||
return sanitizedSorts
|
||||
}
|
||||
|
||||
type buffer struct {
|
||||
r []byte
|
||||
runeBytes [utf8.UTFMax]byte
|
||||
}
|
||||
|
||||
func (b *buffer) write(r rune) {
|
||||
if r < utf8.RuneSelf {
|
||||
b.r = append(b.r, byte(r))
|
||||
return
|
||||
}
|
||||
n := utf8.EncodeRune(b.runeBytes[0:], r)
|
||||
b.r = append(b.r, b.runeBytes[0:n]...)
|
||||
}
|
||||
|
||||
func (b *buffer) indent() {
|
||||
if len(b.r) > 0 {
|
||||
b.r = append(b.r, '_')
|
||||
}
|
||||
}
|
||||
|
||||
func underscore(s string) string {
|
||||
b := buffer{
|
||||
r: make([]byte, 0, len(s)),
|
||||
}
|
||||
var m rune
|
||||
var w bool
|
||||
for _, ch := range s {
|
||||
if unicode.IsUpper(ch) {
|
||||
if m != 0 {
|
||||
if !w {
|
||||
b.indent()
|
||||
w = true
|
||||
}
|
||||
b.write(m)
|
||||
}
|
||||
m = unicode.ToLower(ch)
|
||||
} else {
|
||||
if m != 0 {
|
||||
b.indent()
|
||||
b.write(m)
|
||||
m = 0
|
||||
w = false
|
||||
}
|
||||
b.write(ch)
|
||||
}
|
||||
}
|
||||
if m != 0 {
|
||||
if !w {
|
||||
b.indent()
|
||||
}
|
||||
b.write(m)
|
||||
}
|
||||
return string(b.r)
|
||||
}
|
|
@ -5,9 +5,9 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
func AddAliasToColumnName(column, prefix string) string {
|
||||
if prefix != "" && !strings.HasPrefix(column, prefix+".") {
|
||||
column = WrapStringInDoubleQuotes(prefix) + "." + WrapStringInDoubleQuotes(column)
|
||||
func AddAliasToColumnName(column, alias string) string {
|
||||
if alias != "" && !strings.HasPrefix(column, alias+".") {
|
||||
column = WrapStringInDoubleQuotes(alias) + "." + WrapStringInDoubleQuotes(column)
|
||||
} else {
|
||||
column = WrapStringInDoubleQuotes(column)
|
||||
}
|
||||
|
@ -46,6 +46,10 @@ func BuildConditionIEQ(column string) string {
|
|||
return column + " ILIKE ?"
|
||||
}
|
||||
|
||||
func BuildConditionIn(column string) string {
|
||||
return column + " IN (?)"
|
||||
}
|
||||
|
||||
func BuildConditionArray(column string) string {
|
||||
return column + " = ANY(?)"
|
||||
}
|
||||
|
|
Reference in New Issue