diff --git a/graphql/e2e/custom_logic/cmd/main.go b/graphql/e2e/custom_logic/cmd/main.go index 9981f546562..14d3819cbca 100644 --- a/graphql/e2e/custom_logic/cmd/main.go +++ b/graphql/e2e/custom_logic/cmd/main.go @@ -277,6 +277,20 @@ func postFavMoviesHandler(w http.ResponseWriter, r *http.Request) { check2(w.Write(getDefaultResponse())) } +func postFavMoviesWithBodyHandler(w http.ResponseWriter, r *http.Request) { + err := verifyRequest(r, expectedRequest{ + method: http.MethodPost, + urlSuffix: "/0x123?name=Author", + body: `{"id":"0x123","name":"Author"}`, + headers: nil, + }) + if err != nil { + check2(w.Write([]byte(err.Error()))) + return + } + check2(w.Write(getDefaultResponse())) +} + func verifyHeadersHandler(w http.ResponseWriter, r *http.Request) { err := verifyRequest(r, expectedRequest{ method: http.MethodGet, @@ -384,6 +398,36 @@ func favMoviesCreateHandler(w http.ResponseWriter, r *http.Request) { ]`))) } +func favMoviesCreateWithNullBodyHandler(w http.ResponseWriter, r *http.Request) { + err := verifyRequest(r, expectedRequest{ + method: http.MethodPost, + urlSuffix: "/favMoviesCreateWithNullBody", + body: `{"movies":[{"director":[{"name":"Dir1"}],"name":"Mov1"},{"name":null}]}`, + headers: nil, + }) + if err != nil { + check2(w.Write([]byte(err.Error()))) + return + } + + check2(w.Write([]byte(`[ + { + "id": "0x1", + "name": "Mov1", + "director": [ + { + "id": "0x2", + "name": "Dir1" + } + ] + }, + { + "id": "0x3", + "name": null + } + ]`))) +} + func favMoviesUpdateHandler(w http.ResponseWriter, r *http.Request) { err := verifyRequest(r, expectedRequest{ method: http.MethodPatch, @@ -792,6 +836,39 @@ func userNameHandler(w http.ResponseWriter, r *http.Request) { nameHandler(w, r, &inputBody) } +func userNameWithoutAddressHandler(w http.ResponseWriter, r *http.Request) { + expectedRequest := expectedRequest{ + body: `{"uid":"0x5"}`, + } + + b, err := ioutil.ReadAll(r.Body) + fmt.Println(b, err) + if err != nil { + err = getError("Unable to read request body", err.Error()) + check2(w.Write([]byte(err.Error()))) + return + } + + if string(b) != expectedRequest.body { + err = getError("Unexpected value for request body", string(b)) + } + if err != nil { + check2(w.Write([]byte(err.Error()))) + return + } + + var inputBody input + if err := json.Unmarshal(b, &inputBody); err != nil { + fmt.Println("while doing JSON unmarshal: ", err) + check2(w.Write([]byte(err.Error()))) + return + } + + n := fmt.Sprintf(`"%s"`, inputBody.Name()) + check2(fmt.Fprint(w, n)) + +} + func carHandler(w http.ResponseWriter, r *http.Request) { var inputBody input err := getInput(r, &inputBody) @@ -1134,6 +1211,7 @@ func main() { // for queries http.HandleFunc("/favMovies/", getFavMoviesHandler) http.HandleFunc("/favMoviesPost/", postFavMoviesHandler) + http.HandleFunc("/favMoviesPostWithBody/", postFavMoviesWithBodyHandler) http.HandleFunc("/verifyHeaders", verifyHeadersHandler) http.HandleFunc("/verifyCustomNameHeaders", verifyCustomNameHeadersHandler) http.HandleFunc("/twitterfollowers", twitterFollwerHandler) @@ -1142,7 +1220,7 @@ func main() { http.HandleFunc("/favMoviesCreate", favMoviesCreateHandler) http.HandleFunc("/favMoviesUpdate/", favMoviesUpdateHandler) http.HandleFunc("/favMoviesDelete/", favMoviesDeleteHandler) - + http.HandleFunc("/favMoviesCreateWithNullBody", favMoviesCreateWithNullBodyHandler) // The endpoints below are for testing custom resolution of fields within type definitions. // for testing batch mode http.HandleFunc("/userNames", userNamesHandler) @@ -1154,6 +1232,7 @@ func main() { // for testing single mode http.HandleFunc("/userName", userNameHandler) + http.HandleFunc("/userNameWithoutAddress", userNameWithoutAddressHandler) http.HandleFunc("/checkHeadersForUserName", userNameHandlerWithHeaders) http.HandleFunc("/car", carHandler) http.HandleFunc("/class", classHandler) diff --git a/graphql/e2e/custom_logic/custom_logic_test.go b/graphql/e2e/custom_logic/custom_logic_test.go index 45845d93d90..08881031415 100644 --- a/graphql/e2e/custom_logic/custom_logic_test.go +++ b/graphql/e2e/custom_logic/custom_logic_test.go @@ -37,6 +37,7 @@ const ( alphaURL = "http://localhost:8180/graphql" alphaAdminURL = "http://localhost:8180/admin" subscriptionEndpoint = "ws://localhost:8180/graphql" + groupOnegRPC = "localhost:9180" customTypes = `type MovieDirector @remote { id: ID! name: String! @@ -158,6 +159,42 @@ func TestCustomPostQuery(t *testing.T) { require.JSONEq(t, expected, string(result.Data)) } +func TestCustomPostQueryWithBody(t *testing.T) { + schema := customTypes + ` + type Query { + myFavoriteMoviesPost(id: ID!, name: String!, num: Int): [Movie] @custom(http: { + url: "http://mock:8888/favMoviesPostWithBody/$id?name=$name", + body:"{id:$id,name:$name,num:$num}" + method: "POST" + }) + }` + updateSchemaRequireNoGQLErrors(t, schema) + time.Sleep(2 * time.Second) + + query := ` + query { + myFavoriteMoviesPost(id: "0x123", name: "Author") { + id + name + director { + id + name + } + } + }` + params := &common.GraphQLParams{ + Query: query, + } + + result := params.ExecuteAsPost(t, alphaURL) + require.Nil(t, result.Errors) + + expected := `{"myFavoriteMoviesPost":[{"id":"0x3","name":"Star Wars","director": + [{"id":"0x4","name":"George Lucas"}]},{"id":"0x5","name":"Star Trek","director": + [{"id":"0x6","name":"J.J. Abrams"}]}]}` + require.JSONEq(t, expected, string(result.Data)) +} + func TestCustomQueryShouldForwardHeaders(t *testing.T) { schema := customTypes + ` type Query { @@ -1012,6 +1049,103 @@ func TestCustomFieldsShouldForwardHeaders(t *testing.T) { require.Nilf(t, result.Errors, "%+v", result.Errors) } +func TestCustomFieldsShouldSkipNonEmptyVariable(t *testing.T) { + schema := ` + type User { + id: ID! + address:String + name: String + @custom( + http: { + url: "http://mock:8888/userName" + method: "GET" + body: "{uid: $id,address:$address}" + mode: SINGLE, + secretHeaders: ["GITHUB-API-TOKEN"] + } + ) + age: Int! @search + } + +# Dgraph.Secret GITHUB-API-TOKEN "some-api-token" + ` + updateSchemaRequireNoGQLErrors(t, schema) + time.Sleep(2 * time.Second) + + users := addUsers(t) + queryUser := ` + query ($id: [ID!]){ + queryUser(filter: {id: $id}, order: {asc: age}) { + name + age + } + }` + params := &common.GraphQLParams{ + Query: queryUser, + Variables: map[string]interface{}{"id": []interface{}{ + users[0].ID, users[1].ID, users[2].ID}}, + } + + result := params.ExecuteAsPost(t, alphaURL) + require.Nilf(t, result.Errors, "%+v", result.Errors) +} + +func TestCustomFieldsShouldPassBody(t *testing.T) { + dg, err := testutil.DgraphClient(groupOnegRPC) + require.NoError(t, err) + testutil.DropAll(t, dg) + schema := ` + type User { + id: String! @id @search(by: [hash, regexp]) + address:String + name: String + @custom( + http: { + url: "http://mock:8888/userNameWithoutAddress" + method: "GET" + body: "{uid: $id,address:$address}" + mode: SINGLE, + secretHeaders: ["GITHUB-API-TOKEN"] + } + ) + age: Int! @search + } +# Dgraph.Secret GITHUB-API-TOKEN "some-api-token" + ` + updateSchemaRequireNoGQLErrors(t, schema) + time.Sleep(2 * time.Second) + + params := &common.GraphQLParams{ + Query: `mutation addUser { + addUser(input: [{ id:"0x5", age: 10 }]) { + user { + id + age + } + } + }`, + } + + result := params.ExecuteAsPost(t, alphaURL) + common.RequireNoGQLErrors(t, result) + + queryUser := ` + query ($id: String!){ + queryUser(filter: {id: {eq: $id}}) { + name + age + } + }` + + params = &common.GraphQLParams{ + Query: queryUser, + Variables: map[string]interface{}{"id": "0x5"}, + } + + result = params.ExecuteAsPost(t, alphaURL) + require.Nilf(t, result.Errors, "%+v", result.Errors) +} + func TestCustomFieldsShouldBeResolved(t *testing.T) { // This test adds data, modifies the schema multiple times and fetches the data. // It has the following modes. @@ -1732,6 +1866,85 @@ func TestCustomPostMutation(t *testing.T) { require.JSONEq(t, expected, string(result.Data)) } +func TestCustomPostMutationNullInBody(t *testing.T) { + schema := `type MovieDirector @remote { + id: ID! + name: String! + directed: [Movie] + } + type Movie @remote { + id: ID! + name: String + director: [MovieDirector] + } + input MovieDirectorInput { + id: ID + name: String + directed: [MovieInput] + } + input MovieInput { + id: ID + name: String + director: [MovieDirectorInput] + } + type Mutation { + createMyFavouriteMovies(input: [MovieInput!]): [Movie] @custom(http: { + url: "http://mock:8888/favMoviesCreateWithNullBody", + method: "POST", + body: "{ movies: $input}" + }) + }` + updateSchemaRequireNoGQLErrors(t, schema) + time.Sleep(2 * time.Second) + + params := &common.GraphQLParams{ + Query: ` + mutation createMovies($movs: [MovieInput!]) { + createMyFavouriteMovies(input: $movs) { + id + name + director { + id + name + } + } + }`, + Variables: map[string]interface{}{ + "movs": []interface{}{ + map[string]interface{}{ + "name": "Mov1", + "director": []interface{}{map[string]interface{}{"name": "Dir1"}}, + }, + map[string]interface{}{"name": nil}, + }}, + } + + result := params.ExecuteAsPost(t, alphaURL) + common.RequireNoGQLErrors(t, result) + + expected := ` + { + "createMyFavouriteMovies": [ + { + "id": "0x1", + "name": "Mov1", + "director": [ + { + "id": "0x2", + "name": "Dir1" + } + ] + }, + { + "id": "0x3", + "name": null, + "director": [] + } + ] + }` + require.JSONEq(t, expected, string(result.Data)) +} + func TestCustomPatchMutation(t *testing.T) { schema := customTypes + ` input MovieDirectorInput { diff --git a/graphql/resolve/resolver.go b/graphql/resolve/resolver.go index 156804b272f..32464c65622 100644 --- a/graphql/resolve/resolver.go +++ b/graphql/resolve/resolver.go @@ -1793,6 +1793,7 @@ func (hr *httpResolver) rewriteAndExecute(ctx context.Context, field schema.Fiel } body = string(b) } + b, err := makeRequest(hr.Client, hrc.Method, hrc.URL, body, hrc.ForwardHeaders) if err != nil { return emptyResult(externalRequestError(err, field)) diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index df382f381f4..a1325d43c00 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -1841,22 +1841,18 @@ func parseBodyTemplate(body string) (*interface{}, map[string]bool, error) { return &m, requiredFields, nil } -func getVar(key string, variables map[string]interface{}) (interface{}, error) { +func getVar(key string, variables map[string]interface{}) (interface{}, error, bool) { if !strings.HasPrefix(key, "$") { - return nil, errors.Errorf("expected a variable to start with $. Found: %s", key) + return nil, errors.Errorf("expected a variable to start with $. Found: %s", key), true } val, ok := variables[key[1:]] - if !ok { - return nil, errors.Errorf("couldn't find variable: %s in variables map", key) - } - - return val, nil + return val, nil, ok } func substituteSingleVarInBody(key string, valPtr *interface{}, variables map[string]interface{}) error { // Look it up in the map and replace. - val, err := getVar(key, variables) + val, err, _ := getVar(key, variables) if err != nil { return err } @@ -1868,11 +1864,15 @@ func substituteVarInMapInBody(object, variables map[string]interface{}) error { for k, v := range object { switch val := v.(type) { case string: - vval, err := getVar(val, variables) + vval, err, ok := getVar(val, variables) if err != nil { return err } - object[k] = vval + if ok { + object[k] = vval + } else { + delete(object, k) + } case map[string]interface{}: if err := substituteVarInMapInBody(val, variables); err != nil { return err @@ -1892,7 +1892,7 @@ func substituteVarInSliceInBody(slice []interface{}, variables map[string]interf for k, v := range slice { switch val := v.(type) { case string: - vval, err := getVar(val, variables) + vval, err, _ := getVar(val, variables) if err != nil { return err } diff --git a/graphql/schema/wrappers_test.go b/graphql/schema/wrappers_test.go index af6e4ebeb28..bffeb40b707 100644 --- a/graphql/schema/wrappers_test.go +++ b/graphql/schema/wrappers_test.go @@ -424,11 +424,18 @@ func TestSubstituteVarsInBody(t *testing.T) { nil, }, { - "variable not found error", + "Skip one missing variable in the HTTP body", map[string]interface{}{"postID": "0x9"}, map[string]interface{}{"author": "$id", "post": map[string]interface{}{"id": "$postID"}}, + map[string]interface{}{"post": map[string]interface{}{"id": "0x9"}}, + nil, + }, + { + "Skip all missing variables in the HTTP body", + map[string]interface{}{}, + map[string]interface{}{"author": "$id", "post": map[string]interface{}{"id": "$postID"}}, + map[string]interface{}{"post": map[string]interface{}{}}, nil, - errors.New("couldn't find variable: $id in variables map"), }, }