diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index 8b4285e4bf8..1869d0810f2 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -329,6 +329,7 @@ func RunAll(t *testing.T) { t.Run("add multiple mutations", testMultipleMutations) t.Run("deep XID mutations", deepXIDMutations) t.Run("three level xid", testThreeLevelXID) + t.Run("nested add mutation with @hasInverse", nestedAddMutationWithHasInverse) t.Run("error in multiple mutations", addMultipleMutationWithOneError) t.Run("dgraph directive with reverse edge adds data correctly", addMutationWithReverseDgraphEdge) diff --git a/graphql/e2e/common/mutation.go b/graphql/e2e/common/mutation.go index 76d21af86dd..f7a336d379f 100644 --- a/graphql/e2e/common/mutation.go +++ b/graphql/e2e/common/mutation.go @@ -3652,32 +3652,97 @@ func int64BoundaryTesting(t *testing.T) { //This test checks the range of Int64 //(2^63)=9223372036854775808 addPost1Params := &GraphQLParams{ - Query: `mutation { - addpost1(input: [{title: "Dgraph", numLikes: 9223372036854775807 },{title: "Dgraph1", numLikes: -9223372036854775808 }]) { - post1 { - title - numLikes - } - } + Query: `mutation { + addpost1(input: [{title: "Dgraph", numLikes: 9223372036854775807 },{title: "Dgraph1", numLikes: -9223372036854775808 }]) { + post1 { + title + numLikes + } + } }`, } gqlResponse := addPost1Params.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) - addPost1Expected := `{ - "addpost1": { - "post1": [{ - "title": "Dgraph", - "numLikes": 9223372036854775807 - - },{ - "title": "Dgraph1", - "numLikes": -9223372036854775808 - }] - } + addPost1Expected := `{ + "addpost1": { + "post1": [{ + "title": "Dgraph", + "numLikes": 9223372036854775807 + + },{ + "title": "Dgraph1", + "numLikes": -9223372036854775808 + }] + } }` testutil.CompareJSON(t, addPost1Expected, string(gqlResponse.Data)) filter := map[string]interface{}{"title": map[string]interface{}{"regexp": "/Dgraph.*/"}} deleteGqlType(t, "post1", filter, 2, nil) } + +func nestedAddMutationWithHasInverse(t *testing.T) { + params := &GraphQLParams{ + Query: `mutation addPerson1($input: [AddPerson1Input!]!) { + addPerson1(input: $input) { + person1 { + name + friends { + name + friends { + name + } + } + } + } + }`, + Variables: map[string]interface{}{ + "input": []interface{}{ + map[string]interface{}{ + "name": "Or", + "friends": []interface{}{ + map[string]interface{}{ + "name": "Michal", + "friends": []interface{}{ + map[string]interface{}{ + "name": "Justin", + }, + }, + }, + }, + }, + }, + }, + } + + gqlResponse := postExecutor(t, GraphqlURL, params) + RequireNoGQLErrors(t, gqlResponse) + + expected := `{ + "addPerson1": { + "person1": [ + { + "friends": [ + { + "friends": [ + { + "name": "Or" + }, + { + "name": "Justin" + } + ], + "name": "Michal" + } + ], + "name": "Or" + } + ] + } + }` + testutil.CompareJSON(t, expected, string(gqlResponse.Data)) + + // cleanup + deleteGqlType(t, "Person1", map[string]interface{}{}, 3, nil) +} diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index d0f381401ad..5089880267d 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -177,4 +177,10 @@ type post1{ id: ID title: String! @id @search(by: [regexp]) numLikes: Int64 -} \ No newline at end of file +} + +type Person1 { + id: ID! + name: String! + friends: [Person1] @hasInverse(field: friends) +} diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index 79ce9f18785..31f4b72bbf7 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -68,6 +68,15 @@ ], "upsert": true }, + { + "predicate": "Person1.name", + "type": "string" + }, + { + "predicate": "Person1.friends", + "type": "uid", + "list": true + }, { "predicate": "Post1.comments", "type": "uid", @@ -457,6 +466,17 @@ ], "name": "People" }, + { + "fields": [ + { + "name": "Person1.name" + }, + { + "name": "Person1.friends" + } + ], + "name": "Person1" + }, { "fields": [ { diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index 2e101d54227..5da2c7c654d 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -178,4 +178,10 @@ type post1{ id: ID title: String! @id @search(by: [regexp]) numLikes: Int64 -} \ No newline at end of file +} + +type Person1 { + id: ID! + name: String! + friends: [Person1] @hasInverse(field: friends) +} diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index 853a1dcd668..39767d12e22 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -150,6 +150,15 @@ "predicate": "Person.name", "type": "string" }, + { + "predicate": "Person1.name", + "type": "string" + }, + { + "predicate": "Person1.friends", + "type": "uid", + "list": true + }, { "predicate": "Post.author", "type": "uid" @@ -510,6 +519,17 @@ ], "name": "Person" }, + { + "fields": [ + { + "name": "Person1.name" + }, + { + "name": "Person1.friends" + } + ], + "name": "Person1" + }, { "fields": [ { diff --git a/graphql/resolve/add_mutation_test.yaml b/graphql/resolve/add_mutation_test.yaml index a780c466024..91a80162e4f 100644 --- a/graphql/resolve/add_mutation_test.yaml +++ b/graphql/resolve/add_mutation_test.yaml @@ -2413,3 +2413,61 @@ explanation: "The add mutation should not be allowed since value of @id field is empty." error: { "message": "failed to rewrite mutation payload because encountered an empty value for @id field `State.code`" } + +- + name: "Add mutation for person with @hasInverse" + gqlmutation: | + mutation($input: [AddPersonInput!]!) { + addPerson(input: $input) { + person { + name + } + } + } + gqlvariables: | + { + "input": [ + { + "name": "Or", + "friends": [ + { "name": "Michal", "friends": [{ "name": "Justin" }] } + ] + } + ] + } + dgmutations: + - setjson: | + { + "Person.friends": [ + { + "Person.friends": [ + { + "uid": "_:Person1" + }, + { + "Person.friends": [ + { + "uid": "_:Person2" + } + ], + "Person.name": "Justin", + "dgraph.type": [ + "Person" + ], + "uid": "_:Person3" + } + ], + "Person.name": "Michal", + "dgraph.type": [ + "Person" + ], + "uid": "_:Person2" + } + ], + "Person.name": "Or", + "dgraph.type": [ + "Person" + ], + "uid": "_:Person1" + } + diff --git a/graphql/resolve/mutation_rewriter.go b/graphql/resolve/mutation_rewriter.go index c5c34153a68..f8554533b9b 100644 --- a/graphql/resolve/mutation_rewriter.go +++ b/graphql/resolve/mutation_rewriter.go @@ -1727,7 +1727,27 @@ func squashIntoObject(label string) func(interface{}, interface{}, bool) interfa } asObject = cpy } - asObject[label] = v + + val := v + + // If there is an existing value for the label in the object, then we should append to it + // instead of overwriting it if the existing value is a list. This can happen when there + // is @hasInverse and we are doing nested adds. + existing := asObject[label] + switch ev := existing.(type) { + case []interface{}: + switch vv := v.(type) { + case []interface{}: + ev = append(ev, vv...) + val = ev + case interface{}: + ev = append(ev, vv) + val = ev + default: + } + default: + } + asObject[label] = val return asObject } } diff --git a/graphql/resolve/schema.graphql b/graphql/resolve/schema.graphql index ed3211e6e93..f6e131708da 100644 --- a/graphql/resolve/schema.graphql +++ b/graphql/resolve/schema.graphql @@ -283,6 +283,12 @@ type ThingTwo implements Thing { owner: String } +type Person { + id: ID! + name: String @search(by: [hash]) + friends: [Person] @hasInverse(field: friends) +} + interface A { name: String! @id } \ No newline at end of file