/
graphql.go
383 lines (316 loc) · 12.8 KB
/
graphql.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
package graphql
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"mime"
"net/http"
"github.com/ccbrown/api-fu/graphql/ast"
"github.com/ccbrown/api-fu/graphql/executor"
"github.com/ccbrown/api-fu/graphql/parser"
"github.com/ccbrown/api-fu/graphql/schema"
"github.com/ccbrown/api-fu/graphql/validator"
)
// Directive represents a GraphQL directive.
type Directive = schema.Directive
// Type represents a GraphQL type.
type Type = schema.Type
// NamedType represents any GraphQL named type.
type NamedType = schema.NamedType
// ObjectType represents a GraphQL object type.
type ObjectType = schema.ObjectType
// InterfaceType represents a GraphQL interface type.
type InterfaceType = schema.InterfaceType
// EnumType represents a GraphQL enum type.
type EnumType = schema.EnumType
// ScalarType represents a GraphQL scalar type.
type ScalarType = schema.ScalarType
// UnionType represents a GraphQL union type.
type UnionType = schema.UnionType
// InputObjectType represents a GraphQL input object type.
type InputObjectType = schema.InputObjectType
// NonNullType represents a non-null GraphQL type.
type NonNullType = schema.NonNullType
// ListType represents a GraphQL list type.
type ListType = schema.ListType
// FieldContext is provided to field resolvers and contains important context such as the current
// object and arguments.
type FieldContext = schema.FieldContext
// FieldCostContext contains important context passed to field cost functions.
type FieldCostContext = schema.FieldCostContext
// FieldCost describes the cost of resolving a field, enabling rate limiting and metering.
type FieldCost = schema.FieldCost
// Returns a cost function which returns a constant resolver cost with no multiplier.
func FieldResolverCost(n int) func(FieldCostContext) FieldCost {
return schema.FieldResolverCost(n)
}
// EnumValueDefinition defines a possible value for an enum type.
type EnumValueDefinition = schema.EnumValueDefinition
// InputValueDefinition defines an input value such as an argument.
type InputValueDefinition = schema.InputValueDefinition
// FieldDefinition defines a field on an object type.
type FieldDefinition = schema.FieldDefinition
// DirectiveDefinition defines a directive.
type DirectiveDefinition = schema.DirectiveDefinition
// ValidatorRule defines a rule that the validator will evaluate.
type ValidatorRule = validator.Rule
// Calculates the cost of the given operation and ensures it is not greater than max. If max is -1,
// no limit is enforced. If actual is non-nil, it is set to the actual cost of the operation.
// Queries with costs that are too high to calculate due to overflows always result in an error when
// max is non-negative, and actual will be set to the maximum possible value.
func ValidateCost(operationName string, variableValues map[string]interface{}, max int, actual *int, defaultCost schema.FieldCost) ValidatorRule {
return validator.ValidateCost(operationName, variableValues, max, actual, defaultCost)
}
// IncludeDirective implements the @include directive as defined by the GraphQL spec.
var IncludeDirective = schema.IncludeDirective
// SkipDirective implements the @skip directive as defined by the GraphQL spec.
var SkipDirective = schema.SkipDirective
// IDType implements the ID type as defined by the GraphQL spec. It can be deserialized from a
// string or an integer type, but always serializes to a string.
var IDType = schema.IDType
// StringType implements the String type as defined by the GraphQL spec.
var StringType = schema.StringType
// IntType implements the Int type as defined by the GraphQL spec.
var IntType = schema.IntType
// FloatType implements the Float type as defined by the GraphQL spec.
var FloatType = schema.FloatType
// BooleanType implements the Boolean type as defined by the GraphQL spec.
var BooleanType = schema.BooleanType
// NewNonNullType creates a new non-null type with the given wrapped type.
func NewNonNullType(t Type) *NonNullType {
return schema.NewNonNullType(t)
}
// NewListType creates a new list type with the given element type.
func NewListType(t Type) *ListType {
return schema.NewListType(t)
}
// ResolveResult represents the result of a field resolver. This type is generally used with
// ResolvePromise to pass around asynchronous results.
type ResolveResult = executor.ResolveResult
// ResolvePromise can be used to resolve fields asynchronously. You may return ResolvePromise from
// the field's resolve function. If you do, you must define an IdleHandler for the request. Any time
// request execution is unable to proceed, the idle handler will be invoked. Before the idle handler
// returns, a result must be sent to at least one previously returned ResolvePromise.
type ResolvePromise = executor.ResolvePromise
// Schema represents a GraphQL schema.
type Schema = schema.Schema
// SchemaDefinition defines a GraphQL schema.
type SchemaDefinition = schema.SchemaDefinition
// FeatureSet represents a set of features.
type FeatureSet = schema.FeatureSet
// NewFeatureSet creates a new feature set with the given features.
func NewFeatureSet(features ...string) FeatureSet {
return schema.NewFeatureSet(features...)
}
// NewSchema validates a schema definition and builds a Schema from it.
func NewSchema(def *SchemaDefinition) (*Schema, error) {
return schema.New(def)
}
// Request defines all of the inputs required to execute a GraphQL query.
type Request struct {
Context context.Context
Query string
// In some cases, you may want to optimize by providing the parsed and validated AST document
// instead of Query.
Document *ast.Document
Schema *Schema
OperationName string
VariableValues map[string]interface{}
Features FeatureSet
Extensions map[string]interface{}
InitialValue interface{}
IdleHandler func()
}
// Calculates the cost of the requested operation and ensures it is not greater than max. If max is
// -1, no limit is enforced. If actual is non-nil, it is set to the actual cost of the operation.
// Queries with costs that are too high to calculate due to overflows always result in an error when
// max is non-negative, and actual will be set to the maximum possible value.
func (r *Request) ValidateCost(max int, actual *int, defaultCost schema.FieldCost) ValidatorRule {
return validator.ValidateCost(r.OperationName, r.VariableValues, max, actual, defaultCost)
}
func (r *Request) executorRequest(doc *ast.Document) *executor.Request {
return &executor.Request{
Document: doc,
Schema: r.Schema,
OperationName: r.OperationName,
VariableValues: r.VariableValues,
Features: r.Features,
InitialValue: r.InitialValue,
IdleHandler: r.IdleHandler,
}
}
// NewRequestFromHTTP constructs a Request from an HTTP request. Requests may be GET requests using
// query string parameters or POST requests with either the application/json or application/graphql
// content type. If the request is malformed, an HTTP error code and error are returned.
func NewRequestFromHTTP(r *http.Request) (req *Request, code int, err error) {
req = &Request{
Context: r.Context(),
}
switch r.Method {
case http.MethodGet:
req.Query = r.URL.Query().Get("query")
if variables := r.URL.Query().Get("variables"); variables != "" {
if err := json.Unmarshal([]byte(variables), &req.VariableValues); err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("malformed variables parameter")
}
}
req.OperationName = r.URL.Query().Get("operationName")
if extensions := r.URL.Query().Get("extensions"); extensions != "" {
if err := json.Unmarshal([]byte(extensions), &req.Extensions); err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("malformed extensions parameter")
}
}
case http.MethodPost:
req.Query = r.URL.Query().Get("query")
switch mediaType, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")); mediaType {
case "application/json":
var body struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
Extensions map[string]interface{} `json:"extensions"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("malformed request body")
}
req.Query = body.Query
req.OperationName = body.OperationName
req.VariableValues = body.Variables
req.Extensions = body.Extensions
case "application/graphql":
body, _ := ioutil.ReadAll(r.Body)
req.Query = string(body)
default:
return nil, http.StatusBadRequest, fmt.Errorf("invalid content-type")
}
default:
return nil, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")
}
return req, http.StatusOK, nil
}
// Location represents the location of a character within a query's source text.
type Location struct {
Line int `json:"line"`
Column int `json:"column"`
}
// Error represents a GraphQL error as defined by the spec.
type Error struct {
Message string `json:"message"`
Locations []Location `json:"locations,omitempty"`
Path []interface{} `json:"path,omitempty"`
// To populate this field, your resolvers can return errors that implement ExtendedError.
Extensions map[string]interface{} `json:"extensions,omitempty"`
}
func (err *Error) Error() string {
return err.Message
}
// ExtendedError can be used to add data to a GraphQL error. If a resolver returns an error that
// implements this interface, the error's extensions property will be populated.
type ExtendedError interface {
error
Extensions() map[string]interface{}
}
// Response represents the result of executing a GraphQL query.
type Response struct {
Data *interface{} `json:"data,omitempty"`
Errors []*Error `json:"errors,omitempty"`
}
// IsSubscription returns true if the operation with the given name is a subscription operation.
// operationName can be "", in which case true will be returned if the only operation in the
// document is a subscription. In any error case (such as multiple matching subscriptions), false is
// returned.
func IsSubscription(doc *ast.Document, operationName string) bool {
return executor.IsSubscription(doc, operationName)
}
// ParseAndValidate parses and validates a query.
func ParseAndValidate(query string, schema *Schema, features schema.FeatureSet, additionalRules ...ValidatorRule) (*ast.Document, []*Error) {
var errors []*Error
parsed, parseErrs := parser.ParseDocument([]byte(query))
if len(parseErrs) > 0 {
for _, err := range parseErrs {
errors = append(errors, &Error{
Message: "Syntax error: " + err.Message,
Locations: []Location{
{
Line: err.Location.Line,
Column: err.Location.Column,
},
},
})
}
return nil, errors
}
if validationErrs := validator.ValidateDocument(parsed, schema, features, additionalRules...); len(validationErrs) > 0 {
for _, err := range validationErrs {
locations := make([]Location, len(err.Locations))
for i, loc := range err.Locations {
locations[i].Line = loc.Line
locations[i].Column = loc.Column
}
errors = append(errors, &Error{
Message: "Validation error: " + err.Message,
Locations: locations,
})
}
return nil, errors
}
return parsed, nil
}
func newErrorFromExecutorError(err *executor.Error) *Error {
locations := make([]Location, len(err.Locations))
for i, loc := range err.Locations {
locations[i].Line = loc.Line
locations[i].Column = loc.Column
}
retErr := &Error{
Message: err.Message,
Locations: locations,
Path: err.Path,
}
if ext, ok := err.Unwrap().(ExtendedError); ok {
retErr.Extensions = ext.Extensions()
}
return retErr
}
// Subscribe is used to implement subscription support. For subscribe operations (as indicated via
// IsSubscription), this should be invoked on Execute. On success it will return the result of
// executing the subscription field's resolver.
func Subscribe(r *Request) (interface{}, []*Error) {
doc := r.Document
if doc == nil {
var errors []*Error
doc, errors = ParseAndValidate(r.Query, r.Schema, r.Features)
if len(errors) > 0 {
return nil, errors
}
}
ret, err := executor.Subscribe(r.Context, r.executorRequest(doc))
if err != nil {
return nil, []*Error{newErrorFromExecutorError(err)}
}
return ret, nil
}
// Execute executes a query. If the request does not have a Document defined, the Query field will
// be parsed and validated.
func Execute(r *Request) *Response {
ret := &Response{}
doc := r.Document
if doc == nil {
var errors []*Error
doc, errors = ParseAndValidate(r.Query, r.Schema, r.Features)
if len(errors) > 0 {
return &Response{
Errors: errors,
}
}
}
data, errs := executor.ExecuteRequest(r.Context, r.executorRequest(doc))
var dataInterface interface{}
dataInterface = data
ret.Data = &dataInterface
for _, err := range errs {
ret.Errors = append(ret.Errors, newErrorFromExecutorError(err))
}
return ret
}