diff options
author | Michael Muré <batolettre@gmail.com> | 2018-09-14 12:40:31 +0200 |
---|---|---|
committer | Michael Muré <batolettre@gmail.com> | 2018-09-14 12:41:59 +0200 |
commit | b478cd1bcb4756b20f7f4b15fcf81f23e1a60a02 (patch) | |
tree | 8ce232dcab3dd00708f8ba66c334472457e5980d /vendor/github.com/99designs/gqlgen/handler | |
parent | a3fc9abb921f5ce7084d6ab7473442d0b72b1d78 (diff) | |
download | git-bug-b478cd1bcb4756b20f7f4b15fcf81f23e1a60a02.tar.gz |
graphql: update gqlgen to 0.5.1
fix #6
Diffstat (limited to 'vendor/github.com/99designs/gqlgen/handler')
4 files changed, 640 insertions, 0 deletions
diff --git a/vendor/github.com/99designs/gqlgen/handler/graphql.go b/vendor/github.com/99designs/gqlgen/handler/graphql.go new file mode 100644 index 00000000..9d222826 --- /dev/null +++ b/vendor/github.com/99designs/gqlgen/handler/graphql.go @@ -0,0 +1,283 @@ +package handler + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/99designs/gqlgen/complexity" + "github.com/99designs/gqlgen/graphql" + "github.com/gorilla/websocket" + "github.com/hashicorp/golang-lru" + "github.com/vektah/gqlparser" + "github.com/vektah/gqlparser/ast" + "github.com/vektah/gqlparser/gqlerror" + "github.com/vektah/gqlparser/validator" +) + +type params struct { + Query string `json:"query"` + OperationName string `json:"operationName"` + Variables map[string]interface{} `json:"variables"` +} + +type Config struct { + cacheSize int + upgrader websocket.Upgrader + recover graphql.RecoverFunc + errorPresenter graphql.ErrorPresenterFunc + resolverHook graphql.FieldMiddleware + requestHook graphql.RequestMiddleware + complexityLimit int +} + +func (c *Config) newRequestContext(doc *ast.QueryDocument, query string, variables map[string]interface{}) *graphql.RequestContext { + reqCtx := graphql.NewRequestContext(doc, query, variables) + if hook := c.recover; hook != nil { + reqCtx.Recover = hook + } + + if hook := c.errorPresenter; hook != nil { + reqCtx.ErrorPresenter = hook + } + + if hook := c.resolverHook; hook != nil { + reqCtx.ResolverMiddleware = hook + } + + if hook := c.requestHook; hook != nil { + reqCtx.RequestMiddleware = hook + } + + return reqCtx +} + +type Option func(cfg *Config) + +func WebsocketUpgrader(upgrader websocket.Upgrader) Option { + return func(cfg *Config) { + cfg.upgrader = upgrader + } +} + +func RecoverFunc(recover graphql.RecoverFunc) Option { + return func(cfg *Config) { + cfg.recover = recover + } +} + +// ErrorPresenter transforms errors found while resolving into errors that will be returned to the user. It provides +// a good place to add any extra fields, like error.type, that might be desired by your frontend. Check the default +// implementation in graphql.DefaultErrorPresenter for an example. +func ErrorPresenter(f graphql.ErrorPresenterFunc) Option { + return func(cfg *Config) { + cfg.errorPresenter = f + } +} + +// ComplexityLimit sets a maximum query complexity that is allowed to be executed. +// If a query is submitted that exceeds the limit, a 422 status code will be returned. +func ComplexityLimit(limit int) Option { + return func(cfg *Config) { + cfg.complexityLimit = limit + } +} + +// ResolverMiddleware allows you to define a function that will be called around every resolver, +// useful for tracing and logging. +func ResolverMiddleware(middleware graphql.FieldMiddleware) Option { + return func(cfg *Config) { + if cfg.resolverHook == nil { + cfg.resolverHook = middleware + return + } + + lastResolve := cfg.resolverHook + cfg.resolverHook = func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) { + return lastResolve(ctx, func(ctx context.Context) (res interface{}, err error) { + return middleware(ctx, next) + }) + } + } +} + +// RequestMiddleware allows you to define a function that will be called around the root request, +// after the query has been parsed. This is useful for logging and tracing +func RequestMiddleware(middleware graphql.RequestMiddleware) Option { + return func(cfg *Config) { + if cfg.requestHook == nil { + cfg.requestHook = middleware + return + } + + lastResolve := cfg.requestHook + cfg.requestHook = func(ctx context.Context, next func(ctx context.Context) []byte) []byte { + return lastResolve(ctx, func(ctx context.Context) []byte { + return middleware(ctx, next) + }) + } + } +} + +// CacheSize sets the maximum size of the query cache. +// If size is less than or equal to 0, the cache is disabled. +func CacheSize(size int) Option { + return func(cfg *Config) { + cfg.cacheSize = size + } +} + +const DefaultCacheSize = 1000 + +func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc { + cfg := Config{ + cacheSize: DefaultCacheSize, + upgrader: websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + }, + } + + for _, option := range options { + option(&cfg) + } + + var cache *lru.Cache + if cfg.cacheSize > 0 { + var err error + cache, err = lru.New(DefaultCacheSize) + 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 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + w.Header().Set("Allow", "OPTIONS, GET, POST") + w.WriteHeader(http.StatusOK) + return + } + + if strings.Contains(r.Header.Get("Upgrade"), "websocket") { + connectWs(exec, w, r, &cfg) + return + } + + var reqParams params + switch r.Method { + case http.MethodGet: + reqParams.Query = r.URL.Query().Get("query") + reqParams.OperationName = r.URL.Query().Get("operationName") + + if variables := r.URL.Query().Get("variables"); variables != "" { + if err := jsonDecode(strings.NewReader(variables), &reqParams.Variables); err != nil { + sendErrorf(w, http.StatusBadRequest, "variables could not be decoded") + return + } + } + case http.MethodPost: + if err := jsonDecode(r.Body, &reqParams); err != nil { + sendErrorf(w, http.StatusBadRequest, "json body could not be decoded: "+err.Error()) + return + } + default: + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + w.Header().Set("Content-Type", "application/json") + + var doc *ast.QueryDocument + if cache != nil { + val, ok := cache.Get(reqParams.Query) + if ok { + doc = val.(*ast.QueryDocument) + } + } + if doc == nil { + var qErr gqlerror.List + doc, qErr = gqlparser.LoadQuery(exec.Schema(), reqParams.Query) + if len(qErr) > 0 { + sendError(w, http.StatusUnprocessableEntity, qErr...) + return + } + if cache != nil { + cache.Add(reqParams.Query, doc) + } + } + + op := doc.Operations.ForName(reqParams.OperationName) + if op == nil { + sendErrorf(w, http.StatusUnprocessableEntity, "operation %s not found", reqParams.OperationName) + return + } + + if op.Operation != ast.Query && r.Method == http.MethodGet { + sendErrorf(w, http.StatusUnprocessableEntity, "GET requests only allow query operations") + return + } + + vars, err := validator.VariableValues(exec.Schema(), op, reqParams.Variables) + if err != nil { + sendError(w, http.StatusUnprocessableEntity, err) + return + } + reqCtx := cfg.newRequestContext(doc, reqParams.Query, vars) + ctx := graphql.WithRequestContext(r.Context(), reqCtx) + + defer func() { + if err := recover(); err != nil { + userErr := reqCtx.Recover(ctx, err) + sendErrorf(w, http.StatusUnprocessableEntity, userErr.Error()) + } + }() + + if cfg.complexityLimit > 0 { + queryComplexity := complexity.Calculate(exec, op, vars) + if queryComplexity > cfg.complexityLimit { + sendErrorf(w, http.StatusUnprocessableEntity, "query has complexity %d, which exceeds the limit of %d", queryComplexity, cfg.complexityLimit) + return + } + } + + switch op.Operation { + case ast.Query: + b, err := json.Marshal(exec.Query(ctx, op)) + if err != nil { + panic(err) + } + w.Write(b) + case ast.Mutation: + b, err := json.Marshal(exec.Mutation(ctx, op)) + if err != nil { + panic(err) + } + w.Write(b) + default: + sendErrorf(w, http.StatusBadRequest, "unsupported operation type") + } + }) +} + +func jsonDecode(r io.Reader, val interface{}) error { + dec := json.NewDecoder(r) + dec.UseNumber() + return dec.Decode(val) +} + +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...)}) +} diff --git a/vendor/github.com/99designs/gqlgen/handler/playground.go b/vendor/github.com/99designs/gqlgen/handler/playground.go new file mode 100644 index 00000000..d0ada8ca --- /dev/null +++ b/vendor/github.com/99designs/gqlgen/handler/playground.go @@ -0,0 +1,54 @@ +package handler + +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="//cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"/> + <link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"/> + <script src="//cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"></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 }}', + settings: { + 'request.credentials': 'same-origin' + } + }) + }) +</script> +</body> +</html> +`)) + +func Playground(title string, endpoint string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + err := page.Execute(w, map[string]string{ + "title": title, + "endpoint": endpoint, + "version": "1.6.2", + }) + if err != nil { + panic(err) + } + } +} diff --git a/vendor/github.com/99designs/gqlgen/handler/stub.go b/vendor/github.com/99designs/gqlgen/handler/stub.go new file mode 100644 index 00000000..d237e188 --- /dev/null +++ b/vendor/github.com/99designs/gqlgen/handler/stub.go @@ -0,0 +1,51 @@ +package handler + +import ( + "context" + + "github.com/99designs/gqlgen/graphql" + "github.com/vektah/gqlparser" + "github.com/vektah/gqlparser/ast" +) + +type executableSchemaStub struct { + NextResp chan struct{} +} + +var _ graphql.ExecutableSchema = &executableSchemaStub{} + +func (e *executableSchemaStub) Schema() *ast.Schema { + return gqlparser.MustLoadSchema(&ast.Source{Input: ` + schema { query: Query } + type Query { + me: User! + user(id: Int): User! + } + type User { name: String! } + `}) +} + +func (e *executableSchemaStub) Complexity(typeName, field string, childComplexity int, args map[string]interface{}) (int, bool) { + return 0, false +} + +func (e *executableSchemaStub) Query(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + return &graphql.Response{Data: []byte(`{"name":"test"}`)} +} + +func (e *executableSchemaStub) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { + return graphql.ErrorResponse(ctx, "mutations are not supported") +} + +func (e *executableSchemaStub) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response { + return func() *graphql.Response { + select { + case <-ctx.Done(): + return nil + case <-e.NextResp: + return &graphql.Response{ + Data: []byte(`{"name":"test"}`), + } + } + } +} diff --git a/vendor/github.com/99designs/gqlgen/handler/websocket.go b/vendor/github.com/99designs/gqlgen/handler/websocket.go new file mode 100644 index 00000000..2be1e87f --- /dev/null +++ b/vendor/github.com/99designs/gqlgen/handler/websocket.go @@ -0,0 +1,252 @@ +package handler + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "sync" + + "github.com/99designs/gqlgen/graphql" + "github.com/gorilla/websocket" + "github.com/vektah/gqlparser" + "github.com/vektah/gqlparser/ast" + "github.com/vektah/gqlparser/gqlerror" + "github.com/vektah/gqlparser/validator" +) + +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 TODO: keepalives +) + +type operationMessage struct { + Payload json.RawMessage `json:"payload,omitempty"` + ID string `json:"id,omitempty"` + Type string `json:"type"` +} + +type wsConnection struct { + ctx context.Context + conn *websocket.Conn + exec graphql.ExecutableSchema + active map[string]context.CancelFunc + mu sync.Mutex + cfg *Config +} + +func connectWs(exec graphql.ExecutableSchema, w http.ResponseWriter, r *http.Request, cfg *Config) { + ws, err := cfg.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{}, + exec: exec, + conn: ws, + ctx: r.Context(), + cfg: cfg, + } + + 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: + c.write(&operationMessage{Type: connectionAckMsg}) + 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() { + for { + message := c.readOp() + if message == nil { + return + } + + switch message.Type { + case startMsg: + if !c.subscribe(message) { + return + } + case stopMsg: + c.mu.Lock() + closer := c.active[message.ID] + c.mu.Unlock() + if closer == nil { + c.sendError(message.ID, gqlerror.Errorf("%s is not running, cannot stop", message.ID)) + continue + } + + 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) subscribe(message *operationMessage) bool { + var reqParams params + if err := jsonDecode(bytes.NewReader(message.Payload), &reqParams); err != nil { + c.sendConnectionError("invalid json") + return false + } + + doc, qErr := gqlparser.LoadQuery(c.exec.Schema(), reqParams.Query) + if qErr != nil { + c.sendError(message.ID, qErr...) + return true + } + + op := doc.Operations.ForName(reqParams.OperationName) + if op == nil { + c.sendError(message.ID, gqlerror.Errorf("operation %s not found", reqParams.OperationName)) + return true + } + + vars, err := validator.VariableValues(c.exec.Schema(), op, reqParams.Variables) + if err != nil { + c.sendError(message.ID, err) + return true + } + reqCtx := c.cfg.newRequestContext(doc, reqParams.Query, vars) + ctx := graphql.WithRequestContext(c.ctx, reqCtx) + + if op.Operation != ast.Subscription { + var result *graphql.Response + if op.Operation == ast.Query { + result = c.exec.Query(ctx, op) + } else { + result = c.exec.Mutation(ctx, op) + } + + c.sendData(message.ID, result) + c.write(&operationMessage{ID: message.ID, Type: completeMsg}) + return true + } + + 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 := reqCtx.Recover(ctx, r) + c.sendError(message.ID, &gqlerror.Error{Message: userErr.Error()}) + } + }() + next := c.exec.Subscription(ctx, op) + for result := next(); result != nil; result = next() { + c.sendData(message.ID, result) + } + + c.write(&operationMessage{ID: message.ID, Type: completeMsg}) + + c.mu.Lock() + delete(c.active, message.ID) + c.mu.Unlock() + cancel() + }() + + return true +} + +func (c *wsConnection) sendData(id string, response *graphql.Response) { + b, err := json.Marshal(response) + if err != nil { + c.sendError(id, gqlerror.Errorf("unable to encode json response: %s", err.Error())) + return + } + + c.write(&operationMessage{Type: dataMsg, ID: id, Payload: b}) +} + +func (c *wsConnection) sendError(id string, errors ...*gqlerror.Error) { + var errs []error + for _, err := range errors { + errs = append(errs, 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 err != nil { + c.sendConnectionError("invalid json") + 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() +} |