2
0
mirror of https://github.com/42wim/matterbridge synced 2024-11-17 03:26:07 +00:00
matterbridge/vendor/github.com/graph-gophers/graphql-go/graphql.go
2022-04-01 00:23:19 +02:00

340 lines
11 KiB
Go

package graphql
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
"github.com/graph-gophers/graphql-go/internal/exec"
"github.com/graph-gophers/graphql-go/internal/exec/resolvable"
"github.com/graph-gophers/graphql-go/internal/exec/selected"
"github.com/graph-gophers/graphql-go/internal/query"
"github.com/graph-gophers/graphql-go/internal/schema"
"github.com/graph-gophers/graphql-go/internal/validation"
"github.com/graph-gophers/graphql-go/introspection"
"github.com/graph-gophers/graphql-go/log"
"github.com/graph-gophers/graphql-go/trace"
"github.com/graph-gophers/graphql-go/types"
)
// ParseSchema parses a GraphQL schema and attaches the given root resolver. It returns an error if
// the Go type signature of the resolvers does not match the schema. If nil is passed as the
// resolver, then the schema can not be executed, but it may be inspected (e.g. with ToJSON).
func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) (*Schema, error) {
s := &Schema{
schema: schema.New(),
maxParallelism: 10,
tracer: trace.OpenTracingTracer{},
logger: &log.DefaultLogger{},
panicHandler: &errors.DefaultPanicHandler{},
}
for _, opt := range opts {
opt(s)
}
if s.validationTracer == nil {
if tracer, ok := s.tracer.(trace.ValidationTracerContext); ok {
s.validationTracer = tracer
} else {
s.validationTracer = &validationBridgingTracer{tracer: trace.NoopValidationTracer{}}
}
}
if err := schema.Parse(s.schema, schemaString, s.useStringDescriptions); err != nil {
return nil, err
}
if err := s.validateSchema(); err != nil {
return nil, err
}
r, err := resolvable.ApplyResolver(s.schema, resolver)
if err != nil {
return nil, err
}
s.res = r
return s, nil
}
// MustParseSchema calls ParseSchema and panics on error.
func MustParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) *Schema {
s, err := ParseSchema(schemaString, resolver, opts...)
if err != nil {
panic(err)
}
return s
}
// Schema represents a GraphQL schema with an optional resolver.
type Schema struct {
schema *types.Schema
res *resolvable.Schema
maxDepth int
maxParallelism int
tracer trace.Tracer
validationTracer trace.ValidationTracerContext
logger log.Logger
panicHandler errors.PanicHandler
useStringDescriptions bool
disableIntrospection bool
subscribeResolverTimeout time.Duration
}
func (s *Schema) ASTSchema() *types.Schema {
return s.schema
}
// SchemaOpt is an option to pass to ParseSchema or MustParseSchema.
type SchemaOpt func(*Schema)
// UseStringDescriptions enables the usage of double quoted and triple quoted
// strings as descriptions as per the June 2018 spec
// https://facebook.github.io/graphql/June2018/. When this is not enabled,
// comments are parsed as descriptions instead.
func UseStringDescriptions() SchemaOpt {
return func(s *Schema) {
s.useStringDescriptions = true
}
}
// UseFieldResolvers specifies whether to use struct field resolvers
func UseFieldResolvers() SchemaOpt {
return func(s *Schema) {
s.schema.UseFieldResolvers = true
}
}
// MaxDepth specifies the maximum field nesting depth in a query. The default is 0 which disables max depth checking.
func MaxDepth(n int) SchemaOpt {
return func(s *Schema) {
s.maxDepth = n
}
}
// MaxParallelism specifies the maximum number of resolvers per request allowed to run in parallel. The default is 10.
func MaxParallelism(n int) SchemaOpt {
return func(s *Schema) {
s.maxParallelism = n
}
}
// Tracer is used to trace queries and fields. It defaults to trace.OpenTracingTracer.
func Tracer(tracer trace.Tracer) SchemaOpt {
return func(s *Schema) {
s.tracer = tracer
}
}
// ValidationTracer is used to trace validation errors. It defaults to trace.NoopValidationTracer.
// Deprecated: context is needed to support tracing correctly. Use a Tracer which implements trace.ValidationTracerContext.
func ValidationTracer(tracer trace.ValidationTracer) SchemaOpt { //nolint:staticcheck
return func(s *Schema) {
s.validationTracer = &validationBridgingTracer{tracer: tracer}
}
}
// Logger is used to log panics during query execution. It defaults to exec.DefaultLogger.
func Logger(logger log.Logger) SchemaOpt {
return func(s *Schema) {
s.logger = logger
}
}
// PanicHandler is used to customize the panic errors during query execution.
// It defaults to errors.DefaultPanicHandler.
func PanicHandler(panicHandler errors.PanicHandler) SchemaOpt {
return func(s *Schema) {
s.panicHandler = panicHandler
}
}
// DisableIntrospection disables introspection queries.
func DisableIntrospection() SchemaOpt {
return func(s *Schema) {
s.disableIntrospection = true
}
}
// SubscribeResolverTimeout is an option to control the amount of time
// we allow for a single subscribe message resolver to complete it's job
// before it times out and returns an error to the subscriber.
func SubscribeResolverTimeout(timeout time.Duration) SchemaOpt {
return func(s *Schema) {
s.subscribeResolverTimeout = timeout
}
}
// Response represents a typical response of a GraphQL server. It may be encoded to JSON directly or
// it may be further processed to a custom response type, for example to include custom error data.
// Errors are intentionally serialized first based on the advice in https://github.com/facebook/graphql/commit/7b40390d48680b15cb93e02d46ac5eb249689876#diff-757cea6edf0288677a9eea4cfc801d87R107
type Response struct {
Errors []*errors.QueryError `json:"errors,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
}
// Validate validates the given query with the schema.
func (s *Schema) Validate(queryString string) []*errors.QueryError {
return s.ValidateWithVariables(queryString, nil)
}
// ValidateWithVariables validates the given query with the schema and the input variables.
func (s *Schema) ValidateWithVariables(queryString string, variables map[string]interface{}) []*errors.QueryError {
doc, qErr := query.Parse(queryString)
if qErr != nil {
return []*errors.QueryError{qErr}
}
return validation.Validate(s.schema, doc, variables, s.maxDepth)
}
// Exec executes the given query with the schema's resolver. It panics if the schema was created
// without a resolver. If the context get cancelled, no further resolvers will be called and a
// the context error will be returned as soon as possible (not immediately).
func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) *Response {
if !s.res.Resolver.IsValid() {
panic("schema created without resolver, can not exec")
}
return s.exec(ctx, queryString, operationName, variables, s.res)
}
func (s *Schema) exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, res *resolvable.Schema) *Response {
doc, qErr := query.Parse(queryString)
if qErr != nil {
return &Response{Errors: []*errors.QueryError{qErr}}
}
validationFinish := s.validationTracer.TraceValidation(ctx)
errs := validation.Validate(s.schema, doc, variables, s.maxDepth)
validationFinish(errs)
if len(errs) != 0 {
return &Response{Errors: errs}
}
op, err := getOperation(doc, operationName)
if err != nil {
return &Response{Errors: []*errors.QueryError{errors.Errorf("%s", err)}}
}
// If the optional "operationName" POST parameter is not provided then
// use the query's operation name for improved tracing.
if operationName == "" {
operationName = op.Name.Name
}
// Subscriptions are not valid in Exec. Use schema.Subscribe() instead.
if op.Type == query.Subscription {
return &Response{Errors: []*errors.QueryError{{Message: "graphql-ws protocol header is missing"}}}
}
if op.Type == query.Mutation {
if _, ok := s.schema.EntryPoints["mutation"]; !ok {
return &Response{Errors: []*errors.QueryError{{Message: "no mutations are offered by the schema"}}}
}
}
// Fill in variables with the defaults from the operation
if variables == nil {
variables = make(map[string]interface{}, len(op.Vars))
}
for _, v := range op.Vars {
if _, ok := variables[v.Name.Name]; !ok && v.Default != nil {
variables[v.Name.Name] = v.Default.Deserialize(nil)
}
}
r := &exec.Request{
Request: selected.Request{
Doc: doc,
Vars: variables,
Schema: s.schema,
DisableIntrospection: s.disableIntrospection,
},
Limiter: make(chan struct{}, s.maxParallelism),
Tracer: s.tracer,
Logger: s.logger,
PanicHandler: s.panicHandler,
}
varTypes := make(map[string]*introspection.Type)
for _, v := range op.Vars {
t, err := common.ResolveType(v.Type, s.schema.Resolve)
if err != nil {
return &Response{Errors: []*errors.QueryError{err}}
}
varTypes[v.Name.Name] = introspection.WrapType(t)
}
traceCtx, finish := s.tracer.TraceQuery(ctx, queryString, operationName, variables, varTypes)
data, errs := r.Execute(traceCtx, res, op)
finish(errs)
return &Response{
Data: data,
Errors: errs,
}
}
func (s *Schema) validateSchema() error {
// https://graphql.github.io/graphql-spec/June2018/#sec-Root-Operation-Types
// > The query root operation type must be provided and must be an Object type.
if err := validateRootOp(s.schema, "query", true); err != nil {
return err
}
// > The mutation root operation type is optional; if it is not provided, the service does not support mutations.
// > If it is provided, it must be an Object type.
if err := validateRootOp(s.schema, "mutation", false); err != nil {
return err
}
// > Similarly, the subscription root operation type is also optional; if it is not provided, the service does not
// > support subscriptions. If it is provided, it must be an Object type.
if err := validateRootOp(s.schema, "subscription", false); err != nil {
return err
}
return nil
}
type validationBridgingTracer struct {
tracer trace.ValidationTracer //nolint:staticcheck
}
func (t *validationBridgingTracer) TraceValidation(context.Context) trace.TraceValidationFinishFunc {
return t.tracer.TraceValidation()
}
func validateRootOp(s *types.Schema, name string, mandatory bool) error {
t, ok := s.EntryPoints[name]
if !ok {
if mandatory {
return fmt.Errorf("root operation %q must be defined", name)
}
return nil
}
if t.Kind() != "OBJECT" {
return fmt.Errorf("root operation %q must be an OBJECT", name)
}
return nil
}
func getOperation(document *types.ExecutableDefinition, operationName string) (*types.OperationDefinition, error) {
if len(document.Operations) == 0 {
return nil, fmt.Errorf("no operations in query document")
}
if operationName == "" {
if len(document.Operations) > 1 {
return nil, fmt.Errorf("more than one operation in query document and no operation name given")
}
for _, op := range document.Operations {
return op, nil // return the one and only operation
}
}
op := document.Operations.Get(operationName)
if op == nil {
return nil, fmt.Errorf("no operation with name %q", operationName)
}
return op, nil
}