Browse Source

feat: GraphQL parsing and pretty printing via API

Matthias Ladkau 2 years ago
parent
commit
42f5e24542
4 changed files with 275 additions and 20 deletions
  1. 132 17
      api/v1/graphql.go
  2. 140 0
      api/v1/graphql_test.go
  3. 1 1
      go.mod
  4. 2 2
      go.sum

+ 132 - 17
api/v1/graphql.go

@@ -15,6 +15,7 @@ import (
 	"fmt"
 	"net/http"
 
+	"devt.de/krotik/common/lang/graphql/parser"
 	"devt.de/krotik/common/stringutil"
 	"devt.de/krotik/eliasdb/api"
 	"devt.de/krotik/eliasdb/graphql"
@@ -43,6 +44,8 @@ type graphQLEndpoint struct {
 HandlePOST handles GraphQL queries.
 */
 func (e *graphQLEndpoint) HandlePOST(w http.ResponseWriter, r *http.Request, resources []string) {
+	var err error
+	var res map[string]interface{}
 
 	dec := json.NewDecoder(r.Body)
 	data := make(map[string]interface{})
@@ -52,28 +55,81 @@ func (e *graphQLEndpoint) HandlePOST(w http.ResponseWriter, r *http.Request, res
 		return
 	}
 
-	partData, ok := data["partition"]
-	if !ok && len(resources) > 0 {
-		partData = resources[0]
-		ok = true
-	}
-	if !ok || partData == "" {
-		http.Error(w, "Need a partition", http.StatusBadRequest)
+	toAST, ok1 := data["query-to-ast"]
+	toQuery, ok2 := data["ast-to-query"]
+	if ok1 || ok2 {
+
+		res := make(map[string]interface{})
+
+		if ok1 {
+			resast, err := parser.Parse("request", fmt.Sprint(toAST))
+
+			if err != nil {
+				http.Error(w, err.Error(), http.StatusBadRequest)
+				return
+			}
+
+			res["result-ast"] = resast.Plain()
+		}
+
+		if ok2 {
+			astmap, ok := toQuery.(map[string]interface{})
+
+			if !ok {
+				http.Error(w, "Plain AST object expected as 'ast-to-query' value", http.StatusBadRequest)
+				return
+			}
+
+			// Try to create a proper AST from plain AST
+
+			astnode, err := parser.ASTFromPlain(astmap)
+
+			if err != nil {
+				http.Error(w, err.Error(), http.StatusBadRequest)
+				return
+			}
+
+			// Now pretty print the AST
+
+			ppres, err := parser.PrettyPrint(astnode)
+
+			if err != nil {
+				http.Error(w, err.Error(), http.StatusBadRequest)
+				return
+			}
+
+			res["result-query"] = ppres
+		}
+
+		w.Header().Set("content-type", "application/json; charset=utf-8")
+		json.NewEncoder(w).Encode(res)
+
 		return
-	}
 
-	part := fmt.Sprint(partData)
+	} else {
+		partData, ok := data["partition"]
+		if !ok && len(resources) > 0 {
+			partData = resources[0]
+			ok = true
+		}
+		if !ok || partData == "" {
+			http.Error(w, "Need a partition", http.StatusBadRequest)
+			return
+		}
 
-	if _, ok := data["variables"]; !ok {
-		data["variables"] = nil
-	}
+		part := fmt.Sprint(partData)
 
-	if _, ok := data["operationName"]; !ok {
-		data["operationName"] = nil
-	}
+		if _, ok := data["variables"]; !ok {
+			data["variables"] = nil
+		}
 
-	res, err := graphql.RunQuery(stringutil.CreateDisplayString(part)+" query",
-		part, data, api.GM, nil, false)
+		if _, ok := data["operationName"]; !ok {
+			data["operationName"] = nil
+		}
+
+		res, err = graphql.RunQuery(stringutil.CreateDisplayString(part)+" query",
+			part, data, api.GM, nil, false)
+	}
 
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest)
@@ -151,4 +207,63 @@ func (e *graphQLEndpoint) SwaggerDefs(s map[string]interface{}) {
 			},
 		},
 	}
+
+	s["paths"].(map[string]interface{})["/v1/graphql"] = map[string]interface{}{
+		"post": map[string]interface{}{
+			"summary":     "GraphQL parser and pretty printer endpoint.",
+			"description": "The GraphQL endpoint without specifying a partition should be used to parse a given GraphQL query into an Abstract Syntax Tree or pretty print a given Abstract Syntax Tree into a GraphQL query.",
+			"consumes": []string{
+				"application/json",
+			},
+			"produces": []string{
+				"text/plain",
+				"application/json",
+			},
+			"parameters": []map[string]interface{}{
+				{
+					"name":        "data",
+					"in":          "body",
+					"description": "Query or AST which should be converted.",
+					"required":    true,
+					"schema": map[string]interface{}{
+						"type": "object",
+						"properties": map[string]interface{}{
+							"query-to-ast": map[string]interface{}{
+								"description": "Query which should be parsed.",
+								"type":        "string",
+							},
+							"ast-to-query": map[string]interface{}{
+								"description": "AST which should be pretty printed.",
+								"type":        "object",
+							},
+						},
+					},
+				},
+			},
+			"responses": map[string]interface{}{
+				"200": map[string]interface{}{
+					"description": "The operation was successful.",
+					"schema": map[string]interface{}{
+						"type": "object",
+						"properties": map[string]interface{}{
+							"result-ast": map[string]interface{}{
+								"description": "The resulting AST if a query was parsed.",
+								"type":        "object",
+							},
+							"result-query": map[string]interface{}{
+								"description": "The pretty printed query if an AST was given.",
+								"type":        "string",
+							},
+						},
+					},
+				},
+				"default": map[string]interface{}{
+					"description": "Error response",
+					"schema": map[string]interface{}{
+						"$ref": "#/definitions/Error",
+					},
+				},
+			},
+		},
+	}
 }

+ 140 - 0
api/v1/graphql_test.go

@@ -175,3 +175,143 @@ func TestGraphQLErrors(t *testing.T) {
 		return
 	}
 }
+
+func TestGraphQLParsing(t *testing.T) {
+	queryURL := "http://localhost" + TESTPORT + EndpointGraphQL
+
+	q, err := json.Marshal(map[string]interface{}{
+		"query-to-ast": `{
+  Song
+}`,
+	})
+	errorutil.AssertOk(err)
+	_, _, res := sendTestRequest(queryURL+"main", "POST", q)
+
+	if res != `
+{
+  "result-ast": {
+    "children": [
+      {
+        "children": [
+          {
+            "children": [
+              {
+                "children": [
+                  {
+                    "children": [
+                      {
+                        "name": "Name",
+                        "value": "Song"
+                      }
+                    ],
+                    "name": "Field"
+                  }
+                ],
+                "name": "SelectionSet"
+              }
+            ],
+            "name": "OperationDefinition"
+          }
+        ],
+        "name": "ExecutableDefinition"
+      }
+    ],
+    "name": "Document"
+  }
+}`[1:] {
+		t.Error("Unexpected response:", res)
+		return
+	}
+
+	_, _, res = sendTestRequest(queryURL+"main", "POST", []byte(`{"ast-to-query": {
+  "children": [
+    {
+      "children": [
+        {
+          "children": [
+            {
+              "children": [
+                {
+                  "children": [
+                    {
+                      "name": "Name",
+                      "value": "Song"
+                    }
+                  ],
+                  "name": "Field"
+                }
+              ],
+              "name": "SelectionSet"
+            }
+          ],
+          "name": "OperationDefinition"
+        }
+      ],
+      "name": "ExecutableDefinition"
+    }
+  ],
+  "name": "Document"
+}}`))
+
+	if res != `
+{
+  "result-query": "{\n  Song\n}"
+}`[1:] {
+		t.Error("Unexpected response:", res)
+		return
+	}
+}
+
+func TestGraphQLParsingErrors(t *testing.T) {
+	queryURL := "http://localhost" + TESTPORT + EndpointGraphQL
+
+	q, err := json.Marshal(map[string]interface{}{
+		"query-to-ast": `{{
+  Song
+}`,
+	})
+	errorutil.AssertOk(err)
+	_, _, res := sendTestRequest(queryURL+"main", "POST", q)
+
+	if res != "Parse error in request: Name expected ({) (Line:1 Pos:2)" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+
+	q, err = json.Marshal(map[string]interface{}{
+		"ast-to-query": `aaa`,
+	})
+	errorutil.AssertOk(err)
+	_, _, res = sendTestRequest(queryURL+"main", "POST", q)
+
+	if res != "Plain AST object expected as 'ast-to-query' value" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+
+	q, err = json.Marshal(map[string]interface{}{
+		"ast-to-query": map[string]interface{}{
+			"foo": `Document`,
+		},
+	})
+	errorutil.AssertOk(err)
+	_, _, res = sendTestRequest(queryURL+"main", "POST", q)
+
+	if res != "Found plain ast node without a name: map[foo:Document]" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+
+	q, err = json.Marshal(map[string]interface{}{
+		"ast-to-query": map[string]interface{}{
+			"name": `foo`,
+		},
+	})
+	errorutil.AssertOk(err)
+	_, _, res = sendTestRequest(queryURL+"main", "POST", q)
+
+	if res != "Could not find template for foo (tempkey: foo)" {
+		t.Error("Unexpected response:", res)
+		return
+	}
+}

+ 1 - 1
go.mod

@@ -3,7 +3,7 @@ module devt.de/krotik/eliasdb
 go 1.12
 
 require (
-	devt.de/krotik/common v1.4.6
+	devt.de/krotik/common v1.5.1
 	devt.de/krotik/ecal v1.6.2
 	github.com/gorilla/websocket v1.4.1
 )

+ 2 - 2
go.sum

@@ -1,6 +1,6 @@
 devt.de/krotik/common v1.4.0/go.mod h1:X4nsS85DAxyHkwSg/Tc6+XC2zfmGeaVz+37F61+eSaI=
-devt.de/krotik/common v1.4.6 h1:lRdaOKY57Cd90soMpZF/hAMevRHDcyHY/478FN7IFhs=
-devt.de/krotik/common v1.4.6/go.mod h1:X4nsS85DAxyHkwSg/Tc6+XC2zfmGeaVz+37F61+eSaI=
+devt.de/krotik/common v1.5.1 h1:UV65XZ1W82elEh9mCCUkJ6HYuXDqZXgS/cIbPtax5+4=
+devt.de/krotik/common v1.5.1/go.mod h1:X4nsS85DAxyHkwSg/Tc6+XC2zfmGeaVz+37F61+eSaI=
 devt.de/krotik/ecal v1.6.2 h1:ObIPXVQVYsxuXOZcENoZQMnwgGsN42YkfaCd7aFRdZk=
 devt.de/krotik/ecal v1.6.2/go.mod h1:0qIx3h+EjUnStgdEUnwAeO44UluTSLcpBWXA5zEw0hQ=
 github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=