Stash box client interface (#751)

* Add gql client generation files
* Update dependencies
* Add stash-box client generation to the makefile
* Move scraped scene object matchers to models
* Add stash-box to scrape with dropdown
* Add scrape scene from fingerprint in UI
This commit is contained in:
WithoutPants
2020-09-17 19:57:18 +10:00
committed by GitHub
parent b0b5621337
commit 7a45943e8e
324 changed files with 34978 additions and 17323 deletions

29
vendor/github.com/99designs/gqlgen/graphql/cache.go generated vendored Normal file
View File

@@ -0,0 +1,29 @@
package graphql
import "context"
// Cache is a shared store for APQ and query AST caching
type Cache interface {
// Get looks up a key's value from the cache.
Get(ctx context.Context, key string) (value interface{}, ok bool)
// Add adds a value to the cache.
Add(ctx context.Context, key string, value interface{})
}
// MapCache is the simplest implementation of a cache, because it can not evict it should only be used in tests
type MapCache map[string]interface{}
// Get looks up a key's value from the cache.
func (m MapCache) Get(ctx context.Context, key string) (value interface{}, ok bool) {
v, ok := m[key]
return v, ok
}
// Add adds a value to the cache.
func (m MapCache) Add(ctx context.Context, key string, value interface{}) { m[key] = value }
type NoCache struct{}
func (n NoCache) Get(ctx context.Context, key string) (value interface{}, ok bool) { return nil, false }
func (n NoCache) Add(ctx context.Context, key string, value interface{}) {}

View File

@@ -1,274 +0,0 @@
package graphql
import (
"context"
"fmt"
"sync"
"github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/gqlerror"
)
type Resolver func(ctx context.Context) (res interface{}, err error)
type FieldMiddleware func(ctx context.Context, next Resolver) (res interface{}, err error)
type RequestMiddleware func(ctx context.Context, next func(ctx context.Context) []byte) []byte
type ComplexityLimitFunc func(ctx context.Context) int
type RequestContext struct {
RawQuery string
Variables map[string]interface{}
Doc *ast.QueryDocument
ComplexityLimit int
OperationComplexity int
DisableIntrospection bool
// ErrorPresenter will be used to generate the error
// message from errors given to Error().
ErrorPresenter ErrorPresenterFunc
Recover RecoverFunc
ResolverMiddleware FieldMiddleware
DirectiveMiddleware FieldMiddleware
RequestMiddleware RequestMiddleware
Tracer Tracer
errorsMu sync.Mutex
Errors gqlerror.List
extensionsMu sync.Mutex
Extensions map[string]interface{}
}
func DefaultResolverMiddleware(ctx context.Context, next Resolver) (res interface{}, err error) {
return next(ctx)
}
func DefaultDirectiveMiddleware(ctx context.Context, next Resolver) (res interface{}, err error) {
return next(ctx)
}
func DefaultRequestMiddleware(ctx context.Context, next func(ctx context.Context) []byte) []byte {
return next(ctx)
}
func NewRequestContext(doc *ast.QueryDocument, query string, variables map[string]interface{}) *RequestContext {
return &RequestContext{
Doc: doc,
RawQuery: query,
Variables: variables,
ResolverMiddleware: DefaultResolverMiddleware,
DirectiveMiddleware: DefaultDirectiveMiddleware,
RequestMiddleware: DefaultRequestMiddleware,
Recover: DefaultRecover,
ErrorPresenter: DefaultErrorPresenter,
Tracer: &NopTracer{},
}
}
type key string
const (
request key = "request_context"
resolver key = "resolver_context"
)
func GetRequestContext(ctx context.Context) *RequestContext {
if val, ok := ctx.Value(request).(*RequestContext); ok {
return val
}
return nil
}
func WithRequestContext(ctx context.Context, rc *RequestContext) context.Context {
return context.WithValue(ctx, request, rc)
}
type ResolverContext struct {
Parent *ResolverContext
// The name of the type this field belongs to
Object string
// These are the args after processing, they can be mutated in middleware to change what the resolver will get.
Args map[string]interface{}
// The raw field
Field CollectedField
// The index of array in path.
Index *int
// The result object of resolver
Result interface{}
// IsMethod indicates if the resolver is a method
IsMethod bool
}
func (r *ResolverContext) Path() []interface{} {
var path []interface{}
for it := r; it != nil; it = it.Parent {
if it.Index != nil {
path = append(path, *it.Index)
} else if it.Field.Field != nil {
path = append(path, it.Field.Alias)
}
}
// because we are walking up the chain, all the elements are backwards, do an inplace flip.
for i := len(path)/2 - 1; i >= 0; i-- {
opp := len(path) - 1 - i
path[i], path[opp] = path[opp], path[i]
}
return path
}
func GetResolverContext(ctx context.Context) *ResolverContext {
if val, ok := ctx.Value(resolver).(*ResolverContext); ok {
return val
}
return nil
}
func WithResolverContext(ctx context.Context, rc *ResolverContext) context.Context {
rc.Parent = GetResolverContext(ctx)
return context.WithValue(ctx, resolver, rc)
}
// This is just a convenient wrapper method for CollectFields
func CollectFieldsCtx(ctx context.Context, satisfies []string) []CollectedField {
resctx := GetResolverContext(ctx)
return CollectFields(GetRequestContext(ctx), resctx.Field.Selections, satisfies)
}
// CollectAllFields returns a slice of all GraphQL field names that were selected for the current resolver context.
// The slice will contain the unique set of all field names requested regardless of fragment type conditions.
func CollectAllFields(ctx context.Context) []string {
resctx := GetResolverContext(ctx)
collected := CollectFields(GetRequestContext(ctx), resctx.Field.Selections, nil)
uniq := make([]string, 0, len(collected))
Next:
for _, f := range collected {
for _, name := range uniq {
if name == f.Name {
continue Next
}
}
uniq = append(uniq, f.Name)
}
return uniq
}
// Errorf sends an error string to the client, passing it through the formatter.
func (c *RequestContext) Errorf(ctx context.Context, format string, args ...interface{}) {
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
c.Errors = append(c.Errors, c.ErrorPresenter(ctx, fmt.Errorf(format, args...)))
}
// Error sends an error to the client, passing it through the formatter.
func (c *RequestContext) Error(ctx context.Context, err error) {
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
c.Errors = append(c.Errors, c.ErrorPresenter(ctx, err))
}
// HasError returns true if the current field has already errored
func (c *RequestContext) HasError(rctx *ResolverContext) bool {
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
path := rctx.Path()
for _, err := range c.Errors {
if equalPath(err.Path, path) {
return true
}
}
return false
}
// GetErrors returns a list of errors that occurred in the current field
func (c *RequestContext) GetErrors(rctx *ResolverContext) gqlerror.List {
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
path := rctx.Path()
var errs gqlerror.List
for _, err := range c.Errors {
if equalPath(err.Path, path) {
errs = append(errs, err)
}
}
return errs
}
func equalPath(a []interface{}, b []interface{}) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}
// AddError is a convenience method for adding an error to the current response
func AddError(ctx context.Context, err error) {
GetRequestContext(ctx).Error(ctx, err)
}
// AddErrorf is a convenience method for adding an error to the current response
func AddErrorf(ctx context.Context, format string, args ...interface{}) {
GetRequestContext(ctx).Errorf(ctx, format, args...)
}
// RegisterExtension registers an extension, returns error if extension has already been registered
func (c *RequestContext) RegisterExtension(key string, value interface{}) error {
c.extensionsMu.Lock()
defer c.extensionsMu.Unlock()
if c.Extensions == nil {
c.Extensions = make(map[string]interface{})
}
if _, ok := c.Extensions[key]; ok {
return fmt.Errorf("extension already registered for key %s", key)
}
c.Extensions[key] = value
return nil
}
// ChainFieldMiddleware add chain by FieldMiddleware
func ChainFieldMiddleware(handleFunc ...FieldMiddleware) FieldMiddleware {
n := len(handleFunc)
if n > 1 {
lastI := n - 1
return func(ctx context.Context, next Resolver) (interface{}, error) {
var (
chainHandler Resolver
curI int
)
chainHandler = func(currentCtx context.Context) (interface{}, error) {
if curI == lastI {
return next(currentCtx)
}
curI++
res, err := handleFunc[curI](currentCtx, chainHandler)
curI--
return res, err
}
return handleFunc[0](ctx, chainHandler)
}
}
if n == 1 {
return handleFunc[0]
}
return func(ctx context.Context, next Resolver) (interface{}, error) {
return next(ctx)
}
}

View File

@@ -0,0 +1,92 @@
package graphql
import (
"context"
"time"
"github.com/vektah/gqlparser/v2/ast"
)
type key string
const resolverCtx key = "resolver_context"
// Deprecated: Use FieldContext instead
type ResolverContext = FieldContext
type FieldContext struct {
Parent *FieldContext
// The name of the type this field belongs to
Object string
// These are the args after processing, they can be mutated in middleware to change what the resolver will get.
Args map[string]interface{}
// The raw field
Field CollectedField
// The index of array in path.
Index *int
// The result object of resolver
Result interface{}
// IsMethod indicates if the resolver is a method
IsMethod bool
}
type FieldStats struct {
// When field execution started
Started time.Time
// When argument marshaling finished
ArgumentsCompleted time.Time
// When the field completed running all middleware. Not available inside field middleware!
Completed time.Time
}
func (r *FieldContext) Path() ast.Path {
var path ast.Path
for it := r; it != nil; it = it.Parent {
if it.Index != nil {
path = append(path, ast.PathIndex(*it.Index))
} else if it.Field.Field != nil {
path = append(path, ast.PathName(it.Field.Alias))
}
}
// because we are walking up the chain, all the elements are backwards, do an inplace flip.
for i := len(path)/2 - 1; i >= 0; i-- {
opp := len(path) - 1 - i
path[i], path[opp] = path[opp], path[i]
}
return path
}
// Deprecated: Use GetFieldContext instead
func GetResolverContext(ctx context.Context) *ResolverContext {
return GetFieldContext(ctx)
}
func GetFieldContext(ctx context.Context) *FieldContext {
if val, ok := ctx.Value(resolverCtx).(*FieldContext); ok {
return val
}
return nil
}
func WithFieldContext(ctx context.Context, rc *FieldContext) context.Context {
rc.Parent = GetFieldContext(ctx)
return context.WithValue(ctx, resolverCtx, rc)
}
func equalPath(a ast.Path, b ast.Path) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@@ -0,0 +1,85 @@
package graphql
import (
"context"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
)
const fieldInputCtx key = "field_input_context"
type FieldInputContext struct {
ParentField *FieldContext
ParentInput *FieldInputContext
Field *string
Index *int
}
func (fic *FieldInputContext) Path() ast.Path {
var inputPath ast.Path
for it := fic; it != nil; it = it.ParentInput {
if it.Index != nil {
inputPath = append(inputPath, ast.PathIndex(*it.Index))
} else if it.Field != nil {
inputPath = append(inputPath, ast.PathName(*it.Field))
}
}
// because we are walking up the chain, all the elements are backwards, do an inplace flip.
for i := len(inputPath)/2 - 1; i >= 0; i-- {
opp := len(inputPath) - 1 - i
inputPath[i], inputPath[opp] = inputPath[opp], inputPath[i]
}
if fic.ParentField != nil {
fieldPath := fic.ParentField.Path()
return append(fieldPath, inputPath...)
}
return inputPath
}
func NewFieldInputWithField(field string) *FieldInputContext {
return &FieldInputContext{Field: &field}
}
func NewFieldInputWithIndex(index int) *FieldInputContext {
return &FieldInputContext{Index: &index}
}
func WithFieldInputContext(ctx context.Context, fic *FieldInputContext) context.Context {
if fieldContext := GetFieldContext(ctx); fieldContext != nil {
fic.ParentField = fieldContext
}
if fieldInputContext := GetFieldInputContext(ctx); fieldInputContext != nil {
fic.ParentInput = fieldInputContext
}
return context.WithValue(ctx, fieldInputCtx, fic)
}
func GetFieldInputContext(ctx context.Context) *FieldInputContext {
if val, ok := ctx.Value(fieldInputCtx).(*FieldInputContext); ok {
return val
}
return nil
}
func WrapErrorWithInputPath(ctx context.Context, err error) error {
if err == nil {
return nil
}
inputContext := GetFieldInputContext(ctx)
path := inputContext.Path()
if gerr, ok := err.(*gqlerror.Error); ok {
if gerr.Path == nil {
gerr.Path = path
}
return gerr
} else {
return gqlerror.WrapPath(path, err)
}
}

View File

@@ -0,0 +1,107 @@
package graphql
import (
"context"
"errors"
"github.com/vektah/gqlparser/v2/ast"
)
// Deprecated: Please update all references to OperationContext instead
type RequestContext = OperationContext
type OperationContext struct {
RawQuery string
Variables map[string]interface{}
OperationName string
Doc *ast.QueryDocument
Operation *ast.OperationDefinition
DisableIntrospection bool
Recover RecoverFunc
ResolverMiddleware FieldMiddleware
Stats Stats
}
func (c *OperationContext) Validate(ctx context.Context) error {
if c.Doc == nil {
return errors.New("field 'Doc'is required")
}
if c.RawQuery == "" {
return errors.New("field 'RawQuery' is required")
}
if c.Variables == nil {
c.Variables = make(map[string]interface{})
}
if c.ResolverMiddleware == nil {
return errors.New("field 'ResolverMiddleware' is required")
}
if c.Recover == nil {
c.Recover = DefaultRecover
}
return nil
}
const operationCtx key = "operation_context"
// Deprecated: Please update all references to GetOperationContext instead
func GetRequestContext(ctx context.Context) *RequestContext {
return GetOperationContext(ctx)
}
func GetOperationContext(ctx context.Context) *OperationContext {
if val, ok := ctx.Value(operationCtx).(*OperationContext); ok && val != nil {
return val
}
panic("missing operation context")
}
func WithOperationContext(ctx context.Context, rc *OperationContext) context.Context {
return context.WithValue(ctx, operationCtx, rc)
}
// HasOperationContext checks if the given context is part of an ongoing operation
//
// Some errors can happen outside of an operation, eg json unmarshal errors.
func HasOperationContext(ctx context.Context) bool {
_, ok := ctx.Value(operationCtx).(*OperationContext)
return ok
}
// This is just a convenient wrapper method for CollectFields
func CollectFieldsCtx(ctx context.Context, satisfies []string) []CollectedField {
resctx := GetFieldContext(ctx)
return CollectFields(GetOperationContext(ctx), resctx.Field.Selections, satisfies)
}
// CollectAllFields returns a slice of all GraphQL field names that were selected for the current resolver context.
// The slice will contain the unique set of all field names requested regardless of fragment type conditions.
func CollectAllFields(ctx context.Context) []string {
resctx := GetFieldContext(ctx)
collected := CollectFields(GetOperationContext(ctx), resctx.Field.Selections, nil)
uniq := make([]string, 0, len(collected))
Next:
for _, f := range collected {
for _, name := range uniq {
if name == f.Name {
continue Next
}
}
uniq = append(uniq, f.Name)
}
return uniq
}
// Errorf sends an error string to the client, passing it through the formatter.
// Deprecated: use graphql.AddErrorf(ctx, err) instead
func (c *OperationContext) Errorf(ctx context.Context, format string, args ...interface{}) {
AddErrorf(ctx, format, args...)
}
// Error sends an error to the client, passing it through the formatter.
// Deprecated: use graphql.AddError(ctx, err) instead
func (c *OperationContext) Error(ctx context.Context, err error) {
AddError(ctx, err)
}

View File

@@ -0,0 +1,157 @@
package graphql
import (
"context"
"fmt"
"sync"
"github.com/vektah/gqlparser/v2/gqlerror"
)
type responseContext struct {
errorPresenter ErrorPresenterFunc
recover RecoverFunc
errors gqlerror.List
errorsMu sync.Mutex
extensions map[string]interface{}
extensionsMu sync.Mutex
}
const resultCtx key = "result_context"
func getResponseContext(ctx context.Context) *responseContext {
val, ok := ctx.Value(resultCtx).(*responseContext)
if !ok {
panic("missing response context")
}
return val
}
func WithResponseContext(ctx context.Context, presenterFunc ErrorPresenterFunc, recoverFunc RecoverFunc) context.Context {
return context.WithValue(ctx, resultCtx, &responseContext{
errorPresenter: presenterFunc,
recover: recoverFunc,
})
}
// AddErrorf writes a formatted error to the client, first passing it through the error presenter.
func AddErrorf(ctx context.Context, format string, args ...interface{}) {
c := getResponseContext(ctx)
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
c.errors = append(c.errors, c.errorPresenter(ctx, fmt.Errorf(format, args...)))
}
// AddError sends an error to the client, first passing it through the error presenter.
func AddError(ctx context.Context, err error) {
c := getResponseContext(ctx)
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
c.errors = append(c.errors, c.errorPresenter(ctx, err))
}
func Recover(ctx context.Context, err interface{}) (userMessage error) {
c := getResponseContext(ctx)
return c.recover(ctx, err)
}
// HasFieldError returns true if the given field has already errored
func HasFieldError(ctx context.Context, rctx *FieldContext) bool {
c := getResponseContext(ctx)
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
if len(c.errors) == 0 {
return false
}
path := rctx.Path()
for _, err := range c.errors {
if equalPath(err.Path, path) {
return true
}
}
return false
}
// GetFieldErrors returns a list of errors that occurred in the given field
func GetFieldErrors(ctx context.Context, rctx *FieldContext) gqlerror.List {
c := getResponseContext(ctx)
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
if len(c.errors) == 0 {
return nil
}
path := rctx.Path()
var errs gqlerror.List
for _, err := range c.errors {
if equalPath(err.Path, path) {
errs = append(errs, err)
}
}
return errs
}
func GetErrors(ctx context.Context) gqlerror.List {
resCtx := getResponseContext(ctx)
resCtx.errorsMu.Lock()
defer resCtx.errorsMu.Unlock()
if len(resCtx.errors) == 0 {
return nil
}
errs := resCtx.errors
cpy := make(gqlerror.List, len(errs))
for i := range errs {
errCpy := *errs[i]
cpy[i] = &errCpy
}
return cpy
}
// RegisterExtension allows you to add a new extension into the graphql response
func RegisterExtension(ctx context.Context, key string, value interface{}) {
c := getResponseContext(ctx)
c.extensionsMu.Lock()
defer c.extensionsMu.Unlock()
if c.extensions == nil {
c.extensions = make(map[string]interface{})
}
if _, ok := c.extensions[key]; ok {
panic(fmt.Errorf("extension already registered for key %s", key))
}
c.extensions[key] = value
}
// GetExtensions returns any extensions registered in the current result context
func GetExtensions(ctx context.Context) map[string]interface{} {
ext := getResponseContext(ctx).extensions
if ext == nil {
return map[string]interface{}{}
}
return ext
}
func GetExtension(ctx context.Context, name string) interface{} {
ext := getResponseContext(ctx).extensions
if ext == nil {
return nil
}
return ext[name]
}

View File

@@ -0,0 +1,49 @@
package errcode
import (
"github.com/vektah/gqlparser/v2/gqlerror"
)
const ValidationFailed = "GRAPHQL_VALIDATION_FAILED"
const ParseFailed = "GRAPHQL_PARSE_FAILED"
type ErrorKind int
const (
// issues with graphql (validation, parsing). 422s in http, GQL_ERROR in websocket
KindProtocol ErrorKind = iota
// user errors, 200s in http, GQL_DATA in websocket
KindUser
)
var codeType = map[string]ErrorKind{
ValidationFailed: KindProtocol,
ParseFailed: KindProtocol,
}
// RegisterErrorType should be called by extensions that want to customize the http status codes for errors they return
func RegisterErrorType(code string, kind ErrorKind) {
codeType[code] = kind
}
// Set the error code on a given graphql error extension
func Set(err *gqlerror.Error, value string) {
if err.Extensions == nil {
err.Extensions = map[string]interface{}{}
}
err.Extensions["code"] = value
}
// get the kind of the first non User error, defaults to User if no errors have a custom extension
func GetErrorKind(errs gqlerror.List) ErrorKind {
for _, err := range errs {
if code, ok := err.Extensions["code"].(string); ok {
if kind, ok := codeType[code]; ok && kind != KindUser {
return kind
}
}
}
return KindUser
}

View File

@@ -3,10 +3,10 @@ package graphql
import (
"context"
"github.com/vektah/gqlparser/gqlerror"
"github.com/vektah/gqlparser/v2/gqlerror"
)
type ErrorPresenterFunc func(context.Context, error) *gqlerror.Error
type ErrorPresenterFunc func(ctx context.Context, err error) *gqlerror.Error
type ExtendedError interface {
Extensions() map[string]interface{}
@@ -15,7 +15,7 @@ type ExtendedError interface {
func DefaultErrorPresenter(ctx context.Context, err error) *gqlerror.Error {
if gqlerr, ok := err.(*gqlerror.Error); ok {
if gqlerr.Path == nil {
gqlerr.Path = GetResolverContext(ctx).Path()
gqlerr.Path = GetFieldContext(ctx).Path()
}
return gqlerr
}
@@ -27,7 +27,7 @@ func DefaultErrorPresenter(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{
Message: err.Error(),
Path: GetResolverContext(ctx).Path(),
Path: GetFieldContext(ctx).Path(),
Extensions: extensions,
}
}

View File

@@ -1,29 +1,29 @@
//go:generate go run github.com/matryer/moq -out executable_schema_mock.go . ExecutableSchema
package graphql
import (
"context"
"fmt"
"github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/v2/ast"
)
type ExecutableSchema interface {
Schema() *ast.Schema
Complexity(typeName, fieldName string, childComplexity int, args map[string]interface{}) (int, bool)
Query(ctx context.Context, op *ast.OperationDefinition) *Response
Mutation(ctx context.Context, op *ast.OperationDefinition) *Response
Subscription(ctx context.Context, op *ast.OperationDefinition) func() *Response
Exec(ctx context.Context) ResponseHandler
}
// CollectFields returns the set of fields from an ast.SelectionSet where all collected fields satisfy at least one of the GraphQL types
// passed through satisfies. Providing an empty or nil slice for satisfies will return collect all fields regardless of fragment
// type conditions.
func CollectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []string) []CollectedField {
func CollectFields(reqCtx *OperationContext, selSet ast.SelectionSet, satisfies []string) []CollectedField {
return collectFields(reqCtx, selSet, satisfies, map[string]bool{})
}
func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []string, visited map[string]bool) []CollectedField {
func collectFields(reqCtx *OperationContext, selSet ast.SelectionSet, satisfies []string, visited map[string]bool) []CollectedField {
groupedFields := make([]CollectedField, 0, len(selSet))
for _, sel := range selSet {
@@ -32,7 +32,7 @@ func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []
if !shouldIncludeNode(sel.Directives, reqCtx.Variables) {
continue
}
f := getOrCreateAndAppendField(&groupedFields, sel.Alias, func() CollectedField {
f := getOrCreateAndAppendField(&groupedFields, sel.Alias, sel.ObjectDefinition, func() CollectedField {
return CollectedField{Field: sel}
})
@@ -45,7 +45,7 @@ func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []
continue
}
for _, childField := range collectFields(reqCtx, sel.SelectionSet, satisfies, visited) {
f := getOrCreateAndAppendField(&groupedFields, childField.Name, func() CollectedField { return childField })
f := getOrCreateAndAppendField(&groupedFields, childField.Name, childField.ObjectDefinition, func() CollectedField { return childField })
f.Selections = append(f.Selections, childField.Selections...)
}
@@ -70,7 +70,7 @@ func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []
}
for _, childField := range collectFields(reqCtx, fragment.SelectionSet, satisfies, visited) {
f := getOrCreateAndAppendField(&groupedFields, childField.Name, func() CollectedField { return childField })
f := getOrCreateAndAppendField(&groupedFields, childField.Name, childField.ObjectDefinition, func() CollectedField { return childField })
f.Selections = append(f.Selections, childField.Selections...)
}
default:
@@ -96,9 +96,9 @@ func instanceOf(val string, satisfies []string) bool {
return false
}
func getOrCreateAndAppendField(c *[]CollectedField, name string, creator func() CollectedField) *CollectedField {
func getOrCreateAndAppendField(c *[]CollectedField, name string, objectDefinition *ast.Definition, creator func() CollectedField) *CollectedField {
for i, cf := range *c {
if cf.Alias == name {
if cf.Alias == name && (cf.ObjectDefinition == objectDefinition || (cf.ObjectDefinition != nil && objectDefinition != nil && cf.ObjectDefinition.Name == objectDefinition.Name)) {
return &(*c)[i]
}
}

View File

@@ -0,0 +1,175 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package graphql
import (
"context"
"github.com/vektah/gqlparser/v2/ast"
"sync"
)
var (
lockExecutableSchemaMockComplexity sync.RWMutex
lockExecutableSchemaMockExec sync.RWMutex
lockExecutableSchemaMockSchema sync.RWMutex
)
// Ensure, that ExecutableSchemaMock does implement ExecutableSchema.
// If this is not the case, regenerate this file with moq.
var _ ExecutableSchema = &ExecutableSchemaMock{}
// ExecutableSchemaMock is a mock implementation of ExecutableSchema.
//
// func TestSomethingThatUsesExecutableSchema(t *testing.T) {
//
// // make and configure a mocked ExecutableSchema
// mockedExecutableSchema := &ExecutableSchemaMock{
// ComplexityFunc: func(typeName string, fieldName string, childComplexity int, args map[string]interface{}) (int, bool) {
// panic("mock out the Complexity method")
// },
// ExecFunc: func(ctx context.Context) ResponseHandler {
// panic("mock out the Exec method")
// },
// SchemaFunc: func() *ast.Schema {
// panic("mock out the Schema method")
// },
// }
//
// // use mockedExecutableSchema in code that requires ExecutableSchema
// // and then make assertions.
//
// }
type ExecutableSchemaMock struct {
// ComplexityFunc mocks the Complexity method.
ComplexityFunc func(typeName string, fieldName string, childComplexity int, args map[string]interface{}) (int, bool)
// ExecFunc mocks the Exec method.
ExecFunc func(ctx context.Context) ResponseHandler
// SchemaFunc mocks the Schema method.
SchemaFunc func() *ast.Schema
// calls tracks calls to the methods.
calls struct {
// Complexity holds details about calls to the Complexity method.
Complexity []struct {
// TypeName is the typeName argument value.
TypeName string
// FieldName is the fieldName argument value.
FieldName string
// ChildComplexity is the childComplexity argument value.
ChildComplexity int
// Args is the args argument value.
Args map[string]interface{}
}
// Exec holds details about calls to the Exec method.
Exec []struct {
// Ctx is the ctx argument value.
Ctx context.Context
}
// Schema holds details about calls to the Schema method.
Schema []struct {
}
}
}
// Complexity calls ComplexityFunc.
func (mock *ExecutableSchemaMock) Complexity(typeName string, fieldName string, childComplexity int, args map[string]interface{}) (int, bool) {
if mock.ComplexityFunc == nil {
panic("ExecutableSchemaMock.ComplexityFunc: method is nil but ExecutableSchema.Complexity was just called")
}
callInfo := struct {
TypeName string
FieldName string
ChildComplexity int
Args map[string]interface{}
}{
TypeName: typeName,
FieldName: fieldName,
ChildComplexity: childComplexity,
Args: args,
}
lockExecutableSchemaMockComplexity.Lock()
mock.calls.Complexity = append(mock.calls.Complexity, callInfo)
lockExecutableSchemaMockComplexity.Unlock()
return mock.ComplexityFunc(typeName, fieldName, childComplexity, args)
}
// ComplexityCalls gets all the calls that were made to Complexity.
// Check the length with:
// len(mockedExecutableSchema.ComplexityCalls())
func (mock *ExecutableSchemaMock) ComplexityCalls() []struct {
TypeName string
FieldName string
ChildComplexity int
Args map[string]interface{}
} {
var calls []struct {
TypeName string
FieldName string
ChildComplexity int
Args map[string]interface{}
}
lockExecutableSchemaMockComplexity.RLock()
calls = mock.calls.Complexity
lockExecutableSchemaMockComplexity.RUnlock()
return calls
}
// Exec calls ExecFunc.
func (mock *ExecutableSchemaMock) Exec(ctx context.Context) ResponseHandler {
if mock.ExecFunc == nil {
panic("ExecutableSchemaMock.ExecFunc: method is nil but ExecutableSchema.Exec was just called")
}
callInfo := struct {
Ctx context.Context
}{
Ctx: ctx,
}
lockExecutableSchemaMockExec.Lock()
mock.calls.Exec = append(mock.calls.Exec, callInfo)
lockExecutableSchemaMockExec.Unlock()
return mock.ExecFunc(ctx)
}
// ExecCalls gets all the calls that were made to Exec.
// Check the length with:
// len(mockedExecutableSchema.ExecCalls())
func (mock *ExecutableSchemaMock) ExecCalls() []struct {
Ctx context.Context
} {
var calls []struct {
Ctx context.Context
}
lockExecutableSchemaMockExec.RLock()
calls = mock.calls.Exec
lockExecutableSchemaMockExec.RUnlock()
return calls
}
// Schema calls SchemaFunc.
func (mock *ExecutableSchemaMock) Schema() *ast.Schema {
if mock.SchemaFunc == nil {
panic("ExecutableSchemaMock.SchemaFunc: method is nil but ExecutableSchema.Schema was just called")
}
callInfo := struct {
}{}
lockExecutableSchemaMockSchema.Lock()
mock.calls.Schema = append(mock.calls.Schema, callInfo)
lockExecutableSchemaMockSchema.Unlock()
return mock.SchemaFunc()
}
// SchemaCalls gets all the calls that were made to Schema.
// Check the length with:
// len(mockedExecutableSchema.SchemaCalls())
func (mock *ExecutableSchemaMock) SchemaCalls() []struct {
} {
var calls []struct {
}
lockExecutableSchemaMockSchema.RLock()
calls = mock.calls.Schema
lockExecutableSchemaMockSchema.RUnlock()
return calls
}

View File

@@ -0,0 +1,191 @@
package executor
import (
"context"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/vektah/gqlparser/v2/parser"
"github.com/vektah/gqlparser/v2/validator"
)
// Executor executes graphql queries against a schema.
type Executor struct {
es graphql.ExecutableSchema
extensions []graphql.HandlerExtension
ext extensions
errorPresenter graphql.ErrorPresenterFunc
recoverFunc graphql.RecoverFunc
queryCache graphql.Cache
}
var _ graphql.GraphExecutor = &Executor{}
// New creates a new Executor with the given schema, and a default error and
// recovery callbacks, and no query cache or extensions.
func New(es graphql.ExecutableSchema) *Executor {
e := &Executor{
es: es,
errorPresenter: graphql.DefaultErrorPresenter,
recoverFunc: graphql.DefaultRecover,
queryCache: graphql.NoCache{},
ext: processExtensions(nil),
}
return e
}
func (e *Executor) CreateOperationContext(ctx context.Context, params *graphql.RawParams) (*graphql.OperationContext, gqlerror.List) {
rc := &graphql.OperationContext{
DisableIntrospection: true,
Recover: e.recoverFunc,
ResolverMiddleware: e.ext.fieldMiddleware,
Stats: graphql.Stats{
Read: params.ReadTime,
OperationStart: graphql.GetStartTime(ctx),
},
}
ctx = graphql.WithOperationContext(ctx, rc)
for _, p := range e.ext.operationParameterMutators {
if err := p.MutateOperationParameters(ctx, params); err != nil {
return rc, gqlerror.List{err}
}
}
rc.RawQuery = params.Query
rc.OperationName = params.OperationName
var listErr gqlerror.List
rc.Doc, listErr = e.parseQuery(ctx, &rc.Stats, params.Query)
if len(listErr) != 0 {
return rc, listErr
}
rc.Operation = rc.Doc.Operations.ForName(params.OperationName)
if rc.Operation == nil {
return rc, gqlerror.List{gqlerror.Errorf("operation %s not found", params.OperationName)}
}
var err *gqlerror.Error
rc.Variables, err = validator.VariableValues(e.es.Schema(), rc.Operation, params.Variables)
if err != nil {
errcode.Set(err, errcode.ValidationFailed)
return rc, gqlerror.List{err}
}
rc.Stats.Validation.End = graphql.Now()
for _, p := range e.ext.operationContextMutators {
if err := p.MutateOperationContext(ctx, rc); err != nil {
return rc, gqlerror.List{err}
}
}
return rc, nil
}
func (e *Executor) DispatchOperation(ctx context.Context, rc *graphql.OperationContext) (graphql.ResponseHandler, context.Context) {
ctx = graphql.WithOperationContext(ctx, rc)
var innerCtx context.Context
res := e.ext.operationMiddleware(ctx, func(ctx context.Context) graphql.ResponseHandler {
innerCtx = ctx
tmpResponseContext := graphql.WithResponseContext(ctx, e.errorPresenter, e.recoverFunc)
responses := e.es.Exec(tmpResponseContext)
if errs := graphql.GetErrors(tmpResponseContext); errs != nil {
return graphql.OneShot(&graphql.Response{Errors: errs})
}
return func(ctx context.Context) *graphql.Response {
ctx = graphql.WithResponseContext(ctx, e.errorPresenter, e.recoverFunc)
resp := e.ext.responseMiddleware(ctx, func(ctx context.Context) *graphql.Response {
resp := responses(ctx)
if resp == nil {
return nil
}
resp.Errors = append(resp.Errors, graphql.GetErrors(ctx)...)
resp.Extensions = graphql.GetExtensions(ctx)
return resp
})
if resp == nil {
return nil
}
return resp
}
})
return res, innerCtx
}
func (e *Executor) DispatchError(ctx context.Context, list gqlerror.List) *graphql.Response {
ctx = graphql.WithResponseContext(ctx, e.errorPresenter, e.recoverFunc)
for _, gErr := range list {
graphql.AddError(ctx, gErr)
}
resp := e.ext.responseMiddleware(ctx, func(ctx context.Context) *graphql.Response {
resp := &graphql.Response{
Errors: list,
}
resp.Extensions = graphql.GetExtensions(ctx)
return resp
})
return resp
}
func (e *Executor) PresentRecoveredError(ctx context.Context, err interface{}) *gqlerror.Error {
return e.errorPresenter(ctx, e.recoverFunc(ctx, err))
}
func (e *Executor) SetQueryCache(cache graphql.Cache) {
e.queryCache = cache
}
func (e *Executor) SetErrorPresenter(f graphql.ErrorPresenterFunc) {
e.errorPresenter = f
}
func (e *Executor) SetRecoverFunc(f graphql.RecoverFunc) {
e.recoverFunc = f
}
// parseQuery decodes the incoming query and validates it, pulling from cache if present.
//
// NOTE: This should NOT look at variables, they will change per request. It should only parse and validate
// the raw query string.
func (e *Executor) parseQuery(ctx context.Context, stats *graphql.Stats, query string) (*ast.QueryDocument, gqlerror.List) {
stats.Parsing.Start = graphql.Now()
if doc, ok := e.queryCache.Get(ctx, query); ok {
now := graphql.Now()
stats.Parsing.End = now
stats.Validation.Start = now
return doc.(*ast.QueryDocument), nil
}
doc, err := parser.ParseQuery(&ast.Source{Input: query})
if err != nil {
errcode.Set(err, errcode.ParseFailed)
return nil, gqlerror.List{err}
}
stats.Parsing.End = graphql.Now()
stats.Validation.Start = graphql.Now()
listErr := validator.Validate(e.es.Schema(), doc)
if len(listErr) != 0 {
for _, e := range listErr {
errcode.Set(e, errcode.ValidationFailed)
}
return nil, listErr
}
e.queryCache.Add(ctx, query, doc)
return doc, nil
}

View File

@@ -0,0 +1,159 @@
package executor
import (
"context"
"fmt"
"github.com/99designs/gqlgen/graphql"
)
// Use adds the given extension to this Executor.
func (e *Executor) Use(extension graphql.HandlerExtension) {
if err := extension.Validate(e.es); err != nil {
panic(err)
}
switch extension.(type) {
case graphql.OperationParameterMutator,
graphql.OperationContextMutator,
graphql.OperationInterceptor,
graphql.FieldInterceptor,
graphql.ResponseInterceptor:
e.extensions = append(e.extensions, extension)
e.ext = processExtensions(e.extensions)
default:
panic(fmt.Errorf("cannot Use %T as a gqlgen handler extension because it does not implement any extension hooks", extension))
}
}
// AroundFields is a convenience method for creating an extension that only implements field middleware
func (e *Executor) AroundFields(f graphql.FieldMiddleware) {
e.Use(aroundFieldFunc(f))
}
// AroundOperations is a convenience method for creating an extension that only implements operation middleware
func (e *Executor) AroundOperations(f graphql.OperationMiddleware) {
e.Use(aroundOpFunc(f))
}
// AroundResponses is a convenience method for creating an extension that only implements response middleware
func (e *Executor) AroundResponses(f graphql.ResponseMiddleware) {
e.Use(aroundRespFunc(f))
}
type extensions struct {
operationMiddleware graphql.OperationMiddleware
responseMiddleware graphql.ResponseMiddleware
fieldMiddleware graphql.FieldMiddleware
operationParameterMutators []graphql.OperationParameterMutator
operationContextMutators []graphql.OperationContextMutator
}
func processExtensions(exts []graphql.HandlerExtension) extensions {
e := extensions{
operationMiddleware: func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
return next(ctx)
},
responseMiddleware: func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
return next(ctx)
},
fieldMiddleware: func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
return next(ctx)
},
}
// this loop goes backwards so the first extension is the outer most middleware and runs first.
for i := len(exts) - 1; i >= 0; i-- {
p := exts[i]
if p, ok := p.(graphql.OperationInterceptor); ok {
previous := e.operationMiddleware
e.operationMiddleware = func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
return p.InterceptOperation(ctx, func(ctx context.Context) graphql.ResponseHandler {
return previous(ctx, next)
})
}
}
if p, ok := p.(graphql.ResponseInterceptor); ok {
previous := e.responseMiddleware
e.responseMiddleware = func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
return p.InterceptResponse(ctx, func(ctx context.Context) *graphql.Response {
return previous(ctx, next)
})
}
}
if p, ok := p.(graphql.FieldInterceptor); ok {
previous := e.fieldMiddleware
e.fieldMiddleware = func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
return p.InterceptField(ctx, func(ctx context.Context) (res interface{}, err error) {
return previous(ctx, next)
})
}
}
}
for _, p := range exts {
if p, ok := p.(graphql.OperationParameterMutator); ok {
e.operationParameterMutators = append(e.operationParameterMutators, p)
}
if p, ok := p.(graphql.OperationContextMutator); ok {
e.operationContextMutators = append(e.operationContextMutators, p)
}
}
return e
}
type aroundOpFunc func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler
func (r aroundOpFunc) ExtensionName() string {
return "InlineOperationFunc"
}
func (r aroundOpFunc) Validate(schema graphql.ExecutableSchema) error {
if r == nil {
return fmt.Errorf("OperationFunc can not be nil")
}
return nil
}
func (r aroundOpFunc) InterceptOperation(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
return r(ctx, next)
}
type aroundRespFunc func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response
func (r aroundRespFunc) ExtensionName() string {
return "InlineResponseFunc"
}
func (r aroundRespFunc) Validate(schema graphql.ExecutableSchema) error {
if r == nil {
return fmt.Errorf("ResponseFunc can not be nil")
}
return nil
}
func (r aroundRespFunc) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
return r(ctx, next)
}
type aroundFieldFunc func(ctx context.Context, next graphql.Resolver) (res interface{}, err error)
func (f aroundFieldFunc) ExtensionName() string {
return "InlineFieldFunc"
}
func (f aroundFieldFunc) Validate(schema graphql.ExecutableSchema) error {
if f == nil {
return fmt.Errorf("FieldFunc can not be nil")
}
return nil
}
func (f aroundFieldFunc) InterceptField(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
return f(ctx, next)
}

123
vendor/github.com/99designs/gqlgen/graphql/handler.go generated vendored Normal file
View File

@@ -0,0 +1,123 @@
package graphql
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/vektah/gqlparser/v2/gqlerror"
)
type (
OperationMiddleware func(ctx context.Context, next OperationHandler) ResponseHandler
OperationHandler func(ctx context.Context) ResponseHandler
ResponseHandler func(ctx context.Context) *Response
ResponseMiddleware func(ctx context.Context, next ResponseHandler) *Response
Resolver func(ctx context.Context) (res interface{}, err error)
FieldMiddleware func(ctx context.Context, next Resolver) (res interface{}, err error)
RawParams struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
Extensions map[string]interface{} `json:"extensions"`
ReadTime TraceTiming `json:"-"`
}
GraphExecutor interface {
CreateOperationContext(ctx context.Context, params *RawParams) (*OperationContext, gqlerror.List)
DispatchOperation(ctx context.Context, rc *OperationContext) (ResponseHandler, context.Context)
DispatchError(ctx context.Context, list gqlerror.List) *Response
}
// HandlerExtension adds functionality to the http handler. See the list of possible hook points below
// Its important to understand the lifecycle of a graphql request and the terminology we use in gqlgen
// before working with these
//
// +--- REQUEST POST /graphql --------------------------------------------+
// | +- OPERATION query OpName { viewer { name } } -----------------------+ |
// | | RESPONSE { "data": { "viewer": { "name": "bob" } } } | |
// | +- OPERATION subscription OpName2 { chat { message } } --------------+ |
// | | RESPONSE { "data": { "chat": { "message": "hello" } } } | |
// | | RESPONSE { "data": { "chat": { "message": "byee" } } } | |
// | +--------------------------------------------------------------------+ |
// +------------------------------------------------------------------------+
HandlerExtension interface {
// ExtensionName should be a CamelCase string version of the extension which may be shown in stats and logging.
ExtensionName() string
// Validate is called when adding an extension to the server, it allows validation against the servers schema.
Validate(schema ExecutableSchema) error
}
// OperationParameterMutator is called before creating a request context. allows manipulating the raw query
// on the way in.
OperationParameterMutator interface {
MutateOperationParameters(ctx context.Context, request *RawParams) *gqlerror.Error
}
// OperationContextMutator is called after creating the request context, but before executing the root resolver.
OperationContextMutator interface {
MutateOperationContext(ctx context.Context, rc *OperationContext) *gqlerror.Error
}
// OperationInterceptor is called for each incoming query, for basic requests the writer will be invoked once,
// for subscriptions it will be invoked multiple times.
OperationInterceptor interface {
InterceptOperation(ctx context.Context, next OperationHandler) ResponseHandler
}
// ResponseInterceptor is called around each graphql operation response. This can be called many times for a single
// operation the case of subscriptions.
ResponseInterceptor interface {
InterceptResponse(ctx context.Context, next ResponseHandler) *Response
}
// FieldInterceptor called around each field
FieldInterceptor interface {
InterceptField(ctx context.Context, next Resolver) (res interface{}, err error)
}
// Transport provides support for different wire level encodings of graphql requests, eg Form, Get, Post, Websocket
Transport interface {
Supports(r *http.Request) bool
Do(w http.ResponseWriter, r *http.Request, exec GraphExecutor)
}
)
type Status int
func (p *RawParams) AddUpload(upload Upload, key, path string) *gqlerror.Error {
if !strings.HasPrefix(path, "variables.") {
return gqlerror.Errorf("invalid operations paths for key %s", key)
}
var ptr interface{} = p.Variables
parts := strings.Split(path, ".")
// skip the first part (variables) because we started there
for i, p := range parts[1:] {
last := i == len(parts)-2
if ptr == nil {
return gqlerror.Errorf("path is missing \"variables.\" prefix, key: %s, path: %s", key, path)
}
if index, parseNbrErr := strconv.Atoi(p); parseNbrErr == nil {
if last {
ptr.([]interface{})[index] = upload
} else {
ptr = ptr.([]interface{})[index]
}
} else {
if last {
ptr.(map[string]interface{})[p] = upload
} else {
ptr = ptr.(map[string]interface{})[p]
}
}
}
return nil
}

View File

@@ -0,0 +1,112 @@
package extension
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/99designs/gqlgen/graphql"
"github.com/mitchellh/mapstructure"
)
const errPersistedQueryNotFound = "PersistedQueryNotFound"
const errPersistedQueryNotFoundCode = "PERSISTED_QUERY_NOT_FOUND"
// AutomaticPersistedQuery saves client upload by optimistically sending only the hashes of queries, if the server
// does not yet know what the query is for the hash it will respond telling the client to send the query along with the
// hash in the next request.
// see https://github.com/apollographql/apollo-link-persisted-queries
type AutomaticPersistedQuery struct {
Cache graphql.Cache
}
type ApqStats struct {
// The hash of the incoming query
Hash string
// SentQuery is true if the incoming request sent the full query
SentQuery bool
}
const apqExtension = "APQ"
var _ interface {
graphql.OperationParameterMutator
graphql.HandlerExtension
} = AutomaticPersistedQuery{}
func (a AutomaticPersistedQuery) ExtensionName() string {
return "AutomaticPersistedQuery"
}
func (a AutomaticPersistedQuery) Validate(schema graphql.ExecutableSchema) error {
if a.Cache == nil {
return fmt.Errorf("AutomaticPersistedQuery.Cache can not be nil")
}
return nil
}
func (a AutomaticPersistedQuery) MutateOperationParameters(ctx context.Context, rawParams *graphql.RawParams) *gqlerror.Error {
if rawParams.Extensions["persistedQuery"] == nil {
return nil
}
var extension struct {
Sha256 string `mapstructure:"sha256Hash"`
Version int64 `mapstructure:"version"`
}
if err := mapstructure.Decode(rawParams.Extensions["persistedQuery"], &extension); err != nil {
return gqlerror.Errorf("invalid APQ extension data")
}
if extension.Version != 1 {
return gqlerror.Errorf("unsupported APQ version")
}
fullQuery := false
if rawParams.Query == "" {
// client sent optimistic query hash without query string, get it from the cache
query, ok := a.Cache.Get(ctx, extension.Sha256)
if !ok {
err := gqlerror.Errorf(errPersistedQueryNotFound)
errcode.Set(err, errPersistedQueryNotFoundCode)
return err
}
rawParams.Query = query.(string)
} else {
// client sent optimistic query hash with query string, verify and store it
if computeQueryHash(rawParams.Query) != extension.Sha256 {
return gqlerror.Errorf("provided APQ hash does not match query")
}
a.Cache.Add(ctx, extension.Sha256, rawParams.Query)
fullQuery = true
}
graphql.GetOperationContext(ctx).Stats.SetExtension(apqExtension, &ApqStats{
Hash: extension.Sha256,
SentQuery: fullQuery,
})
return nil
}
func GetApqStats(ctx context.Context) *ApqStats {
rc := graphql.GetOperationContext(ctx)
if rc == nil {
return nil
}
s, _ := rc.Stats.GetExtension(apqExtension).(*ApqStats)
return s
}
func computeQueryHash(query string) string {
b := sha256.Sum256([]byte(query))
return hex.EncodeToString(b[:])
}

View File

@@ -0,0 +1,88 @@
package extension
import (
"context"
"fmt"
"github.com/99designs/gqlgen/complexity"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/vektah/gqlparser/v2/gqlerror"
)
const errComplexityLimit = "COMPLEXITY_LIMIT_EXCEEDED"
// ComplexityLimit allows you to define a limit on query complexity
//
// If a query is submitted that exceeds the limit, a 422 status code will be returned.
type ComplexityLimit struct {
Func func(ctx context.Context, rc *graphql.OperationContext) int
es graphql.ExecutableSchema
}
var _ interface {
graphql.OperationContextMutator
graphql.HandlerExtension
} = &ComplexityLimit{}
const complexityExtension = "ComplexityLimit"
type ComplexityStats struct {
// The calculated complexity for this request
Complexity int
// The complexity limit for this request returned by the extension func
ComplexityLimit int
}
// FixedComplexityLimit sets a complexity limit that does not change
func FixedComplexityLimit(limit int) *ComplexityLimit {
return &ComplexityLimit{
Func: func(ctx context.Context, rc *graphql.OperationContext) int {
return limit
},
}
}
func (c ComplexityLimit) ExtensionName() string {
return complexityExtension
}
func (c *ComplexityLimit) Validate(schema graphql.ExecutableSchema) error {
if c.Func == nil {
return fmt.Errorf("ComplexityLimit func can not be nil")
}
c.es = schema
return nil
}
func (c ComplexityLimit) MutateOperationContext(ctx context.Context, rc *graphql.OperationContext) *gqlerror.Error {
op := rc.Doc.Operations.ForName(rc.OperationName)
complexity := complexity.Calculate(c.es, op, rc.Variables)
limit := c.Func(ctx, rc)
rc.Stats.SetExtension(complexityExtension, &ComplexityStats{
Complexity: complexity,
ComplexityLimit: limit,
})
if complexity > limit {
err := gqlerror.Errorf("operation has complexity %d, which exceeds the limit of %d", complexity, limit)
errcode.Set(err, errComplexityLimit)
return err
}
return nil
}
func GetComplexityStats(ctx context.Context) *ComplexityStats {
rc := graphql.GetOperationContext(ctx)
if rc == nil {
return nil
}
s, _ := rc.Stats.GetExtension(complexityExtension).(*ComplexityStats)
return s
}

View File

@@ -0,0 +1,29 @@
package extension
import (
"context"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// EnableIntrospection enables clients to reflect all of the types available on the graph.
type Introspection struct{}
var _ interface {
graphql.OperationContextMutator
graphql.HandlerExtension
} = Introspection{}
func (c Introspection) ExtensionName() string {
return "Introspection"
}
func (c Introspection) Validate(schema graphql.ExecutableSchema) error {
return nil
}
func (c Introspection) MutateOperationContext(ctx context.Context, rc *graphql.OperationContext) *gqlerror.Error {
rc.DisableIntrospection = false
return nil
}

View File

@@ -0,0 +1,32 @@
package lru
import (
"context"
"github.com/99designs/gqlgen/graphql"
lru "github.com/hashicorp/golang-lru"
)
type LRU struct {
lru *lru.Cache
}
var _ graphql.Cache = &LRU{}
func New(size int) *LRU {
cache, err := lru.New(size)
if err != nil {
// An error is only returned for non-positive cache size
// and we already checked for that.
panic("unexpected error creating cache: " + err.Error())
}
return &LRU{cache}
}
func (l LRU) Get(ctx context.Context, key string) (value interface{}, ok bool) {
return l.lru.Get(key)
}
func (l LRU) Add(ctx context.Context, key string, value interface{}) {
l.lru.Add(key, value)
}

View File

@@ -0,0 +1,180 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/executor"
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/vektah/gqlparser/v2/gqlerror"
)
type (
Server struct {
transports []graphql.Transport
exec *executor.Executor
}
)
func New(es graphql.ExecutableSchema) *Server {
return &Server{
exec: executor.New(es),
}
}
func NewDefaultServer(es graphql.ExecutableSchema) *Server {
srv := New(es)
srv.AddTransport(transport.Websocket{
KeepAlivePingInterval: 10 * time.Second,
})
srv.AddTransport(transport.Options{})
srv.AddTransport(transport.GET{})
srv.AddTransport(transport.POST{})
srv.AddTransport(transport.MultipartForm{})
srv.SetQueryCache(lru.New(1000))
srv.Use(extension.Introspection{})
srv.Use(extension.AutomaticPersistedQuery{
Cache: lru.New(100),
})
return srv
}
func (s *Server) AddTransport(transport graphql.Transport) {
s.transports = append(s.transports, transport)
}
func (s *Server) SetErrorPresenter(f graphql.ErrorPresenterFunc) {
s.exec.SetErrorPresenter(f)
}
func (s *Server) SetRecoverFunc(f graphql.RecoverFunc) {
s.exec.SetRecoverFunc(f)
}
func (s *Server) SetQueryCache(cache graphql.Cache) {
s.exec.SetQueryCache(cache)
}
func (s *Server) Use(extension graphql.HandlerExtension) {
s.exec.Use(extension)
}
// AroundFields is a convenience method for creating an extension that only implements field middleware
func (s *Server) AroundFields(f graphql.FieldMiddleware) {
s.exec.AroundFields(f)
}
// AroundOperations is a convenience method for creating an extension that only implements operation middleware
func (s *Server) AroundOperations(f graphql.OperationMiddleware) {
s.exec.AroundOperations(f)
}
// AroundResponses is a convenience method for creating an extension that only implements response middleware
func (s *Server) AroundResponses(f graphql.ResponseMiddleware) {
s.exec.AroundResponses(f)
}
func (s *Server) getTransport(r *http.Request) graphql.Transport {
for _, t := range s.transports {
if t.Supports(r) {
return t
}
}
return nil
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
err := s.exec.PresentRecoveredError(r.Context(), err)
resp := &graphql.Response{Errors: []*gqlerror.Error{err}}
b, _ := json.Marshal(resp)
w.WriteHeader(http.StatusUnprocessableEntity)
w.Write(b)
}
}()
r = r.WithContext(graphql.StartOperationTrace(r.Context()))
transport := s.getTransport(r)
if transport == nil {
sendErrorf(w, http.StatusBadRequest, "transport not supported")
return
}
transport.Do(w, r, s.exec)
}
func sendError(w http.ResponseWriter, code int, errors ...*gqlerror.Error) {
w.WriteHeader(code)
b, err := json.Marshal(&graphql.Response{Errors: errors})
if err != nil {
panic(err)
}
w.Write(b)
}
func sendErrorf(w http.ResponseWriter, code int, format string, args ...interface{}) {
sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)})
}
type OperationFunc func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler
func (r OperationFunc) ExtensionName() string {
return "InlineOperationFunc"
}
func (r OperationFunc) Validate(schema graphql.ExecutableSchema) error {
if r == nil {
return fmt.Errorf("OperationFunc can not be nil")
}
return nil
}
func (r OperationFunc) InterceptOperation(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
return r(ctx, next)
}
type ResponseFunc func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response
func (r ResponseFunc) ExtensionName() string {
return "InlineResponseFunc"
}
func (r ResponseFunc) Validate(schema graphql.ExecutableSchema) error {
if r == nil {
return fmt.Errorf("ResponseFunc can not be nil")
}
return nil
}
func (r ResponseFunc) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
return r(ctx, next)
}
type FieldFunc func(ctx context.Context, next graphql.Resolver) (res interface{}, err error)
func (f FieldFunc) ExtensionName() string {
return "InlineFieldFunc"
}
func (f FieldFunc) Validate(schema graphql.ExecutableSchema) error {
if f == nil {
return fmt.Errorf("FieldFunc can not be nil")
}
return nil
}
func (f FieldFunc) InterceptField(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
return f(ctx, next)
}

View File

@@ -0,0 +1,26 @@
package transport
import (
"encoding/json"
"fmt"
"net/http"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// SendError sends a best effort error to a raw response writer. It assumes the client can understand the standard
// json error response
func SendError(w http.ResponseWriter, code int, errors ...*gqlerror.Error) {
w.WriteHeader(code)
b, err := json.Marshal(&graphql.Response{Errors: errors})
if err != nil {
panic(err)
}
w.Write(b)
}
// SendErrorf wraps SendError to add formatted messages
func SendErrorf(w http.ResponseWriter, code int, format string, args ...interface{}) {
SendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)})
}

View File

@@ -0,0 +1,208 @@
package transport
import (
"encoding/json"
"io"
"io/ioutil"
"mime"
"net/http"
"os"
"strings"
"github.com/99designs/gqlgen/graphql"
)
// MultipartForm the Multipart request spec https://github.com/jaydenseric/graphql-multipart-request-spec
type MultipartForm struct {
// MaxUploadSize sets the maximum number of bytes used to parse a request body
// as multipart/form-data.
MaxUploadSize int64
// MaxMemory defines the maximum number of bytes used to parse a request body
// as multipart/form-data in memory, with the remainder stored on disk in
// temporary files.
MaxMemory int64
}
var _ graphql.Transport = MultipartForm{}
func (f MultipartForm) Supports(r *http.Request) bool {
if r.Header.Get("Upgrade") != "" {
return false
}
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return false
}
return r.Method == "POST" && mediaType == "multipart/form-data"
}
func (f MultipartForm) maxUploadSize() int64 {
if f.MaxUploadSize == 0 {
return 32 << 20
}
return f.MaxUploadSize
}
func (f MultipartForm) maxMemory() int64 {
if f.MaxMemory == 0 {
return 32 << 20
}
return f.MaxMemory
}
func (f MultipartForm) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
w.Header().Set("Content-Type", "application/json")
start := graphql.Now()
var err error
if r.ContentLength > f.maxUploadSize() {
writeJsonError(w, "failed to parse multipart form, request body too large")
return
}
r.Body = http.MaxBytesReader(w, r.Body, f.maxUploadSize())
if err = r.ParseMultipartForm(f.maxMemory()); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
if strings.Contains(err.Error(), "request body too large") {
writeJsonError(w, "failed to parse multipart form, request body too large")
return
}
writeJsonError(w, "failed to parse multipart form")
return
}
defer r.Body.Close()
var params graphql.RawParams
if err = jsonDecode(strings.NewReader(r.Form.Get("operations")), &params); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonError(w, "operations form field could not be decoded")
return
}
var uploadsMap = map[string][]string{}
if err = json.Unmarshal([]byte(r.Form.Get("map")), &uploadsMap); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonError(w, "map form field could not be decoded")
return
}
var upload graphql.Upload
for key, paths := range uploadsMap {
if len(paths) == 0 {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "invalid empty operations paths list for key %s", key)
return
}
file, header, err := r.FormFile(key)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to get key %s from form", key)
return
}
defer file.Close()
if len(paths) == 1 {
upload = graphql.Upload{
File: file,
Size: header.Size,
Filename: header.Filename,
ContentType: header.Header.Get("Content-Type"),
}
if err := params.AddUpload(upload, key, paths[0]); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
return
}
} else {
if r.ContentLength < f.maxMemory() {
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to read file for key %s", key)
return
}
for _, path := range paths {
upload = graphql.Upload{
File: &bytesReader{s: &fileBytes, i: 0, prevRune: -1},
Size: header.Size,
Filename: header.Filename,
ContentType: header.Header.Get("Content-Type"),
}
if err := params.AddUpload(upload, key, path); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
return
}
}
} else {
tmpFile, err := ioutil.TempFile(os.TempDir(), "gqlgen-")
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to create temp file for key %s", key)
return
}
tmpName := tmpFile.Name()
defer func() {
_ = os.Remove(tmpName)
}()
_, err = io.Copy(tmpFile, file)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
if err := tmpFile.Close(); err != nil {
writeJsonErrorf(w, "failed to copy to temp file and close temp file for key %s", key)
return
}
writeJsonErrorf(w, "failed to copy to temp file for key %s", key)
return
}
if err := tmpFile.Close(); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to close temp file for key %s", key)
return
}
for _, path := range paths {
pathTmpFile, err := os.Open(tmpName)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to open temp file for key %s", key)
return
}
defer pathTmpFile.Close()
upload = graphql.Upload{
File: pathTmpFile,
Size: header.Size,
Filename: header.Filename,
ContentType: header.Header.Get("Content-Type"),
}
if err := params.AddUpload(upload, key, path); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
return
}
}
}
}
}
params.ReadTime = graphql.TraceTiming{
Start: start,
End: graphql.Now(),
}
rc, gerr := exec.CreateOperationContext(r.Context(), &params)
if gerr != nil {
resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), gerr)
w.WriteHeader(statusFor(gerr))
writeJson(w, resp)
return
}
responses, ctx := exec.DispatchOperation(r.Context(), rc)
writeJson(w, responses(ctx))
}

View File

@@ -0,0 +1,87 @@
package transport
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// GET implements the GET side of the default HTTP transport
// defined in https://github.com/APIs-guru/graphql-over-http#get
type GET struct{}
var _ graphql.Transport = GET{}
func (h GET) Supports(r *http.Request) bool {
if r.Header.Get("Upgrade") != "" {
return false
}
return r.Method == "GET"
}
func (h GET) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
w.Header().Set("Content-Type", "application/json")
raw := &graphql.RawParams{
Query: r.URL.Query().Get("query"),
OperationName: r.URL.Query().Get("operationName"),
}
raw.ReadTime.Start = graphql.Now()
if variables := r.URL.Query().Get("variables"); variables != "" {
if err := jsonDecode(strings.NewReader(variables), &raw.Variables); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonError(w, "variables could not be decoded")
return
}
}
if extensions := r.URL.Query().Get("extensions"); extensions != "" {
if err := jsonDecode(strings.NewReader(extensions), &raw.Extensions); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonError(w, "extensions could not be decoded")
return
}
}
raw.ReadTime.End = graphql.Now()
rc, err := exec.CreateOperationContext(r.Context(), raw)
if err != nil {
w.WriteHeader(statusFor(err))
resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), err)
writeJson(w, resp)
return
}
op := rc.Doc.Operations.ForName(rc.OperationName)
if op.Operation != ast.Query {
w.WriteHeader(http.StatusNotAcceptable)
writeJsonError(w, "GET requests only allow query operations")
return
}
responses, ctx := exec.DispatchOperation(r.Context(), rc)
writeJson(w, responses(ctx))
}
func jsonDecode(r io.Reader, val interface{}) error {
dec := json.NewDecoder(r)
dec.UseNumber()
return dec.Decode(val)
}
func statusFor(errs gqlerror.List) int {
switch errcode.GetErrorKind(errs) {
case errcode.KindProtocol:
return http.StatusUnprocessableEntity
default:
return http.StatusOK
}
}

View File

@@ -0,0 +1,54 @@
package transport
import (
"mime"
"net/http"
"github.com/99designs/gqlgen/graphql"
)
// POST implements the POST side of the default HTTP transport
// defined in https://github.com/APIs-guru/graphql-over-http#post
type POST struct{}
var _ graphql.Transport = POST{}
func (h POST) Supports(r *http.Request) bool {
if r.Header.Get("Upgrade") != "" {
return false
}
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return false
}
return r.Method == "POST" && mediaType == "application/json"
}
func (h POST) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
w.Header().Set("Content-Type", "application/json")
var params *graphql.RawParams
start := graphql.Now()
if err := jsonDecode(r.Body, &params); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonErrorf(w, "json body could not be decoded: "+err.Error())
return
}
params.ReadTime = graphql.TraceTiming{
Start: start,
End: graphql.Now(),
}
rc, err := exec.CreateOperationContext(r.Context(), params)
if err != nil {
w.WriteHeader(statusFor(err))
resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), err)
writeJson(w, resp)
return
}
ctx := graphql.WithOperationContext(r.Context(), rc)
responses, ctx := exec.DispatchOperation(ctx, rc)
writeJson(w, responses(ctx))
}

View File

@@ -0,0 +1,26 @@
package transport
import (
"net/http"
"github.com/99designs/gqlgen/graphql"
)
// Options responds to http OPTIONS and HEAD requests
type Options struct{}
var _ graphql.Transport = Options{}
func (o Options) Supports(r *http.Request) bool {
return r.Method == "HEAD" || r.Method == "OPTIONS"
}
func (o Options) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
switch r.Method {
case http.MethodOptions:
w.WriteHeader(http.StatusOK)
w.Header().Set("Allow", "OPTIONS, GET, POST")
case http.MethodHead:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}

View File

@@ -0,0 +1,25 @@
package transport
import (
"errors"
"io"
)
type bytesReader struct {
s *[]byte
i int64 // current reading index
prevRune int // index of previous rune; or < 0
}
func (r *bytesReader) Read(b []byte) (n int, err error) {
if r.s == nil {
return 0, errors.New("byte slice pointer is nil")
}
if r.i >= int64(len(*r.s)) {
return 0, io.EOF
}
r.prevRune = -1
n = copy(b, (*r.s)[r.i:])
r.i += int64(n)
return
}

View File

@@ -0,0 +1,30 @@
package transport
import (
"encoding/json"
"fmt"
"io"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
)
func writeJson(w io.Writer, response *graphql.Response) {
b, err := json.Marshal(response)
if err != nil {
panic(err)
}
w.Write(b)
}
func writeJsonError(w io.Writer, msg string) {
writeJson(w, &graphql.Response{Errors: gqlerror.List{{Message: msg}}})
}
func writeJsonErrorf(w io.Writer, format string, args ...interface{}) {
writeJson(w, &graphql.Response{Errors: gqlerror.List{{Message: fmt.Sprintf(format, args...)}}})
}
func writeJsonGraphqlError(w io.Writer, err ...*gqlerror.Error) {
writeJson(w, &graphql.Response{Errors: err})
}

View File

@@ -0,0 +1,316 @@
package transport
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/gorilla/websocket"
"github.com/vektah/gqlparser/v2/gqlerror"
)
const (
connectionInitMsg = "connection_init" // Client -> Server
connectionTerminateMsg = "connection_terminate" // Client -> Server
startMsg = "start" // Client -> Server
stopMsg = "stop" // Client -> Server
connectionAckMsg = "connection_ack" // Server -> Client
connectionErrorMsg = "connection_error" // Server -> Client
dataMsg = "data" // Server -> Client
errorMsg = "error" // Server -> Client
completeMsg = "complete" // Server -> Client
connectionKeepAliveMsg = "ka" // Server -> Client
)
type (
Websocket struct {
Upgrader websocket.Upgrader
InitFunc WebsocketInitFunc
KeepAlivePingInterval time.Duration
}
wsConnection struct {
Websocket
ctx context.Context
conn *websocket.Conn
active map[string]context.CancelFunc
mu sync.Mutex
keepAliveTicker *time.Ticker
exec graphql.GraphExecutor
initPayload InitPayload
}
operationMessage struct {
Payload json.RawMessage `json:"payload,omitempty"`
ID string `json:"id,omitempty"`
Type string `json:"type"`
}
WebsocketInitFunc func(ctx context.Context, initPayload InitPayload) (context.Context, error)
)
var _ graphql.Transport = Websocket{}
func (t Websocket) Supports(r *http.Request) bool {
return r.Header.Get("Upgrade") != ""
}
func (t Websocket) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
ws, err := t.Upgrader.Upgrade(w, r, http.Header{
"Sec-Websocket-Protocol": []string{"graphql-ws"},
})
if err != nil {
log.Printf("unable to upgrade %T to websocket %s: ", w, err.Error())
SendErrorf(w, http.StatusBadRequest, "unable to upgrade")
return
}
conn := wsConnection{
active: map[string]context.CancelFunc{},
conn: ws,
ctx: r.Context(),
exec: exec,
Websocket: t,
}
if !conn.init() {
return
}
conn.run()
}
func (c *wsConnection) init() bool {
message := c.readOp()
if message == nil {
c.close(websocket.CloseProtocolError, "decoding error")
return false
}
switch message.Type {
case connectionInitMsg:
if len(message.Payload) > 0 {
c.initPayload = make(InitPayload)
err := json.Unmarshal(message.Payload, &c.initPayload)
if err != nil {
return false
}
}
if c.InitFunc != nil {
ctx, err := c.InitFunc(c.ctx, c.initPayload)
if err != nil {
c.sendConnectionError(err.Error())
c.close(websocket.CloseNormalClosure, "terminated")
return false
}
c.ctx = ctx
}
c.write(&operationMessage{Type: connectionAckMsg})
c.write(&operationMessage{Type: connectionKeepAliveMsg})
case connectionTerminateMsg:
c.close(websocket.CloseNormalClosure, "terminated")
return false
default:
c.sendConnectionError("unexpected message %s", message.Type)
c.close(websocket.CloseProtocolError, "unexpected message")
return false
}
return true
}
func (c *wsConnection) write(msg *operationMessage) {
c.mu.Lock()
c.conn.WriteJSON(msg)
c.mu.Unlock()
}
func (c *wsConnection) run() {
// We create a cancellation that will shutdown the keep-alive when we leave
// this function.
ctx, cancel := context.WithCancel(c.ctx)
defer func() {
cancel()
c.close(websocket.CloseAbnormalClosure, "unexpected closure")
}()
// Create a timer that will fire every interval to keep the connection alive.
if c.KeepAlivePingInterval != 0 {
c.mu.Lock()
c.keepAliveTicker = time.NewTicker(c.KeepAlivePingInterval)
c.mu.Unlock()
go c.keepAlive(ctx)
}
for {
start := graphql.Now()
message := c.readOp()
if message == nil {
return
}
switch message.Type {
case startMsg:
c.subscribe(start, message)
case stopMsg:
c.mu.Lock()
closer := c.active[message.ID]
c.mu.Unlock()
if closer != nil {
closer()
}
case connectionTerminateMsg:
c.close(websocket.CloseNormalClosure, "terminated")
return
default:
c.sendConnectionError("unexpected message %s", message.Type)
c.close(websocket.CloseProtocolError, "unexpected message")
return
}
}
}
func (c *wsConnection) keepAlive(ctx context.Context) {
for {
select {
case <-ctx.Done():
c.keepAliveTicker.Stop()
return
case <-c.keepAliveTicker.C:
c.write(&operationMessage{Type: connectionKeepAliveMsg})
}
}
}
func (c *wsConnection) subscribe(start time.Time, message *operationMessage) {
ctx := graphql.StartOperationTrace(c.ctx)
var params *graphql.RawParams
if err := jsonDecode(bytes.NewReader(message.Payload), &params); err != nil {
c.sendError(message.ID, &gqlerror.Error{Message: "invalid json"})
c.complete(message.ID)
return
}
params.ReadTime = graphql.TraceTiming{
Start: start,
End: graphql.Now(),
}
rc, err := c.exec.CreateOperationContext(ctx, params)
if err != nil {
resp := c.exec.DispatchError(graphql.WithOperationContext(ctx, rc), err)
switch errcode.GetErrorKind(err) {
case errcode.KindProtocol:
c.sendError(message.ID, resp.Errors...)
default:
c.sendResponse(message.ID, &graphql.Response{Errors: err})
}
c.complete(message.ID)
return
}
ctx = graphql.WithOperationContext(ctx, rc)
if c.initPayload != nil {
ctx = withInitPayload(ctx, c.initPayload)
}
ctx, cancel := context.WithCancel(ctx)
c.mu.Lock()
c.active[message.ID] = cancel
c.mu.Unlock()
go func() {
defer func() {
if r := recover(); r != nil {
userErr := rc.Recover(ctx, r)
c.sendError(message.ID, &gqlerror.Error{Message: userErr.Error()})
}
}()
responses, ctx := c.exec.DispatchOperation(ctx, rc)
for {
response := responses(ctx)
if response == nil {
break
}
c.sendResponse(message.ID, response)
}
c.complete(message.ID)
c.mu.Lock()
delete(c.active, message.ID)
c.mu.Unlock()
cancel()
}()
}
func (c *wsConnection) sendResponse(id string, response *graphql.Response) {
b, err := json.Marshal(response)
if err != nil {
panic(err)
}
c.write(&operationMessage{
Payload: b,
ID: id,
Type: dataMsg,
})
}
func (c *wsConnection) complete(id string) {
c.write(&operationMessage{ID: id, Type: completeMsg})
}
func (c *wsConnection) sendError(id string, errors ...*gqlerror.Error) {
errs := make([]error, len(errors))
for i, err := range errors {
errs[i] = err
}
b, err := json.Marshal(errs)
if err != nil {
panic(err)
}
c.write(&operationMessage{Type: errorMsg, ID: id, Payload: b})
}
func (c *wsConnection) sendConnectionError(format string, args ...interface{}) {
b, err := json.Marshal(&gqlerror.Error{Message: fmt.Sprintf(format, args...)})
if err != nil {
panic(err)
}
c.write(&operationMessage{Type: connectionErrorMsg, Payload: b})
}
func (c *wsConnection) readOp() *operationMessage {
_, r, err := c.conn.NextReader()
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
return nil
} else if err != nil {
c.sendConnectionError("invalid json: %T %s", err, err.Error())
return nil
}
message := operationMessage{}
if err := jsonDecode(r, &message); err != nil {
c.sendConnectionError("invalid json")
return nil
}
return &message
}
func (c *wsConnection) close(closeCode int, message string) {
c.mu.Lock()
_ = c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(closeCode, message))
c.mu.Unlock()
_ = c.conn.Close()
}

View File

@@ -0,0 +1,57 @@
package transport
import "context"
type key string
const (
initpayload key = "ws_initpayload_context"
)
// InitPayload is a structure that is parsed from the websocket init message payload. TO use
// request headers for non-websocket, instead wrap the graphql handler in a middleware.
type InitPayload map[string]interface{}
// GetString safely gets a string value from the payload. It returns an empty string if the
// payload is nil or the value isn't set.
func (p InitPayload) GetString(key string) string {
if p == nil {
return ""
}
if value, ok := p[key]; ok {
res, _ := value.(string)
return res
}
return ""
}
// Authorization is a short hand for getting the Authorization header from the
// payload.
func (p InitPayload) Authorization() string {
if value := p.GetString("Authorization"); value != "" {
return value
}
if value := p.GetString("authorization"); value != "" {
return value
}
return ""
}
func withInitPayload(ctx context.Context, payload InitPayload) context.Context {
return context.WithValue(ctx, initpayload, payload)
}
// GetInitPayload gets a map of the data sent with the connection_init message, which is used by
// graphql clients as a stand-in for HTTP headers.
func GetInitPayload(ctx context.Context) InitPayload {
payload, ok := ctx.Value(initpayload).(InitPayload)
if !ok {
return nil
}
return payload
}

View File

@@ -20,6 +20,8 @@ func UnmarshalID(v interface{}) (string, error) {
return string(v), nil
case int:
return strconv.Itoa(v), nil
case int64:
return strconv.FormatInt(v, 10), nil
case float64:
return fmt.Sprintf("%f", v), nil
case bool:

View File

@@ -1,7 +1,7 @@
// introspection implements the spec defined in https://github.com/facebook/graphql/blob/master/spec/Section%204%20--%20Introspection.md#schema-introspection
package introspection
import "github.com/vektah/gqlparser/ast"
import "github.com/vektah/gqlparser/v2/ast"
type (
Directive struct {

View File

@@ -3,7 +3,7 @@ package introspection
import (
"strings"
"github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/v2/ast"
)
type Schema struct {
@@ -11,7 +11,7 @@ type Schema struct {
}
func (s *Schema) Types() []Type {
var types []Type
types := make([]Type, 0, len(s.schema.Types))
for _, typ := range s.schema.Types {
if strings.HasPrefix(typ.Name, "__") {
continue
@@ -34,7 +34,7 @@ func (s *Schema) SubscriptionType() *Type {
}
func (s *Schema) Directives() []Directive {
var res []Directive
res := make([]Directive, 0, len(s.schema.Directives))
for _, d := range s.schema.Directives {
res = append(res, s.directiveFromDef(d))
@@ -44,19 +44,19 @@ func (s *Schema) Directives() []Directive {
}
func (s *Schema) directiveFromDef(d *ast.DirectiveDefinition) Directive {
var locs []string
for _, loc := range d.Locations {
locs = append(locs, string(loc))
locs := make([]string, len(d.Locations))
for i, loc := range d.Locations {
locs[i] = string(loc)
}
var args []InputValue
for _, arg := range d.Arguments {
args = append(args, InputValue{
args := make([]InputValue, len(d.Arguments))
for i, arg := range d.Arguments {
args[i] = InputValue{
Name: arg.Name,
Description: arg.Description,
DefaultValue: defaultValue(arg.DefaultValue),
Type: WrapTypeFromType(s.schema, arg.Type),
})
}
}
return Directive{

View File

@@ -3,7 +3,7 @@ package introspection
import (
"strings"
"github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/v2/ast"
)
type Type struct {
@@ -152,6 +152,10 @@ func (t *Type) EnumValues(includeDeprecated bool) []EnumValue {
res := []EnumValue{}
for _, val := range t.def.EnumValues {
if !includeDeprecated && val.Directives.ForName("deprecated") != nil {
continue
}
res = append(res, EnumValue{
Name: val.Name,
Description: val.Description,

View File

@@ -1,9 +1,11 @@
package graphql
func OneShot(resp *Response) func() *Response {
import "context"
func OneShot(resp *Response) ResponseHandler {
var oneshot bool
return func() *Response {
return func(context context.Context) *Response {
if oneshot {
return nil
}

View File

@@ -0,0 +1,62 @@
package playground
import (
"html/template"
"net/http"
)
var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8/>
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
<link rel="shortcut icon" href="https://graphcool-playground.netlify.com/favicon.png">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"
integrity="{{ .cssSRI }}" crossorigin="anonymous"/>
<link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"
integrity="{{ .faviconSRI }}" crossorigin="anonymous"/>
<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"
integrity="{{ .jsSRI }}" crossorigin="anonymous"></script>
<title>{{.title}}</title>
</head>
<body>
<style type="text/css">
html { font-family: "Open Sans", sans-serif; overflow: hidden; }
body { margin: 0; background: #172a3a; }
</style>
<div id="root"/>
<script type="text/javascript">
window.addEventListener('load', function (event) {
const root = document.getElementById('root');
root.classList.add('playgroundIn');
const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:'
GraphQLPlayground.init(root, {
endpoint: location.protocol + '//' + location.host + '{{.endpoint}}',
subscriptionsEndpoint: wsProto + '//' + location.host + '{{.endpoint }}',
shareEnabled: true,
settings: {
'request.credentials': 'same-origin'
}
})
})
</script>
</body>
</html>
`))
func Handler(title string, endpoint string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
err := page.Execute(w, map[string]string{
"title": title,
"endpoint": endpoint,
"version": "1.7.20",
"cssSRI": "sha256-cS9Vc2OBt9eUf4sykRWukeFYaInL29+myBmFDSa7F/U=",
"faviconSRI": "sha256-GhTyE+McTU79R4+pRO6ih+4TfsTOrpPwD8ReKFzb3PM=",
"jsSRI": "sha256-4QG1Uza2GgGdlBL3RCBCGtGeZB6bDbsw8OltCMGeJsA=",
})
if err != nil {
panic(err)
}
}
}

View File

@@ -5,7 +5,7 @@ import (
"encoding/json"
"fmt"
"github.com/vektah/gqlparser/gqlerror"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// Errors are intentionally serialized first based on the advice in

60
vendor/github.com/99designs/gqlgen/graphql/stats.go generated vendored Normal file
View File

@@ -0,0 +1,60 @@
package graphql
import (
"context"
"fmt"
"time"
)
type Stats struct {
OperationStart time.Time
Read TraceTiming
Parsing TraceTiming
Validation TraceTiming
// Stats collected by handler extensions. Dont use directly, the extension should provide a type safe way to
// access this.
extension map[string]interface{}
}
type TraceTiming struct {
Start time.Time
End time.Time
}
var ctxTraceStart key = "trace_start"
// StartOperationTrace captures the current time and stores it in context. This will eventually be added to request
// context but we want to grab it as soon as possible. For transports that can only handle a single graphql query
// per http requests you dont need to call this at all, the server will do it for you. For transports that handle
// multiple (eg batching, subscriptions) this should be called before decoding each request.
func StartOperationTrace(ctx context.Context) context.Context {
return context.WithValue(ctx, ctxTraceStart, Now())
}
// GetStartTime should only be called by the handler package, it will be set into request context
// as Stats.Start
func GetStartTime(ctx context.Context) time.Time {
t, ok := ctx.Value(ctxTraceStart).(time.Time)
if !ok {
panic(fmt.Sprintf("missing start time: %T", ctx.Value(ctxTraceStart)))
}
return t
}
func (c *Stats) SetExtension(name string, data interface{}) {
if c.extension == nil {
c.extension = map[string]interface{}{}
}
c.extension[name] = data
}
func (c *Stats) GetExtension(name string) interface{} {
if c.extension == nil {
return nil
}
return c.extension[name]
}
// Now is time.Now, except in tests. Then it can be whatever you want it to be.
var Now = time.Now

View File

@@ -1,58 +0,0 @@
package graphql
import (
"context"
)
var _ Tracer = (*NopTracer)(nil)
type Tracer interface {
StartOperationParsing(ctx context.Context) context.Context
EndOperationParsing(ctx context.Context)
StartOperationValidation(ctx context.Context) context.Context
EndOperationValidation(ctx context.Context)
StartOperationExecution(ctx context.Context) context.Context
StartFieldExecution(ctx context.Context, field CollectedField) context.Context
StartFieldResolverExecution(ctx context.Context, rc *ResolverContext) context.Context
StartFieldChildExecution(ctx context.Context) context.Context
EndFieldExecution(ctx context.Context)
EndOperationExecution(ctx context.Context)
}
type NopTracer struct{}
func (NopTracer) StartOperationParsing(ctx context.Context) context.Context {
return ctx
}
func (NopTracer) EndOperationParsing(ctx context.Context) {
}
func (NopTracer) StartOperationValidation(ctx context.Context) context.Context {
return ctx
}
func (NopTracer) EndOperationValidation(ctx context.Context) {
}
func (NopTracer) StartOperationExecution(ctx context.Context) context.Context {
return ctx
}
func (NopTracer) StartFieldExecution(ctx context.Context, field CollectedField) context.Context {
return ctx
}
func (NopTracer) StartFieldResolverExecution(ctx context.Context, rc *ResolverContext) context.Context {
return ctx
}
func (NopTracer) StartFieldChildExecution(ctx context.Context) context.Context {
return ctx
}
func (NopTracer) EndFieldExecution(ctx context.Context) {
}
func (NopTracer) EndOperationExecution(ctx context.Context) {
}

View File

@@ -6,9 +6,10 @@ import (
)
type Upload struct {
File io.Reader
Filename string
Size int64
File io.Reader
Filename string
Size int64
ContentType string
}
func MarshalUpload(f Upload) Marshaler {

View File

@@ -1,3 +1,3 @@
package graphql
const Version = "v0.9.0"
const Version = "v0.12.2"