Merge pull request #5 from zdam-egzamin-zawodowy/gqlgen
gqlgen configuration
This commit is contained in:
commit
147b11b87a
5
go.mod
5
go.mod
|
@ -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
14
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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ń."
|
||||
)
|
|
@ -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
|
@ -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"`
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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} }
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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!]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
scalar Time
|
||||
scalar Upload
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
type Role string
|
||||
|
||||
const (
|
||||
RoleUser Role = "user"
|
||||
RoleAdmin Role = "admin"
|
||||
RoleUser Role = "user"
|
||||
)
|
||||
|
||||
func (role Role) IsValid() bool {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
Reference in New Issue