mirror of
https://github.com/42wim/matterbridge.git
synced 2024-11-26 20:52:02 -08:00
340 lines
11 KiB
Go
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
|
|
}
|