206 lines
6.4 KiB
Go
206 lines
6.4 KiB
Go
package graphql
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"encoding/json"
|
|
|
|
"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"
|
|
)
|
|
|
|
// 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{},
|
|
validationTracer: trace.NoopValidationTracer{},
|
|
logger: &log.DefaultLogger{},
|
|
}
|
|
for _, opt := range opts {
|
|
opt(s)
|
|
}
|
|
|
|
if err := s.schema.Parse(schemaString); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resolver != nil {
|
|
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 *schema.Schema
|
|
res *resolvable.Schema
|
|
|
|
maxDepth int
|
|
maxParallelism int
|
|
tracer trace.Tracer
|
|
validationTracer trace.ValidationTracer
|
|
logger log.Logger
|
|
}
|
|
|
|
// SchemaOpt is an option to pass to ParseSchema or MustParseSchema.
|
|
type SchemaOpt func(*Schema)
|
|
|
|
// 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.
|
|
func ValidationTracer(tracer trace.ValidationTracer) SchemaOpt {
|
|
return func(s *Schema) {
|
|
s.validationTracer = 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
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
doc, qErr := query.Parse(queryString)
|
|
if qErr != nil {
|
|
return []*errors.QueryError{qErr}
|
|
}
|
|
|
|
return validation.Validate(s.schema, doc, 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 == nil {
|
|
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()
|
|
errs := validation.Validate(s.schema, doc, 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)}}
|
|
}
|
|
|
|
r := &exec.Request{
|
|
Request: selected.Request{
|
|
Doc: doc,
|
|
Vars: variables,
|
|
Schema: s.schema,
|
|
},
|
|
Limiter: make(chan struct{}, s.maxParallelism),
|
|
Tracer: s.tracer,
|
|
Logger: s.logger,
|
|
}
|
|
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 getOperation(document *query.Document, operationName string) (*query.Operation, 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
|
|
}
|