Merge pull request #5 from zdam-egzamin-zawodowy/gqlgen

gqlgen configuration
This commit is contained in:
Dawid Wysokiński 2021-03-06 13:30:14 +01:00 committed by GitHub
commit 147b11b87a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 10202 additions and 47 deletions

5
go.mod
View File

@ -4,6 +4,7 @@ go 1.16
require (
github.com/99designs/gqlgen v0.13.0
github.com/agnivade/levenshtein v1.1.0 // indirect
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
@ -14,5 +15,9 @@ require (
github.com/pkg/errors v0.9.1
github.com/sethvargo/go-password v0.2.0
github.com/sirupsen/logrus v1.8.0
github.com/vektah/gqlparser/v2 v2.1.0
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

14
go.sum
View File

@ -4,8 +4,11 @@ github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyy
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@ -16,6 +19,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/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=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@ -212,8 +217,9 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-20210218155724-8ebf48af031b h1:lAZ0/chPUDWwjqosYR0X4M490zQhMsiJ4K3DbA7o+3g=
golang.org/x/sys v0.0.0-20210218155724-8ebf48af031b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b h1:ggRgirZABFolTmi3sn6Ivd9SipZwLedQ5wR0aAKnFxU=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -228,8 +234,9 @@ golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -261,8 +268,9 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -39,10 +39,10 @@ func UserFromContext(ctx context.Context) (*models.User, error) {
return nil, err
}
gc, ok := user.(*models.User)
u, ok := user.(*models.User)
if !ok {
err := fmt.Errorf("*models.User has wrong type")
return nil, err
}
return gc, nil
return u, nil
}

View File

@ -0,0 +1,36 @@
package middleware
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/zdam-egzamin-zawodowy/backend/internal/graphql/dataloader"
)
var (
dataLoaderToContext contextKey = "data_loader"
)
func DataLoaderToContext(cfg dataloader.Config) gin.HandlerFunc {
return func(c *gin.Context) {
ctx := context.WithValue(c.Request.Context(), dataLoaderToContext, dataloader.New(cfg))
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
func DataLoaderFromContext(ctx context.Context) (*dataloader.DataLoader, error) {
dataLoader := ctx.Value(dataLoaderToContext)
if dataLoader == nil {
err := fmt.Errorf("could not retrieve dataloader.DataLoader")
return nil, err
}
dl, ok := dataLoader.(*dataloader.DataLoader)
if !ok {
err := fmt.Errorf("dataloader.DataLoader has wrong type")
return nil, err
}
return dl, nil
}

View File

@ -0,0 +1,45 @@
package dataloader
import (
"context"
"time"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
"github.com/zdam-egzamin-zawodowy/backend/internal/qualification"
)
type Config struct {
QualificationRepo qualification.Repository
}
type DataLoader struct {
QualificationByID *QualificationLoader
}
func New(cfg Config) *DataLoader {
return &DataLoader{
QualificationByID: NewQualificationLoader(QualificationLoaderConfig{
Wait: 2 * time.Millisecond,
Fetch: func(ids []int) ([]*models.Qualification, []error) {
qualificationsNotInOrder, _, err := cfg.QualificationRepo.Fetch(context.Background(), &qualification.FetchConfig{
Filter: &models.QualificationFilter{
ID: ids,
},
Count: false,
})
if err != nil {
return nil, []error{err}
}
qualificationByID := make(map[int]*models.Qualification)
for _, qualification := range qualificationsNotInOrder {
qualificationByID[qualification.ID] = qualification
}
qualifications := make([]*models.Qualification, len(ids))
for i, id := range ids {
qualifications[i] = qualificationByID[id]
}
return qualifications, nil
},
}),
}
}

View File

@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package dataloader
import (
"sync"
"time"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
)
// QualificationLoaderConfig captures the config to create a new QualificationLoader
type QualificationLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Qualification, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewQualificationLoader creates a new QualificationLoader given a fetch, wait, and maxBatch
func NewQualificationLoader(config QualificationLoaderConfig) *QualificationLoader {
return &QualificationLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// QualificationLoader batches and caches requests
type QualificationLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Qualification, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Qualification
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *qualificationLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type qualificationLoaderBatch struct {
keys []int
data []*models.Qualification
error []error
closing bool
done chan struct{}
}
// Load a Qualification by key, batching and caching will be applied automatically
func (l *QualificationLoader) Load(key int) (*models.Qualification, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Qualification.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *QualificationLoader) LoadThunk(key int) func() (*models.Qualification, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Qualification, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &qualificationLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Qualification, error) {
<-batch.done
var data *models.Qualification
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *QualificationLoader) LoadAll(keys []int) ([]*models.Qualification, []error) {
results := make([]func() (*models.Qualification, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
qualifications := make([]*models.Qualification, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
qualifications[i], errors[i] = thunk()
}
return qualifications, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Qualifications.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *QualificationLoader) LoadAllThunk(keys []int) func() ([]*models.Qualification, []error) {
results := make([]func() (*models.Qualification, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Qualification, []error) {
qualifications := make([]*models.Qualification, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
qualifications[i], errors[i] = thunk()
}
return qualifications, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *QualificationLoader) Prime(key int, value *models.Qualification) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *QualificationLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *QualificationLoader) unsafeSet(key int, value *models.Qualification) {
if l.cache == nil {
l.cache = map[int]*models.Qualification{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *qualificationLoaderBatch) keyIndex(l *QualificationLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *qualificationLoaderBatch) startTimer(l *QualificationLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *qualificationLoaderBatch) end(l *QualificationLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@ -0,0 +1,77 @@
package httpdelivery
import (
"fmt"
"time"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/gin-gonic/gin"
"github.com/zdam-egzamin-zawodowy/backend/internal/graphql/directive"
"github.com/zdam-egzamin-zawodowy/backend/internal/graphql/generated"
"github.com/zdam-egzamin-zawodowy/backend/internal/graphql/resolvers"
"github.com/zdam-egzamin-zawodowy/backend/pkg/mode"
)
const (
playgroundTTL = time.Hour / time.Second
graphqlEndpoint = "/graphql"
playgroundEndpoint = "/"
)
type Config struct {
Resolver *resolvers.Resolver
Directive *directive.Directive
}
func Attach(group *gin.RouterGroup, cfg Config) error {
if cfg.Resolver == nil {
return fmt.Errorf("Graphql resolver cannot be nil")
}
gqlHandler := graphqlHandler(cfg.Resolver, cfg.Directive)
group.GET(graphqlEndpoint, gqlHandler)
group.POST(graphqlEndpoint, gqlHandler)
if mode.Get() == mode.DevelopmentMode {
group.GET(playgroundEndpoint, playgroundHandler())
}
return nil
}
// Defining the GraphQL handler
func graphqlHandler(r *resolvers.Resolver, d *directive.Directive) gin.HandlerFunc {
cfg := generated.Config{Resolvers: r}
cfg.Directives.Authenticated = d.Authenticated
cfg.Directives.HasRole = d.HasRole
srv := handler.New(generated.NewExecutableSchema(cfg))
srv.AddTransport(transport.GET{})
srv.AddTransport(transport.POST{})
srv.AddTransport(transport.MultipartForm{
MaxUploadSize: 32 << 18,
MaxMemory: 32 << 18,
})
srv.Use(extension.AutomaticPersistedQuery{
Cache: lru.New(100),
})
if mode.Get() == mode.DevelopmentMode {
srv.Use(extension.Introspection{})
}
return func(c *gin.Context) {
c.Header("Cache-Control", "no-store, must-revalidate")
srv.ServeHTTP(c.Writer, c.Request)
}
}
// Defining the Playground handler
func playgroundHandler() gin.HandlerFunc {
h := playground.Handler("Playground", graphqlEndpoint)
return func(c *gin.Context) {
c.Header("Cache-Control", fmt.Sprintf(`public, max-age=%d`, playgroundTTL))
h.ServeHTTP(c.Writer, c.Request)
}
}

View File

@ -0,0 +1,36 @@
package directive
import (
"context"
"fmt"
"github.com/99designs/gqlgen/graphql"
"github.com/zdam-egzamin-zawodowy/backend/internal/gin/middleware"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
errorutils "github.com/zdam-egzamin-zawodowy/backend/pkg/utils/error"
)
type Directive struct{}
func (d *Directive) Authenticated(ctx context.Context, obj interface{}, next graphql.Resolver, yes bool) (interface{}, error) {
_, err := middleware.UserFromContext(ctx)
if yes && err != nil {
return nil, errorutils.Wrap(err, messageMustBeSignedIn)
} else if !yes && err == nil {
return nil, fmt.Errorf(messageMustBeSignedOut)
}
return next(ctx)
}
func (d *Directive) HasRole(ctx context.Context, obj interface{}, next graphql.Resolver, role models.Role) (interface{}, error) {
user, err := middleware.UserFromContext(ctx)
if err != nil {
return nil, errorutils.Wrap(err, messageMustBeSignedIn)
}
if user.Role != role {
return nil, fmt.Errorf(messageUnauthorized)
}
return next(ctx)
}

View File

@ -0,0 +1,7 @@
package directive
const (
messageMustBeSignedIn = "Musisz być zalogowany by wykonać daną akcje."
messageMustBeSignedOut = "Musisz być wylogowany by wykonać daną akcje."
messageUnauthorized = "Brak uprawnień."
)

View File

@ -1,5 +1,6 @@
#!/bin/sh
cd ./internal/graphql
go get -u github.com/99designs/gqlgen
go run github.com/99designs/gqlgen
go mod tidy
cd ../..

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package generated
import (
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
)
type ProfessionList struct {
Total int `json:"total"`
Items []*models.Profession `json:"items"`
}
type QualificationList struct {
Total int `json:"total"`
Items []*models.Qualification `json:"items"`
}
type QuestionList struct {
Total int `json:"total"`
Items []*models.Question `json:"items"`
}
type UserList struct {
Total int `json:"total"`
Items []*models.User `json:"items"`
}
type UserWithToken struct {
Token string `json:"token"`
User *models.User `json:"user"`
}

View File

@ -10,4 +10,58 @@ resolver:
filename: resolvers/resolver.go
package: resolvers
type: Resolver
struct_tag: gqlgen
struct_tag: gqlgen
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Role:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.Role
User:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.User
UserFilter:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.UserFilter
UserInput:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.UserInput
UpdateManyUsersInput:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.UserInput
Profession:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.Profession
ProfessionFilter:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.ProfessionFilter
ProfessionInput:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.ProfessionInput
Qualification:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.Qualification
QualificationFilter:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.QualificationFilter
QualificationFilterOr:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.QualificationFilterOr
QualificationInput:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.QualificationInput
Answer:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.Answer
Question:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.Question
QuestionFilter:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.QuestionFilter
QuestionInput:
model:
- github.com/zdam-egzamin-zawodowy/backend/internal/models.QuestionInput

View File

@ -0,0 +1,16 @@
package resolvers
import (
"context"
"github.com/99designs/gqlgen/graphql"
)
func shouldCount(ctx context.Context) bool {
for _, field := range graphql.CollectFieldsCtx(ctx, nil) {
if field.Name == "total" {
return true
}
}
return false
}

View File

@ -0,0 +1,59 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"github.com/zdam-egzamin-zawodowy/backend/internal/graphql/generated"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
"github.com/zdam-egzamin-zawodowy/backend/internal/profession"
"github.com/zdam-egzamin-zawodowy/backend/pkg/utils"
)
func (r *mutationResolver) CreateProfession(ctx context.Context, input models.ProfessionInput) (*models.Profession, error) {
return r.ProfessionUsecase.Store(ctx, &input)
}
func (r *mutationResolver) UpdateProfession(ctx context.Context, id int, input models.ProfessionInput) (*models.Profession, error) {
return r.ProfessionUsecase.UpdateOneByID(ctx, id, &input)
}
func (r *mutationResolver) DeleteProfessions(ctx context.Context, ids []int) ([]*models.Profession, error) {
return r.ProfessionUsecase.Delete(ctx, &models.ProfessionFilter{
ID: ids,
})
}
func (r *queryResolver) Professions(
ctx context.Context,
filter *models.ProfessionFilter,
limit *int,
offset *int,
sort []string,
) (*generated.ProfessionList, error) {
var err error
list := &generated.ProfessionList{}
list.Items, list.Total, err = r.ProfessionUsecase.Fetch(
ctx,
&profession.FetchConfig{
Count: shouldCount(ctx),
Filter: filter,
Limit: utils.SafeIntPointer(limit, profession.DefaultLimit),
Offset: utils.SafeIntPointer(offset, 0),
Sort: sort,
},
)
return list, err
}
func (r *queryResolver) Profession(ctx context.Context, id *int, slug *string) (*models.Profession, error) {
if id != nil {
return r.ProfessionUsecase.GetByID(ctx, *id)
} else if slug != nil {
return r.ProfessionUsecase.GetBySlug(ctx, *slug)
}
return nil, nil
}

View File

@ -0,0 +1,59 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"github.com/zdam-egzamin-zawodowy/backend/internal/graphql/generated"
"github.com/zdam-egzamin-zawodowy/backend/internal/models"
"github.com/zdam-egzamin-zawodowy/backend/internal/qualification"
"github.com/zdam-egzamin-zawodowy/backend/pkg/utils"
)
func (r *mutationResolver) CreateQualification(ctx context.Context, input models.QualificationInput) (*models.Qualification, error) {
return r.QualificationUsecase.Store(ctx, &input)
}
func (r *mutationResolver) UpdateQualification(ctx context.Context, id int, input models.QualificationInput) (*models.Qualification, error) {
return r.QualificationUsecase.UpdateOneByID(ctx, id, &input)
}
func (r *mutationResolver) DeleteQualifications(ctx context.Context, ids []int) ([]*models.Qualification, error) {
return r.QualificationUsecase.Delete(ctx, &models.QualificationFilter{
ID: ids,
})
}
func (r *queryResolver) Qualifications(
ctx context.Context,
filter *models.QualificationFilter,
limit *int,
offset *int,
sort []string,
) (*generated.QualificationList, error) {
var err error
list := &generated.QualificationList{}
list.Items, list.Total, err = r.QualificationUsecase.Fetch(
ctx,
&qualification.FetchConfig{
Count: shouldCount(ctx),
Filter: filter,
Limit: utils.SafeIntPointer(limit, qualification.DefaultLimit),
Offset: utils.SafeIntPointer(offset, 0),
Sort: sort,
},
)
return list, err
}
func (r *queryResolver) Qualification(ctx context.Context, id *int, slug *string) (*models.Qualification, error) {
if id != nil {
return r.QualificationUsecase.GetByID(ctx, *id)
} else if slug != nil {
return r.QualificationUsecase.GetBySlug(ctx, *slug)
}
return nil, nil
}

View File

@ -0,0 +1,71 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"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/models"
"github.com/zdam-egzamin-zawodowy/backend/internal/question"
"github.com/zdam-egzamin-zawodowy/backend/pkg/utils"
)
func (r *mutationResolver) CreateQuestion(ctx context.Context, input models.QuestionInput) (*models.Question, error) {
return r.QuestionUsecase.Store(ctx, &input)
}
func (r *mutationResolver) UpdateQuestion(ctx context.Context, id int, input models.QuestionInput) (*models.Question, error) {
return r.QuestionUsecase.UpdateOneByID(ctx, id, &input)
}
func (r *mutationResolver) DeleteQuestions(ctx context.Context, ids []int) ([]*models.Question, error) {
return r.QuestionUsecase.Delete(ctx, &models.QuestionFilter{
ID: ids,
})
}
func (r *mutationResolver) GenerateTest(ctx context.Context, qualificationIDs []int, limit *int) ([]*models.Question, error) {
return r.QuestionUsecase.GenerateTest(ctx, &question.GenerateTestConfig{
Qualifications: qualificationIDs,
Limit: utils.SafeIntPointer(limit, question.TestMaxLimit),
})
}
func (r *queryResolver) Questions(
ctx context.Context,
filter *models.QuestionFilter,
limit *int,
offset *int,
sort []string,
) (*generated.QuestionList, error) {
var err error
list := &generated.QuestionList{}
list.Items, list.Total, err = r.QuestionUsecase.Fetch(
ctx,
&question.FetchConfig{
Count: shouldCount(ctx),
Filter: filter,
Limit: utils.SafeIntPointer(limit, question.DefaultLimit),
Offset: utils.SafeIntPointer(offset, 0),
Sort: sort,
},
)
return list, err
}
func (r *questionResolver) Qualification(ctx context.Context, obj *models.Question) (*models.Qualification, error) {
if obj != nil && obj.Qualification != nil {
return obj.Qualification, nil
}
if obj != nil || obj.QualificationID > 0 {
if dataloader, err := middleware.DataLoaderFromContext(ctx); err == nil && dataloader != nil {
return dataloader.QualificationByID.Load(obj.QualificationID)
}
}
return nil, nil
}

View File

@ -0,0 +1,30 @@
package resolvers
import (
"github.com/zdam-egzamin-zawodowy/backend/internal/auth"
"github.com/zdam-egzamin-zawodowy/backend/internal/graphql/generated"
"github.com/zdam-egzamin-zawodowy/backend/internal/profession"
"github.com/zdam-egzamin-zawodowy/backend/internal/qualification"
"github.com/zdam-egzamin-zawodowy/backend/internal/question"
"github.com/zdam-egzamin-zawodowy/backend/internal/user"
)
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct {
AuthUsecase auth.Usecase
UserUsecase user.Usecase
ProfessionUsecase profession.Usecase
QualificationUsecase qualification.Usecase
QuestionUsecase question.Usecase
}
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type questionResolver struct{ *Resolver }
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
func (r *Resolver) Question() generated.QuestionResolver { return &questionResolver{r} }

View File

@ -0,0 +1,89 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"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/models"
"github.com/zdam-egzamin-zawodowy/backend/internal/user"
"github.com/zdam-egzamin-zawodowy/backend/pkg/utils"
)
func (r *mutationResolver) CreateUser(ctx context.Context, input models.UserInput) (*models.User, error) {
return r.UserUsecase.Store(ctx, &input)
}
func (r *mutationResolver) UpdateUser(ctx context.Context, id int, input models.UserInput) (*models.User, error) {
return r.UserUsecase.UpdateOneByID(ctx, id, &input)
}
func (r *mutationResolver) UpdateManyUsers(ctx context.Context, ids []int, input models.UserInput) ([]*models.User, error) {
return r.UserUsecase.UpdateMany(
ctx,
&models.UserFilter{
ID: ids,
},
&input,
)
}
func (r *mutationResolver) DeleteUsers(ctx context.Context, ids []int) ([]*models.User, error) {
return r.UserUsecase.Delete(ctx, &models.UserFilter{
ID: ids,
})
}
func (r *mutationResolver) SignIn(
ctx context.Context,
email string,
password string,
staySignedIn *bool,
) (*generated.UserWithToken, error) {
var err error
userWithToken := &generated.UserWithToken{}
userWithToken.User, userWithToken.Token, err = r.AuthUsecase.SignIn(
ctx,
email,
password,
utils.SafeBoolPointer(staySignedIn, false),
)
if err != nil {
return nil, err
}
return userWithToken, nil
}
func (r *queryResolver) Users(
ctx context.Context,
filter *models.UserFilter,
limit *int,
offset *int,
sort []string,
) (*generated.UserList, error) {
var err error
userList := &generated.UserList{}
userList.Items, userList.Total, err = r.UserUsecase.Fetch(
ctx,
&user.FetchConfig{
Count: shouldCount(ctx),
Filter: filter,
Limit: utils.SafeIntPointer(limit, user.DefaultLimit),
Offset: utils.SafeIntPointer(offset, 0),
Sort: sort,
},
)
return userList, err
}
func (r *queryResolver) User(ctx context.Context, id int) (*models.User, error) {
return r.UserUsecase.GetByID(ctx, id)
}
func (r *queryResolver) Me(ctx context.Context) (*models.User, error) {
user, _ := middleware.UserFromContext(ctx)
return user, nil
}

View File

@ -0,0 +1,7 @@
directive @goField(
forceResolver: Boolean
name: String
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
directive @authenticated(yes: Boolean!) on FIELD_DEFINITION
directive @hasRole(role: Role!) on FIELD_DEFINITION

View File

@ -0,0 +1,61 @@
type Profession {
id: ID!
slug: String!
name: String!
description: String
createdAt: Time!
}
type ProfessionList {
total: Int!
items: [Profession!]
}
input ProfessionInput {
name: String
description: String
}
input ProfessionFilter {
id: [ID!]
idNEQ: [ID!]
slug: [String!]
slugNEQ: [String!]
name: [String!]
nameNEQ: [String!]
nameIEQ: String
nameMATCH: String
descriptionIEQ: String
descriptionMATCH: String
createdAt: Time
createdAtGT: Time
createdAtGTE: Time
createdAtLT: Time
createdAtLTE: Time
}
extend type Query {
professions(
filter: ProfessionFilter
limit: Int
offset: Int
sort: [String!]
): ProfessionList!
profession(id: Int, slug: String): Profession
}
extend type Mutation {
createProfession(input: ProfessionInput!): Profession
@authenticated(yes: true)
@hasRole(role: ADMIN)
updateProfession(id: ID!, input: ProfessionInput!): Profession
@authenticated(yes: true)
@hasRole(role: ADMIN)
deleteProfessions(ids: [ID!]!): [Profession!]
@authenticated(yes: true)
@hasRole(role: ADMIN)
}

View File

@ -0,0 +1,87 @@
type Qualification {
id: ID!
slug: String!
name: String!
code: String!
formula: String
description: String
createdAt: Time!
}
type QualificationList {
total: Int!
items: [Qualification!]
}
input QualificationInput {
name: String
description: String
code: String
formula: String
associateProfession: [Int!]
dissociateProfession: [Int!]
}
input QualificationFilterOr {
nameMatch: String
nameIEQ: String
codeMatch: String
codeIEQ: String
}
input QualificationFilter {
id: [ID!]
idNEQ: [ID!]
slug: [String!]
slugNEQ: [String!]
formula: [String!]
formulaNEQ: [String!]
name: [String!]
nameNEQ: [String!]
nameIEQ: String
nameMATCH: String
code: [String!]
codeNEQ: [String!]
codeIEQ: String
codeMATCH: String
descriptionIEQ: String
descriptionMATCH: String
professionID: [Int!]
createdAt: Time
createdAtGT: Time
createdAtGTE: Time
createdAtLT: Time
createdAtLTE: Time
or: QualificationFilterOr
}
extend type Query {
qualifications(
filter: QualificationFilter
limit: Int
offset: Int
sort: [String!]
): QualificationList!
qualification(id: Int, slug: String): Qualification
}
extend type Mutation {
createQualification(input: QualificationInput!): Qualification
@authenticated(yes: true)
@hasRole(role: ADMIN)
updateQualification(id: ID!, input: QualificationInput!): Qualification
@authenticated(yes: true)
@hasRole(role: ADMIN)
deleteQualifications(ids: [ID!]!): [Qualification!]
@authenticated(yes: true)
@hasRole(role: ADMIN)
}

View File

@ -0,0 +1,95 @@
enum Answer {
A
B
C
D
}
type Question {
id: ID!
from: String
content: String!
explanation: String
correctAnswer: Answer!
image: String
answerA: Answer
answerAImage: String
answerB: Answer
answerBImage: String
answerC: Answer
answerCImage: String
answerD: Answer
answerDImage: String
qualification: Qualification @goField(forceResolver: true)
createdAt: Time!
updatedAt: Time!
}
type QuestionList {
total: Int!
items: [Question!]
}
input QuestionInput {
content: String
from: String
explanation: String
correctAnswer: Answer
qualificationID: Int
image: Upload
deleteImage: Boolean
answerA: Answer
answerAImage: Upload
deleteAnswerAImage: Boolean
answerB: Answer
answerBImage: Upload
deleteAnswerBImage: Boolean
answerC: Answer
answerCImage: Upload
deleteAnswerCImage: Boolean
answerD: Answer
answerDImage: Upload
deleteAnswerDImage: Boolean
}
input QuestionFilter {
id: [ID!]
idNEQ: [ID!]
from: [String!]
contentIEQ: String
contentMATCH: String
qualificationID: [Int!]
qualificationIDNEQ: [Int!]
qualificationFilter: QualificationFilter
createdAt: Time
createdAtGT: Time
createdAtGTE: Time
createdAtLT: Time
createdAtLTE: Time
}
extend type Query {
questions(
filter: QuestionFilter
limit: Int
offset: Int
sort: [String!]
): QuestionList! @authenticated(yes: true) @hasRole(role: ADMIN)
}
extend type Mutation {
createQuestion(input: QuestionInput!): Question
@authenticated(yes: true)
@hasRole(role: ADMIN)
updateQuestion(id: ID!, input: QuestionInput!): Question
@authenticated(yes: true)
@hasRole(role: ADMIN)
deleteQuestions(ids: [ID!]!): [Question!]
@authenticated(yes: true)
@hasRole(role: ADMIN)
generateTest(qualificationIDs: [ID!]!, limit: Int): [Question!]
}

View File

@ -0,0 +1,2 @@
scalar Time
scalar Upload

View File

@ -0,0 +1,93 @@
enum Role {
ADMIN
USER
}
type User {
id: ID!
displayName: String!
role: Role!
email: String!
activated: Boolean!
createdAt: Time!
}
type UserList {
total: Int!
items: [User!]
}
input UserInput {
displayName: String
password: String
email: String
role: Role
activated: Boolean
}
input UpdateManyUsersInput {
role: Role
activated: Boolean
}
input UserFilter {
id: [ID!]
idNEQ: [ID!]
activated: Boolean
displayName: [String!]
displayNameNEQ: [String!]
displayNameIEQ: String
displayNameMATCH: String
email: [String!]
emailNEQ: [String!]
emailIEQ: String
emailMATCH: String
role: [Role!]
roleNEQ: [Role!]
createdAt: Time
createdAtGT: Time
createdAtGTE: Time
createdAtLT: Time
createdAtLTE: Time
}
type UserWithToken {
token: String!
user: User!
}
extend type Query {
users(
filter: UserFilter
limit: Int
offset: Int
sort: [String!]
): UserList! @authenticated(yes: true) @hasRole(role: ADMIN)
user(id: Int!): User @authenticated(yes: true) @hasRole(role: ADMIN)
me: User
}
extend type Mutation {
createUser(input: UserInput!): User
@authenticated(yes: true)
@hasRole(role: ADMIN)
updateUser(id: ID!, input: UserInput!): User
@authenticated(yes: true)
@hasRole(role: ADMIN)
updateManyUsers(ids: [ID!]!, input: UpdateManyUsersInput!): [User!]
@authenticated(yes: true)
@hasRole(role: ADMIN)
deleteUsers(ids: [ID!]!): [User!]
@authenticated(yes: true)
@hasRole(role: ADMIN)
signIn(
email: String!
password: String!
staySignedIn: Boolean
): UserWithToken @authenticated(yes: false)
}

View File

@ -47,12 +47,10 @@ func (input *ProfessionInput) IsEmpty() bool {
func (input *ProfessionInput) Sanitize() *ProfessionInput {
if input.Name != nil {
trimmed := strings.TrimSpace(*input.Name)
input.Name = &trimmed
*input.Name = strings.TrimSpace(*input.Name)
}
if input.Description != nil {
trimmed := strings.TrimSpace(*input.Description)
input.Description = &trimmed
*input.Description = strings.TrimSpace(*input.Description)
}
return input

View File

@ -66,20 +66,16 @@ func (input *QualificationInput) IsEmpty() bool {
func (input *QualificationInput) Sanitize() *QualificationInput {
if input.Name != nil {
trimmed := strings.TrimSpace(*input.Name)
input.Name = &trimmed
*input.Name = strings.TrimSpace(*input.Name)
}
if input.Description != nil {
trimmed := strings.TrimSpace(*input.Description)
input.Description = &trimmed
*input.Description = strings.TrimSpace(*input.Description)
}
if input.Code != nil {
trimmed := strings.TrimSpace(*input.Code)
input.Code = &trimmed
*input.Code = strings.TrimSpace(*input.Code)
}
if input.Formula != nil {
trimmed := strings.TrimSpace(*input.Formula)
input.Formula = &trimmed
*input.Formula = strings.TrimSpace(*input.Formula)
}
return input
@ -155,6 +151,9 @@ type QualificationFilter struct {
Slug []string `json:"slug" xml:"slug" gqlgen:"slug"`
SlugNEQ []string `json:"slugNEQ" xml:"slugNEQ" gqlgen:"slugNEQ"`
Formula []string `gqlgen:"formula" json:"formula" xml:"formula"`
FormulaNEQ []string `json:"formulaNEQ" xml:"formulaNEQ" gqlgen:"formulaNEQ"`
Name []string `gqlgen:"name" json:"name" xml:"name"`
NameNEQ []string `json:"nameNEQ" xml:"nameNEQ" gqlgen:"nameNEQ"`
NameMATCH string `json:"nameMATCH" xml:"nameMATCH" gqlgen:"nameMATCH"`
@ -165,9 +164,6 @@ type QualificationFilter struct {
CodeMATCH string `json:"codeMATCH" xml:"codeMATCH" gqlgen:"codeMATCH"`
CodeIEQ string `gqlgen:"codeIEQ" json:"codeIEQ" xml:"codeIEQ"`
Formula []string `gqlgen:"formula" json:"formula" xml:"formula"`
FormulaNEQ []string `json:"formulaNEQ" xml:"formulaNEQ" gqlgen:"formulaNEQ"`
DescriptionMATCH string `gqlgen:"descriptionMATCH" json:"descriptionMATCH" xml:"descriptionMATCH"`
DescriptionIEQ string `json:"descriptionIEQ" xml:"descriptionIEQ" gqlgen:"descriptionIEQ"`

View File

@ -83,16 +83,13 @@ func (input *QuestionInput) IsEmpty() bool {
func (input *QuestionInput) Sanitize() *QuestionInput {
if input.Content != nil {
trimmed := strings.TrimSpace(*input.Content)
input.Content = &trimmed
*input.Content = strings.TrimSpace(*input.Content)
}
if input.From != nil {
trimmed := strings.TrimSpace(*input.From)
input.From = &trimmed
*input.From = strings.TrimSpace(*input.From)
}
if input.Explanation != nil {
trimmed := strings.TrimSpace(*input.Explanation)
input.Explanation = &trimmed
*input.Explanation = strings.TrimSpace(*input.Explanation)
}
return input

View File

@ -10,8 +10,8 @@ import (
type Role string
const (
RoleUser Role = "user"
RoleAdmin Role = "admin"
RoleUser Role = "user"
)
func (role Role) IsValid() bool {

View File

@ -68,6 +68,20 @@ func (input *UserInput) IsEmpty() bool {
input.Activated == nil
}
func (input *UserInput) Sanitize() *UserInput {
if input.DisplayName != nil {
*input.DisplayName = strings.TrimSpace(*input.DisplayName)
}
if input.Password != nil {
*input.Password = strings.ToLower(strings.TrimSpace(*input.Password))
}
if input.Email != nil {
*input.Email = strings.ToLower(strings.TrimSpace(*input.Email))
}
return input
}
func (input *UserInput) ToUser() *User {
u := &User{
Activated: input.Activated,
@ -87,25 +101,6 @@ 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 {

15
pkg/utils/safe_pointer.go Normal file
View File

@ -0,0 +1,15 @@
package utils
func SafeBoolPointer(v *bool, def bool) bool {
if v == nil {
return def
}
return *v
}
func SafeIntPointer(s *int, def int) int {
if s == nil {
return def
}
return *s
}