add a new field to the Profession graphql type (Qualifications)

This commit is contained in:
Dawid Wysokiński 2021-03-21 18:19:22 +01:00
parent 827a353fba
commit e936b9b0ed
15 changed files with 199 additions and 30 deletions

2
go.mod
View File

@ -19,7 +19,7 @@ require (
github.com/sirupsen/logrus v1.8.0 github.com/sirupsen/logrus v1.8.0
github.com/vektah/gqlparser/v2 v2.1.0 github.com/vektah/gqlparser/v2 v2.1.0
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/sys v0.0.0-20210313110737-8e9fff1a3a18 // indirect golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

4
go.sum
View File

@ -223,8 +223,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210218155724-8ebf48af031b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210218155724-8ebf48af031b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210313110737-8e9fff1a3a18 h1:jxr7/dEo+rR29uEBoLSWJ1tRHCFAMwFbGUU9nRqzpds= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc=
golang.org/x/sys v0.0.0-20210313110737-8e9fff1a3a18/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

View File

@ -2,24 +2,31 @@ package dataloader
import ( import (
"context" "context"
"github.com/zdam-egzamin-zawodowy/backend/internal/profession"
"time" "time"
"github.com/zdam-egzamin-zawodowy/backend/internal/models" "github.com/zdam-egzamin-zawodowy/backend/internal/models"
"github.com/zdam-egzamin-zawodowy/backend/internal/qualification" "github.com/zdam-egzamin-zawodowy/backend/internal/qualification"
) )
const (
wait = 2 * time.Millisecond
)
type Config struct { type Config struct {
ProfessionRepo profession.Repository
QualificationRepo qualification.Repository QualificationRepo qualification.Repository
} }
type DataLoader struct { type DataLoader struct {
QualificationByID *QualificationLoader QualificationByID *QualificationLoader
QualificationsByProfessionID *QualificationSliceByProfessionIDLoader
} }
func New(cfg Config) *DataLoader { func New(cfg Config) *DataLoader {
return &DataLoader{ return &DataLoader{
QualificationByID: NewQualificationLoader(QualificationLoaderConfig{ QualificationByID: NewQualificationLoader(QualificationLoaderConfig{
Wait: 2 * time.Millisecond, Wait: wait,
Fetch: func(ids []int) ([]*models.Qualification, []error) { Fetch: func(ids []int) ([]*models.Qualification, []error) {
qualificationsNotInOrder, _, err := cfg.QualificationRepo.Fetch(context.Background(), &qualification.FetchConfig{ qualificationsNotInOrder, _, err := cfg.QualificationRepo.Fetch(context.Background(), &qualification.FetchConfig{
Filter: &models.QualificationFilter{ Filter: &models.QualificationFilter{
@ -41,5 +48,22 @@ func New(cfg Config) *DataLoader {
return qualifications, nil return qualifications, nil
}, },
}), }),
QualificationsByProfessionID: NewQualificationSliceByProfessionIDLoader(QualificationSliceByProfessionIDLoaderConfig{
Wait: wait,
Fetch: func(ids []int) ([][]*models.Qualification, []error) {
m, err := cfg.ProfessionRepo.GetAssociatedQualifications(context.Background(), ids...)
if err != nil {
return nil, []error{err}
}
qualifications := make([][]*models.Qualification, len(ids))
for i, id := range ids {
qualifications[i] = m[id]
}
return qualifications, nil
},
}),
} }
} }

View File

@ -194,5 +194,8 @@ func getComplexityRoot() generated.ComplexityRoot {
complexityRoot.Mutation.UpdateUser = func(childComplexity int, id int, input models.UserInput) int { complexityRoot.Mutation.UpdateUser = func(childComplexity int, id int, input models.UserInput) int {
return 200 + childComplexity return 200 + childComplexity
} }
complexityRoot.Profession.Qualifications = func(childComplexity int) int {
return 50 + childComplexity
}
return complexityRoot return complexityRoot
} }

View File

@ -38,6 +38,7 @@ type Config struct {
type ResolverRoot interface { type ResolverRoot interface {
Mutation() MutationResolver Mutation() MutationResolver
Profession() ProfessionResolver
Query() QueryResolver Query() QueryResolver
Question() QuestionResolver Question() QuestionResolver
} }
@ -66,11 +67,12 @@ type ComplexityRoot struct {
} }
Profession struct { Profession struct {
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
Description func(childComplexity int) int Description func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Name func(childComplexity int) int Name func(childComplexity int) int
Slug func(childComplexity int) int Qualifications func(childComplexity int) int
Slug func(childComplexity int) int
} }
ProfessionList struct { ProfessionList struct {
@ -166,6 +168,9 @@ type MutationResolver interface {
DeleteUsers(ctx context.Context, ids []int) ([]*models.User, error) DeleteUsers(ctx context.Context, ids []int) ([]*models.User, error)
SignIn(ctx context.Context, email string, password string, staySignedIn *bool) (*UserWithToken, error) SignIn(ctx context.Context, email string, password string, staySignedIn *bool) (*UserWithToken, error)
} }
type ProfessionResolver interface {
Qualifications(ctx context.Context, obj *models.Profession) ([]*models.Qualification, error)
}
type QueryResolver interface { type QueryResolver interface {
Professions(ctx context.Context, filter *models.ProfessionFilter, limit *int, offset *int, sort []string) (*ProfessionList, error) Professions(ctx context.Context, filter *models.ProfessionFilter, limit *int, offset *int, sort []string) (*ProfessionList, error)
Profession(ctx context.Context, id *int, slug *string) (*models.Profession, error) Profession(ctx context.Context, id *int, slug *string) (*models.Profession, error)
@ -392,6 +397,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Profession.Name(childComplexity), true return e.complexity.Profession.Name(childComplexity), true
case "Profession.qualifications":
if e.complexity.Profession.Qualifications == nil {
break
}
return e.complexity.Profession.Qualifications(childComplexity), true
case "Profession.slug": case "Profession.slug":
if e.complexity.Profession.Slug == nil { if e.complexity.Profession.Slug == nil {
break break
@ -860,6 +872,7 @@ directive @hasRole(role: Role!) on FIELD_DEFINITION
name: String! name: String!
description: String description: String
createdAt: Time! createdAt: Time!
qualifications: [Qualification!]! @goField(forceResolver: true)
} }
type ProfessionList { type ProfessionList {
@ -3011,6 +3024,41 @@ func (ec *executionContext) _Profession_createdAt(ctx context.Context, field gra
return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res)
} }
func (ec *executionContext) _Profession_qualifications(ctx context.Context, field graphql.CollectedField, obj *models.Profession) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Profession",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Profession().Qualifications(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]*models.Qualification)
fc.Result = res
return ec.marshalNQualification2ᚕᚖgithubᚗcomᚋzdamᚑegzaminᚑzawodowyᚋbackendᚋinternalᚋmodelsᚐQualificationᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _ProfessionList_total(ctx context.Context, field graphql.CollectedField, obj *ProfessionList) (ret graphql.Marshaler) { func (ec *executionContext) _ProfessionList_total(ctx context.Context, field graphql.CollectedField, obj *ProfessionList) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -7085,25 +7133,39 @@ func (ec *executionContext) _Profession(ctx context.Context, sel ast.SelectionSe
case "id": case "id":
out.Values[i] = ec._Profession_id(ctx, field, obj) out.Values[i] = ec._Profession_id(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ atomic.AddUint32(&invalids, 1)
} }
case "slug": case "slug":
out.Values[i] = ec._Profession_slug(ctx, field, obj) out.Values[i] = ec._Profession_slug(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ atomic.AddUint32(&invalids, 1)
} }
case "name": case "name":
out.Values[i] = ec._Profession_name(ctx, field, obj) out.Values[i] = ec._Profession_name(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ atomic.AddUint32(&invalids, 1)
} }
case "description": case "description":
out.Values[i] = ec._Profession_description(ctx, field, obj) out.Values[i] = ec._Profession_description(ctx, field, obj)
case "createdAt": case "createdAt":
out.Values[i] = ec._Profession_createdAt(ctx, field, obj) out.Values[i] = ec._Profession_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ atomic.AddUint32(&invalids, 1)
} }
case "qualifications":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Profession_qualifications(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -7967,6 +8029,43 @@ func (ec *executionContext) marshalNProfessionList2ᚖgithubᚗcomᚋzdamᚑegza
return ec._ProfessionList(ctx, sel, v) return ec._ProfessionList(ctx, sel, v)
} }
func (ec *executionContext) marshalNQualification2ᚕᚖgithubᚗcomᚋzdamᚑegzaminᚑzawodowyᚋbackendᚋinternalᚋmodelsᚐQualificationᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.Qualification) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNQualification2ᚖgithubᚗcomᚋzdamᚑegzaminᚑzawodowyᚋbackendᚋinternalᚋmodelsᚐQualification(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) marshalNQualification2ᚖgithubᚗcomᚋzdamᚑegzaminᚑzawodowyᚋbackendᚋinternalᚋmodelsᚐQualification(ctx context.Context, sel ast.SelectionSet, v *models.Qualification) graphql.Marshaler { func (ec *executionContext) marshalNQualification2ᚖgithubᚗcomᚋzdamᚑegzaminᚑzawodowyᚋbackendᚋinternalᚋmodelsᚐQualification(ctx context.Context, sel ast.SelectionSet, v *models.Qualification) graphql.Marshaler {
if v == nil { if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {

View File

@ -5,6 +5,7 @@ package resolvers
import ( import (
"context" "context"
"github.com/zdam-egzamin-zawodowy/backend/internal/gin/middleware"
"github.com/zdam-egzamin-zawodowy/backend/internal/graphql/generated" "github.com/zdam-egzamin-zawodowy/backend/internal/graphql/generated"
"github.com/zdam-egzamin-zawodowy/backend/internal/models" "github.com/zdam-egzamin-zawodowy/backend/internal/models"
@ -57,3 +58,15 @@ func (r *queryResolver) Profession(ctx context.Context, id *int, slug *string) (
return nil, nil return nil, nil
} }
func (r *professionResolver) Qualifications(
ctx context.Context,
obj *models.Profession,
) ([]*models.Qualification, error) {
if obj != nil {
if dataloader, err := middleware.DataLoaderFromContext(ctx); err == nil && dataloader != nil {
return dataloader.QualificationsByProfessionID.Load(obj.ID)
}
}
return []*models.Qualification{}, nil
}

View File

@ -23,8 +23,10 @@ type Resolver struct {
type mutationResolver struct{ *Resolver } type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
type professionResolver struct{ *Resolver }
type questionResolver struct{ *Resolver } type questionResolver struct{ *Resolver }
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
func (r *Resolver) Question() generated.QuestionResolver { return &questionResolver{r} } func (r *Resolver) Profession() generated.ProfessionResolver { return &professionResolver{r} }
func (r *Resolver) Question() generated.QuestionResolver { return &questionResolver{r} }

View File

@ -4,6 +4,7 @@ type Profession {
name: String! name: String!
description: String description: String
createdAt: Time! createdAt: Time!
qualifications: [Qualification!]! @goField(forceResolver: true)
} }
type ProfessionList { type ProfessionList {

View File

@ -19,4 +19,5 @@ type Repository interface {
UpdateMany(ctx context.Context, f *models.ProfessionFilter, 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) Delete(ctx context.Context, f *models.ProfessionFilter) ([]*models.Profession, error)
Fetch(ctx context.Context, cfg *FetchConfig) ([]*models.Profession, int, error) Fetch(ctx context.Context, cfg *FetchConfig) ([]*models.Profession, int, error)
GetAssociatedQualifications(ctx context.Context, ids ...int) (map[int][]*models.Qualification, error)
} }

View File

@ -1,8 +1,9 @@
package repository package repository
const ( const (
messageNameIsAlreadyTaken = "Istnieje już zawód o podanej nazwie." messageNameIsAlreadyTaken = "Istnieje już zawód o podanej nazwie."
messageFailedToSaveModel = "Wystąpił błąd podczas zapisywania zawodu, prosimy spróbować później." messageFailedToSaveModel = "Wystąpił błąd podczas zapisywania zawodu."
messageFailedToDeleteModel = "Wystąpił błąd podczas usuwania zawodu, prosimy spróbować później." messageFailedToDeleteModel = "Wystąpił błąd podczas usuwania zawodu."
messageFailedToFetchModel = "Wystąpił błąd podczas pobierania zawodów, prosimy spróbować później." messageFailedToFetchModel = "Wystąpił błąd podczas pobierania zawodów."
messageFailedToFetchAssociatedQualifications = "Wystąpił błąd poczas pobierania powiązanych kwalifikacji."
) )

View File

@ -3,6 +3,7 @@ package repository
import ( import (
"context" "context"
"fmt" "fmt"
sqlutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/sql"
"strings" "strings"
errorutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/error" errorutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/error"
@ -101,3 +102,26 @@ func (repo *pgRepository) Fetch(ctx context.Context, cfg *profession.FetchConfig
} }
return items, total, nil return items, total, nil
} }
func (repo *pgRepository) GetAssociatedQualifications(
ctx context.Context,
ids ...int,
) (map[int][]*models.Qualification, error) {
m := make(map[int][]*models.Qualification)
for _, id := range ids {
m[id] = []*models.Qualification{}
}
qualificationToProfession := []*models.QualificationToProfession{}
if err := repo.
Model(&qualificationToProfession).
Context(ctx).
Where(sqlutils.BuildConditionArray("profession_id"), pg.Array(ids)).
Relation("Qualification").
Select(); err != nil {
return nil, errorutils.Wrap(err, messageFailedToFetchAssociatedQualifications)
}
for _, record := range qualificationToProfession {
m[record.ProfessionID] = append(m[record.ProfessionID], record.Qualification)
}
return m, nil
}

View File

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

View File

@ -2,8 +2,8 @@ package repository
const ( const (
messageSimilarRecordExists = "Istnieje już podobne pytanie." messageSimilarRecordExists = "Istnieje już podobne pytanie."
messageFailedToSaveModel = "Wystąpił błąd podczas zapisywania pytania, prosimy spróbować później." messageFailedToSaveModel = "Wystąpił błąd podczas zapisywania pytania."
messageFailedToDeleteModel = "Wystąpił błąd podczas usuwania pytania, prosimy spróbować później." messageFailedToDeleteModel = "Wystąpił błąd podczas usuwania pytania."
messageFailedToFetchModel = "Wystąpił błąd podczas pobierania pytań, prosimy spróbować później." messageFailedToFetchModel = "Wystąpił błąd podczas pobierania pytań."
messageItemNotFound = "Nie znaleziono pytania." messageItemNotFound = "Nie znaleziono pytania."
) )

View File

@ -2,7 +2,7 @@ package repository
const ( const (
messageEmailIsAlreadyTaken = "Istnieje już użytkownik o podanym adresie e-mail." messageEmailIsAlreadyTaken = "Istnieje już użytkownik o podanym adresie e-mail."
messageFailedToSaveModel = "Wystąpił błąd podczas zapisywania użytkownika, prosimy spróbować później." messageFailedToSaveModel = "Wystąpił błąd podczas zapisywania użytkownika."
messageFailedToDeleteModel = "Wystąpił błąd podczas usuwania użytkownika, prosimy spróbować później." messageFailedToDeleteModel = "Wystąpił błąd podczas usuwania użytkownika."
messageFailedToFetchModel = "Wystąpił błąd podczas pobierania użytkowników, prosimy spróbować później." messageFailedToFetchModel = "Wystąpił błąd podczas pobierania użytkowników."
) )

View File

@ -123,6 +123,7 @@ func main() {
graphql.Use( graphql.Use(
middleware.GinContextToContext(), middleware.GinContextToContext(),
middleware.DataLoaderToContext(dataloader.Config{ middleware.DataLoaderToContext(dataloader.Config{
ProfessionRepo: professionRepository,
QualificationRepo: qualificationRepository, QualificationRepo: qualificationRepository,
}), }),
middleware.Authenticate(authUsecase), middleware.Authenticate(authUsecase),